メソッドを置き換えたmockをもう少しstrictにしてみたい

メソッドを置き換えたmockをもう少しstrictにしてみたい。mockのpatchなどでobjectを置き換える時に属性の存在まではspecやspec_setで対応できるのだけれど。メソッドのsignatureまで含めて置き換え前のものと同じかどうか確認したい。

例えば、存在しない属性へのアクセスはエラーになる(これは期待通り)

class Ob:
    def hello(self, name):
        return "hello:{}".format(name)


class Tests(unittest.TestCase):
    def test_attr_missing(self):
        # 属性無しはOK
        m = mock.Mock(spec_set=Ob)
        with self.assertRaises(AttributeError):
            m.bye()

一方でこれはダメ、元のメソッドから見たら引数不足なものの、mockはそれを関知しない(これは期待通りではない)

class Tests(unittest.TestCase):
    def test_mismatch_signature(self):
        m = mock.Mock(spec_set=Ob)
        m.hello.side_effect = lambda: "*replaced*"

        # Ob.hello()から見たら不正な呼び出しなのだけれど。置き換えたmockとは合っているので通ってしまう
        got = m.hello()
        self.assertEqual(got, "*replaced*")

本来であればTypeErrorなどが発生してほしい。

Ob().hello()
# TypeError: hello() missing 1 required positional argument: 'name'

現状のwork-around

雑に replace_method_with_signature_check() という名前の関数を定義している。

これを使うと以下の様にAssertionErrorが出るようになる。

class Tests(unittest.TestCase):
    def test_mismatch_signature(self):
        m = mock.Mock(spec_set=Ob)

        # Ob.hello()に対して引数が不足した定義
        def hello():
            return "*replaced*"

        replace_method_with_signature_check(m, hello)

        got = m.hello()
        self.assertEqual(got, "*replaced*")

# AssertionError: expected hello()'s signature: (name), but ()

ちゃんとsignatureを考慮して見てくれる。もちろん、まともなsignatureの合った定義に書き換えたら呼び出し側の引数の不一致がわかりTypeErrorになる。

一応mockじゃないものに利用してしまった場合の事も考慮して type(m)m.__class__ を比較している(mockとmock以外を見分けるイディオム)。

実装

実装は以下の様な感じ。

import inspect

def replace_method_with_signature_check(m, fn, name=None):
    """mock中のmethodをsignatureを考慮して書き換えるもの"""
    spec = m.__class__
    typ = type(m)
    name = name or fn.__name__

    assert typ != spec, "{} == {}, maybe spec is not set?".format(typ, spec)

    sig_repr = str(inspect.signature(getattr(spec, name)))
    sig_repr = sig_repr.replace('(self, ', '(')  # xxx work-around
    fn_sig_repr = str(inspect.signature(fn))
    assert sig_repr == fn_sig_repr, "expected {}()'s signature: {}, but {}".format(name, sig_repr, fn_sig_repr)
    attr = getattr(m, name)
    attr.side_effect = fn

微妙な点も残っていて、メソッドの置き換えを考慮するのに、self部分をカットする部分がすごく雑。これは isnpect.signature() で取れる値の引数部分が変更不可能なせいでもあるのだけれど。本当に真面目に頑張るのならinspect.getfullargspec()の方を利用した方が良いかもしれない。

置き換えをオブジェクトで

もうちょっと不格好じゃない形で置き換えをしたい場合にはオブジェクトにしたほうが良いのかもしれない。

class MethodReplacer:
    def __init__(self, m):
        self.m = m

    def __getattr__(self, name):
        return partial(replace_method_with_signature_check, self.m, name=name)


m = mock.Mock(spec_set=Ob)
rep = MethodReplacer(m)
rep.hello(lambda name: "*replaced*")

dictknifeにdictmapを追加してみた

github.com

はじめに

dictknifeにdictmap()を追加してみた。

round dict

例えばstack overflowのこの質問に対する回答がシンプルになる。

これを

d = [
    {
        'A': 0.700000000,
        'B': 0.255555555
    }, {
        'B': 0.55555555,
        'C': 0.55555555
    }, {
        'A': 0.255555555,
        'B': 0.210000000,
        'C': 0.2400000000
    }
]

