mkdocstrings#
mkdocs-material と mkdocstrings を組み合わせて Python の docstring を HTML で確認できるようにする。
参考#
install#
Note
markdown_extentions で使用する拡張機能は予め install しておく
使い方#
以下ディレクトリ・ファイルを用意する - docs - リファレンスファイル.md - pythonモジュールを格納するディレクトリ - 対象ファイル.py - mkdocs.yml
.md ファイル#
- docs 配下に用意した
.md
ファイル内で docstring を抽出したいpython ファイルを指定する - ファイル名は任意だが 「モジュール名-reference.md」とするのが分かりやすい
:::
のあとに半角スペース、続けて、root ディレクトリからpythonファイルの場所を指定する。このとき拡張子は不要。
mkdocs.yml#
mkdocs の設定が記述されたファイルを用意し、プラグインでmkdocstrings
を指定する。
自動コード参照ページ#
以下のディレクトリを例に、あるオブジェクトのドキュメントを作成する際、project.lorem
のように手動で指定することになるが、
プロジェクトが大きくなるにつれて手動で.md ファイルへの記述は限界がある。
mkdocs-gen-files
プラグインではこの問題を解決する
手順は以下の通り
mkdocs-gen-files
は、ビルド時に Python スクリプトを実行できます。
上記例では実行する Python スクリプトは 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 を追加します。
plugins:
- search
- gen-files:
scripts:
- docs/gen_ref_pages.py
- literate-nav:
nav_file: SUMMARY.md
- mkdocstrings
次に、Python スクリプトを修正します。
"""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行に置き換えます。
nav:
# rest of the navigation...
# defer to gen-files + literate-nav
- Code Reference: reference/ #
# rest of the navigation...
ページをセクション自体にバインドする#
最後に mkdocs-section-index
pluginを追加します。
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())
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.py
58行目で
extensions=[ext, *self._markdown_config.get("extensions", ())],
にて空のリストが渡され、
error となる
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
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>