#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Sphinxドキュメント自動生成スクリプト。
指定されたPythonスクリプトファイルに対して、Docstringの追加、プログラム解説の生成、
Sphinx用の各種RST/MDファイルの作成、および関連するファイルのバックアップと置換を自動化します。
`add_docstring.py` と `explain_program5.py` を利用して、AIによるドキュメント生成を行います。
:doc:`make_sphinx_files_usage`
"""
import os
import sys
import argparse
import shutil
from glob import glob
import subprocess
import traceback
from pathlib import Path
from datetime import datetime
SCRIPT_FULLPATH = os.path.abspath(sys.argv[0])
SCRIPT_DIR = os.path.dirname(SCRIPT_FULLPATH)
SCRIPT_BASENAME = os.path.splitext(os.path.basename(SCRIPT_FULLPATH))[0]
# 起動スクリプトのディレクトリに基づいたパス設定
ADD_DOCSTRING_PATH = os.path.join(SCRIPT_DIR, "add_docstring.py")
EXPLAIN_PROGRAM_PATH = os.path.join(SCRIPT_DIR, "explain_program5.py")
DEFAULT_INI_PATH = os.path.join(SCRIPT_DIR, f"{SCRIPT_BASENAME}.ini")
[ドキュメント]
def relative_from_source(argv0: str) -> str:
"""
ファイルパスから'source'ディレクトリ以下の相対パスを取得する。
指定されたファイルパスを解決し、そのパス中に 'source' ディレクトリがあれば、
その直下からの相対パス(ファイル名を除いたディレクトリ部分)を返す。
'source' が見つからない場合は空文字列を返す。
:param argv0: プログラムのパス。通常 `sys.argv[0]` が渡される。
:returns: 'source' ディレクトリ以下の相対パスの文字列。
"""
p = Path(argv0).resolve()
parts = p.parts
# 最初に出現する "source" を探す
try:
idx = parts.index("source")
except ValueError:
# raise RuntimeError("'source' ディレクトリがパスに含まれていません")
return ""
# source 以下のパス(ファイル名付き)
rel_path = Path(*parts[idx+1:])
# ファイル名を削除してディレクトリだけにする
return str(rel_path.parent)
[ドキュメント]
def run_step(message, cmd_list):
"""
作業ステップを表示し、外部コマンドを実行します。
指定されたメッセージを表示した後、`subprocess.run` を使用してコマンドリストを実行します。
コマンドの実行結果が成功(終了コード0)であれば `True` を返し、
エラーが発生した場合はエラーメッセージを表示して `False` を返します。
:param message: 実行するステップのメッセージ。
:param cmd_list: 実行するコマンドとその引数を要素とするリスト。
:returns: コマンドが正常に実行された場合は `True`、それ以外は `False`。
"""
print(f"\n>>> {message}")
print(f" コマンド: {' '.join(cmd_list)}")
try:
result = subprocess.run(cmd_list, text=True, errors='ignore')
# result = subprocess.run(cmd_list, capture_output=True, text=True, encoding='utf-8', errors='ignore')
if result.returncode != 0:
print(f"!!! エラーが発生しました:\n{result.stderr}")
return False
return True
except Exception as e:
print(f"!!! 実行エラー: {e}")
return False
[ドキュメント]
def make_init_py(path):
"""
指定されたパスに `__init__.py` ファイルを作成します。
既にファイルが存在する場合はその旨を表示し、存在しない場合は空の `__init__.py` ファイルを作成します。
これはPythonパッケージとして認識させるために必要です。
:param path: `__init__.py` を作成するパス。
"""
if os.path.exists(path):
print(f">>> Step: {path} が見つかりました。")
else:
print(f">>> Step: {path} を作成中...")
with open(path, "w", encoding="utf-8") as f:
f.write("")
print(f" Done: {path}")
[ドキュメント]
def make_index_template(path, module_path):
"""
`index.template` ファイルを作成または更新します。
プロジェクト全体のSphinxインデックスファイルとなる `index.template` を作成または追記します。
ファイルが存在しない場合は新規作成し、基本的なtoctree構造を含みます。
存在する場合は、指定された `module_path` をtoctreeに追加します。
:param path: `index.template` ファイルのパス。
:param module_path: Sphinxドキュメントのインデックスに追加するモジュールパス。
"""
if os.path.exists(path):
print(f">>> Step: {path} に追加中...")
with open(path, "a", encoding="utf-8") as f:
f.write(f" {module_path}_index\n")
print(f" Done: {path}")
else:
print(f">>> Step: {path} を作成中...")
content = f"""\
プロジェクト全体ドキュメント
============================
.. toctree::
:maxdepth: 2
:caption: メインメニュー:
:hidden:
:glob:
{module_path}_index
"""
with open(path, "w", encoding="utf-8") as f:
f.write(content)
print(f" Done: {path}")
[ドキュメント]
def make_index_rst(index_rst, base_name, package_path):
"""
Sphinxのモジュール別インデックス (`_index.rst`) ファイルを作成します。
指定された `base_name` と `package_path` を使用して、
モジュールごとのドキュメントのトップページとなる `.rst` ファイルを作成します。
このファイルには、usage, examples, apiへのリンクを含むtoctreeが定義されます。
:param index_rst: 作成する `.rst` ファイルのパス。
:param base_name: ベースとなるファイル名(拡張子なし)。
:param package_path: Pythonパッケージのフルパス。
"""
print(f">>> Step: {index_rst} を作成中...")
index_content = f"""{base_name} ドキュメント
============================================================================
.. toctree::
:maxdepth: 1
{base_name}_usage
{base_name}_examples
{base_name}_api
"""
with open(index_rst, "w", encoding="utf-8") as f:
f.write(index_content)
print(f" Done: {index_rst}")
[ドキュメント]
def make_api_rst(api_rst, base_name, package_path):
"""
SphinxのAPIドキュメント (`_api.rst`) ファイルを作成します。
指定された `base_name` と `package_path` を使用して、
PythonモジュールのAPIリファレンスとなる `.rst` ファイルを作成します。
`automodule` ディレクティブを用いて、自動的にメンバー、非公開メンバー、継承情報を抽出する設定を行います。
:param api_rst: 作成するAPI `.rst` ファイルのパス。
:param base_name: ベースとなるファイル名(拡張子なし)。
:param package_path: Pythonパッケージのフルパス。
"""
print(f">>> Step: {api_rst} を作成中...")
api_content = f"""{base_name} プログラム仕様
============================================================================
.. currentmodule:: {package_path}
.. automodule:: {package_path}
:members:
:undoc-members:
:show-inheritance:
"""
with open(api_rst, "w", encoding="utf-8") as f:
f.write(api_content)
print(f" Done: {api_rst}")
[ドキュメント]
def make_examples_md(examples_md, infile, base_name):
"""
実行例を示すMarkdownファイル (`_examples.md`) を作成します。
指定された `infile` に対して `--help` オプションを実行してヘルプ出力を取得し、
現在のディレクトリ内の関連する画像ファイルやデータファイルを検出し、
それらを組み込んだMarkdown形式の実行例ファイルを作成します。
:param examples_md: 作成する実行例Markdownファイルのパス。
:param infile: ヘルプ出力を取得する対象のPythonスクリプトファイル。
:param base_name: ベースとなるファイル名(拡張子なし)。
"""
print(f">>> Step: {examples_md} テンプレートを作成中...")
# 画像ファイルの自動検出
image_files = sorted(
f for f in os.listdir(".")
if f.startswith(base_name) and f.lower().endswith((".png", ".jpg", ".jpeg"))
)
# データファイルの自動検出(CSV / Excel / TXT)
data_files = sorted(
f for f in os.listdir(".")
if f.startswith(base_name) and f.lower().endswith((".csv", ".xlsx", ".xls", ".txt"))
)
print("Image files:", image_files)
print("Data files:", data_files)
# ヘルプ出力を取得
print(" help logを取得します")
result = subprocess.run(["python", infile, "--help"],
text=True, capture_output = True)
print(" return code:", result.returncode)
if result.returncode == 0:
help_log = result.stdout + "\n" + result.stderr
# print(" help log:", help_log)
else:
help_log = "(ヘルプの自動取得に失敗しました。ここに実行ログを貼り付けてください)"
if data_files:
data_section = "## データファイル\n"
for df in data_files:
data_section += f"- [{df}](./{df})\n"
data_section += "\n"
else:
data_section = "## 生成されたデータファイル\n(データファイルが見つかりませんでした)\n\n"
if image_files:
image_section = "## 画像ファイル\n\n"
for img in image_files:
image_section += f"- [{img}](./{img})\n"
image_section += f"\n\n"
else:
image_section = "## 生成された画像一覧\n(画像ファイルが見つかりませんでした)\n\n"
examples_content = f"""# {base_name} 実行例
## help出力 `{base_name}.py --help`
<pre style="background-color: #f4f4f4; border: 1px solid #ccc; padding: 10px; border-radius: 5px; font-family: 'Courier New', Courier, monospace; overflow-x: auto;">
{help_log}
</pre>
{data_section}
{image_section}
"""
with open(examples_md, "w", encoding="utf-8") as f:
f.write(examples_content)
print(f" Done: {examples_md}")
[ドキュメント]
def main(args):
"""
Sphinxドキュメント生成の主要な処理を実行します。
入力ファイルから基本情報を抽出し、`__init__.py`, `index.template`,
`_index.rst`, `_api.rst`, `_examples.md` といったSphinx関連ファイルを生成します。
その後、`explain_program5.py` と `add_docstring.py` を実行して、
プログラム解説とDocstringを生成・追加し、最後に元のファイルをバックアップし、
Docstringが追加されたファイルで置き換えます。
:param args: コマンドライン引数を格納した `argparse.Namespace` オブジェクト。
"""
infile = args.infile
if not os.path.exists(infile):
print(f"エラー: 入力ファイル '{infile}' が見つかりません。")
return
# 基本情報の整理
base_name = os.path.splitext(os.path.basename(infile))[0]
date_str = datetime.now().strftime("%Y%m%d")
# ファイル名定義
init_py = "__init__.py"
index_template = "index.template"
docstring_out = f"{base_name}_docstring.py"
backup_file = f"{base_name}_{date_str}.py"
usage_md = f"{base_name}_usage.md"
examples_md = f"{base_name}_examples.md"
index_rst = f"{base_name}_index.rst"
api_rst = f"{base_name}_api.rst"
# カレントディレクトリ名からパッケージパスを判定 (010...等の数値ディレクトリを想定)
current_dir_name = os.path.basename(os.getcwd())
# フォルダ名が 010xx_page のような形式ならモジュールパスに含める
# module_path = f"{current_dir_name}.{base_name}" if "page" in current_dir_name else base_name
if args.subdir is not None and args.subdir != "":
module_path = os.path.join(args.subdir, base_name)
package_path = f"{args.subdir}.{base_name}"
else:
package_path = module_path
module_path = base_name
print("="*60)
print(f" プロジェクト: {base_name} のSphinxファイル自動生成を開始します")
print(f" モジュールパス: {module_path}")
print(f" パッケージパス: {package_path}")
print("="*60)
make_init_py(init_py)
make_index_template(index_template, module_path)
make_index_rst(index_rst, base_name, package_path)
make_api_rst(api_rst, base_name, package_path)
make_examples_md(examples_md, infile, base_name)
args_list = ["--api", args.api, "--update", str(args.update), "--overwrite", str(args.overwrite),
"--pause", str(args.pause)]
# explain_program.py の実行
if not run_step("Step: EXPLAIN_PROGRAM_PATH を実行してプログラム解説を生成中...",
["python", EXPLAIN_PROGRAM_PATH, infile, *args_list]):
return
# add_docstring.py の実行
if not run_step("Step: ADD_DOCSTRING_PATH を実行してDocstringを追加中...",
["python", ADD_DOCSTRING_PATH, infile, *args_list]):
return
#======================================================================
# バックアップの作成
print(f">>> Step: オリジナルファイルのバックアップを作成中...")
if os.path.exists(infile):
shutil.copy2(infile, backup_file)
print(f" Done: {infile} -> {backup_file}")
else:
print("!!! バックアップ対象のファイルが見つかりません。")
return
#======================================================================
# ファイルの置き換え
print(f">>> Step: 生成されたDocstring版ファイルを {infile} にリネーム中...")
if os.path.exists(docstring_out):
os.replace(docstring_out, infile)
print(f" Done: {docstring_out} -> {infile}")
with open(docstring_out, "w") as fp:
fp.write("")
print(f" ダミーの空ファイル {docstring_out} を作りました")
else:
print("!!! Docstring版ファイルが生成されていなかったため、リネームをスキップします。")
return
print("\n" + "="*60)
print(" 全ての自動生成プロセスが正常に終了しました。")
print("="*60)
[ドキュメント]
def initialize():
"""
コマンドライン引数パーサーを初期化し、設定します。
`argparse.ArgumentParser` オブジェクトを生成し、スクリプトの説明、
必要な引数 (`infile`)、およびオプション引数 (`subdir`, `api`, `update`, `overwrite`, `pause`) を定義します。
:returns: 設定済みの `ArgumentParser` オブジェクト。
"""
parser = argparse.ArgumentParser(
description="Sphinxドキュメント生成の一連のルーチン(Docstring追加、バックアップ、解説生成、RST作成)を自動化します。"
)
parser.add_argument("infile", help="対象となるPythonスクリプトファイル名 (例: mu_fit.py)")
parser.add_argument("--subdir", default=None, help="sourceディレクトリからの相対パス")
parser.add_argument("--api", choices=["openai", "openai5", "google", "gemini"], default='google')
parser.add_argument("-u", "--update", type=int, default=1)
parser.add_argument("-w", "--overwrite", type=int, default=0)
parser.add_argument("-p", "--pause", type=int, default=0)
return parser
[ドキュメント]
def read_args(parser):
"""
コマンドライン引数を解析し、整形して返します。
与えられた `ArgumentParser` を使用してコマンドライン引数を解析します。
`subdir` が指定されていない場合は `relative_from_source` 関数を使って自動的に設定します。
解析された引数を表示し、`argparse.Namespace` オブジェクトとして返します。
:param parser: `argparse.ArgumentParser` オブジェクト。
:returns: 解析された引数を格納する `argparse.Namespace` オブジェクト。
"""
args = parser.parse_args()
if args.subdir is None:
args.subdir = relative_from_source(args.infile)
print()
print("Args:")
print(f" {args.infile=}")
print(f" {args.subdir=}")
print(f" {args.api=}")
print(f" {args.update=}")
print(f" {args.overwrite=}")
print(f" {args.pause=}")
return args
if __name__ == "__main__":
print()
print(f"=== {sys.argv[0]} ===")
print(f"{EXPLAIN_PROGRAM_PATH=}")
print(f"{ADD_DOCSTRING_PATH=}")
parser = initialize()
args = read_args(parser)
try:
main(args)
except Exception:
print("\n" + "!"*60)
print(" 予期せぬ致命的なエラーが発生しました。")
traceback.print_exc()
print("!"*60)
sys.exit(1)