2014年5月5日月曜日

unittest.mockの簡単な紹介

単体テストに欠かせないツールのひとつに、モックがあります。
Pythonにも3.3からモックモジュールが追加されました。
unittest.mockといいます。今回はこのモジュールの簡単な使い方を紹介します。

モックが必要になる場面として、システム日付の取得があります。
例えば、当日が閏日かどうか判定する関数を考えてみましょう。
なにも考えずに関数を実装すると、次のようになると思います。
def is_leap():
    d = datetime.date.today()
    if 2 == d.month and 29 == d.day:
        print(str(d) + ' is LEAP')
        return True
    else:
        print(str(d) + ' is NOT LEAP')
        return False

このままではOSの時計を設定しないと試験ができないので、
日付をパラメータとして渡すようにします。
def is_leap(d):
    if 2 == d.month and 29 == d.day:
        print(str(d) + ' is LEAP')
        return True
    else:
        print(str(d) + ' is NOT LEAP')
        return False

日付を注入できるようにしたことで、テストしやすくなりました。
例えば、このようにします。
assert True == is_leap(datetime.date(2012, 2, 29))
assert False == is_leap(datetime.date(2012, 3, 1))

unittest.mockを使うと、次のような感じになります。
d = unittest.mock.MagicMock()
d.month = 2
d.day = 29
assert True == is_leap(d)

モックを使わないほうが単純です。こういう場合はdateオブジェクトを使ったほうがいいでしょう。
さて、現実には全てのコードを自由に変更できるとは限りません。
たとえそんな場合でも、unittest.mock.patchを呼ぶことで、対応できる場合があります。
is_leap関数を修正せずに次のようにします。
with unittest.mock.patch('datetime.date') as dt:
    dt.today().month = 2
    dt.today().day = 29
    assert True == is_leap()

datetime.date.today()メソッドが返すオブジェクトをモックオブジェクトと置き換え、
属性を設定することでテストをパスしています。
確かにこの方法でも試験はできますが、日付をパラメータ化するほうがベターだと思います。

他にも、unittest.mockにはメソッドの呼び出しを記録する機能もあります。
詳しいことはオフィシャルのドキュメントを参照してください。

参考:

2014年4月28日月曜日

xyzzyでブックマーク機能

Emacsのブックマーク機能で紹介したEmacsのブックマーク機能が便利だったので、xyzzyでも使いたくなりました。
Lispの勉強も兼ねて、自分で実装してみました。
プログラムは記事の最後に載せます。

機能
実装済みの機能は以下のとおりです。

  • 現在表示しているバッファをブックマークとして登録する
  • ブックマークの一覧を表示する
  • ブックマークの一覧からファイルを開く
  • ブックマークを削除する
使用方法
まず、プログラムを読み込みます。専用のパッケージを定義したので、合わせてuse-packageします。

(require "bookmark")
(use-package "bookmark")

次に、キーバインドの設定をします。今のところ公開している関数は次の2つです。
  1. bookmark-add-current-buffer: 表示しているバッファをブックマークする
  2. bookmark-list-bookmarks: ブックマークの一覧を表示する
(global-set-key '(#\C-c #\a #\b) 'bookmark-add-current-buffer)
(global-set-key '(#\C-c #\l #\b) 'bookmark-list-bookmarks)
bookmark-add-current-bufferを呼ぶと、現在のバッファをブックマークします。その際、ブックマーク名を入力できます。
bookmark-list-bookmarksを呼ぶと、ブックマーク一覧を表示します。一覧からファイルの表示と、ブックマークの削除ができます。ブックマークしたファイルの表示は[f]キーを押下し、ブックマークの削除は[d]キーです。

ブックマーク一覧を実装するときは、xyzzy付属のbuf-menu.lが参考になりました。
よろしければ使ってみてください。

2014年4月21日月曜日

Emacs: 検索/置換チュートリアル


Google+で流れてきたEmacsのチュートリアルを日本語に訳してみました。

