check_sphinx_api_rst.py

プログラムの動作

check_sphinx_api_rst.py は、Sphinxプロジェクト内のAPIドキュメントファイル (*_api.rst) を再帰的に走査し、autodoc 拡張機能が円滑に動作するために問題となりやすい記述を検出・修正するスクリプトです。

主な機能は以下の通りです。

  1. モジュール名の検証: .. automodule:: ディレクティブに指定されたモジュール名に、Pythonのimport文で不適切とされるハイフン (-) やパス区切り文字 (\/) が含まれていないかをチェックします。

  2. __main__ ガードのチェック: .. automodule:: で指定されたモジュールに対応するPythonソースファイルが、sys.argvargv を使用しているにもかかわらず、if __name__ == "__main__": ガードで保護されていない可能性がないかをチェックします。これにより、Sphinxがドキュメント生成時にモジュールをインポートした際に意図しないスクリプト実行を防ぎます。

  3. 自動修正機能: --fix オプションが指定された場合、検出された以下の問題を自動的に修正します(--dry-run と併用することで変更をシミュレーションできます)。

    • automodule ディレクティブ内のモジュール名に含まれる \/. に、-_ に置換します。

    • 対応するPythonソースファイルの名前(ハイフンを含む場合)をリネームします。

    • *_api.rst ファイル名自体(ハイフンを含む場合)をリネームします。

    • 同じディレクトリ内の関連する _usage.rst_examples.rst_index.rst.md ファイル内の参照(ファイル名のステム部分)も更新します。

このスクリプトは、Sphinxを用いたPythonプロジェクトで、APIドキュメントの記述ミスや、ドキュメント生成時の予期せぬサイドエフェクト(例: import 時のコマンドライン引数処理)を防ぐことを目的としています。

原理

本プログラムは、主に正規表現を用いたテキスト解析とファイルシステム操作によって動作します。

  1. ファイル検索: find_files 関数は、pathlib.Pathrglob メソッドを利用して、指定されたルートディレクトリ以下から、ワイルドカードパターンに一致するファイルを再帰的に検索します。これにより、Sphinxプロジェクト内のすべての *_api.rst ファイルを効率的に見つけ出します。

  2. テキストの読み書き: read_text 関数は、ファイルのエンコーディングを自動的に判別するために、UTF-8、CP932、Shift_JIS、Latin-1 の順にデコードを試みます。これにより、多様な環境で作成されたファイルを確実に読み込みます。 write_text 関数は、ファイルへの書き込みをUTF-8エンコーディングで行います。--dry-run オプションが指定された場合は、実際のファイル書き込みはスキップされます。

  3. 正規表現によるパターンマッチング:

    • AUTOMODULE_RE: r"(^\s*\.\.\s+automodule::\s+)([^\s]+)(\s*$)" RSTファイル内の .. automodule:: <module_name> 形式の行を検出します。これにより、automodule ディレクティブに指定されたモジュール名 (<module_name>) を抽出します。

    • MAIN_GUARD_RE: r"if\s+__name__\s*==\s*['\"]__main__['\"]\s*:" Pythonソースファイル内で if __name__ == "__main__": という形式のメインガード文を検出します。

    • ARGV_RE: r"\b(?:sys\s*\.\s*)?argv\b" Pythonソースファイル内で argv または sys.argv の使用を検出します。\b は単語境界を表し、?: は非捕捉グループです。

  4. モジュール名の補正ロジック: safe_module_name 関数は、Pythonの import 文で安全に扱えるモジュール名に変換するロジックを実装しています。具体的には、モジュール名に含まれるパス区切り文字 \ および /. (ピリオド) に、ハイフン -_ (アンダースコア) に置換します。 例えば、my-package/sub_module.py というファイルパスが my-package.sub_module というモジュール名として指定された場合、この関数は my_package.sub_module に変換します。

  5. argv 保護チェック (has_unprotected_argv): この関数は、与えられたPythonコードのテキストに対して ARGV_REargv の使用を検出し、同時に MAIN_GUARD_REif __name__ == "__main__": ガードの有無をチェックします。argv が使用されているにもかかわらずメインガードが存在しない場合に True を返します。これは厳密な制御フロー解析ではなく、autodoc がモジュールをインポートする際に、意図しないスクリプトの実行を防ぐための簡易的なチェックです。

  6. 自動修正アルゴリズム (fix_module_name): --fix オプションが有効な場合、fix_module_name 関数は以下の連鎖的な修正処理を実行します。

    • まず safe_module_name を使ってモジュール名を補正します。

    • 次に、元のモジュール名と補正後のモジュール名から、対応するPythonソースファイルのパス (.py) を計算します。元のPythonファイル名にハイフンが含まれる場合、新しい名前にリネームします。

    • RSTファイル内の .. automodule:: ディレクティブに記載されているモジュール名を、補正後の名前に置換します。

    • RSTファイル名自体にハイフンが含まれる場合、アンダースコアに置換してリネームします。

    • 最後に、RSTファイルと同じディレクトリにある関連するドキュメントファイル(_usage, _examples, _index など)の参照(ファイル名のステム部分)も、ハイフンからアンダースコアへの変更に合わせて更新します。これは、replace_in_filerename_file を使用して行われます。