こうしたいとのこと。

expected = [
    {
        'A': 0.70,
        'B': 0.25
    },
    {
        'B': 0.55,
        'C': 0.55
    },
    {
        'A': 0.25,
        'B': 0.21,
        'C': 0.24
    },
]

こういう感じで書けるようになった。

from dictknife import dictmap  # noqa
dictmap(lambda x: round(x, 2) if isinstance(x, float) else x, d)

# [{'A': 0.7, 'B': 0.26}, {'C': 0.56, 'B': 0.56}, {'C': 0.24, 'A': 0.26, 'B': 0.21}]

(上の入力に限ってなら以下でも良い)

dictmap(lambda x: round(x, 2), d)

ところでmutableとimmutable

ところで元の質問の回答はmutableで元の辞書を破壊的に更新するものだけれど。新しい同様の形状のdictを作り直すみたいな操作も考えられる。 実際に上のdictmap()で行われているのは新しいdictを作り直す操作。

破壊的に更新したい場合には以下の様にすれば良い。

import copy


d0 = copy.copy(d)
d1 = dictmap(lambda x: round(x, 2) if isinstance(x, float) else x, d, mutable=True)

assert id(d0) == id(d1)  # idは同じ(元のobject)

補足説明

ちょっとだけ補足しておくと元の値と同じdictのオブジェクトを使う。例えば渡された値がOrderedDictならOrderedDictが使われる。

元々欲しかった機能

dictmap()は実は副次的に追加したもので元々欲しかったのはdict内の数値っぽい値の文字列を数値に直す機能。 特にcsvやtsvに変換されたものを読み込む時に文字列として解釈されるのがわりとだるかった。 (ちなみに直接上のdictmap()が使われているわけではない。immutableな操作とmutableな操作を使い分ける部分が使われている)

テキトウに名前はguess()にしているけれど変えるかもしれない。こういう感じに動く。

この値の文字列部分がfloatやintに変換される。

v = [
    {
        "ints": [{
            "zero": "0",
            "nums": ["10", "-2000"]
        }],
        "floatnums": [{
            "full": "0.1",
            "mini": ".1",
            "epsilon": "5.551115123125783e-17"
        }],
        "infs": ["inf", "-inf"]
    }
]
guess(v)

こういう感じ。

[
    {
        'floatnums': [{
            'full': 0.1,
            'mini': 0.1,
            'epsilon': 5.551115123125783e-17
        }],
        'ints': [{
            'nums': [10, -2000],
            'zero': 0
        }],
        'infs': [float('inf'), float('-inf')]
    }
]

infとかnanの扱いはとりあえずpythonのそれだけ。

標準ライブラリの範囲でpythonでweb APIのmock serverが欲しくなった場合

mock serverが欲しい

web apiのmock serverが欲しくなることがある。結構その場その場で欲しくなる機能というのが微妙に違ったりするので毎回そらで手書きする事が多い(本当は上手に抽象化できるのかもしれないけれどできていない)。今回の機能は以下の様な感じ。

  • 既に素となるresponseを返すサーバーは存在する
  • 元のサーバーが返すresponseの一部を書き換えて返したい
  • 一回の操作で複数のpathにrequestすることは無い(pathによる分岐が不要)

素となるresponseはある

例えば以下の様なresponse。実際にはもっと複雑。firstnameとlastnameを返す。

{
  "firstName": "Foo",
  "lastName": "Bar"
}

何かの加工を加えた結果を返したい

APIの変更などを適用した結果を仮に返して利用したい場合がある。ここでmock serverが欲しい。 加工した結果を返したい。例えばfullnameを追加するとか。

{
  "firstName": "Foo",
  "lastName": "Bar",
  "fullName": "Foo Bar"
}

つまり以下のような操作が欲しいということ。

# dは素となるresponse
d["fullName"] = "{d[firstName]} {d[lastName]}".format(d=d)

標準ライブラリのみのmock server

