apistarのJSON response でdatetimeを含んだdictを返しても大丈夫なようにする

apistarというdjango rest frameworkなどを作っている人たちが作っているフレームワークがある。まだ開発中。既存のpythonフレームワークとは異なりapiサーバーの実装の方をデフォルトとしているのでちょっとしたものを作る分には便利。

このapistarでよくあるjson.dumpsできないdictをそのままJSONレスポンスにする方法を調べたのでメモ。

前提

$ pip freeze | grep apistar
apistar==0.5.25

開発が進むとともにこのメモの内容は古くなる可能性がある

defaultではエラーになる

例えばテキトウにdatetime.now()を含んだdictを返すAPIを作ってみる。

from apistar import App, Route
from datetime import datetime

def hello() -> dict:
    return {
        "hello": "world",
        "now": datetime.now(),
    }


if __name__ == "__main__":
    routes = [
        Route("/", "GET", hello),
    ]
    app = App(routes=routes)
    app.serve('127.0.0.1', 5000, debug=True)

エラーになる。

  File "/usr/lib/python3.6/_weakrefset.py", line 75, in __contains__
    return wr in self.data
RecursionError: maximum recursion depth exceeded in comparison

これをどうにかしたい。

現状ではrendering方法をカスタマイズする方法はそんざいしていない

現状ではrendering方法をカスタマイズする方法はそんざいしていない。実際中のコードを見てみると以下のようになっている。

class JSONResponse(Response):
    media_type = 'application/json'
    charset = None
    options = {
        'ensure_ascii': False,
        'allow_nan': False,
        'indent': None,
        'separators': (',', ':'),
    }

    def render(self, content: typing.Any) -> bytes:
        options = {'default': self.default}
        options.update(self.options)
        return json.dumps(content, **options).encode('utf-8')

    def default(self, obj: typing.Any) -> typing.Any:
        if isinstance(obj, types.Type):
            return dict(obj)
        error = "Object of type '%s' is not JSON serializable."
        return TypeError(error % type(obj).__name__)

json.dumpsのdefaultにJSONResponse.defaultが渡されてはいるものの。dict()を試すということしかしていない。そんなわけでまだ対応していないようだった。

work-around

仕方がないので以下の様なコードを書く。ついでにtypingも使っておく。

import typing
from apistar import App, Route
from apistar.http import Response, HTMLResponse, JSONResponse
from apistar.server.components import ReturnValue
from datetime import datetime


class MyApp(App):
    def render_response(self, return_value: ReturnValue) -> Response:
        if isinstance(return_value, Response):
            return return_value
        elif isinstance(return_value, str):
            return HTMLResponse(return_value)
        return MyJSONResponse(return_value)


class MyJSONResponse(JSONResponse):
    def default(self, obj: typing.Any) -> typing.Any:
        if isinstance(obj, datetime):
            return obj.isoformat()
        return super().default(obj)


def hello() -> dict:
    return {
        "hello": "world",
        "now": datetime.now(),
    }


if __name__ == "__main__":
    routes = [
        Route("/", "GET", hello),
    ]
    app = App(routes=routes)
    app.serve('127.0.0.1', 5000, debug=True)

今度は通る。

$ python app.py &
$ http -b :5000/
{
    "hello": "world",
    "now": "2018-05-26T15:02:56.854735"
}

余談

とは言え、色々apistarの機能は開発中で変わることもありうる。ついでに、swagger(openAPI)に対応しようとしたりgraphqlもサポートできたら良いねみたいなissueもあったりする通り、型をどこかで定義してそこからのマッピングをしようという方向に考えているように見える。

実際にtypingを使った型を定義してそちらを使って自動でJSONに変換しようというような例がドキュメントにも存在している。なのでそちらの方向に進んでいくような気もしている。

makefileのforeachのハマりどころ

スクランナーとしてのmakeのN回目。あんまり深追いしたくはないという気持ちもありつつ。

以前書いたようにmakefile中ではbashの関数などの定義ができない。そんなわけでdefineとcallを使う。これはこれで便利。ただforeachまで使おうとするとちょっとハマるかもというポイントがあるのでメモ。

define,callを使って処理をまとめる

# この定義はforeachを使う所で上手くいかなくなるので注意(良くない例)
define F
   echo $(1)
   echo $(1)
endef

f0:
  $(call F,foo)
  $(call F,bar)

これは以下の様に展開される。

$ make -n f0
echo foo
echo foo
echo bar
echo bar

なるほど。良い。

foreachを使いたい

さて、Makefileで複数の対象を候補に同じ処理を呼びたいということもある。つまるところ対象としてlistを指定したい。このようなときにはforeachを使う。使うのだけれど。これに少しだけハマりどころが存在する(こういうところがあるのでMakeの知識はバッドノウハウっぽい。本当は代替のタスクランナーがあれば乗り換えたい。ただしできればインストールにパッケージマネージャが不要なものが良い(LLなどはこのあたりで候補から外れる))。

以下の様にリスト(xs)を用意して、先程定義したFをそのリストに対して使おうとしてみる。

# この定義はforeachを使う所で上手くいかなくなるので注意(良くない例)
define F
   echo $(1)
   echo $(1)
endef

xs := foo bar
f1:
  $(foreach x,$(xs),$(call F,$(x)))

-n で実際に実行されるスクリプトを見てみると、一部期待通りではない形になっている。もちろん当然ではあるけれど。このようなコードではfooでの末尾を実行したタイミングで不用意に引数としてbarの先頭行が渡されるということになり。エラーが出てしまう。

$ make f1 -n
echo foo
echo foo        echo bar
echo bar

Makefileのdefineはテキストを定義するというだけの意味なので。endefの手前までの部分、つまるところ改行を含まない定義がforeachによって呼ばれるのでダメ。

期待通りに書くには以下の様にする必要がある。

# こちらは期待通りに動く
define F
   echo $(1)
   echo $(1)

endef

xs := foo bar
f2:
  $(foreach x,$(xs),$(call F,$(x)))

最後に1行改行が必要。こうすると上手くいく。

$ make f2 -n
echo foo
echo foo
echo bar
echo bar

foreachが使えるようになるとできること

ちなみにforeachが使えるようになるとできることが色々ある。wildcardとbasename,addsuffixあたりを組み合わせると便利。 (あんまりmakeのことを覚えても仕方がないというところもあるので$(shell ...) やバッククォートによるコマンドの実行を使った形の方が共有はしやすいかもしれない)

例えば、setup.pyを持つものをpython packageだとすると。これらの全部のテストを実行するには以下で良い。

define testT
   (cd $(dir $(1)) && python setup.py -q test)

endef

testall:
  $(foreach p,$(wildcard */setup.py),$(call testT,$(p)))

実際に実行してみる

$ make -n
(cd foo/ && python setup.py -q test)
(cd bar/ && python setup.py -q test)

$ make
(cd foo/ && python setup.py -q test)

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK
(cd bar/ && python setup.py -q test)

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK

注意点として、makeでのコマンドは1行毎に違うシェルで実行されるので(正確な表現ではないけれど)以下ではダメ。

define testT 
    cd $(dir $(1))
    python setup.py -q test  # cdする前の位置で実行される
    cd ..

endef