これらの処理は、--dry-run オプションが指定されている場合は実際にはファイルシステムに変更を加えず、実行予定のアクションをログとして記録するにとどまります。

必要な非標準ライブラリとインストール方法

このプログラムは、Pythonの標準ライブラリのみを使用しており、特別な非標準ライブラリのインストールは不要です。Pythonがインストールされていれば、そのまま実行できます。

必要な入力ファイル

本プログラムは、Sphinxプロジェクトのソースディレクトリ内の以下の種類のファイルを読み込みます。

  1. Sphinx APIドキュメントファイル (デフォルト: *_api.rst):

    • --files オプションで指定されたワイルドカードパターンに一致するRSTファイルが対象となります。デフォルトでは _api.rst で終わるファイルが走査されます。

    • これらのファイルは、PythonモジュールのAPIドキュメントを生成するために .. automodule:: ディレクティブを含んでいることが期待されます。

    • 期待されるファイル形式と内容:

      .. automodule:: my-module-name
         :members:
         :undoc-members:
         :show-inheritance:
      

      または

      .. automodule:: my_package.my_module
         :members:
      

      (ここで my-module-namemy_package/my_module に問題がないかチェックされます。)

  2. 対応するPythonソースファイル (.py):

    • *_api.rst ファイル内の .. automodule:: ディレクティブで参照されているPythonモジュールに対応する .py ファイル。

    • my_module_api.rst のようなファイル名の場合、同じディレクトリ内の my_module.py も対応するファイルとして推定されます。

    • 期待されるファイル形式と内容: これらのPythonファイルは、Sphinxによってインポートされるため、グローバルスコープで sys.argvargv を使用している場合、if __name__ == "__main__": ガードで保護されていることが推奨されます。

      # good_module.py
      import sys
      
      def main():
          print(f"Arguments: {sys.argv}")
      
      if __name__ == "__main__":
          main()
      
      # bad_module.py (検出対象)
      import sys
      
      print(f"Arguments: {sys.argv}") # __main__ ガードなしで argv が使われている
      
  3. 関連するドキュメントファイル (.rst, .md):

    • --fix オプションが使用された場合、リネームされる *_api.rst ファイルと同じディレクトリに存在する他のRSTファイルやMarkdownファイル(*_usage.rst, *_examples.rst, *_index.rst, *_api.md, *_usage.md など)も、ファイル名のリファレンス更新のために読み込まれる可能性があります。

プログラムは、指定された --root ディレクトリ(デフォルトは ./source)以下からこれらのファイルを再帰的に検索します。

生成される出力ファイル

本プログラムは、実行結果を以下のファイルに出力します。

  • failed_api_rst.txt (デフォルト)

    • --outfile オプションで出力ファイル名を変更できます。

    • このファイルには、チェックで問題が検出された *_api.rst ファイルのパスと、その問題の理由が詳細に記述されます。

    • --fix オプションが指定された場合、実際に実行された(または --dry-run が指定された場合は実行予定の)ファイルシステム変更アクションのログも追記されます。