mock serverは標準ライブラリの範囲で実装したい。概ねwsgirefを使ったコードWSGI Appを作るだけ。 こういう感じのコードを使う。コードのほとんどはWSGI Appを作るところなのでだいたいコピペ(ちなみにwsgirefを使って雑なapi serverを使う例はここに書いた)。

import json
from wsgiref.simple_server import make_server


def make_App(filename, transform, usereload):
    def App(environ, start_response):
        status = '200 OK'
        headers = [('Content-type', 'Application/json; charset=utf-8')]
        start_response(status, headers)
        with open(filename) as rf:
            d = json.load(rf)

        d["fullName"] = "{d[firstName]} {d[lastName]}".format(d=d)

        return [json.dumps(d).encode("utf-8")]

    return App


if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("--port", required=True, type=int)
    parser.add_argument("filename")
    args = parser.parse_args()

    httpd = make_server('', args.port, make_App(args.filename))
    print("Serving on port {}...".format(args.port))
    # Serve until process is killed
    httpd.serve_forever()

ちなみにpathからファイル名を決めるようにしようとかやってしまうと元となるapiのpathのことを考えたりする必要がでてきてだるくなる(逆にそれが不要というのは色々な種類のrequestをさばく必要がないという今回の要件に依存しているのだけれど)。

そしてできればgoだけですませたい(最近はgoを書くことも多い)気持ちがあったのだけれど(今週はgo強化週間的な気持ち)。どうも面倒だったので結局pythonで書いてしまった(型の定義したくないJSONをふわっと更新したい場合にちょっとめんどうだった。あとresponseのJSONの順序を保持したかった)。

もう少し標準ライブラリの範囲で汎用的にしてみる

ところでもう少し汎用的にして見る。標準ライブラリの範囲で。標準ライブラリに限定した理由はどこでも動かすため。

たぶん外部のライブラリを使って良いならもっと良い方法がいっぱいある。1つだけ残念な点があるとすればPYTHONPATHの指定が必要になってしまっている点(sys.pathはもっといや)。

こんな感じで使う。

$ PYTHONPATH=. python mock.py --reload --port 33333 --transform=fullname:transform data.json

firstname,lastnameから補完されてfullnameが返ってくる。

$ http -b :33333
{"firstName": "Foo", "lastName": "Bar", "fullName": "Foo Bar"}

変更箇所を別ファイルとして指定できるようにしておくと試行錯誤のときに便利。この時指定しているtransformは以下の様な感じ。

fullname.py

def transform(d):
    d["fullName"] = "{d[firstName]} {d[lastName]}".format(d=d)
    return d

ちなみにhot reload(?)のような形で動作できるようにもしたのでどのような変更が必要か定まっていない場合にも便利。サーバーを立ち上げ直さなくても変更が自動的に適用される(再読込しているだけ)。実際のところこの振る舞いを付け加えたかった。

コードはこんな感じ。

import json
from wsgiref.simple_server import make_server
from importlib import import_module, reload


def make_App(filename, transform, usereload):
    def App(environ, start_response):
        status = '200 OK'
        headers = [('Content-type', 'Application/json; charset=utf-8')]
        start_response(status, headers)
        with open(filename) as rf:
            d = json.load(rf)

        if transform is not None:
            modname, fnname = transform.rsplit(":", 1)
            m = import_module(modname)
            if usereload:
                m = reload(m)
            fn = getattr(m, fnname)

            transformed = fn(d)
            if transformed is not None:
                d = transformed

        return [json.dumps(d).encode("utf-8")]

    return App


if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("--port", required=True, type=int)
    parser.add_argument("--transform", default=None)
    parser.add_argument("--reload", action="store_true")
    parser.add_argument("filename")
    args = parser.parse_args()

    httpd = make_server('', args.port, make_App(args.filename, args.transform, args.reload))
    print("Serving on port {}...".format(args.port))
    # Serve until process is killed
    httpd.serve_forever()

外部ライブラリに依存して良いなら

外部ライブラリに依存して良いなら、個人的な用途ならmagicalimportを使えばPYTHONPATHを無くせるし。直接WSGI Appを手書きするより何かパッケージを使った方が楽な気はする(flaskはimportと同時にjinja2を読み込んでしまうのでちょっとだけイラッとする(ちなみにjinja2のimportは2.10で早くなったことを教えてもらったりした))。あと毎回moduleをreloadは気持ち悪いなーとか。

