Skip to content

mkdocstrings#

mkdocs-material と mkdocstrings を組み合わせて Python の docstring を HTML で確認できるようにする。

参考#

install#

pip install mkdocstrings

使い方#

以下ディレクトリ・ファイルを用意する - docs - リファレンスファイル.md - pythonモジュールを格納するディレクトリ - 対象ファイル.py - mkdocs.yml

.md ファイル#

  • docs 配下に用意した.mdファイル内で docstring を抽出したいpython ファイルを指定する
  • ファイル名は任意だが 「モジュール名-reference.md」とするのが分かりやすい
  • ::: のあとに半角スペース、続けて、root ディレクトリからpythonファイルの場所を指定する。このとき拡張子は不要。
/docs/<任意のファイル名>.md
# Reference

::: pythonモジュールを格納するディレクトリ.対象ファイル

mkdocs.yml#

mkdocs の設定が記述されたファイルを用意し、プラグインでmkdocstrings を指定する。

mkdocs.yml
site_name: My Docs

theme:
  name: "material"

plugins:
  - search
  - mkdocstrings

自動コード参照ページ#

Recipes

以下のディレクトリを例に、あるオブジェクトのドキュメントを作成する際、project.loremのように手動で指定することになるが、 プロジェクトが大きくなるにつれて手動で.md ファイルへの記述は限界がある。

📁 repo
└─╴📁 src
    └─╴📁 project
        ├─╴📄 lorem
        ├─╴📄 ipsum
        ├─╴📄 dolor
        ├─╴📄 sit
        └─╴📄 amet

mkdocs-gen-filesプラグインではこの問題を解決する

手順は以下の通り

mkdocs.yml
plugins:
- search  # 
- gen-files:
    scripts:
    - docs/gen_ref_pages.py  # 
- mkdocstrings

mkdocs-gen-files は、ビルド時に Python スクリプトを実行できます。 上記例では実行する Python スクリプトは docs フォルダーにあり、gen_ref_pages.pyという名前で保存されています。

docs/gen_ref_pages.py
"""Generate the code reference pages."""

from pathlib import Path

import mkdocs_gen_files

for path in sorted(Path("src").rglob("*.py")):  # src フォルダ内のpyファイルを再帰的に探索
    module_path = path.relative_to("src").with_suffix("")  # モジュール パスは project/lorem のようになります。 mkdocstrings autodoc 識別子を構築するために使用されます。
    doc_path = path.relative_to("src").with_suffix(".md")  # これは Markdown ページへの相対パスです。
    full_doc_path = Path("reference", doc_path)  # これは Markdown ページへの絶対パスです。ここでは、すべての参照ページを参照フォルダーに入れます。

    parts = list(module_path.parts)

    if parts[-1] == "__init__":  # 
        parts = parts[:-1]
    elif parts[-1] == "__main__":
        continue

    with mkdocs_gen_files.open(full_doc_path, "w") as fd:  # (?)この部分は、Python モジュールにのみ関連します。 __main__ モジュールをスキップし、モジュール部分から __init__ を削除します。これは、インポート中に暗黙的に行われるためです。
        identifier = ".".join(parts)  # autodoc 識別子を作成します。ここでは Python モジュールをドキュメント化するため、識別子は project.lorem のようにドット区切りのパスになります。
        print("::: " + identifier, file=fd)  # 

    mkdocs_gen_files.set_edit_path(full_doc_path, path)  # ページに edit_uri を設定することもできます。

Note

python スクリプトファイルから見た際の、対象ファイルへの相対パスを指定すること。
mkdocs_gen_files.set_edit_path(full_doc_path, Path("<相対パスを指定>") / path)

gen_ref_pages.pyを用いると、ドキュメントを作成するたびにフォルダーが自動的に作成されます。

このフォルダーには、各ソース モジュールの Markdown ページが含まれており、これらの各ページには::: project.module (モジュールはlorem、ipsumなど)`` のような 1行が記述されています。