Emacs: 検索/置換チュートリアル

このページでは、Emacsの検索/置換機能を紹介します。
大文字と小文字を区別するかどうかについて紹介します。
正規表現にマッチした文字列を大文字や小文字に変換する方法について紹介します。

検索/置換コマンド
最も便利な検索/置換コマンドについて紹介します。
これらは、メニューのEdit > Replaceにあります。
コマンド名 キーバインド 対象 目的
query-replace M-% 有効なリージョン
カーソルから後方
対話的な検索/置換
query-replace-regexp C-M-% 有効なリージョン
カーソルから後方
正規表現による対話的な検索/置換
dired-do-query-replace-regexp diredでQ マークしたファイル 複数ファイルに対する対話的な検索/置換

例:query-replaceを呼び、検索文字列と置換文字列を入力します。

コマンドが確認を求めた時の一般的なコマンドは以下のとおりです。
  • y - 置換を実行する
  • n - スキップする
  • ! - これ以降確認なしで置換する
  • C-g - キャンセル(置換を元に戻すにはundoを呼ぶ)
dired-do-query-replace-regexpについては、Interactively Find/Replace String Patterns on Multiple Filesを参照してください。

一括置換
Emacsにはreplace-stringとreplace-regexpコマンドもあります。
それらはquery-replaceとquery-replace-regexpの対話的でないバージョンです。
一度の実行で確認せずにすべて置換します。
練習段階では、これらはさほど便利ではありません。
対話的なバージョンを使用し、!を入力すれば一括で置換できます。

デフォルトの大小文字区別: 自動
デフォルトでは、検索文字に大文字を含むとき、自動的に大小文字を区別して検索します。そうでなければ、大証文字を区別せずに検索します。
デフォルトでは、置換後の文字列の大小文字検索にマッチした文字列に依存します。
例えば、検索文字列が「here」で置換文字列が「dragon」のときについて考えます。
Emacsは「here」「Here」「HERE」のいずれかを検索します。
そして、「here」を「dragon」で置換し、「Here」を「Dragon」で置換し、「HERE」を「DRAGON」で置換します。

入力したとおりの文字列で置換したいときは、case-replace変数へnilをセットします。set-variableを使用します。

自動大小文字区別のON/OFF
検索と置換の両方で入力した通りの大小文字を使用したいときはtoggle-case-fold-searchを呼ぶか、メニューのOptions > Case-Insensitive Searchを使用します。
キーやエイリアスを割り当てることもできます。(Emacs: How to Define Keys) (Emacs: Defining Alias to Increase Productivity)

正規表現にマッチした文字列の大小文字を強制的に置換する
検索に正規表現を使用しており、大文字や小文字に置換したいとき、「\,(upcase \1)」や「\,(downcase \1)」を使用できます。
例えば、次のようなテキストについて考えます。
<p>once upon a time ...</p>
<p>there is a dragon who lived in ...</p>
<p>princess tana is still witing ...</p> 
すべてのパラグラフを大文字で開始したいとき、<p>に続く一文字をキャプチャする「<p>\{[a-z]\}」のようなパターンを使います。

キャプチャした文字を大文字に置換するために、置換文字列に「<p>\,(upcase \1)」を使用します。「\,」は続く文字列がlisp式であることを示します。「(upcase \1)」はlisp式です。「upcase」はlisp関数で、「\1」は1番目のキャプチャ文字列です。

「\,」を使用したより複雑な置換については、Regex Replace with a Function in Emacs Lispを参照してください。

参考
Emacs: Find/Replace Tutorial

2014年4月14日月曜日

シェルスクリプト初心者の私がまず覚えたこと9つ

今年の4月にシェルスクリプトを書く機会がありました。今までシェルを使った経験は多少あったものの、シェルスクリプトを書くのは初めてでした。シェルスクリプトを書くにあたり、まず抑えたことを紹介します。

  1. 1行目のおまじない
  2. 変数の使い方
  3. 関数の使い方
  4. 制御文
  5. バッククォート
  6. 終了ステータス
  7. 設定ファイル
  8. 文字列操作
  9. 文字列のエスケープ
