テキトウなyamlの値からswagger spec経由でgoのstruct定義のコードを生成してみる

テキトウなyamlの値からswagger spec経由でgoのstruct定義のコードを生成してみる。

使うのはdictknifegoaway

やること

  1. 素となるデータの例を書く(yaml)
  2. データからswagger specを生成
  3. swagger specからコードを生成

以下の様な形で変換が行われる。

config.yaml - json2swagger -> config-spec.yaml - swagger2go -> config/config.go

primitiveな状態

こういう感じ。

# need https://github.com/podhmo/{goaway,dictknife}

default:
  mkdir -p dst config
  swaggerknife json2swagger --name=config config.yaml --dst dst/config-spec.yaml
  swagger2go dst/config-spec.yaml --position=config --file=config.go --ref="#/definitions/config"
  goimports -w config/*.go

素となるデータは以下の様な形式

config.yaml

appconf:
  endpoint: http://foo.bar.jp/api
  key: hmm
  secret: hai

swagger specを生成

swagger specを生成する

$ swaggerknife json2swagger --name=config config.yaml --dst dst/config-spec.yaml

生成されたのは以下のようなもの。

config-spec.yaml

definitions:
  appconf:
    type: object
    properties:
      endpoint:
        type: string
        format: url
        example: http://foo.bar.jp/api
        x-go-type: net/url.URL
      key:
        type: string
        example: hmm
      secret:
        type: string
        example: hai
    required:
    - endpoint
    - key
    - secret
  config:
    type: object
    properties:
      appconf:
        $ref: '#/definitions/appconf'
    required:
    - appconf

goのコードの生成

以下の様な感じで実行(positionsなどを付けているのはGOPATH以下に生成しないようにするため)

$ swagger2go dst/config-spec.yaml --position=config --file=config.go --ref="#/definitions/config"

以下の様なコードが生成される。validationなどは含まれていない。

package main

// Config :
type Config struct {
    Appconf Appconf `json:"appconf"`
}

// Appconf :
type Appconf struct {
    Endpoint string `json:"endpoint"`
    Key      string `json:"key"`
    Secret   string `json:"secret"`
}

別の型(url.URL)に対応したい

別の型(url.URL)に対応したい。やろうと思えばできる。

  • json2swagger – swagger specの生成時に(format,x-go-typeを付加する)
  • swagger2go – goのコード生成時にx-go-typeを見て利用する型を変更する

swagger specの生成時に情報付加

json2swaggerの --emitter--detector というオプションで利用するオブジェクトを差し替えられる。なので以下の様なコードを用意してやる。

resolve.py

from dictknife.swaggerknife.json2swagger import Detector as _Detector
from dictknife.swaggerknife.json2swagger import Emitter as _Emitter


class Detector(_Detector):
    def resolve_type(self, value):
        if isinstance(value, str) and value.startswith(("http://", "https://")):
            return "string", "url"
        return super().resolve_type(value)


class Emitter(_Emitter):
    def make_primitive_schema(self, info):
        d = super().make_primitive_schema(info)
        if d.get("format") == "url":
            d["x-go-type"] = "net/url.URL"
        return d

そして実行時に --detector="./resolve.py:Detector" --emitter="./resolve.py:Emitter" を追加してあげる。 すると実行結果などは以下の様に変わる。

diff --git a/daily/20170617/Makefile b/daily/20170617/Makefile
index 652840b..ccd3240 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,8 @@
 # need https://github.com/podhmo/{goaway,dictknife}
+EXTRA = --detector="./resolve.py:Detector" --emitter="./resolve.py:Emitter"
 
 default:
    mkdir -p dst config
-  swaggerknife json2swagger --name=config config.yaml > dst/config-spec.yaml
+   swaggerknife json2swagger ${EXTRA} --name=config config.yaml --dst dst/config-spec.yaml
    swagger2go dst/config-spec.yaml --position=config --file=config.go --ref="#/definitions/config"
    goimports -w config/*.go
diff --git a/daily/20170617/dst/config-spec.yaml b/daily/20170617/dst/config-spec.yaml
index 112e1f3..e29fb54 100644
--- a/daily/20170617/dst/config-spec.yaml
+++ b/daily/20170617/dst/config-spec.yaml
@@ -4,7 +4,9 @@ definitions:
     properties:
       endpoint:
         type: string
+        format: url
         example: http://foo.bar.jp/api
+        x-go-type: net/url.URL
       key:
         type: string
         example: hmm
diff --git a/config/config.go b/config/config.go
index 9a00a57..f51bfe8 100644
--- a/config/config.go
+++ b/config/config.go
@@ -1,5 +1,9 @@
 package main
 
+import (
+   "net/url"
+)
+
 // Config :
 type Config struct {
    Appconf Appconf `json:"appconf"`
@@ -7,7 +11,7 @@ type Config struct {
 
 // Appconf :
 type Appconf struct {
-  Endpoint string `json:"endpoint"`
-  Key      string `json:"key"`
-  Secret   string `json:"secret"`
+   Endpoint url.URL `json:"endpoint"`
+   Key      string  `json:"key"`
+   Secret   string  `json:"secret"`
 }

swagger2goは x-go-type というkeyの情報が存在したらそちらを使うようになっている。なのでnet/urlのURLを使うようなコードが生成される。 おしまい。

goのコードを生成すること自体のは記事は昔書いていた

docutilsを直接利用してRestからmarkdown(のsubset)を生成するツールを作ってみる

docutils

ReSTのためのライブラリ

ReST?

markdownのようなもの

first step

docutilsではpublisherと呼ばれるものがもっともトップレベルのapplication。 内部でドキュメントをparseしてnode treeを作成した後に各writerのtranslateで出力される。

reader+parser -> node tree -> {Writer0,Writer1,Writer2}

本当はもう少し細かく内部で色々分かれているのだけれど。とりあえず以下が分かれば良い。

  • parserがparseしたらnode treeが作られる
  • writerはnode treeを走査してoutputを作る

null writer

最もシンプルな入力と処理は以下の様なもの

00app.py

from docutils.writers.null import Writer
from docutils.core import publish_cmdline

publish_cmdline(writer=Writer())

例えば以下のような入力を渡すとする。

00hello.rst

hello title
========================================

hello section
----------------------------------------

hello text

null.Writerは何もしない。

$ python 00app.py --traceback src/00hello.rst

とは言え内部で何が行われているかわからないとどうしようもない。--debug を付けてみる

$ python 00app.py --debug --traceback src/00hello.rst

StateMachine.run: input_lines (line_offset=-1):
| hello title
| ========================================
| 
| hello section
| ----------------------------------------
| 
| hello text

StateMachine.run: bof transition

... snip

src/00hello.rst:: (DEBUG/0) docutils.nodes.Node.walk calling dispatch_visit for document
src/00hello.rst:: (DEBUG/0) docutils.nodes.NodeVisitor.dispatch_visit calling _nop for document
src/00hello.rst:: (DEBUG/0) docutils.nodes.Node.walk calling dispatch_visit for title
src/00hello.rst:: (DEBUG/0) docutils.nodes.NodeVisitor.dispatch_visit calling _nop for title
src/00hello.rst:: (DEBUG/0) docutils.nodes.Node.walk calling dispatch_visit for Text
src/00hello.rst:: (DEBUG/0) docutils.nodes.NodeVisitor.dispatch_visit calling _nop for Text
src/00hello.rst:: (DEBUG/0) docutils.nodes.Node.walk calling dispatch_visit for subtitle
src/00hello.rst:: (DEBUG/0) docutils.nodes.NodeVisitor.dispatch_visit calling _nop for subtitle
src/00hello.rst:: (DEBUG/0) docutils.nodes.Node.walk calling dispatch_visit for Text
src/00hello.rst:: (DEBUG/0) docutils.nodes.NodeVisitor.dispatch_visit calling _nop for Text
src/00hello.rst:: (DEBUG/0) docutils.nodes.Node.walk calling dispatch_visit for paragraph
src/00hello.rst:: (DEBUG/0) docutils.nodes.NodeVisitor.dispatch_visit calling _nop for paragraph
src/00hello.rst:: (DEBUG/0) docutils.nodes.Node.walk calling dispatch_visit for Text
src/00hello.rst:: (DEBUG/0) docutils.nodes.NodeVisitor.dispatch_visit calling _nop for Text

何か生成されたnodeを走査してそうなことが分かる。

writerを自作してみる

writerを自作してみる。通常writerはおおよそ以下の様な構造になっている。

from docutils.writers import Writer

class Writer(Writer):
    def __init__(self):
        super().__init__()
        self.translator_class = Translator

    def translate(self):
        self.visitor = visitor = self.translator_class(self.document)
        self.document.walkabout(visitor)
        self.output = "*hmm*"
  1. Translatorを作成する
  2. translator(visitor)が何か操作を行う。
  3. 出力結果はself.outputに含まれる

例えば以下の様なTranslatorを用意してみる。

from docutils import nodes

class Translator(nodes.NodeVisitor):
    def __init__(self, document):
        super().__init__(document)
        self._depth = 0

    def dispatch_visit(self, node):
        self._depth += 1
        return super().dispatch_visit(node)

    def dispatch_departure(self, node):
        r = super().dispatch_departure(node)
        self._depth -= 1
        return r

    def unknown_visit(self, node):
        i = self._depth
        nodename = node.__class__.__name__
        logger.debug("%svisit %s[%d] :%s", "  " * i, nodename, i, node)

    def unknown_departure(self, node):
        i = self._depth
        nodename = node.__class__.__name__
        logger.debug("%sdeparture %s[%d] :%s", "  " * i, nodename, i, node)

unknown_visit()unknown_departure() Visitorが渡されたNodeに対応するメソッドを持っていない時に呼ばれるメソッド。ここでログ出力を行うことにしたのでvisitorが走査される様がどのようなものか確認できる。

以下の様な感じ。

DEBUG:__main__:  visit document[1] :<document ids="hello-title" names="hello\ title" source="src/00hello.rst" title="hello title"><title>hello title</title><subtitle ids="hello-section" names="hello\ section">hello section</subtitle><paragraph>hello text</paragraph></document>
DEBUG:__main__:    visit title[2] :<title>hello title</title>
DEBUG:__main__:      visit Text[3] :hello title
DEBUG:__main__:      departure Text[3] :hello title
DEBUG:__main__:    departure title[2] :<title>hello title</title>
DEBUG:__main__:    visit subtitle[2] :<subtitle ids="hello-section" names="hello\ section">hello section</subtitle>
DEBUG:__main__:      visit Text[3] :hello section
DEBUG:__main__:      departure Text[3] :hello section
DEBUG:__main__:    departure subtitle[2] :<subtitle ids="hello-section" names="hello\ section">hello section</subtitle>
DEBUG:__main__:    visit paragraph[2] :<paragraph>hello text</paragraph>
DEBUG:__main__:      visit Text[3] :hello text
DEBUG:__main__:      departure Text[3] :hello text
DEBUG:__main__:    departure paragraph[2] :<paragraph>hello text</paragraph>
DEBUG:__main__:  departure document[1] :<document ids="hello-title" names="hello\ title" source="src/00hello.rst" title="hello title"><title>hello title</title><subtitle ids="hello-section" names="hello\ section">hello section</subtitle><paragraph>hello text</paragraph></document>
*hmm*

visit_title(), visit_subtitle()などが呼ばれ後にvisitTextが呼ばれるみたいな構造になっていることが分かる。 そのあと writer.output に代入された "*hmm*" の値が表示されている(こちらは標準出力) そんなわけで後はvisitorをどのように定義すれば良いかということだけがわかればmarkdownへの変更は難しく無さそう。

自作のmarkdown writer

とりあえず以下の文章を変換できるようにしてみる

src/02sample.rst

タイトル
========================================

何か文章

サブタイトル
----------------------------------------

本文本文本文本文本文本文本文本文本文本文本文本文本文本文(ここでお経を唱えると功徳が積める)

サブタイトル2
----------------------------------------

箇条書き

- item0
- item1
- item2

こういう感じに

$ python 02app.py --traceback src/02sample.rst

変換後は以下のようなmarkdown

# タイトル

何か文章

## サブタイトル

本文本文本文本文本文本文本文本文本文本文本文本文本文本文(ここでお経を唱えると功徳が積める)

## サブタイトル2

箇条書き
- item0
- item1
- item2

code

02app.py

from io import StringIO
from docutils.writers import Writer
from docutils import nodes


class Writer(Writer):
    def __init__(self):
        super().__init__()
        self.translator_class = MarkdownTranslator

    def translate(self):
        self.visitor = visitor = self.translator_class(self.document)
        self.document.walkabout(visitor)
        self.output = visitor.io.getvalue().rstrip("\n")


class MarkdownTranslator(nodes.NodeVisitor):
    def __init__(self, document):
        super().__init__(document)
        self.io = StringIO()
        self.section_level = 1

    def visit_section(self, node):
        self.section_level += 1

    def depart_section(self, node):
        self.section_level -= 1

    def visit_title(self, node):
        if self.section_level > 1:
            self.io.write("\n")
        self.io.write("#" * self.section_level)
        self.io.write(" ")
        self.io.write(node.astext())
        self.io.write("\n")
        raise nodes.SkipNode

    def visit_paragraph(self, node):
        self.io.write('\n')

    def depart_paragraph(self, node):
        self.io.write('\n')

    def visit_Text(self, node):
        self.io.write(node.astext())

    def visit_list_item(self, node):
        self.io.write("- ")
        for c in node.children:
            self.io.write(c.astext())
        self.io.write('\n')
        raise nodes.SkipNode

    def unknown_visit(self, node):
        pass

    def unknown_departure(self, node):
        pass


if __name__ == "__main__":
    from docutils.core import publish_cmdline
    publish_cmdline(writer=Writer())

本当は

本当はdirectivesとかroleの話がしたかった(こちらが本命)。

jsonからswagger specを生成するコマンドに変更を加えた

github.com

昨日作ったコマンドに変更を加えた。変更点は以下

  • 複数のファイルを渡せるようにした
  • minimapを表示するようにしてみた
  • validationのための --emit=jsonschema を止めた

複数のファイルを渡せるようにした

複数のファイルを渡せるようにした。requiredなどの扱いが顕著に変わる。

例えば、以下の様な2つのファイルを利用して生成したschemaはnicknameがrequiredではない。

foo.json

{
  "name": "foo",
  "age": 20,
  "nickname": "F"
}

bar.json

{
  "name": "bar",
  "age": 22
}
$ swaggerknife json2swagger foo.json bar.json --name person --dst person-spec.yaml

person-spec.yaml

definitions:
  person:
    type: object
    properties:
      name:
        type: string
        example: bar
      age:
        type: integer
        example: 22
      nickname:
        type: string
        example: F
    required:
    - name
    - age

minimapを表示するようにしてみた

--with-minimap をつけるとminimapを表示。暫定的な機能。

{
  "server": {
    "host": "localhost",
    "port": "3306",
    "slave": [
      {"weight": 1, "ip": "10.0.0.1"},
      {"weight": 5, "ip": "10.0.0.2"},
      {"weight": 3, "ip": "10.0.0.3"},
      {"weight": 2, "ip": "10.0.0.4"}
    ]
  },
  "db": {
    "user": "root",
    "pass": "pass"
  }
}

標準出力に出てしまうので注意。そしてJSONにはコメントが無いので注意(invalidなJSONになる)。

$ swaggerknife json2swagger --with-minimap --name config config.json

何となくどのような形かは分かるはず。

# minimap ###
# *config
#   * server
#     * slave[]
#   * db
definitions:
  slaveItem:
    type: object
    properties:
      weight:
        type: integer
        example: 1
      ip:
        type: string
        example: 10.0.0.1
    required:
    - weight
    - ip
  slave:
    type: array
    items:
      $ref: '#/definitions/slaveItem'
  server:
    type: object
    properties:
      host:
        type: string
        example: localhost
      port:
        type: string
        example: '3306'
      slave:
        $ref: '#/definitions/slave'
    required:
    - host
    - port
    - slave
  db:
    type: object
    properties:
      user:
        type: string
        example: root
      pass:
        type: string
        example: pass
    required:
    - user
    - pass
  config:
    type: object
    properties:
      server:
        $ref: '#/definitions/server'
      db:
        $ref: '#/definitions/db'
    required:
    - server
    - db

validationのための --emit=jsonschema を止めた

これは別途tojsonschemaというサブコマンドを作った(経緯は昨日の記事参照)。

以前の以下の様な呼び出しが

$ swaggerknife json2swagger --emit=jsonschema config.json --dst schema.json

以下の様に変わる。

$ swaggerknife json2swagger config.json | swaggerknife tojsonschema --dst schema.json

あと

あと、用例もそれなりに書いた。

http://dictknife.readthedocs.io/en/latest/commands3.html#json2swagger

jsonからswagger specを生成するコマンドをdictknifeに取り込んだ

以前に、jsonからswagger specを生成するツールを作っていたのだけれど。とくにリポジトリを分けておく必要性もなかったのでdictknifeに取り込んだ。

ついでに、swagger独自っぽい雰囲気のしたコマンドはswaggerknifeとして別のコマンドにすることにした。

以前は2つだったコマンドが3つになった。

  • dictknife
  • jsonknife
  • swaggerknife

jsonknife flatten は実質definitions以下だけを対象にしているなどからswaggerknifeに移動した。

$ swaggerknife -h
Usage: swaggerknife [OPTIONS] COMMAND [ARGS]...

Options:
  --log [WARNING|INFO|NOTSET|CRITICAL|WARN|ERROR|DEBUG]
                                  logging level
  -h, --help                      Show this message and exit.

Commands:
  flatten       flatten jsonschema sub definitions
  json2swagger  json2swagger

json2swagger

実際のところyaml2swaggerにもtoml2swaggerにもなるのだけれど。名前はjson2swaggerのまま。後で良い名前が思いついたらそれに変える。

例えば、以下のようなjsonから以下のようなswagger specが生成できる(例は http://qiita.com/futoase/items/fd697a708fcbcee104de から)。

config.json

{
  "server": {
    "host": "localhost",
    "port": "3306",
    "slave": [
      {"weight": 1, "ip": "10.0.0.1"},
      {"weight": 5, "ip": "10.0.0.2"},
      {"weight": 3, "ip": "10.0.0.3"},
      {"weight": 2, "ip": "10.0.0.4"}
    ]
  },
  "db": {
    "user": "root",
    "pass": "pass"
  }
}

以下のようなコマンドを経由してswagger specを生成

$ swaggerknife json2swagger --name config config.json > config.spec.yaml

config.spec.yaml

definitions:
  slaveItem:
    type: object
    properties:
      weight:
        type: integer
        example: 1
      ip:
        type: string
        example: 10.0.0.1
    required:
    - weight
    - ip
  slave:
    type: array
    items:
      $ref: '#/definitions/slaveItem'
  server:
    type: object
    properties:
      host:
        type: string
        example: localhost
      port:
        type: string
        example: '3306'
      slave:
        $ref: '#/definitions/slave'
    required:
    - host
    - port
    - slave
  db:
    type: object
    properties:
      user:
        type: string
        example: root
      pass:
        type: string
        example: pass
    required:
    - user
    - pass
  config:
    type: object
    properties:
      server:
        $ref: '#/definitions/server'
      db:
        $ref: '#/definitions/db'
    required:
    - server
    - db

requiredなどはきつめに設定する様になっている。

validationしたい

validationしたいのだけれど。swaggerのvalidationを入れるのもめんどくさかったのでjsonschemaでvalidationできるようにした。 jsonschemaとしてはinvalidではあるけれどjsonschemaというpythonのライブラリでは検証できる程度には整えた出力を返すようにした。

# jsonschema コマンドが実行できるようになる
$ pip install jsonschema
# --dstでjsonやjsの拡張子のファイルを指定するとJSONのフォーマットで返す
$ swaggerknife json2swagger --emit=jsonschema config.json --dst schema.json
# とりあえずjsonschemaで検証
$ jsonschema -i config.json schema.json

yaml形式でschemaを渡すことなどはできなそうなので、yamlを受け取れるvalidation用のコマンドがあっても良いかもしれない。 ちなみにschema.json以下のような感じ。

{
  "type": "object",
  "properties": {
    "server": {
      "$ref": "#/definitions/server"
    },
    "db": {
      "$ref": "#/definitions/db"
    }
  },
  "required": [
    "server",
    "db"
  ],
  "definitions": {
    "slaveItem": {
      "type": "object",
      "properties": {
        "weight": {
          "type": "integer",
          "example": 1
        },
        "ip": {
          "type": "string",
          "example": "10.0.0.1"
        }
      },
      "required": [
        "weight",
        "ip"
      ]
    },
    "slave": {
      "type": "array",
      "items": {
        "$ref": "#/definitions/slaveItem"
      }
    },
    "server": {
      "type": "object",
      "properties": {
        "host": {
          "type": "string",
          "example": "localhost"
        },
        "port": {
          "type": "string",
          "example": "3306"
        },
        "slave": {
          "$ref": "#/definitions/slave"
        }
      },
      "required": [
        "host",
        "port",
        "slave"
      ]
    },
    "db": {
      "type": "object",
      "properties": {
        "user": {
          "type": "string",
          "example": "root"
        },
        "pass": {
          "type": "string",
          "example": "pass"
        }
      },
      "required": [
        "user",
        "pass"
      ]
    }
  }
}

pythonでwarningsパッケージを使った警告の表示のメモ

警告を表示したいことがある。例えば、次のversionで廃止予定の関数を使った時の警告など。 こういうときには warningsパッケージを使う。

first step

00warn.py

import warnings


def foo():
    warnings.warn("hmm")
    return "hai"


if __name__ == "__main__":
    print(foo())
    print(foo())

warnings.warn で警告メッセージを表示する。defaultではUserWarningの警告レベルで警告メッセージが出る。同じメッセージは2度表示されない。

$ python 00warn.py
00warn.py:5: UserWarning: hmm
  warnings.warn("hmm")
hai

開発者向けのDeprecationWarning

開発者向けにDeprecationWarningの警告レベルで警告メッセージを表示したい場合がある。これは通常のpythonの実行では表示されない。

01warn.py

import warnings


def foo():
    warnings.warn("hmm")
    warnings.warn("hmm. foo is deprecated", DeprecationWarning)
    return "hai"


if __name__ == "__main__":
    print(foo())
    print(foo())

通常のpythonの実行では表示されない。-W default などを付けて実行すると表示されるようになる

$ python 02warn.py
02warn.py:5: UserWarning: hmm
  warnings.warn("hmm")
hai
hai
$ python -W default 02warn.py
02warn.py:5: UserWarning: hmm
  warnings.warn("hmm")
02warn.py:6: DeprecationWarning: hmm. foo is deprecated
  warnings.warn("hmm. foo is deprecated", DeprecationWarning)
hai
hai

すごくまじめにAPIの機能を廃止する時の作業

APIの機能を廃止する時の作業はまじめにやるなら以下の様な形になる。

  1. PendingDeprecationWarning (将来消される予定のAPI)
  2. DeprecationWarning (既に廃止されているAPI。ただし移行期間用にまだコードは消されていない)
  3. APIを消す

DeprecationWarningは眼に見えないという点がちょっとつらいかもしれない?

一時的に警告をfilterしたい場合

capture_warningsとsimple_filterを組み合せると良い。引数に渡す値は以下くらいの雑な認識良さそう。

  • always 全部表示
  • ignore 全部無視

03warn.py

import warnings


def foo():
    warnings.warn("hmm")
    warnings.warn("hmm. foo is deprecated", DeprecationWarning)
    warnings.warn("hmm. foo is deprecated on next version", PendingDeprecationWarning)
    return "hai"


if __name__ == "__main__":
    print("1.")
    print("----------------------------------------")
    with warnings.catch_warnings():
        warnings.simplefilter("ignore")
        print(foo())

    print("2.")
    print("----------------------------------------")
    with warnings.catch_warnings():
        warnings.simplefilter("always")
        print(foo())

1回目のfoo呼び出しでは全ての警告メッセージが無視される。 2回目のfoo呼び出しでは全ての警告メッセージを表示される。

実行結果。

$ python 03warn.py
1.
----------------------------------------
hai
2.
----------------------------------------
03warn.py:5: UserWarning: hmm
  warnings.warn("hmm")
03warn.py:6: DeprecationWarning: hmm. foo is deprecated
  warnings.warn("hmm. foo is deprecated", DeprecationWarning)
03warn.py:7: PendingDeprecationWarning: hmm. foo is deprecated on next version
  warnings.warn("hmm. foo is deprecated on next version", PendingDeprecationWarning)
hai

警告メッセージを表示するdecoratorを作る場合

警告メッセージを表示するdecoratorを作る場合はwarnings.warnのstacklevelを調整する。

05withdecorator.py

import warnings


def useless(msg, cls=UserWarning):
    def _useless(fn):
        def decorated(*args, **kwargs):
            warnings.warn(msg, stacklevel=2)
            return fn(*args, **kwargs)

        return decorated

    return _useless


@useless("foo is useless")
def foo():
    return "foo"


if __name__ == "__main__":
    print(foo())

呼び出した位置をしっかり警告してくれる。

$ python 05withdecorator.py
05withdecorator.py:21: UserWarning: foo is useless
  print(foo())
foo

警告のテストの書き方

警告のテストについては、ドキュメントにも例が載っている

pyramidというweb application frameworkのテストを例に警告の表示のテストの書き方のメモ。

tests/test_config/test_util.py

class TestDeprecatedPredicates(unittest.TestCase):
    def test_it(self):
        import warnings
        with warnings.catch_warnings(record=True) as w:
            warnings.filterwarnings('always')
            from pyramid.config.predicates import XHRPredicate
            self.assertEqual(len(w), 1)

capture_warningsで記録しておく。警告が記録されたら増えるのでlenで長さを確認する。

see also

この記事は分かりやすかった

sphinxで雑にコード例と実行結果を表示したい場合

sphinxで雑にコード例と実行結果を表示したい場合がある。コード例はliteralincludeを使うと便利。一方実行結果をどうするのかというのが悩みどころ。

現状はMakefileで出力結果を生成してliteralincludeで埋め込んでいる。

こういう感じ

.. literalinclude:: ../../examples/code.py

result

.. literalinclude:: ../../examples/code.output

この時code.pyからcode.outputを生成しておく必要がある。

実行結果を出力したファイルを生成

以下のようなMakefileを作っている

examples/Makefile

%.output : %.py
  python $< > $@

default: $(shell ls *.py | sed 's/\.py$$/.output/g')

clean:
  rm -f *.output

*.output を生成するルールを定義している。 あとは、既存の *.py から生成するべき *.output ファイルの名前を列挙している(これがdefaultタスクの依存になっている)。

実際に利用する場合

実際に利用する場合には、トップレベルのMakefileで実行結果を出力するタスクを呼んだ後にsphinxを呼ぶタスクを呼んでいる。

ディレクトリ階層

ディレクトリ階層は以下の様な感じ。

.
├── Makefile
├── docs
│   └── Makefile  # ここにsphinx用のファイルが色々
└── examples
    ├── Makefile
    ├── code.output
    └── code.py

トップレベルのMakefileから以下の順でmakeを呼ぶ

  1. コードの実行結果を更新する用のMakefile(examples/Makefile)
  2. docs用のMakefile(examples/Makefile)

トップレベルのMakefile

トップレベルのMakefileは以下のようなもの

docs: run
  $(MAKE) html -C docs
.PHONY: docs

run:
  $(MAKE) -C examples
.PHONY: run

ここなどで使っている。

http://dictknife.readthedocs.io/en/latest/library.html

sphinxのliteralincludeが絶対パスを表示するのが嫌だった

sphinxのliteralincludeが絶対パスを表示するのが嫌だった。 以下の様なmonkey patchをあてるextensionを作ってあげると無理矢理変更できる(わるい)

def setup(app):
    # monkey patch
    import os.path
    from difflib import unified_diff
    from sphinx.directives.code import LiteralIncludeReader

    def show_diff(self, location=None):
        new_lines = self.read_file(self.filename)
        old_filename = self.options.get('diff')
        old_lines = self.read_file(old_filename)
        diff = unified_diff(old_lines, new_lines, os.path.basename(old_filename), os.path.basename(self.filename))
        return list(diff)

    LiteralIncludeReader.show_diff = show_diff

    return {"version": "0.0"}

使いたかった場所

本当はdictknifeのドキュメントで使いたかったのだけれど。readthedocsが使っているsphinxのversionが古かったようでLiteralIncludeReaderが存在しなかったっぽい。古いsphinx用にコードを書き換えるのが面倒なので今は使っていない。

使うなら readthedocsの 高度なオプションInstall Project をonにしてあげて、conf.pyに以下のような変更をすれば有効になる。

# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.

# extensions = ['sphinx.ext.viewcode']
extensions = ['sphinx.ext.viewcode', 'dictknife._sphinx']

dictknifeというrepositoryを使っているので dictknife._sphinx だけれど。そのあたりはお好みで。