failed_api_rst.txt の内容例:

path/to/my-module_api.rst
  - invalid module name contains '-': my-module-name
  - corresponding py file not found for module: my-module-name
path/to/another_module_api.rst
  - argv is used but __main__ guard was not found: path/to/another_module.py

[ACTIONS]
- RENAME path/to/my-module.py -> path/to/my_module.py
- REPLACE automodule in path/to/my-module_api.rst: my-module-name -> my_module_name
- RENAME path/to/my-module_api.rst -> path/to/my_module_api.rst
- REPLACE in path/to/my_module_api.rst: my-module -> my_module
- REPLACE in path/to/my_module_usage.rst: my-module -> my_module
- RENAME path/to/my-module_usage.rst -> path/to/my_module_usage.rst

この出力ファイルは、検出された問題のレビューや、--fix オプションによる変更内容の確認に役立ちます。

コマンドラインでの使用例 (Usage)

check_sphinx_api_rst.py は以下の形式で実行します。

python check_sphinx_api_rst.py [OPTIONS]

利用可能なオプションは以下の通りです。

  • --root <path>

    • Sphinxソースのルートディレクトリを指定します。

    • デフォルト: ./source

  • --files <pattern>

    • 対象とするRSTファイルのワイルドカードパターンを指定します。

    • デフォルト: *_api.rst

  • --outfile <path>

    • 結果を書き出す出力ファイルのパスを指定します。

    • デフォルト: failed_api_rst.txt

  • --exclude <keyword>

    • パスに特定のキーワードが含まれるファイルをチェック対象から除外します。複数回指定可能です。

    • 例: --exclude legacy --exclude archive

  • --show-ok {0,1}

    • 問題がなかったファイルもコンソール出力に表示するかどうかを指定します。

    • 0: 表示しない (デフォルト)

    • 1: 表示する

  • --fix

    • 検出された問題(automodule 名のハイフンやパス区切り文字)を自動的に修正します。関連するファイル名や参照も更新されます。

  • --dry-run

    • --fix オプションと併用することで、ファイルシステムへの変更を行わずに、実行予定の修正内容のみを表示します。

コマンドラインでの具体的な使用例

ここでは、いくつかの具体的な使用例とその結果について説明します。 以下のようなファイル構造と内容が存在すると仮定します。

./source/
├── index.rst
├── conf.py
├── package_a/
│   ├── __init__.py
│   ├── module-with-hyphen_api.rst
│   ├── module-with-hyphen.py
│   └── util_module_api.rst
│       └── util_module.py (sys.argv を __main__ ガードなしで使用)
└── package_b/
    ├── __init__.py
    └── good_module_api.rst
        └── good_module.py (問題なし)

source/package_a/module-with-hyphen_api.rst:

.. automodule:: package_a.module-with-hyphen
   :members:

source/package_a/module-with-hyphen.py:

# 何らかのPythonコード

source/package_a/util_module_api.rst:

.. automodule:: package_a.util_module
   :members:

source/package_a/util_module.py:

# main guard なしで argv を使用
import sys
print(f"Script arguments: {sys.argv}")

def some_func():
    pass

1. デフォルト設定で問題をチェック

Sphinxプロジェクトのルートディレクトリがカレントディレクトリにある場合、または --root で指定できる場合、_api.rst ファイルを対象に問題を検出します。

python check_sphinx_api_rst.py --root ./source

実行結果の説明: このコマンドは、./source ディレクトリ以下にある *_api.rst ファイルを走査し、モジュール名の問題や __main__ ガードなしの argv 使用を検出します。 コンソールには、検出された問題のあるファイルとその理由が表示されます。また、failed_api_rst.txt にも詳細が書き込まれます。

root    : /path/to/project/source
pattern : *_api.rst
found   : 3 files
fix     : False
dry-run : False

[FAILED] package_a/module-with-hyphen_api.rst
         - invalid module name contains '-': package_a.module-with-hyphen
[FAILED] package_a/util_module_api.rst
         - argv is used but __main__ guard was not found: package_a/util_module.py