それぞれについて以下に記載します。

2014年4月7日月曜日

GZIP圧縮に対応したHTTPクライアントを作る

前回の記事で予告したとおり、今回はC#でGZIP圧縮に対応したHTTPクライアントを作ります。
ポイントは3つあります。

  1. GZipStreamを使って入力データを圧縮する
  2. HttpClientHandlerのAutomaticDecompressionプロパティを設定する
  3. HTTPリクエストヘッダのContent-Encodingをgzipに設定する

GZipStreamを使って入力データを圧縮する
圧縮用にMemoryStreamをインスタンス化し、GZipStreamでラップします。
usingをネストしているのはGZipStreamをCloseするタイミングで圧縮が実行されるからです。
byte[] data = null;

using (var memory = new MemoryStream()) {
  using (var gzip = new GZipStream(memory, CompressionMode.Compress)) {
    int input = 0;
    byte[] buffer = new byte[1];

    while (-1 != (input = Console.Read())) {
      buffer[0] = Convert.ToByte(input);
      gzip.Write(buffer, 0, buffer.Length);
    }
  }
  data = memory.ToArray();
}

HttpClientHandlerのAutomaticDecompressionプロパティを設定する
リクエストボディの圧縮は実装する必要がありますが、レスポンスの解凍は.NET Frameworkが対応しています。
var clientHandler = new HttpClientHandler();
clientHandler.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;

HTTPリクエストヘッダのContent-Encodingをgzipに設定する
コンテンツがgzip圧縮されていることを示すため、Content-Encodingをgzipに設定します。
using(var client = new HttpClient(clientHandler))
using(var content = new ByteArrayContent(data))
{
  content.Headers.ContentEncoding.Add("gzip");
  using(var message = client.PostAsync(url, content).Result)
  {
    Console.WriteLine(message.Content.ReadAsStringAsync().Result);
  }
}

一般的に、HTTPリクエストが圧縮を必要とするほど大きくなることはありません。
全ての画面データを毎回やりとりするような設計であれば、効果があるかもしれません。

プログラム全体を以下に記載します。

2014年3月31日月曜日

FilterでHTTP通信のGZIP圧縮に対応する

HTTP/1.1では仕様に電文の圧縮が盛り込まれています。
今回はサーブレットフィルタでGZIP圧縮に対応してみたいと思います。

必要なクラス
圧縮されたHTTPリクエストの解凍と、HTTPレスポンスの圧縮を実現するには、次の5つのクラスが必要です。

  1. GZIPFilter - Filterを実装します。HTTPヘッダを確認し、圧縮/解凍をするかどうか判断します。
  2. GZIPRequest - HttpServletRequestWrapperを継承します。GZIPServletInputStreamを返します。
  3. GZIPServletInputStream - HTTPリクエストを解凍するInputStreamです。
  4. GZIPResponse - HttpServletResponseWrapperを継承します。GZIPServletOutputStreamを返します。
  5. GZIPServletOutputStream - HTTPレスポンスを圧縮するOutputStreamです。
ほか、リクエストをそのままレスポンスするEchoServletを準備します。

2014年3月24日月曜日

PythonでEvernoteへメールを送る - 添付ファイル追加

PythonでEvernoteへメールを送る」で載せたプログラムへ、ファイル添付機能を追加しました。
合わせてクラス設計を見直しました。

# -*- coding: utf-8 -*-

# ---- CONFIGURATIONS ----
EVERNOTE_MAIL_ADDRESS = 'evernote_mail_address'
FROM_ADDRESS = 'from_address'
SMTP_HOST = 'smtp_host'
SMTP_PORT = 25
# ---- CONFIGURATIONS ----

import datetime
import mimetypes
import optparse
import os.path
import smtplib
import sys

from email import encoders
from email.header import Header
from email.mime.audio import MIMEAudio
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