ただし、生成された.mdページを実際に mkdocs.yml`ファイル内の MkDocs ナビゲーションに追加する必要があります。

nav:
# rest of the navigation...
- Code Reference:
  - project:
    - lorem: reference/project/lorem.md
    - ipsum: reference/project/ipsum.md
    - dolor: reference/project/dolor.md
    - sit: reference/project/sit.md
    - amet: reference/project/amet.md
# rest of the navigation...

もちろん mdkocs.ymlファイルへの ナビゲーションタブの記述も自動化できます。次の手順で紹介します。

Bug

以下mkdocs-literate-navにてmkdocs.ymlへのナビゲーションインデックスの紐付けが確認できませんでした。 python スクリプト + 手動でmkdocs.yml ファイルへの追記をおすすめします。(半自動案)

リテラルナビゲーションファイルを生成する#

mdkocs.ymlファイルへの ナビゲーションタブの記述も自動化するためには、追加のプラグインmkdocs-literate-navが必要になります。
このプラグインを使用すると、ナビゲーション全体またはその一部を Markdown ページにプレーンな Markdown リストとして指定できます。

例にならってYAMLファイルへplugin を追加します。

mdkcos.yml
plugins: 
- search
- gen-files:
    scripts:
    - docs/gen_ref_pages.py
- literate-nav:
    nav_file: SUMMARY.md
- mkdocstrings

次に、Python スクリプトを修正します。

docs/gen_ref_pages.py
"""Generate the code reference pages and navigation."""
from pathlib import Path
import mkdocs_gen_files

nav = mkdocs_gen_files.Nav()

for path in sorted(Path("src").rglob("*.py")):
    module_path = path.relative_to("src").with_suffix("")
    doc_path = path.relative_to("src").with_suffix(".md")
    full_doc_path = Path("reference", doc_path)

    parts = tuple(module_path.parts)

    if parts[-1] == "__init__":
        parts = parts[:-1]
    elif parts[-1] == "__main__":
        continue

    nav[parts] = doc_path.as_posix()  # 

    with mkdocs_gen_files.open(full_doc_path, "w") as fd:
        ident = ".".join(parts)
        fd.write(f"::: {ident}")

    mkdocs_gen_files.set_edit_path(full_doc_path, path)

with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file:  # 
    nav_file.writelines(nav.build_literate_nav())  # 

先ほどmkdocs.yml内に手動で追加したナビゲーションを削除して、以下のように1行に置き換えます。

mdkocs.yml
nav:
# rest of the navigation...
# defer to gen-files + literate-nav
- Code Reference: reference/  # 
# rest of the navigation...

ページをセクション自体にバインドする#

最後に mkdocs-section-indexpluginを追加します。

Demo はCrystal Bookが参考になります。

```yml title="mkdocs.yml" hi_lines="8" plugins: - search - gen-files: scripts: - docs/gen_ref_pages.py - literate-nav: nav_file: SUMMARY.md - section-index - mkdocstrings

現在のスクリプトでは、フォルダーに対応するセクションは、それらをクリックすると展開または折りたたまれ、
その__init__下のモジュール (または関連する場合は他の言語の同等のモジュール) が表示されます。
私たちは公開 API を文書化しており、ユーザーが明示的に__init__モジュールをインポートしないことを考えると、
それらを取り除き、代わりにセクション自体の内部に文書を表示できるとよいでしょう。


以下のように`gen_ref_pages.py`を修正します。

```py title="docs/gen_ref_pages.py" hl_lines="16 17"
"""Generate the code reference pages and navigation."""
from pathlib import Path
import mkdocs_gen_files

nav = mkdocs_gen_files.Nav()

for path in sorted(Path("src").rglob("*.py")):
    module_path = path.relative_to("src").with_suffix("")
    doc_path = path.relative_to("src").with_suffix(".md")
    full_doc_path = Path("reference", doc_path)

    parts = tuple(module_path.parts)

    if parts[-1] == "__init__":
        parts = parts[:-1]
        doc_path = doc_path.with_name("index.md")
        full_doc_path = full_doc_path.with_name("index.md")
    elif parts[-1] == "__main__":
        continue

    nav[parts] = doc_path.as_posix()

    with mkdocs_gen_files.open(full_doc_path, "w") as fd:
        ident = ".".join(parts)
        fd.write(f"::: {ident}")

    mkdocs_gen_files.set_edit_path(full_doc_path, path)

with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file:
    nav_file.writelines(nav.build_literate_nav())

ヒント

literate-nav でうまくいかない場合はこちらのヘルプを確認

literate-nav は動作せず

後述のerror1 / error2 を参照

error1#

mdx_truly_sane_listsを使用するとSUMMARY.md に記載のリストのHTML変換に失敗し、 mkdocs_literate_nav\parser.pyにてエラーとなる。

```yml mkdocs.yml site_name: My Docs

