ai.make_sphinx_files のソースコード

#!/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"![{img}](./{img})\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)