class EverMailClient(object):
 def __init__(self, smtp):
  assert isinstance(smtp, smtplib.SMTP)
  self._smtp = smtp
  
 def send(self, mail):
  assert isinstance(mail, EverMail)
  msg = mail.as_MIME()
  mail_from = msg['From']
  mail_to = msg['To']
  self._smtp.sendmail(mail_from, mail_to, msg.as_string())


class EverMail(object):
 def __init__(self, title, from_address, to_address, body, attached_files):
  assert isinstance(title, EverMailTitle)
  assert isinstance(body, EverMailBody)
  self._title = title
  self._from_address = from_address
  self._to_address = to_address
  self._body = body
  self._attached_files = attached_files
  
 def as_MIME(self):
  msg = MIMEMultipart()
  msg['Subject'] = Header(str(self._title), 'utf-8')
  msg['From'] = self._from_address
  msg['To'] = self._to_address
  msg.attach(self._body.as_MIME())
  for attached_file in self._attached_files:
   msg.attach(attached_file.as_MIME())
  return msg


class EverMailTitle(object):
 def __init__(self, title, notebook, tags):
  self._title = title if title else str(datetime.datetime.now())
  self._notebook = notebook if notebook else ''
  self._tags = tags if tags else []
  
 def __str__(self):
  result = self._title
  if 0 < len(self._notebook):
   result += ' @' + self._notebook
  for tag in self._tags:
   result += ' #' + tag
  return result


class EverMailBody(object):
 def __init__(self, stdin):
  assert hasattr(stdin, 'read')
  self._stdin = stdin
  
 def as_MIME(self):
  body = sys.stdin.read()
  msg = MIMEText(bytes(body))
  msg.set_charset('utf-8')
  return msg

  
class AttachedFile(object):
 def __init__(self, path):
  self._path = path
  
 def as_MIME(self):
  ctype, encoding = mimetypes.guess_type(path)
  if ctype is None or encoding is not None:
   ctype = 'application/octet-stream'
  maintype, subtype = ctype.split('/', 1)
  msg = None
  with open(self._path, 'rb') as f:
   if maintype == 'text':
    msg = MIMEText(f.read(), _subtype=subtype)
   elif maintype == 'image':
    msg = MIMEImage(f.read(), _subtype=subtype)
   elif maintype == 'audio':
    msg = MIMEAudio(f.read(), _subtype=subtype)
   else:
    msg = MIMEBase(maintype, subtype)
    msg.set_payload(f.read())
    encoders.encode_base64(msg)
  file_name = os.path.basename(self._path) 
  msg.add_header('Content-Disposition', 'attachment', filename=file_name)
  return msg


class EverMailOptionParser(optparse.OptionParser):
 def __init__(self):
  optparse.OptionParser.__init__(self)
  self.add_option('--notebook', '-n', action='store')
  self.add_option('--tag', '-t', action='append')

    
def create_smtp_client():
 smtp = smtplib.SMTP(SMTP_HOST, SMTP_PORT)
 smtp.set_debuglevel(1)
 return smtp
 
 
if __name__ == '__main__':
 option_parser = EverMailOptionParser()
 (options, args) = option_parser.parse_args()
 note_title = args[0] if args and 0 < len(args) else ''
 title = EverMailTitle(note_title,
                       options.notebook,
                       options.tag)
 body = EverMailBody(sys.stdin)
 attached_files = []
 if 2 <= len(args):
  attached_files = [AttachedFile(path) for path in args[1]]
 mail = EverMail(title,
                 FROM_ADDRESS,
                 EVERNOTE_MAIL_ADDRESS,
                 body,
                 attached_files)
 
 smtp = None
 try:
  smtp = create_smtp_client()
  client = EverMailClient(smtp)
  client.send(mail)
 finally:
  if smtp: smtp.close()

複数ファイルを添付するとうまくいかないところが問題です。
また時間のあるときに対応したいと思います。