csvのheaderを気にしてOrderedDictで読む方法

問題

例えば以下のようなcsvがあるときに、読み込んだ後にcsvのheaderの順序を保持してほしいという場合がある。

"age"    "name"
"20"    "foo"

カジュアルに以下の様なコードで読んだ場合に順序は不定。

import csv
import sys


r = csv.DictReader(sys.stdin, delimiter="\t")
print(list(r))

順序は不定。

[{'name': 'foo', 'age': '20'}]
[{'name': 'foo', 'age': '20'}]
[{'age': '20', 'name': 'foo'}]

対応方法

csv.DictReaderを継承するしか無いの?

例えば以下の様な感じにする。dict_factoryみたいな形で引数になっているとありがたいのだけれど。

import csv
import sys
from collections import OrderedDict


class OrderedDictReader(csv.DictReader):
    def __next__(self):
        if self.line_num == 0:
            # Used only for its side effect.
            self.fieldnames
        row = next(self.reader)
        self.line_num = self.reader.line_num

        # unlike the basic reader, we prefer not to return blanks,
        # because we will typically wind up with a dict full of None
        # values
        while row == []:
            row = next(self.reader)
        d = OrderedDict(zip(self.fieldnames, row))
        lf = len(self.fieldnames)
        lr = len(row)
        if lf < lr:
            d[self.restkey] = row[lf:]
        elif lf > lr:
            for key in self.fieldnames[lr:]:
                d[key] = self.restval
        return d


r = OrderedDictReader(sys.stdin, delimiter="\t")
print(list(r))

今度は大丈夫。

[OrderedDict([('age', '20'), ('name', 'foo')])]
[OrderedDict([('age', '20'), ('name', 'foo')])]
[OrderedDict([('age', '20'), ('name', 'foo')])]

Vim Vixenを使い始めた

vim vixenを使い始めた

最近、Vim Vixenというfirefoxの拡張を使い始めました。いわゆるひとつのvim的な操作感をブラウザにもというやつです。元々はvimperatorを使っていたのですが、firefox57(?)からWebExtensions以外のものが動かなくなるらしいという話で乗り換え先を探していたりしていたのでした(一時的にVimFxを使ったりもしていましたがこれも57の壁を超えることができなさそうなかんじです)。

細かな調整

実際使ってみて細かな部分の操作に違いがあるもののそれなりに良さそうという感じです。ただ個人的にちょっとだけ設定を書き換えたい部分があり一部追加でキーバインドを設定していたりします(いつもはデフォルト設定をそのまま利用する派)

記憶と染み付いてしまった手癖によれば、vimperatorは、前のタブや次のタブに移動する操作がgT,gtで行えるのですがその機能がVim Vixenには無いようでした(あとで調べてみるとVim VixenはK,Jで行う派なようです)。このキーバインドを追加するのはとても簡単で拡張の設定部分で読み込まれるJSONに以下を追加するだけです(about:addonsを開いてVim Vixenを選択して表示されるJSONを書き換える)。

@@ -26,6 +26,8 @@
     "u": { "type": "tabs.reopen" },
     "K": { "type": "tabs.prev", "count": 1 },
     "J": { "type": "tabs.next", "count": 1 },
+    "gT": { "type": "tabs.prev", "count": 1 },
+    "gt": { "type": "tabs.next", "count": 1 },
     "r": { "type": "tabs.reload", "cache": false },
     "R": { "type": "tabs.reload", "cache": true },
     "zi": { "type": "zoom.in" },

キーマップの割当を変更していると、当然、この拡張の設定部分で指定できるコマンド(e.g. tabs.prevみたいなやつのことです)がどのようなものかが気になったりするのですが、それに関してはコードを読むとこのあたりのものが使えるようです。

あと、ちょっと覗いてみた限りでは結構読みやすい感じのコードだし。どこかで時間を見つけて全部読んでおいても良いかなみたいな気持ちになりました。

WebExtensions

