rcmdnk's blog

20231124_numpydoc_200_200

Pythonでdocstring部分の構造をチェクする numpydocを使う。

numpydoc

Validation — numpydoc

Pythonのdocstringの書き方としては Google形式Numpy形式Sphinx形式 などがあります。

numpydocはそののNumpy形式なdocstringをチェックするための公式ツール。

必要な項目が入っているかどうか、型の定義の仕方、コロンの位置など細かい部分まで チェックしてくれます。

コードとは直接関係のないところなので実行結果には基本的に影響が無いところですが せっかく書くなら決まったフォーマットになってて欲しいので ツールを使ってチェックしていい感じにします。

使い方

pipで

1
$ pip install numpydoc

として入れるとvalidate-docstringsというコマンドが使えるようになります。

1
$ validate-docstrings test_file.py

のような形でファイルをチェックできます。

PythonのプロジェクトをGitで管理している場合にはpre-commit用のhookも用意されているので、 .pre-commit-config.yamlに、

.pre-commit-config.yaml
1
2
3
4
- repo: https://github.com/numpy/numpydoc
  rev: 1.7.0
  hooks:
    - id: numpydoc-validation

のように書いておけばcommit時にpythonファイルに対してvalidate-docstringsがかけられます。

また、 pyproject-pre-commit も対応しているので、特にformatterやlinterを使ってる場合にはこれで一括管理すると便利です。

.pre-commit-config.yaml
1
2
3
4
5
6
7
8
repos:
- repo: https://github.com/rcmdnk/pyproject-pre-commit
  rev: v0.1.1
  hooks:
  - id: black
  - id: flake8
  - id: mypy
  - id: numpydoc-validation

としておいて、

1
$ pip install pyproject-pre-commit

などでpyproject-pre-commitをインストールしておけばblackなどとともに numpydocも環境にインストールされ、pre-commit以外でもコマンドを使えるようになって便利です。

設定ファイル

チェックする項目などの設定は設定ファイルから行います。

設定ファイルはPythonのプロジェクトで使われるpyproject.tomlsetup.cfgです。

tomlならtool.numpydoc_validationという項目で、cfgならtool:numpydoc_validationという項目を作って設定を書いていきます。

設定項目はcheck, exclude, override_SS05というものがありますが、全て必要な項目をリストで与える形になっています。

tomlなら[a, b, ...]といったリストで、cfgならコンマで区切ったa,b,...といった形で。

check

主に使うのはchecksという値で、どの項目をチェックするかをリストで与えます。 何も与えないとすべてチェックしますが、 結構色々ときつく縛られるので必要なものだけ選ぶような形になるかと思います。

Built-in Validation Checks

チョット特殊なのがallを最初に与えると、それ以下のものを除く、という形になります。

.pre-commit-config.yaml
1
checks = ["EX01", "SA01", "ES01"]

ならEX01, SA01, ES01のみをチェックする、ですが、

.pre-commit-config.yaml
1
checks = ["all", "EX01", "SA01", "ES01"]

なら、EX01, SA01, ES01を除く他のすべてをチェックする、になります。

調整する場合にはallでチェックしてみて、エラーが出たらそのままで良いものだな、と思ったら そのCheck IDをchecksallのあとに加えていく、といった形がやりやすいです。

exclude

リストで与え、それらの値を クラス名や関数名が含んでいたらチェックをスキップするための値。

正規表現が使えて例にある

.pre-commit-config.yaml
1
2
3
4
exclude = [  # don't report on objects that match any of these regex
    '\.undocumented_method$',
    '\.__repr__$',
]

だとundocumented_method__repr__という関数が除外されます。

最初に\.でピリオドから始まるような指定をしていますが、 クラスの関数であれば<クラス名>.<関数名>となりますし、 numpydocではファイルの直下に書かれている関数であればそのファイルをモジュールとみなして

test.py
1
2
def undocumented_method():
    pass

ならtest.undocumented_methodというitemとして認識するので これでundocumented_methodという名前に一致してその前後に別の文字列がついていれば スキップされません。

.pre-commit-config.yaml
1
2
exclude = [ 'no_doc' ]
]

みたいなシンプルなものにしておけばno_docを含むあらゆるものが無視されます。

ただ、numpydocの適用を回避するためだけに名前を変えるのは微妙なので、 無視したい関数がある場合は

1
2
def func():  # numpydoc ignore=GL08
    pass

といった感じでnumpydoc ignoreのコメントでスキップの適用が可能です。

GL08はドキュメントが無い時にエラーを出すチェックです。

もし、簡単なコメントだけ書いてあって中身のチェックはしなくて良いと言う場合には それらのエラーに関する項目を書く必要があります。

override_SS05

SS05はコメントの一番最初の行のまとめ文に関するチェックで、 Generatesの様な三人称単数現在形にせずに原型のGenerateで始めよ、というもの。

が、このチェックが単純なチェックで最後がsで終わるかどうか、だけを見ています。

https://github.com/numpy/numpydoc/blob/13b0f815763b3be13cb1cd34fd285f186fd5a142/numpydoc/validate.py#L676C1-L677C4

最後がsで終わる動詞が全て引っかかってしまう状態です。

したがってProcessAccessといった単語で始めた場合もSS05のエラーが出てしまいます。 これを避けるために始まっても良い単語としてoverride_SS05を定義しておきます。

例にある通り、

.pre-commit-config.yaml
1
2
3
4
5
override_SS05 = [
    '^Process ',
    '^Assess ',
    '^Access ',
]

辺りは最初から書いておいても良いかと思います。後は必要になったら追加で。

override_XXXX

上のoverride_SS05SS05に関するスキップ項目の追加ですが、 他のものでもIDを入れることでスキップ項目を追加できます。