theme: name: "material" features: - navigation.tabs - navigation.tabs.sticky - navigation.top

plugins: - search - same-dir - gen-files: scripts: - docs/gen_ref_pages.py - literate-nav: nav_file: SUMMARY.md markdown_extensions: - mdx_truly_sane_lists - section-index - mkdocstrings:

nav: - Code Reference: reference/

markdown_extensions: - toc: permalink: true - mdx_truly_sane_lists: nested_indent: 2 truly_sane: True

```py title="gen_ref_pages.py"
"""Generate the code reference pages and navigation."""

from pathlib import Path

import mkdocs_gen_files

nav = mkdocs_gen_files.Nav()

for path in sorted(Path("src").rglob("*.py")):
    module_path = path.relative_to("src").with_suffix("")
    doc_path = path.relative_to("src").with_suffix(".md")
    full_doc_path = Path("reference", doc_path)

    parts = tuple(module_path.parts)

    if parts[-1] == "__init__":
        parts = parts[:-1]
        doc_path = doc_path.with_name("index.md")
        full_doc_path = full_doc_path.with_name("index.md")
    elif parts[-1] == "__main__":
        continue

    nav[parts] = doc_path.as_posix()

    with mkdocs_gen_files.open(full_doc_path, "w") as fd:
        ident = ".".join(parts)
        fd.write(f"::: {ident}")

    mkdocs_gen_files.set_edit_path(full_doc_path, ".." / path)

with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file:
    nav_file.writelines(nav.build_literate_nav())

error
raceback (most recent call last):
  File "c:\users\omron\appdata\local\programs\python\python39\lib\runpy.py", line 197, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "c:\users\omron\appdata\local\programs\python\python39\lib\runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "C:\Users\omron\AppData\Local\Programs\Python\Python39\Scripts\mkdocs.exe\__main__.py", line 7, in <module>
  File "c:\users\omron\appdata\local\programs\python\python39\lib\site-packages\click\core.py", line 1130, in __call__
    return self.main(*args, **kwargs)
  File "c:\users\omron\appdata\local\programs\python\python39\lib\site-packages\click\core.py", line 1055, in main
    rv = self.invoke(ctx)
  File "c:\users\omron\appdata\local\programs\python\python39\lib\site-packages\click\core.py", line 1657, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "c:\users\omron\appdata\local\programs\python\python39\lib\site-packages\click\core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "c:\users\omron\appdata\local\programs\python\python39\lib\site-packages\click\core.py", line 760, in invoke
    return __callback(*args, **kwargs)
  File "c:\users\omron\appdata\local\programs\python\python39\lib\site-packages\mkdocs\__main__.py", line 192, in build_command   
    build.build(config.load_config(**kwargs), dirty=not clean)
  File "c:\users\omron\appdata\local\programs\python\python39\lib\site-packages\mkdocs\commands\build.py", line 282, in build     
    files = config['plugins'].run_event('files', files, config=config)
  File "c:\users\omron\appdata\local\programs\python\python39\lib\site-packages\mkdocs\plugins.py", line 102, in run_event        
    result = method(item, **kwargs)
  File "c:\users\omron\appdata\local\programs\python\python39\lib\site-packages\mkdocs_literate_nav\plugin.py", line 45, in on_files
    config["nav"] = resolve_directories_in_nav(
  File "c:\users\omron\appdata\local\programs\python\python39\lib\site-packages\mkdocs_literate_nav\plugin.py", line 108, in resolve_directories_in_nav
    result = nav_parser.resolve_yaml_nav(nav_data)
  File "c:\users\omron\appdata\local\programs\python\python39\lib\site-packages\mkdocs_literate_nav\parser.py", line 198, in resolve_yaml_nav
    return self._resolve_wildcards([self._resolve_yaml_nav(x) for x in nav])
  File "c:\users\omron\appdata\local\programs\python\python39\lib\site-packages\mkdocs_literate_nav\parser.py", line 161, in _resolve_wildcards
    self.markdown_to_nav((val.value,) + roots)
  File "c:\users\omron\appdata\local\programs\python\python39\lib\site-packages\mkdocs_literate_nav\parser.py", line 73, in markdown_to_nav
    return self._resolve_wildcards(self._list_element_to_nav(ext.nav, root, first_item), roots)
  File "c:\users\omron\appdata\local\programs\python\python39\lib\site-packages\mkdocs_literate_nav\parser.py", line 97, in _list_element_to_nav
    child = next(children)
  File "c:\users\omron\appdata\local\programs\python\python39\lib\site-packages\mkdocs_literate_nav\parser.py", line 295, in _iter_children_without_tail
    raise LiterateNavParseError(
mkdocs_literate_nav.parser.LiterateNavParseError: Expected no text after <a href="paramiko/index.md">paramiko</a>, but got '\n    
* '.
The problematic item:

<li><a href="paramiko/index.md">paramiko</a>
    * <a href="paramiko/_version.md">95version</a>
    * <a href="paramiko/_winapi.md">95winapi</a>
    * <a href="paramiko/agent.md">agent</a>
    * <a href="paramiko/auth_handler.md">auth_handler</a>
    * <a href="paramiko/ber.md">ber</a>
    * <a href="paramiko/buffered_pipe.md">buffered_pipe</a>
    * <a href="paramiko/channel.md">channel</a>
    * <a href="paramiko/client.md">client</a>
    * <a href="paramiko/common.md">common</a>
    * <a href="paramiko/compress.md">compress</a>
    * <a href="paramiko/config.md">config</a>
    * <a href="paramiko/dsskey.md">dsskey</a>
    * <a href="paramiko/ecdsakey.md">ecdsakey</a>
    * <a href="paramiko/ed25519key.md">ed25519key</a>
    * <a href="paramiko/file.md">file</a>
    * <a href="paramiko/hostkeys.md">hostkeys</a>
    * <a href="paramiko/kex_curve25519.md">kex_curve25519</a>
    * <a href="paramiko/kex_ecdh_nist.md">kex_ecdh_nist</a>
    * <a href="paramiko/kex_gex.md">kex_gex</a>
    * <a href="paramiko/kex_group1.md">kex_group1</a>
    * <a href="paramiko/kex_group14.md">kex_group14</a>
    * <a href="paramiko/kex_group16.md">kex_group16</a>
    * <a href="paramiko/kex_gss.md">kex_gss</a>
    * <a href="paramiko/message.md">message</a>
    * <a href="paramiko/packet.md">packet</a>
    * <a href="paramiko/pipe.md">pipe</a>
    * <a href="paramiko/pkey.md">pkey</a>
    * <a href="paramiko/primes.md">primes</a>
    * <a href="paramiko/proxy.md">proxy</a>
    * <a href="paramiko/py3compat.md">py3compat</a>
    * <a href="paramiko/rsakey.md">rsakey</a>
    * <a href="paramiko/server.md">server</a>
    * <a href="paramiko/sftp.md">sftp</a>
    * <a href="paramiko/sftp_attr.md">sftp_attr</a>
    * <a href="paramiko/sftp_client.md">sftp_client</a>
    * <a href="paramiko/sftp_file.md">sftp_file</a>
    * <a href="paramiko/sftp_handle.md">sftp_handle</a>
    * <a href="paramiko/sftp_server.md">sftp_server</a>
    * <a href="paramiko/sftp_si.md">sftp_si</a>
    * <a href="paramiko/ssh_exception.md">ssh_exception</a>
    * <a href="paramiko/ssh_gss.md">ssh_gss</a>
    * <a href="paramiko/transport.md">transport</a>
    * <a href="paramiko/util.md">util</a>
    * <a href="paramiko/win_openssh.md">win_openssh</a>
    * <a href="paramiko/win_pageant.md">win_pageant</a></li>

error2#

mdx_truly_sane_listsを使用しない場合はmkdocs_literate_nav\parser.py58行目で extensions=[ext, *self._markdown_config.get("extensions", ())],にて空のリストが渡され、 error となる

mkdocs.yml
site_name: My Docs

theme:
  name: "material"
  features:
    - navigation.tabs
    - navigation.tabs.sticky
    - navigation.top


plugins:
  - search
  - same-dir
  - markdown-exec
  - gen-files:
      scripts:
        - docs/gen_ref_pages.py
  - literate-nav:
      nav_file: SUMMARY.md
  - section-index
  - mkdocstrings:

nav:
  - Code Reference: reference/

markdown_extensions:
  - toc:
      permalink: true
error
C:\Users\omron\Desktop\tmpdoc> mkdocs build
INFO     -  Cleaning site directory
INFO     -  Building documentation to directory: C:\Users\omron\Desktop\tmpdoc\site
Traceback (most recent call last):
  File "c:\users\omron\appdata\local\programs\python\python39\lib\runpy.py", line 197, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "c:\users\omron\appdata\local\programs\python\python39\lib\runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "C:\Users\omron\AppData\Local\Programs\Python\Python39\Scripts\mkdocs.exe\__main__.py", line 7, in <module>    
  File "c:\users\omron\appdata\local\programs\python\python39\lib\site-packages\click\core.py", line 1130, in __call__
    return self.main(*args, **kwargs)
  File "c:\users\omron\appdata\local\programs\python\python39\lib\site-packages\click\core.py", line 1055, in main    
    rv = self.invoke(ctx)
  File "c:\users\omron\appdata\local\programs\python\python39\lib\site-packages\click\core.py", line 1657, in invoke  
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "c:\users\omron\appdata\local\programs\python\python39\lib\site-packages\click\core.py", line 1404, in invoke  
    return ctx.invoke(self.callback, **ctx.params)
  File "c:\users\omron\appdata\local\programs\python\python39\lib\site-packages\click\core.py", line 760, in invoke
    return __callback(*args, **kwargs)
  File "c:\users\omron\appdata\local\programs\python\python39\lib\site-packages\mkdocs\__main__.py", line 192, in build_command
    build.build(config.load_config(**kwargs), dirty=not clean)
  File "c:\users\omron\appdata\local\programs\python\python39\lib\site-packages\mkdocs\commands\build.py", line 282, in build
    files = config['plugins'].run_event('files', files, config=config)
  File "c:\users\omron\appdata\local\programs\python\python39\lib\site-packages\mkdocs\plugins.py", line 102, in run_event
    result = method(item, **kwargs)
  File "c:\users\omron\appdata\local\programs\python\python39\lib\site-packages\mkdocs_literate_nav\plugin.py", line 45, in on_files
    config["nav"] = resolve_directories_in_nav(
  File "c:\users\omron\appdata\local\programs\python\python39\lib\site-packages\mkdocs_literate_nav\plugin.py", line 108, in resolve_directories_in_nav
    result = nav_parser.resolve_yaml_nav(nav_data)
  File "c:\users\omron\appdata\local\programs\python\python39\lib\site-packages\mkdocs_literate_nav\parser.py", line 198, in resolve_yaml_nav
    return self._resolve_wildcards([self._resolve_yaml_nav(x) for x in nav])
  File "c:\users\omron\appdata\local\programs\python\python39\lib\site-packages\mkdocs_literate_nav\parser.py", line 161, in _resolve_wildcards
    self.markdown_to_nav((val.value,) + roots)
  File "c:\users\omron\appdata\local\programs\python\python39\lib\site-packages\mkdocs_literate_nav\parser.py", line 58, in markdown_to_nav
    extensions=[ext, *self._markdown_config.get("extensions", ())],
TypeError: Value after * must be an iterable, not NoneType
C:\Users\omron\Desktop\tmpdoc>