checked : 3
skipped : 0
failed  : 2 / 3
actions : 0
output  : /path/to/project/failed_api_rst.txt

2. --fix --dry-run で修正をシミュレーション

--fix を指定する前に、--dry-run を併用してどのような変更が加えられるかをプレビューできます。

python check_sphinx_api_rst.py --root ./source --fix --dry-run

実行結果の説明: コンソールには、検出された問題に加えて、--fix が有効だった場合に実行されるであろうアクションが表示されます。ファイルシステムは一切変更されません。failed_api_rst.txt にもこれらのアクションが記録されます。

root    : /path/to/project/source
pattern : *_api.rst
found   : 3 files
fix     : True
dry-run : True

[FIX]    package_a/module-with-hyphen_api.rst
         - RENAME /path/to/project/source/package_a/module-with-hyphen.py -> /path/to/project/source/package_a/module_with_hyphen.py
         - REPLACE automodule in /path/to/project/source/package_a/module-with-hyphen_api.rst: package_a.module-with-hyphen -> package_a.module_with_hyphen
         - RENAME /path/to/project/source/package_a/module-with-hyphen_api.rst -> /path/to/project/source/package_a/module_with_hyphen_api.rst
         - REPLACE in /path/to/project/source/package_a/index.rst: module-with-hyphen -> module_with_hyphen
[FAILED] package_a/util_module_api.rst
         - argv is used but __main__ guard was not found: package_a/util_module.py

checked : 3
skipped : 0
failed  : 1 / 3
actions : 4
output  : /path/to/project/failed_api_rst.txt

注意: package_a/util_module.pyargv 問題は自動修正の対象ではないため、FAILED として残ります。

3. --fix で実際に修正を実行

--dry-run を付けずに --fix を実行すると、実際にファイルシステムが変更されます。

python check_sphinx_api_rst.py --root ./source --fix

実行結果の説明: 上記の --dry-run の例と同じアクションが実行され、ファイル名のリネームやRSTファイルの内容変更が実際に行われます。 この実行後、ファイル構造は以下のようになるはずです。

./source/
├── index.rst
├── conf.py
├── package_a/
│   ├── __init__.py
│   ├── module_with_hyphen_api.rst  <- 名前が変更された
│   ├── module_with_hyphen.py      <- 名前が変更された
│   └── util_module_api.rst
│       └── util_module.py (sys.argv を __main__ ガードなしで使用)
└── package_b/
    ├── __init__.py
    └── good_module_api.rst
        └── good_module.py (問題なし)

コンソール出力は --dry-run と似ていますが、今回は実際に変更がコミットされています。

4. 特定のファイルパターンを指定してチェック

--files オプションを使用して、特定の命名規則を持つファイルのみを対象にできます。

python check_sphinx_api_rst.py --root ./source --files "util*_api.rst"

実行結果の説明: この例では util*_api.rst に一致するファイルのみがチェックされ、util_module_api.rst の問題が報告されます。

root    : /path/to/project/source
pattern : util*_api.rst
found   : 1 files
fix     : False
dry-run : False

[FAILED] package_a/util_module_api.rst
         - argv is used but __main__ guard was not found: package_a/util_module.py

checked : 1
skipped : 0
failed  : 1 / 1
actions : 0
output  : /path/to/project/failed_api_rst.txt

5. 除外パスとOKファイルの表示

--exclude で特定のディレクトリやファイルを除外し、--show-ok 1 で問題のないファイルも表示します。

python check_sphinx_api_rst.py --root ./source --exclude "package_a" --show-ok 1

実行結果の説明: package_a を含むパスはスキップされ、それ以外のファイルで問題がなければ [OK] と表示されます。

root    : /path/to/project/source
pattern : *_api.rst
found   : 3 files
fix     : False
dry-run : False

[SKIP]   package_a/module_with_hyphen_api.rst
[SKIP]   package_a/util_module_api.rst
[OK]     package_b/good_module_api.rst

checked : 1
skipped : 2
failed  : 0 / 1
actions : 0
output  : /path/to/project/failed_api_rst.txt