それはそうと、なんとなく最近は電車の中での暇な時間にmozillaのextensionsのページを見ています。今までブラウザの拡張は使うことのみで開発する気にはとてもなれなかったのですが(これは古い形式のXULとか触るfirefoxの拡張のころもchromeの拡張のときも同様です)。なんとなくの暇つぶしの読み物としては結構面白い感じです。

基本的にはmanifest.jsonをみてエントリーポイントなどを把握したらあとは各自で気になる部分を見ていく感じで、モジュールなどの分け方は普通のjsの開発と同様の感じでやっていくようでした(ちなみにVim Vixenはuiはreactでコードはwebpackでまとめるというよくある感じの構成)。

知らなかったこと

個人的には、開発にはchromeをそれと並行しての通常のウェブサイトの閲覧にはfirefoxみたいな感じで常にブラウザを複数同時に使っていて、開発に関しては基本的にchromeでだけ作業することが多かったので結構知らないことがあったりしました。たとえば、about:debuggingというページの存在など知らない機能の存在を初めて知るなどしてなるほどーと思ったりしていました。

結構、getting startedの記事だけでも得るものはあって、古のgreasemonkeyみたいな拡張は簡単に作れるし。何よりどこかに登録したり良い感じの構成にしてzipにまとめたりなどせずとも、about:debuggingから自分で作った拡張(仮)のmanifest.jsonを読み込むだけで手軽に拡張を有効にしてみたりと簡単にできるのに驚いたりしていました。

あと、どういう感じの実行結果になるかというのが動画になっているあたりに中の人の頑張りを感じたりしました(たしかに操作感(Look and Feel的な)を確認する上ではスクリーンショットの添付よりは動画のほうが全然良い(文章で説明するよりもわかりやすい))。

日々の日記に温かみ(?)を

今まで結構頑なに何かの作業ログや実行結果や手順を貼り付けると言った事実や、おそらくこうなるであろうという仮定や考えなどしか、文章として出力していなかったのですが。もうちょっと日々の文章中に記述対象に対する周辺の事柄(主に何をおもったのかとか習慣的なこと)を付記する感じで情報を追加するという作業を加えてみることにしてみました。

そういう記事にはemotionalというタグをつけることにしてみました。

logging入門

長すぎにならない程度に使い方をまとめてみる。

loggingの使い方

ライブラリの利用者

既に存在するアプリを実行するファイルの場合

if __name__ == "__main__":
    import logging
    logging.basicConfig(level=logging.DEBUG)  # or INFO or WARNING or ERROR
    run()

個人的には時間もみたいので以下の様にしている。

 logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")

設定可能な属性について詳しくはここ

ライブラリの作成者

ライブラリ内の話。基本的にはモジュール毎に __name__ でロガーを作る。アプリを作成する際にはご自由に。

import logging
logger = logging.getLogger(__name__)


# おもに問題を診断するときにのみ関心があるような、詳細な情報。
logger.debug("hmm")
# 想定された通りのことが起こったことの確認。
logger.info("hmm")
# 想定外のことが起こった、または問題が近く起こりそうである (例えば、'disk space low') ことの表示。
logger.warning("hmm")
# より重大な問題により、ソフトウェアがある機能を実行できないこと。
logger.error("hmm")
# プログラム自体が実行を続けられないことを表す、重大なエラー。
logger.critical("hmm")

最悪、debug,info,errorだけ使えば良い。

例外発生時のtracebackを出力したい場合

stack traceもログに出力したい場合には exc_info=True をつける

try:
    foo()
except:
    logger.warning("hmm", exc_info=True)

うるさいloggerを黙らせる

ロガーの名前が foo.bar.boo の場合

logging.getLogger("foo.bar.boo").setLevel(logging.CRITICAL)

(sentryで適切にaggregationしたい場合)

loggerに渡す文字列をaggregation用のidとして利用できるようにする。具体的にはformat文字列などを利用して文字列を生成しない。

# ok
logger.info("name: %s", name)
# ng
logger.info("name: {}".format(name))

後者は文字列フォーマットの適用結果がsentryに送られてしまうため。適切に発生したエラーをaggregateできない。

ses also