SS02は最初のSummary部分が小文字で始まるとエラーとしますが、

.pre-commit-config.yaml
1
2
3
override_SS02 = [
    '^test '
]

としておけば、test ...と始まるSummaryに関しては許されます。

ただ他の部分に関してはあまりうまく使えるところは少ないかも。

特に直したところ

GL01: 先頭の部分がクォート行の次に来ないといけない

1
2
3
4
5
6
def func(x: int) -> None:
    """Summary of func.

    Parameters
    ----------
    ...

みたいなのはだめで、

1
2
3
4
5
6
7
def func(x: int) -> None:
    """
    Summary of func.

    Parameters
    ----------
    ...

の様にクォートの行とは分けないといけません。

これに関しては何かのlinterかドキュメントがクォート行と同じにしろ と言っていた気がして敢えてそうしていたのですが、 改めて Style guide を見てもそのようなことはないし、 flake8-docstrings とかを見ても特にそうったものはないし、 何かで思い込みをしていたのかも。

見た目としてはむしろ次の行から始まる形の方が見やすいので素直に従って直します。

PR10: 型を定義する際にその直前のコロンには前後にスペースを入れる

1
2
3
4
5
6
7
8
9
def func(x: int) -> None:
    """
    Summary of func.

    Parameters
    ----------
    x : int
        X parameter.
    ...

x: intではなくx : int

実際のコード中での定義では逆にコロンの前にスペースを空けないことが推奨されているので それとは違う形になり、ちょっと気持ち悪いところですが、 それに対する答えはここに詳細があります。

python - Why do definitions have a space before the colon in NumPy docstring sections? - Stack Overflow

reStructuredTextの Definition Lists に準拠していて、それがそうなっているから、と。

reStructuredTextではLink関連やblockを作る際にコロンを使うので、 そういったmarkupと区別するため、markupと解釈されないようにスペースを空けて 単独のコロンでコロンそのものとみなされるようにしているようです。

Numpy StyleはreStructuredTextに準拠しているので Sphinxを使ってHTML化することが可能で そういった際にきちんと書き方を揃えて置く必要があります。

が、実際そういったことをしないことも多いので、 この辺はもしかしたらみやすさ的に通常コードと同様のスペースなし、というのもありかもしれません。

個人的にはどれもいずれドキュメント化とかするかもしれないので 一応スペースを空けてしたがっておこうと思ってます。

RT02: Returnsセクションで返り値が1つだけの場合は型のみ書く

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def func(x: int, y: int) -> int:
    """
    Summary of func.

    Parameters
    ----------
    x : int
        X parameter.

    Returns
    -------
    int
        Sum of x and y.
    """
    return x + y

返り値が1つの場合にはその定義部分で名前とかをつけずに型だけ書きます。

1
2
3
4
5
6
    """
    Returns
    -------
    sum : int
        Sum of x and y.
    """

みたいにしてはだめ。

ただし、2つ以上返すような場合はそれぞれの名前をつけてもOK。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def func(x: int, y: int) -> tuple[int, int]:
    """
    Summary of func.

    Parameters
    ----------
    x : int
        X parameter.

    Returns
    -------
    sum: int
        Sum of x and y.
    diff: int
        Diff of x and y.
    """
    return x + y, x - y

1つでも名前をつけてる所がほとんどだったので直しました。

設定したもの

上のものとかを一通り直して、最終的に設定したものは以下のようなもの。

.pre-commit-config.yaml
1
2
3
4
5
6
7
8
9
10
[tool.numpydoc_validation]
checks = [
    "all",   # report on all checks, except the below
    "EX01",  # "No examples section found"
    "SA01",  # "See Also section not found"
    "ES01",  # "No extended summary found"
    "GL08",  # "The object does not have a docstring"
    "RT01",  # "No Returns section found"
    "PR01",  # "Parameters {missing_params} not documented"
]

EX01, SA01Exsample, See Alsoのセクションが無いとエラーになりますが、 これらに関してはすべてのオブジェクトに無くても良いと思うのでスキップ。

SA01は最初にセクションが始まる前、1行summaryの次に1行明けて詳細説明を書くものですが、 それが無いとエラーになります。 特に短い関数で1行で簡潔に説明できるならわざわざそれ以上書くのもあれなので これもスキップ。

GL08はすべてのクラスや関数に関してdocstringを要求します。 これはflake8-docstringsとかでも検知する ものですが、関数内でlambda的に作る関数みたいなものに対しても全てに対して要求するので ちょっとスキップ。 ほかにもクラス内の関数で書いてないものもあったりで全てに要求しなくても良いかな、とも思うので。 また、モジュールレベル(ファイルレベル)にも無いと怒られるのでちょっときついです。

flake8-docstringsだとクラスのもの、関数のもの、といった感じで個別にスキップできるので 基本的にクラスのものが無いときだけエラーを出すようにしています。

RT01は引数に関しては頑張って書いていても返り値に関して書いてない部分が多いため。 返り値は関数名や最初のSummaryの説明で自明なものが多いのでそういったものにまで わざわざ書かなくても良いかなと思って書いてない部分が結構あり。

今後新たに始めるときにはチェックに入れ手も良いかも、程度で。

PR01はクラス継承で inherit-docstring を使ってdocstringを継承したりすると、 書いてない、と怒られてしまうので外してます。

これはツール側でdocstringを解釈した上でチェックしてもらうよう何かラッパーツールみたいのをつくるとか 出来たら嬉しいかも。

Sponsored Links
Sponsored Links

« Pythonでキーワード引数の展開(**kw)に対するアノテーション EvernoteからObsidianへの移行 »

}