Sphinx作成支援プログラム
Sphinxマニュアル作成を支援するプログラムの使い方をまとめています。
生成AIを使うプログラムでは、API KEYを 環境変数 OPENAI_API_KEY あるいは GOOGLE_API_KEYに設定してください。また、AI modelのデフォルトは
explain_program5.iniで設定されていますが、このファイルは無くても問題ありません。explain_program5.py: 引数で与えたpythonプログラムのマニュアル(usage) Markdownを作成します。プロンプトは
explain_program5.iniで設定します。add_docstring.py: 引数で与えたpythonプログラムにsphinx互換のdocstringを追加します。プロンプトは
add_docstring.iniで設定します。make_sphinx_files.py: 引数で与えたpythonプログラムに対して、上記プログラムを呼び出すとともに、関連するSphinxファイルを作成します
debug_sphinx-build.py: sphinx-buildでimportエラーになるファイルを発見する
files2md_tags.py: 指定したファイルMarkdown書式のリンクを出力する。画像ファイルの場合はサムネイルを作成する。
burst_sphinx_indexes.py: Sphinx toctree globパターン展開スクリプト
save_clipboard.py: (Windowsのみ) クリップボードのテキストや画像をファイルに保存する
compare_dir_files.py: sphinxのsourceツリーと、元のプログラムファイルツリーを比較し、存在しないファイルを抽出する
AI共通設定ファイル ai.env
1account_inf_path=d:/MyWebs/Database/accounts.env
2
3min_translate_length = 0
4allowed_translation_length_ratio = 5.0
5html_template_path = 'template_translate.html'
6max_tokens = 5000
7
8#openai_model = "gpt-3.5-turbo"
9openai_model = "gpt-4o"
10#openai_model = "gpt-4.5"
11#openai_model5=gpt-5
12#openai_model5=gpt-5-mini
13openai_model5=gpt-5-nano
14#openai_model5=gpt-5-chat-latest
15
16temperature = 0.3
17#reasoning_effort=minimal
18reasoning_effort=low
19#reasoning_effort=medium
20#reasoning_effort=high
21
22
23#gemini_model = "gemini-1.5-pro-latest"
24gemini_model = "gemini-2.5-flash"
25tsleep_rpm = 4
explain_program5.py (プログラム)
1"""
2プログラムコードをAIでドキュメント化するツール。
3
4指定されたワイルドカードパターンに一致するソースコードファイルを読み込み、
5設定ファイルに基づいたプロンプトを使用してAI(OpenAIまたはGoogle Gemini)に
6ドキュメントを生成させ、指定された出力ファイルに書き出します。
7
8:doc:`explain_program5_usage`
9"""
10
11#!/usr/bin/env python3
12# -*- coding: utf-8 -*-
13
14import os
15import sys
16import argparse
17import glob
18import time
19import re
20from pathlib import Path
21
22# 既存のライブラリ(環境に合わせてパスを通してください)
23try:
24 from tkai_lib import read_ai_config
25 from tkai_lib import query_openai4, query_openai5, query_google
26 from tkai_lib import extract_openai5_text
27except ImportError:
28 print("Error: tkai_lib が見つかりません。パスを確認してください。", file=sys.stderr)
29 sys.exit(1)
30
31#=========================================================
32# INI Reading Logic (make_textbook5.py より継承)
33#=========================================================
34def search_file(infile=None):
35 """
36 指定されたファイルパスまたはデフォルトのINIファイルを探索します。
37
38 infileが指定されない場合、スクリプトの実行ディレクトリとカレントディレクトリから
39 デフォルトのINIファイル(スクリプト名.ini)を探します。
40 infileが指定された場合、そのパスが存在するか、またはスクリプトの実行ディレクトリからの
41 相対パスで存在するかを確認します。
42
43 :param infile: str, 探索するファイルパス。デフォルトはNoneで、その場合はデフォルトINIファイル名を使用。
44 :returns: str または None, 見つかったファイルの絶対パス。見つからない場合はNone。
45 """
46 script_path = os.path.abspath(sys.argv[0])
47 script_dir = os.path.dirname(script_path)
48 script_name = os.path.splitext(os.path.basename(script_path))[0]
49 default_ini = f"{script_name}.ini"
50
51 if infile is None:
52 for path in [os.getcwd(), script_dir]:
53 candidate = os.path.join(path, default_ini)
54 if os.path.isfile(candidate): return candidate
55 return None
56
57 if not os.path.isfile(infile):
58 candidate = os.path.join(script_dir, infile)
59 if os.path.isfile(candidate): return candidate
60 return None
61 return infile
62
63def read_ini(inifile=None):
64 """
65 指定されたINIファイルから設定を読み込み、変数を展開します。
66
67 key=value形式の行を解析し、3重引用符で囲まれた複数行の値をサポートします。
68 また、`$VARIABLE_NAME` 形式の変数を、既に読み込まれた設定値で展開します。
69 コメント行(`#` または `;` で始まる)と空行は無視されます。
70
71 :param inifile: str または None, 読み込むINIファイルのパス。Noneの場合はsearch_fileで探索される。
72 :returns: dict, 読み込まれた設定をキーと値のペアで格納した辞書。
73 :raises FileNotFoundError: INIファイルが見つからない場合。
74 """
75 path = search_file(inifile)
76 if path is None:
77 raise FileNotFoundError("INIファイルが見つかりませんでした")
78
79 result = {}
80 variables = {}
81 current_key = None
82 multiline_val = []
83 multiline_delim = None
84
85 with open(path, 'r', encoding='utf-8') as f:
86 for line in f:
87 line = line.rstrip()
88
89 if not line or line.startswith('#') or line.startswith(';'):
90 continue
91
92 # 複数行値の終了判定(stripで判定)
93 if multiline_delim:
94 if line.strip() == multiline_delim:
95 val = '\n'.join(multiline_val)
96 result[current_key] = val
97 variables[current_key] = val
98 current_key = None
99 multiline_val = []
100 multiline_delim = None
101 else:
102 multiline_val.append(line)
103 continue
104
105 # key=val の解析
106 if '=' in line:
107 key, val = map(str.strip, line.split('=', 1))
108 val = val.strip()
109
110 # 複数行値の開始判定(空文字でも対応)
111 if (val == '"""' or val == "'''" or
112 (val.startswith('"""') and not val.endswith('"""')) or \
113 (val.startswith("'''") and not val.endswith("'''")) ):
114 multiline_delim = val[:3]
115 content = val[3:]
116 multiline_val = [content] if content else []
117 current_key = key
118 continue
119
120 # 単一行の複数行値
121 if (val.startswith('"""') and val.endswith('"""')) or \
122 (val.startswith("'''") and val.endswith("'''")):
123 val = val[3:-3]
124
125 result[key] = val
126 variables[key] = val
127
128 # 変数展開(あとから一括処理)
129 for key, val in result.items():
130 def expand_var(match):
131 var_name = match.group(1)
132 return variables.get(var_name, match.group(0))
133 result[key] = re.sub(r"\$(\w+)\b", expand_var, val)
134
135 return result
136
137#=========================================================
138# Language Dictionary
139#=========================================================
140language_dict = {
141 ".py": "python", ".pl": "perl", ".pm": "perl", ".c" : "C",
142 ".cpp": "C++", ".pas": "pascal", ".f" : "fortran", ".js" : "Javascript",
143 ".java": "Java", ".go": "Go", ".sh": "bash script", ".html": "HTML"
144}
145
146def get_program_type(path):
147 """
148 プログラムファイルのパスからプログラムのタイプ(`main` または `lib`)を判定します。
149
150 ファイル拡張子が`.pm`の場合、またはファイル名が`tk`で始まる場合、ライブラリ(`lib`)と判定します。
151 それ以外の場合はメインプログラム(`main`)と判定します。
152
153 :param path: str, プログラムファイルのパス。
154 :returns: str, プログラムのタイプ (`'main'` または `'lib'`)。
155 """
156 base = os.path.basename(path)
157 name, ext = os.path.splitext(base)
158 if ext == ".pm" or base.startswith("tk"): return 'lib'
159 return 'main'
160
161def initialize():
162 """
163 コマンドライン引数パーサーを初期化し、設定します。
164
165 `argparse.ArgumentParser` を設定し、必要な引数とオプションを定義します。
166 `pattern` (必須, ワイルドカード), `output` (任意), `--inifile`, `--api`,
167 `-t` (`--program_type`), `-u` (`--update`), `-w` (`--overwrite`) オプションが含まれます。
168 デフォルトのINIファイルパスはスクリプト名から自動生成されます。
169
170 :returns: argparse.ArgumentParser, 初期化されたコマンドライン引数パーサーオブジェクト。
171 """
172 ini_path = os.path.splitext(sys.argv[0])[0] + ".ini"
173
174 parser = argparse.ArgumentParser(description="プログラムコードをAIでドキュメント化するツール")
175 parser.add_argument("pattern", help="対象ファイルのワイルドカード(例: '*.py')")
176 parser.add_argument("output", nargs="?", help="出力名(単一ファイル時のみ)")
177 parser.add_argument("--inifile", default=ini_path, help="プロンプト設定ファイル")
178 parser.add_argument("--api", choices=["openai", "openai5", "google", "gemini"], default='google')
179 parser.add_argument("-t", "--program_type", choices=["", "main", "lib"], default="")
180 parser.add_argument("-u", "--update", type=int, default=0)
181 parser.add_argument("-w", "--overwrite", type=int, default=0)
182 return parser
183
184def main():
185 """
186 プログラムの主要な処理を実行します。
187
188 `initialize` 関数で初期化された引数を解析し、AI設定ファイル (`ai.env`) と
189 プロンプトINIファイルを読み込みます。
190 指定されたパターンに一致するファイルを検索し、一つずつ処理します。
191 出力ファイルが既に存在し、更新フラグや上書きフラグが設定されていない場合はスキップします。
192 ソースコードを読み込み、言語タイプとプログラムタイプを判定し、
193 INIファイルの設定に基づいてプロンプトを構築します。
194 AIサービス(OpenAIまたはGoogle Gemini)に問い合わせてドキュメントを生成し、
195 生成されたドキュメントを出力ファイルに書き込みます。
196 各ファイル処理後に1秒間待機し、最後にユーザーからの終了入力を待ちます。
197
198 :returns: None
199 """
200 parser = initialize()
201 args = parser.parse_args()
202
203 # AI設定の読み込み
204 read_ai_config("ai.env")
205
206 # プロンプトINIの読み込み
207 try:
208 ini_data = read_ini(args.inifile)
209 print(f"Loaded INI: {search_file(args.inifile)}")
210 except Exception as e:
211 print(f"Error loading INI: {e}")
212 sys.exit(1)
213
214 files = glob.glob(args.pattern)
215 if not files: sys.exit(1)
216
217 outputs = [args.output] if args.output else [os.path.splitext(f)[0] + ".md" for f in files]
218
219 for inp, out in zip(files, outputs):
220 if os.path.exists(out) and not args.overwrite and (not args.update or os.path.getmtime(out) >= os.path.getmtime(inp)):
221 print(f"Skip: {out}")
222 continue
223
224 print(f"Processing: {inp} -> {out}")
225 try:
226 code = Path(inp).read_text(encoding="utf-8")
227 except:
228 code = Path(inp).read_text(encoding="shift-jis")
229
230 ext = os.path.splitext(inp)[1]
231 lang = language_dict.get(ext, "text")
232 p_type = args.program_type or get_program_type(inp)
233
234 # プロンプト構築
235 tpl_key = f"PROMPT_{p_type.upper()}"
236 template = ini_data.get(tpl_key, ini_data.get("PROMPT_MAIN"))
237 role = ini_data.get("SYSTEM_ROLE", "Assistant")
238
239 prompt = template.replace("{{script_name}}", inp).replace("{{code}}", code).replace("{{language}}", lang)
240
241 # AI呼び出し
242 if args.api == "openai5":
243 res = query_openai5(prompt, os.getenv("openai_model5"), instructions=role)
244 doc = extract_openai5_text(res)
245 elif args.api == "openai":
246 res = query_openai4(prompt, os.getenv("openai_model"), role=role)
247 doc = res.choices[0].message.content
248 else:
249 res = query_google(prompt, os.getenv("gemini_model"), role=role)
250 doc = res.text if res else None
251
252 if doc:
253 Path(out).write_text(doc, encoding="utf-8")
254 print(f"Done: {out}")
255 time.sleep(1)
256
257 input("\nPress ENTER to terminate>>\n")
258
259if __name__ == "__main__":
260 main()
explain_program5.ini (設定ファイル)
1SYSTEM_ROLE = """
2You are a helpful assistant that generates clear, well-structured documentation for the given program.
3"""
4
5PROMPT_MAIN = """
6以下は {{language}}言語 で記述されたプログラム `{{script_name}}` のソースコードです。
7このコードを解析し、以下の「構成」と「出力形式の絶対条件」に従って、技術ドキュメント(Markdown)を作成してください。
8
9### 1. 全体要件(厳守)
101. **適切な改行**: 各セクションの間や、見出しの前後には必ず空行(連続した改行)を1行以上挿入してください。
112. **全体をコードブロックで囲わない**
123. **冒頭のタイトル**: 出力の1行目はMarkdownの大見出し # を使った以下の形式とし、直後に必ず1行の空行を入れてください。
13# {{script_name}} ドキュメント
14(ここに空行を入れる)
15
16### 1. 構成案
171. **見出し**: 以下の各章は、Markdownの第二段階見出し ## で始める
182. **プログラムの動作**: プログラムの目的、主な機能、および解決する課題。
193. **原理**: 使用されている数式・物理式やアルゴリズムの解説。
204. **必要な非標準ライブラリとインストール方法**: `pip` コマンド等を含めること。
215. **必要な入力ファイル**: 期待されるファイル形式やデータ構造。
226. **生成される出力ファイル**: 保存されるファイル名とその内容。
237. **コマンドラインでの使用例 (Usage)**: 基本的な実行コマンドと引数の説明。
248. **コマンドラインでの具体的な使用例**: 具体的な引数を与えた実行例とその実行結果の説明。
25
26
27### 2. 出力形式に関する絶対条件(厳守)
28- **禁止記号と代替表記(重要)**:
29 - **三重引用符( `"""` および `'''` )の出力は、いかなる理由があっても厳禁とします。**
30 - プログラミング上の記号として言及が必要な場合は、必ず **「3重引用符」** というテキストに置き換えて記述してください。
31 - 例:「docstringを3重引用符で囲む」とし、「docstringを """ で囲む」とは書かないこと。
32
33- **数式の共通仕様**:
34 - **インライン数式**: $a^2 + b^2 = c^2$ のように、ドル記号1つで囲む。
35 - **ブロック数式**: 独立した行に記述し、**その前後は必ず空行で挟んでください。**
36 例:
37
38 $$\rho = \frac{\pi d}{\ln 2} \cdot \frac{R_A + R_B}{2} \cdot f$$
39
40- **プログラム名**: マニュアル内では `{{script_name}}` として扱ってください。
41
42---
43### 対象プログラムコード
44```{{language}}
45{{code}}
46```
47"""
48
49
50PROMPT_LIB = """
51以下は {{language}}言語 で記述されたライブラリ `{{script_name}}` のソースコードです。
52このコードを解析し、以下の「構成」と「出力形式の絶対条件」に従って、技術ドキュメント(Markdown)を作成してください。
53
54### 1. 全体要件(厳守)
551. **適切な改行**: 各セクションの間や、見出しの前後には必ず空行(連続した改行)を1行以上挿入してください。
562. **全体をコードブロックで囲わない**
573. **冒頭のタイトル**: 出力の1行目はMarkdownの大見出し # を使った以下の形式とし、直後に必ず1行の空行を入れてください。
58# {{script_name}} ドキュメント
59(ここに空行を入れる)
60
61### 2. 構成案
621. **見出し**: 以下の各章は、Markdownの第二段階見出し ## で始める
632. **ライブラリの機能や目的**: ライブラリの目的、主な機能、および解決する課題。
643. **importする方法**: このライブラリを他のプログラムからimportする方法。
654. **必要な非標準ライブラリとインストール方法**: `pip` コマンド等を含めること。
665. **importできる変数と関数**: 変数には説明するコメントを、関数には関数の動作と引数・戻り値の説明を入れること。
676. **main scriptとして実行したときの動作**
68
69### 3. 出力形式に関する絶対条件(厳守)
70- **禁止記号と代替表記(重要)**:
71 - **三重引用符( `"""` および `'''` )の出力は、いかなる理由があっても厳禁とします。**
72 - プログラミング上の記号として言及が必要な場合は、必ず **「3重引用符」** というテキストに置き換えて記述してください。
73 - 例:「docstringを3重引用符で囲む」とし、「docstringを """ で囲む」とは書かないこと。
74
75- **数式の共通仕様**:
76 - **インライン数式**: $a^2 + b^2 = c^2$ のように、ドル記号1つで囲む。
77 - **ブロック数式**: 独立した行に記述し、**その前後は必ず空行で挟んでください。**
78 例:
79
80 $$\rho = \frac{\pi d}{\ln 2} \cdot \frac{R_A + R_B}{2} \cdot f$$
81
82- **ライブラリ名**: マニュアル内では `{{script_name}}` として扱ってください。
83
84---
85### 対象プログラムコード
86```{{language}}
87{{code}}
88```
89"""
add_docstring.py (プログラム)
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3"""
4PythonコードにSphinx形式のDocstringを自動追加するツール。
5
6このスクリプトは、指定されたPythonソースファイルの内容をAIモデルに送信し、
7Sphinx形式のDocstringが挿入された完成版のコードを生成します。
8コマンドライン引数で入力ファイルパターン、出力ファイル名、
9INIファイルによるプロンプト設定、使用するAIモデルなどを指定できます。
10生成されたDocstringは、関数やクラスの概要、詳細説明、引数、戻り値などを
11日本語で記述します。既存のファイルを上書きするか、更新日時でスキップするかなど、
12柔軟なファイル処理オプションを提供します。
13
14:doc:`add_docstring_usage`
15"""
16
17import os
18import sys
19import argparse
20import glob
21import time
22import re
23import traceback
24from pathlib import Path
25
26# AI連携用ライブラリのインポート
27try:
28 from tkai_lib import read_ai_config
29 from tkai_lib import query_openai4, query_openai5, query_google
30 from tkai_lib import extract_openai5_text
31except ImportError:
32 print("Error: tkai_lib.py が見つかりません。ライブラリのパスを確認してください。", file=sys.stderr)
33 sys.exit(1)
34
35# =========================================================
36# INI検索・読み込みロジック (explain_program5.py の設計を継承)
37# =========================================================
38def search_file(infile=None):
39 """
40 指定されたファイルを作業ディレクトリとスクリプトディレクトリから検索する。
41
42 `infile` が指定されない場合、実行スクリプト名からデフォルトのINIファイル名を生成します。
43 まずカレントディレクトリでファイルを検索し、見つからない場合はスクリプトが
44 配置されているディレクトリで検索します。
45
46 :param infile: str, optional: 検索対象のファイル名。デフォルトはNone。
47 :returns: str or None: 見つかったファイルの絶対パス、またはNone。
48 """
49 script_path = os.path.abspath(sys.argv[0])
50 script_dir = os.path.dirname(script_path)
51 script_name = os.path.splitext(os.path.basename(script_path))[0]
52 default_ini = f"{script_name}.ini"
53
54 search_target = infile if infile else default_ini
55
56 candidate1 = os.path.join(os.getcwd(), search_target)
57 if os.path.isfile(candidate1):
58 return candidate1
59
60 candidate2 = os.path.join(script_dir, search_target)
61 if os.path.isfile(candidate2):
62 return candidate2
63
64 return None
65
66def read_ini(inifile=None):
67 """
68 INIファイルの内容を読み込み、辞書として返す。
69
70 `search_file` を使用してINIファイルのパスを特定し、ファイルを読み込みます。
71 `#` または `;` で始まる行はコメントとしてスキップされます。
72 `key = value` 形式の行を解析し、トリプルクォート (`\"\"\"` または `\'\'\'`) を
73 使用した複数行の値をサポートします。また、読み込み後には `$VAR` 形式の
74 変数を展開します。
75
76 :param inifile: str, optional: 読み込むINIファイルのパス。デフォルトはNone。
77 :returns: dict: INIファイルから読み込んだ設定を格納する辞書。
78 :raises FileNotFoundError: 指定されたINIファイルが見つからない場合。
79 """
80 path = search_file(inifile)
81 if path is None:
82 raise FileNotFoundError(f"INIファイルが見つかりませんでした: {inifile}")
83
84 result = {}
85 variables = {}
86 current_key = None
87 multiline_val = []
88 multiline_delim = None
89
90 with open(path, 'r', encoding='utf-8') as f:
91 for line in f:
92 line = line.rstrip()
93 if not line or line.startswith('#') or line.startswith(';'):
94 continue
95
96 if multiline_delim:
97 if line.strip() == multiline_delim:
98 val = '\n'.join(multiline_val)
99 result[current_key] = val
100 variables[current_key] = val
101 current_key = None
102 multiline_val = []
103 multiline_delim = None
104 else:
105 multiline_val.append(line)
106 continue
107
108 if '=' in line:
109 key, val = map(str.strip, line.split('=', 1))
110 if (val == '\"\"\"' or val == "\'\'\'" or
111 (val.startswith('\"\"\"') and not val.endswith('\"\"\"')) or
112 (val.startswith("\'\'\'") and not val.endswith("\'\'\'"))):
113 multiline_delim = val[:3]
114 content = val[3:]
115 multiline_val = [content] if content else []
116 current_key = key
117 continue
118
119 if (val.startswith('\"\"\"') and val.endswith('\"\"\"')) or \
120 (val.startswith("\'\'\'") and val.endswith("\'\'\'")):
121 val = val[3:-3]
122
123 result[key] = val
124 variables[key] = val
125
126 for key, val in result.items():
127 def expand_var(match):
128 return variables.get(match.group(1), match.group(0))
129 result[key] = re.sub(r"\$(\w+)\b", expand_var, val)
130
131 return result
132
133# =========================================================
134# メイン処理
135# =========================================================
136def initialize():
137 """
138 コマンドライン引数パーサーを初期化し、設定する。
139
140 `argparse.ArgumentParser` を初期化し、ツールの説明を設定します。
141 必要な引数(`pattern`, `output`)とオプション引数(`--inifile`, `--api`,
142 `--update`, `--overwrite`, `--pause`)を追加します。
143 `default_ini_name` は実行スクリプト名に基づいて生成されます。
144
145 :returns: argparse.ArgumentParser: 設定済みのパーサーオブジェクト。
146 """
147 default_ini_name = os.path.splitext(os.path.basename(sys.argv[0]))[0] + ".ini"
148
149 parser = argparse.ArgumentParser(description="PythonコードにSphinx形式のDocstringを自動追加するツール")
150 # explain_program5.py に合わせた引数構成
151 parser.add_argument("pattern", help="対象ファイルのワイルドカード(例: '*.py')")
152 parser.add_argument("output", nargs="?", help="出力名(単一ファイル時のみ)")
153 parser.add_argument("--inifile", default=default_ini_name, help="プロンプト設定ファイルパス")
154 parser.add_argument("--api", choices=["openai", "openai5", "google", "gemini"], default='google')
155 parser.add_argument("-u", "--update", type=int, default=0, help="1の場合、ソースが新しい場合のみ更新")
156 parser.add_argument("-w", "--overwrite", type=int, default=0, help="1の場合、既存ファイルを上書き")
157 parser.add_argument("-p", "--pause", type=int, default=1)
158 return parser
159
160def read_args(parser):
161 """
162 ArgumentParserから引数を解析し、その値を表示する。
163
164 `parser.parse_args()` を呼び出して引数を解析し、
165 解析された引数の値を標準出力に表示します。
166
167 :param parser: argparse.ArgumentParser: 初期化済みのArgumentParserオブジェクト。
168 :returns: argparse.Namespace: 解析された引数を格納するオブジェクト。
169 """
170 args = parser.parse_args()
171 print("Args:")
172 print(f" {args.pattern=}")
173 print(f" {args.output=}")
174 print(f" {args.inifile=}")
175 print(f" {args.api=}")
176 print(f" {args.update=}")
177 print(f" {args.overwrite=}")
178 print(f" {args.pause=}")
179 return args
180
181def main():
182 """
183 スクリプトのメイン処理を実行する。
184
185 `initialize` と `read_args` を呼び出してコマンドライン引数を処理します。
186 AI設定ファイル (`ai.env`) を読み込み、指定されたINIファイルからプロンプト設定を取得します。
187 ワイルドカードパターンに合致するファイル群を処理し、各ファイルについて
188 更新・上書きルールに従って処理をスキップするか判断します。
189 ファイル内容を読み込み、プロンプトを構築してAIモデルを呼び出します。
190 AIからの応答からDocstringを抽出し、指定された出力ファイルに書き込みます。
191 エラー発生時はメッセージを表示し、スタックトレースを出力します。
192 API呼び出しの負荷軽減のため、ファイル処理ごとに1秒待機し、
193 `pause` 引数が設定されている場合、終了前にユーザー入力を待ちます。
194
195 :returns: None
196 :raises SystemExit: INIファイルの読み込み失敗時や、マッチするファイルがない場合に発生。
197 """
198 print()
199 print(f"=== {sys.argv[0]} ===")
200
201 parser = initialize()
202 args = read_args(parser)
203
204 read_ai_config("ai.env")
205
206 try:
207 ini_data = read_ini(args.inifile)
208 print(f"Loaded INI: {search_file(args.inifile)}")
209 except Exception as e:
210 print(f"Error loading INI: {e}")
211 sys.exit(1)
212
213 # ワイルドカード展開
214 files = glob.glob(args.pattern)
215 if not files:
216 print(f"No files matched pattern: {args.pattern}")
217 sys.exit(1)
218
219 # 出力ファイル名のリスト作成
220 if args.output and len(files) == 1:
221 outputs = [args.output]
222 else:
223 outputs = [os.path.splitext(f)[0] + "_docstring.py" for f in files]
224
225 for inp, out in zip(files, outputs):
226 # 更新・上書きチェックロジック
227 if os.path.exists(out) and not args.overwrite:
228 # updateモードかつ、出力ファイルの方が新しい場合はスキップ
229 if not args.update or os.path.getmtime(out) >= os.path.getmtime(inp):
230 print(f"Skip: {out}")
231 continue
232
233 print(f"Processing: {inp} -> {out}")
234
235 try:
236 code = Path(inp).read_text(encoding="utf-8")
237 except:
238 try:
239 code = Path(inp).read_text(encoding="shift-jis")
240 except Exception as e:
241 print(f"Error reading {inp}: {e}")
242 continue
243
244 base_name = os.path.splitext(os.path.basename(inp))[0]
245 template = ini_data.get("PROMPT_MAIN", "")
246 role = ini_data.get("SYSTEM_ROLE", "Assistant")
247
248 prompt = template.replace("{{script_name}}", inp)\
249 .replace("{{code}}", code)\
250 .replace("add_docstring", base_name)
251
252 try:
253 if args.api == "openai5":
254 res = query_openai5(prompt, os.getenv("openai_model5"), instructions=role)
255 doc = extract_openai5_text(res)
256 elif args.api == "openai":
257 res = query_openai4(prompt, os.getenv("openai_model"), role=role)
258 doc = res.choices[0].message.content
259 else:
260 res = query_google(prompt, os.getenv("gemini_model"), role=role)
261 doc = res.text if res else None
262
263 if doc:
264 # Markdownブロックの除去
265 doc = doc.strip()
266 if doc.startswith("```"):
267 doc = re.sub(r'^```[a-zA-Z]*\n', '', doc)
268 doc = re.sub(r'\n```$', '', doc)
269
270 Path(out).write_text(doc, encoding="utf-8")
271 print(f"Done: {out}")
272 else:
273 print(f"Error: No response from AI for {inp}")
274
275 except Exception:
276 print(f"\n!!! Error during AI processing for {inp} !!!")
277 traceback.print_exc()
278
279 time.sleep(1) # API連続呼び出しの負荷軽減
280
281 if args.pause:
282 input("\nPress ENTER to terminate>>\n")
283
284if __name__ == "__main__":
285 main()
add_docstring.ini (設定ファイル)
1# システムの役割
2SYSTEM_ROLE = """
3あなたはPythonのドキュメンテーションエンジニアです。提供するソースコードの内容を解析し、
4Sphinx (reStructuredText) 形式に最適化された高品質なDocstringのみを生成してください。
5"""
6
7# メインのプロンプトテンプレート
8PROMPT_MAIN = """
9【絶対遵守ルール】
10 1. 既存のロジック(実行コード)は一切変更しないこと。
11 2. Docstringは日本語で出力すること。
12 3. 出力は、既存のコードにDocstringを挿入した「完成版のコード全体」として出力すること。
13 4. 各関数・クラスの冒頭に、適切な \"\"\"Docstring\"\"\" を追加すること。
14
15【Docstringの構成要素】
16 - 概要: 1行で何をする関数か記述。
17 - 詳細説明: 必要に応じて動作の詳細を記述。
18 - 引数 (Parameters): :param name: 形式で型と説明を記述。
19 - 戻り値 (Returns): :returns: 形式で型と説明を記述。
20 - 関連リンク: モジュールの冒頭に、別のドキュメントへのリンク(例: :doc:`{{base_name}}_usage`)を含めること。
21
22【対象ソースコード: {{script_name}}】
23{{code}}
24"""
make_sphinx_files.py (プログラム)
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3"""
4Sphinxドキュメント自動生成スクリプト。
5
6指定されたPythonスクリプトファイルに対して、Docstringの追加、プログラム解説の生成、
7Sphinx用の各種RST/MDファイルの作成、および関連するファイルのバックアップと置換を自動化します。
8`add_docstring.py` と `explain_program5.py` を利用して、AIによるドキュメント生成を行います。
9
10:doc:`make_sphinx_files_usage`
11"""
12
13import os
14import sys
15import argparse
16import shutil
17from glob import glob
18import subprocess
19import traceback
20from pathlib import Path
21from datetime import datetime
22
23
24SCRIPT_FULLPATH = os.path.abspath(sys.argv[0])
25SCRIPT_DIR = os.path.dirname(SCRIPT_FULLPATH)
26SCRIPT_BASENAME = os.path.splitext(os.path.basename(SCRIPT_FULLPATH))[0]
27
28# 起動スクリプトのディレクトリに基づいたパス設定
29ADD_DOCSTRING_PATH = os.path.join(SCRIPT_DIR, "add_docstring.py")
30EXPLAIN_PROGRAM_PATH = os.path.join(SCRIPT_DIR, "explain_program5.py")
31DEFAULT_INI_PATH = os.path.join(SCRIPT_DIR, f"{SCRIPT_BASENAME}.ini")
32
33
34def relative_from_source(argv0: str) -> str:
35 """
36 ファイルパスから'source'ディレクトリ以下の相対パスを取得する。
37
38 指定されたファイルパスを解決し、そのパス中に 'source' ディレクトリがあれば、
39 その直下からの相対パス(ファイル名を除いたディレクトリ部分)を返す。
40 'source' が見つからない場合は空文字列を返す。
41
42 :param argv0: プログラムのパス。通常 `sys.argv[0]` が渡される。
43 :returns: 'source' ディレクトリ以下の相対パスの文字列。
44 """
45 p = Path(argv0).resolve()
46 parts = p.parts
47
48 # 最初に出現する "source" を探す
49 try:
50 idx = parts.index("source")
51 except ValueError:
52# raise RuntimeError("'source' ディレクトリがパスに含まれていません")
53 return ""
54
55 # source 以下のパス(ファイル名付き)
56 rel_path = Path(*parts[idx+1:])
57
58 # ファイル名を削除してディレクトリだけにする
59 return str(rel_path.parent)
60
61
62def run_step(message, cmd_list):
63 """
64 作業ステップを表示し、外部コマンドを実行します。
65
66 指定されたメッセージを表示した後、`subprocess.run` を使用してコマンドリストを実行します。
67 コマンドの実行結果が成功(終了コード0)であれば `True` を返し、
68 エラーが発生した場合はエラーメッセージを表示して `False` を返します。
69
70 :param message: 実行するステップのメッセージ。
71 :param cmd_list: 実行するコマンドとその引数を要素とするリスト。
72 :returns: コマンドが正常に実行された場合は `True`、それ以外は `False`。
73 """
74 print(f"\n>>> {message}")
75 print(f" コマンド: {' '.join(cmd_list)}")
76 try:
77 result = subprocess.run(cmd_list, text=True, errors='ignore')
78# result = subprocess.run(cmd_list, capture_output=True, text=True, encoding='utf-8', errors='ignore')
79 if result.returncode != 0:
80 print(f"!!! エラーが発生しました:\n{result.stderr}")
81 return False
82 return True
83 except Exception as e:
84 print(f"!!! 実行エラー: {e}")
85 return False
86
87def make_init_py(path):
88 """
89 指定されたパスに `__init__.py` ファイルを作成します。
90
91 既にファイルが存在する場合はその旨を表示し、存在しない場合は空の `__init__.py` ファイルを作成します。
92 これはPythonパッケージとして認識させるために必要です。
93
94 :param path: `__init__.py` を作成するパス。
95 """
96 if os.path.exists(path):
97 print(f">>> Step: {path} が見つかりました。")
98 else:
99 print(f">>> Step: {path} を作成中...")
100 with open(path, "w", encoding="utf-8") as f:
101 f.write("")
102 print(f" Done: {path}")
103
104
105def make_index_template(path, module_path):
106 """
107 `index.template` ファイルを作成または更新します。
108
109 プロジェクト全体のSphinxインデックスファイルとなる `index.template` を作成または追記します。
110 ファイルが存在しない場合は新規作成し、基本的なtoctree構造を含みます。
111 存在する場合は、指定された `module_path` をtoctreeに追加します。
112
113 :param path: `index.template` ファイルのパス。
114 :param module_path: Sphinxドキュメントのインデックスに追加するモジュールパス。
115 """
116 if os.path.exists(path):
117 print(f">>> Step: {path} に追加中...")
118 with open(path, "a", encoding="utf-8") as f:
119 f.write(f" {module_path}_index\n")
120 print(f" Done: {path}")
121 else:
122 print(f">>> Step: {path} を作成中...")
123 content = f"""\
124プロジェクト全体ドキュメント
125============================
126
127.. toctree::
128 :maxdepth: 2
129 :caption: メインメニュー:
130 :hidden:
131 :glob:
132
133 {module_path}_index
134"""
135 with open(path, "w", encoding="utf-8") as f:
136 f.write(content)
137 print(f" Done: {path}")
138
139def make_index_rst(index_rst, base_name, package_path):
140 """
141 Sphinxのモジュール別インデックス (`_index.rst`) ファイルを作成します。
142
143 指定された `base_name` と `package_path` を使用して、
144 モジュールごとのドキュメントのトップページとなる `.rst` ファイルを作成します。
145 このファイルには、usage, examples, apiへのリンクを含むtoctreeが定義されます。
146
147 :param index_rst: 作成する `.rst` ファイルのパス。
148 :param base_name: ベースとなるファイル名(拡張子なし)。
149 :param package_path: Pythonパッケージのフルパス。
150 """
151 print(f">>> Step: {index_rst} を作成中...")
152 index_content = f"""{base_name} ドキュメント
153============================================================================
154
155.. toctree::
156 :maxdepth: 1
157
158 {base_name}_usage
159 {base_name}_examples
160 {base_name}_api
161"""
162 with open(index_rst, "w", encoding="utf-8") as f:
163 f.write(index_content)
164 print(f" Done: {index_rst}")
165
166def make_api_rst(api_rst, base_name, package_path):
167 """
168 SphinxのAPIドキュメント (`_api.rst`) ファイルを作成します。
169
170 指定された `base_name` と `package_path` を使用して、
171 PythonモジュールのAPIリファレンスとなる `.rst` ファイルを作成します。
172 `automodule` ディレクティブを用いて、自動的にメンバー、非公開メンバー、継承情報を抽出する設定を行います。
173
174 :param api_rst: 作成するAPI `.rst` ファイルのパス。
175 :param base_name: ベースとなるファイル名(拡張子なし)。
176 :param package_path: Pythonパッケージのフルパス。
177 """
178 print(f">>> Step: {api_rst} を作成中...")
179 api_content = f"""{base_name} プログラム仕様
180============================================================================
181
182.. currentmodule:: {package_path}
183
184.. automodule:: {package_path}
185 :members:
186 :undoc-members:
187 :show-inheritance:
188"""
189 with open(api_rst, "w", encoding="utf-8") as f:
190 f.write(api_content)
191 print(f" Done: {api_rst}")
192
193def make_examples_md(examples_md, infile, base_name):
194 """
195 実行例を示すMarkdownファイル (`_examples.md`) を作成します。
196
197 指定された `infile` に対して `--help` オプションを実行してヘルプ出力を取得し、
198 現在のディレクトリ内の関連する画像ファイルやデータファイルを検出し、
199 それらを組み込んだMarkdown形式の実行例ファイルを作成します。
200
201 :param examples_md: 作成する実行例Markdownファイルのパス。
202 :param infile: ヘルプ出力を取得する対象のPythonスクリプトファイル。
203 :param base_name: ベースとなるファイル名(拡張子なし)。
204 """
205 print(f">>> Step: {examples_md} テンプレートを作成中...")
206
207# 画像ファイルの自動検出
208 image_files = sorted(
209 f for f in os.listdir(".")
210 if f.startswith(base_name) and f.lower().endswith((".png", ".jpg", ".jpeg"))
211 )
212
213# データファイルの自動検出(CSV / Excel / TXT)
214 data_files = sorted(
215 f for f in os.listdir(".")
216 if f.startswith(base_name) and f.lower().endswith((".csv", ".xlsx", ".xls", ".txt"))
217 )
218
219 print("Image files:", image_files)
220 print("Data files:", data_files)
221
222
223 # ヘルプ出力を取得
224 print(" help logを取得します")
225 result = subprocess.run(["python", infile, "--help"],
226 text=True, capture_output = True)
227 print(" return code:", result.returncode)
228 if result.returncode == 0:
229 help_log = result.stdout + "\n" + result.stderr
230# print(" help log:", help_log)
231 else:
232 help_log = "(ヘルプの自動取得に失敗しました。ここに実行ログを貼り付けてください)"
233
234 if data_files:
235 data_section = "## データファイル\n"
236 for df in data_files:
237 data_section += f"- [{df}](./{df})\n"
238 data_section += "\n"
239 else:
240 data_section = "## 生成されたデータファイル\n(データファイルが見つかりませんでした)\n\n"
241
242 if image_files:
243 image_section = "## 画像ファイル\n\n"
244 for img in image_files:
245 image_section += f"- [{img}](./{img})\n"
246 image_section += f"\n\n"
247 else:
248 image_section = "## 生成された画像一覧\n(画像ファイルが見つかりませんでした)\n\n"
249
250
251 examples_content = f"""# {base_name} 実行例
252
253## help出力 `{base_name}.py --help`
254
255<pre style="background-color: #f4f4f4; border: 1px solid #ccc; padding: 10px; border-radius: 5px; font-family: 'Courier New', Courier, monospace; overflow-x: auto;">
256{help_log}
257</pre>
258
259{data_section}
260
261{image_section}
262
263"""
264
265 with open(examples_md, "w", encoding="utf-8") as f:
266 f.write(examples_content)
267 print(f" Done: {examples_md}")
268
269
270def main(args):
271 """
272 Sphinxドキュメント生成の主要な処理を実行します。
273
274 入力ファイルから基本情報を抽出し、`__init__.py`, `index.template`,
275 `_index.rst`, `_api.rst`, `_examples.md` といったSphinx関連ファイルを生成します。
276 その後、`explain_program5.py` と `add_docstring.py` を実行して、
277 プログラム解説とDocstringを生成・追加し、最後に元のファイルをバックアップし、
278 Docstringが追加されたファイルで置き換えます。
279
280 :param args: コマンドライン引数を格納した `argparse.Namespace` オブジェクト。
281 """
282 infile = args.infile
283 if not os.path.exists(infile):
284 print(f"エラー: 入力ファイル '{infile}' が見つかりません。")
285 return
286
287 # 基本情報の整理
288 base_name = os.path.splitext(os.path.basename(infile))[0]
289 date_str = datetime.now().strftime("%Y%m%d")
290
291 # ファイル名定義
292 init_py = "__init__.py"
293 index_template = "index.template"
294 docstring_out = f"{base_name}_docstring.py"
295 backup_file = f"{base_name}_{date_str}.py"
296 usage_md = f"{base_name}_usage.md"
297 examples_md = f"{base_name}_examples.md"
298 index_rst = f"{base_name}_index.rst"
299 api_rst = f"{base_name}_api.rst"
300
301 # カレントディレクトリ名からパッケージパスを判定 (010...等の数値ディレクトリを想定)
302 current_dir_name = os.path.basename(os.getcwd())
303 # フォルダ名が 010xx_page のような形式ならモジュールパスに含める
304# module_path = f"{current_dir_name}.{base_name}" if "page" in current_dir_name else base_name
305 if args.subdir is not None and args.subdir != "":
306 module_path = os.path.join(args.subdir, base_name)
307 package_path = f"{args.subdir}.{base_name}"
308 else:
309 package_path = module_path
310 module_path = base_name
311
312 print("="*60)
313 print(f" プロジェクト: {base_name} のSphinxファイル自動生成を開始します")
314 print(f" モジュールパス: {module_path}")
315 print(f" パッケージパス: {package_path}")
316 print("="*60)
317
318 make_init_py(init_py)
319 make_index_template(index_template, module_path)
320 make_index_rst(index_rst, base_name, package_path)
321 make_api_rst(api_rst, base_name, package_path)
322 make_examples_md(examples_md, infile, base_name)
323
324 args_list = ["--api", args.api, "--update", str(args.update), "--overwrite", str(args.overwrite),
325 "--pause", str(args.pause)]
326
327 # explain_program.py の実行
328 if not run_step("Step: EXPLAIN_PROGRAM_PATH を実行してプログラム解説を生成中...",
329 ["python", EXPLAIN_PROGRAM_PATH, infile, *args_list]):
330 return
331
332 # add_docstring.py の実行
333 if not run_step("Step: ADD_DOCSTRING_PATH を実行してDocstringを追加中...",
334 ["python", ADD_DOCSTRING_PATH, infile, *args_list]):
335 return
336
337#======================================================================
338 # バックアップの作成
339 print(f">>> Step: オリジナルファイルのバックアップを作成中...")
340 if os.path.exists(infile):
341 shutil.copy2(infile, backup_file)
342 print(f" Done: {infile} -> {backup_file}")
343 else:
344 print("!!! バックアップ対象のファイルが見つかりません。")
345 return
346
347#======================================================================
348 # ファイルの置き換え
349 print(f">>> Step: 生成されたDocstring版ファイルを {infile} にリネーム中...")
350 if os.path.exists(docstring_out):
351 os.replace(docstring_out, infile)
352 print(f" Done: {docstring_out} -> {infile}")
353 with open(docstring_out, "w") as fp:
354 fp.write("")
355 print(f" ダミーの空ファイル {docstring_out} を作りました")
356 else:
357 print("!!! Docstring版ファイルが生成されていなかったため、リネームをスキップします。")
358 return
359
360
361 print("\n" + "="*60)
362 print(" 全ての自動生成プロセスが正常に終了しました。")
363 print("="*60)
364
365
366def initialize():
367 """
368 コマンドライン引数パーサーを初期化し、設定します。
369
370 `argparse.ArgumentParser` オブジェクトを生成し、スクリプトの説明、
371 必要な引数 (`infile`)、およびオプション引数 (`subdir`, `api`, `update`, `overwrite`, `pause`) を定義します。
372
373 :returns: 設定済みの `ArgumentParser` オブジェクト。
374 """
375 parser = argparse.ArgumentParser(
376 description="Sphinxドキュメント生成の一連のルーチン(Docstring追加、バックアップ、解説生成、RST作成)を自動化します。"
377 )
378 parser.add_argument("infile", help="対象となるPythonスクリプトファイル名 (例: mu_fit.py)")
379 parser.add_argument("--subdir", default=None, help="sourceディレクトリからの相対パス")
380
381 parser.add_argument("--api", choices=["openai", "openai5", "google", "gemini"], default='google')
382 parser.add_argument("-u", "--update", type=int, default=1)
383 parser.add_argument("-w", "--overwrite", type=int, default=0)
384 parser.add_argument("-p", "--pause", type=int, default=0)
385 return parser
386
387
388def read_args(parser):
389 """
390 コマンドライン引数を解析し、整形して返します。
391
392 与えられた `ArgumentParser` を使用してコマンドライン引数を解析します。
393 `subdir` が指定されていない場合は `relative_from_source` 関数を使って自動的に設定します。
394 解析された引数を表示し、`argparse.Namespace` オブジェクトとして返します。
395
396 :param parser: `argparse.ArgumentParser` オブジェクト。
397 :returns: 解析された引数を格納する `argparse.Namespace` オブジェクト。
398 """
399 args = parser.parse_args()
400 if args.subdir is None:
401 args.subdir = relative_from_source(args.infile)
402
403 print()
404 print("Args:")
405 print(f" {args.infile=}")
406 print(f" {args.subdir=}")
407 print(f" {args.api=}")
408 print(f" {args.update=}")
409 print(f" {args.overwrite=}")
410 print(f" {args.pause=}")
411
412 return args
413
414
415if __name__ == "__main__":
416 print()
417 print(f"=== {sys.argv[0]} ===")
418 print(f"{EXPLAIN_PROGRAM_PATH=}")
419 print(f"{ADD_DOCSTRING_PATH=}")
420
421 parser = initialize()
422 args = read_args(parser)
423
424 try:
425 main(args)
426 except Exception:
427 print("\n" + "!"*60)
428 print(" 予期せぬ致命的なエラーが発生しました。")
429 traceback.print_exc()
430 print("!"*60)
431 sys.exit(1)
debug_sphinx-build.py (プログラム)
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3
4"""
5Sphinx autodocにおけるimportエラーを調査するためのユーティリティスクリプト。
6
7このスクリプトは、Sphinxのビルドプロセスで発生する可能性のあるPythonモジュールのimportエラーをデバッグするために設計されています。
8特に、`autodoc_mock_imports`の設定が正しく機能しているか、または他の原因でimportエラーが発生しているかを特定するのに役立ちます。
9
10目的:
11- `./source/conf.py` をインポートし、`autodoc_mock_imports` の設定を読み込む。
12- 読み取った `autodoc_mock_imports` に基づいて、ダミーモジュールを `sys.modules` に登録する。
13- 指定された `source` ディレクトリ配下のすべての `.py` ファイルを再帰的にインポートする。
14- インポート中に発生したエラーを記録し、必要に応じて処理を継続する。
15
16仕様:
17- スクリプト実行時、`--source` 引数で指定されたディレクトリ (デフォルトは `source`) をSphinxのソースディレクトリとして扱います。
18- `conf.py` は他のファイルよりも先に個別にインポートされ、その設定が適用されます。
19- `conf.py` 以外の `.py` ファイルは、`source` ディレクトリからの相対パスに基づいて一意な疑似モジュール名でインポートされます。
20- ファイル名が `_数字列` で終わるファイル (例: `sample_1.py`, `test_2024.py`) はスキップされます。
21- デフォルトでは、インポート中に例外が発生した場合、トレースバックを表示してスクリプトは終了します。
22- `--keep-going` オプションを指定すると、エラーが発生しても処理を最後まで継続し、最後にすべての失敗一覧を表示します。
23- `--mock` オプションを使用すると、`conf.py` をインポートする前に手動でモジュールをモックできます。
24- `sys.path` にカレントディレクトリやソースディレクトリを追加するオプションも提供されます。
25
26:doc:`debug_sphinx-build_usage`
27"""
28
29from __future__ import annotations
30
31import argparse
32import importlib.util
33import re
34import sys
35import traceback
36import types
37from pathlib import Path
38
39
40class DummyObject:
41 """
42 どんな属性アクセスや関数呼び出しでも受け流すダミーオブジェクト。
43
44 このクラスは、存在しないモジュールやオブジェクトに対する参照、
45 メソッド呼び出しなどが発生した場合にエラーを回避するために使用されます。
46 どのような操作に対しても自身を返すか、新しい `DummyObject` を生成することで、
47 実行を中断させずに処理を継続させます。
48 """
49
50 def __init__(self, name: str = "dummy"):
51 """
52 DummyObject を初期化します。
53
54 :param name: ダミーオブジェクトの内部的な名前。デバッグ表示に使用されます。
55 :type name: str
56 """
57 self._name = name
58
59 def __call__(self, *args, **kwargs):
60 """
61 オブジェクトが関数として呼び出されたときに実行されます。
62
63 呼び出されたことを示す新しい `DummyObject` を返します。
64
65 :param args: 呼び出し時の位置引数。
66 :param kwargs: 呼び出し時のキーワード引数。
67 :returns: 新しい `DummyObject` インスタンス。
68 :rtype: DummyObject
69 """
70 return DummyObject(f"{self._name}()")
71
72 def __getattr__(self, name: str):
73 """
74 オブジェクトの属性がアクセスされたときに実行されます。
75
76 アクセスされた属性名を持つ新しい `DummyObject` を返します。
77
78 :param name: アクセスされた属性の名前。
79 :type name: str
80 :returns: 新しい `DummyObject` インスタンス。
81 :rtype: DummyObject
82 """
83 return DummyObject(f"{self._name}.{name}")
84
85 def __repr__(self):
86 """
87 オブジェクトの文字列表現を返します。
88
89 :returns: `DummyObject` の文字列表現。
90 :rtype: str
91 """
92 return f"<DummyObject {self._name}>"
93
94
95class DummyModule(types.ModuleType):
96 """
97 属性アクセス時に `DummyObject` を返すダミーモジュール。
98
99 `sys.modules` に登録することで、存在しないモジュールがimportされようとした際に、
100 実際には何もしないダミーのモジュールとして機能させます。
101 これにより、モジュール内の属性アクセスや関数呼び出しがすべて `DummyObject` に
102 よって処理され、`AttributeError` や `ImportError` を回避できます。
103 """
104
105 def __getattr__(self, name: str):
106 """
107 モジュールの属性がアクセスされたときに実行されます。
108
109 アクセスされた属性名を持つ `DummyObject` を生成し、その属性として設定して返します。
110
111 :param name: アクセスされた属性の名前。
112 :type name: str
113 :returns: 新しい `DummyObject` インスタンス。
114 :rtype: DummyObject
115 """
116 dummy = DummyObject(f"{self.__name__}.{name}")
117 setattr(self, name, dummy)
118 return dummy
119
120
121def install_mock_modules(module_names: list[str]) -> None:
122 """
123 指定したモジュール名を `sys.modules` にダミーモジュールとして登録する。
124
125 モジュール名が `pkg.subpkg` のように階層的である場合、`pkg` と `pkg.subpkg` の両方を
126 ダミーモジュールとして登録し、ネストされたimportをサポートします。
127 既に登録されているモジュールは上書きされません。
128
129 :param module_names: モックするモジュール名のリスト。
130 :type module_names: list[str]
131 :returns: None
132 """
133 for name in module_names:
134 name = name.strip()
135 if not name:
136 continue
137
138 parts = name.split(".")
139 for i in range(1, len(parts) + 1):
140 subname = ".".join(parts[:i])
141 if subname not in sys.modules:
142 sys.modules[subname] = DummyModule(subname)
143
144
145def load_module_from_file(module_name: str, file_path: Path):
146 """
147 任意の .py ファイルを `module_name` という名前でインポートする。
148
149 `importlib.util` を使用してファイルからモジュールをロードし、
150 `sys.modules` に登録します。これにより、通常の `import` 文と同様に
151 モジュールをロードし、そのオブジェクトにアクセスできるようになります。
152
153 :param module_name: インポートするモジュールの名前。`sys.modules` に登録される名前となります。
154 :type module_name: str
155 :param file_path: インポート対象の .py ファイルのパス。
156 :type file_path: Path
157 :returns: ロードされたモジュールオブジェクト。
158 :raises ImportError: モジュールのスペックを生成できなかった場合。
159 """
160 spec = importlib.util.spec_from_file_location(module_name, str(file_path))
161 if spec is None or spec.loader is None:
162 raise ImportError(f"spec を作成できませんでした: {file_path}")
163
164 module = importlib.util.module_from_spec(spec)
165 sys.modules[module_name] = module
166 spec.loader.exec_module(module)
167 return module
168
169
170def make_module_name(source_dir: Path, py_file: Path) -> str:
171 """
172 `source_dir` 配下の相対パスから、一意な疑似モジュール名を生成する。
173
174 ファイルパスをPythonの識別子として有効な形式に変換し、モジュール名の衝突を避けるために
175 `sphinx_debug.` というプレフィックスを付与します。
176 ファイル名やディレクトリ名に含まれるハイフン (`-`) やスペース (` `) はアンダースコア (`_`) に変換され、
177 無効な文字を含む識別子にはさらにプレフィックスが追加されます。
178
179 :param source_dir: Sphinxのソースディレクトリのパス。このパスからの相対パスがモジュール名の基になります。
180 :type source_dir: Path
181 :param py_file: 疑似モジュール名を生成する対象の .py ファイルのパス。
182 :type py_file: Path
183 :returns: 生成された一意な疑似モジュール名。
184 :rtype: str
185 """
186 rel = py_file.relative_to(source_dir).with_suffix("")
187 parts = []
188
189 for p in rel.parts:
190 p2 = p.replace("-", "_").replace(" ", "_")
191 if not p2.isidentifier():
192 p2 = "_" + "".join(ch if (ch.isalnum() or ch == "_") else "_" for ch in p2)
193 parts.append(p2)
194
195 return "sphinx_debug." + ".".join(parts)
196
197
198def should_skip(py_file: Path) -> tuple[bool, str]:
199 """
200 特定の条件に基づいてファイルをスキップするかどうかを判定する。
201
202 以下の条件に合致する場合、ファイルはスキップされます。
203 - ファイル名が `conf.py` である場合 (このファイルは個別に先に処理されるため)。
204 - ファイルのステム (拡張子を除いた部分) が `_数字列` で終わる場合。
205 例: `sample_1.py`, `test_2024.py`
206
207 :param py_file: 判定対象のファイルパス。
208 :type py_file: Path
209 :returns: スキップするかどうかの真偽値と、スキップ理由の文字列のタプル。
210 :rtype: tuple[bool, str]
211 """
212 if py_file.name == "conf.py":
213 return True, "conf.py は先に個別 import 済み"
214
215 if py_file.stem.endswith("_docstring"):
216 return True, "stem が _docstring で終わる"
217
218 if re.search(r"_\d+$", py_file.stem):
219 return True, "stem が _日付 で終わる"
220
221 return False, ""
222
223
224def parse_mock_arg(mock_text: str) -> list[str]:
225 """
226 コマンドライン引数 `--mock` で渡された文字列をモジュール名のリストに変換する。
227
228 入力文字列はセミコロン (`;`) で区切られたモジュール名を含むと想定されます。
229 各モジュール名から前後の空白が除去され、空の文字列は無視されます。
230 例: `"whisper;torch;numba"` は `["whisper", "torch", "numba"]` に変換されます。
231
232 :param mock_text: モックするモジュール名をセミコロン区切りで含む文字列。
233 :type mock_text: str
234 :returns: モックするモジュール名のリスト。
235 :rtype: list[str]
236 """
237 if not mock_text:
238 return []
239 return [x.strip() for x in mock_text.split(";") if x.strip()]
240
241
242def record_error(errors: list[dict], phase: str, file_path: Path, exc: BaseException) -> None:
243 """
244 発生したエラーの詳細をエラーリストに記録する。
245
246 エラー情報には、発生フェーズ、ファイルパス、エラータイプ、メッセージ、完全なトレースバックが含まれます。
247 これは `--keep-going` オプションが指定された場合に、複数のエラーをまとめて報告するために使用されます。
248
249 :param errors: エラー情報を格納するリスト。各エラーは辞書としてこのリストに追加されます。
250 :type errors: list[dict]
251 :param phase: エラーが発生した処理フェーズを示す文字列(例: "conf.py", "module")。
252 :type phase: str
253 :param file_path: エラーが発生したファイルのパス。
254 :type file_path: Path
255 :param exc: 発生した例外オブジェクト。
256 :type exc: BaseException
257 :returns: None
258 """
259 tb_text = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
260 errors.append(
261 {
262 "phase": phase,
263 "file": str(file_path),
264 "error_type": type(exc).__name__,
265 "message": str(exc),
266 "traceback": tb_text,
267 }
268 )
269
270
271def print_error_summary(errors: list[dict]) -> None:
272 """
273 記録されたエラーの要約を表示する。
274
275 エラーがない場合はその旨を表示し、ある場合は各エラーのファイルパス、エラータイプ、
276 エラーメッセージを一覧形式でコンソールに出力します。
277
278 :param errors: 記録されたエラー情報のリスト。
279 :type errors: list[dict]
280 :returns: None
281 """
282 print("\n" + "=" * 80)
283 print("[SUMMARY] import 失敗一覧")
284 print("=" * 80)
285
286 if not errors:
287 print("[SUMMARY] エラーはありません")
288 return
289
290 for i, err in enumerate(errors, 1):
291 print(f"{i}. [{err['phase']}] {err['file']}")
292 print(f" {err['error_type']}: {err['message']}")
293
294 print("-" * 80)
295 print(f"合計 {len(errors)} 件の import エラーがありました")
296
297
298def main():
299 """
300 スクリプトのメイン処理を実行する。
301
302 コマンドライン引数を解析し、Sphinxのソースディレクトリ内のPythonファイルを順次インポートします。
303 `conf.py` をロードして `autodoc_mock_imports` を取得し、それらのモジュールをモックします。
304 その後、ソースディレクトリ内の残りの `.py` ファイルをインポートし、
305 `--keep-going` オプションに応じてエラー処理を行います。
306 最終的に、記録されたエラーの要約を表示し、エラーがあれば非ゼロの終了コードで終了します。
307
308 :returns: None
309 """
310 parser = argparse.ArgumentParser(
311 description="Sphinx import エラー調査用: conf.py と source配下の *.py を順に import する"
312 )
313 parser.add_argument(
314 "--source",
315 default="source",
316 help="Sphinx source ディレクトリ (default: source)",
317 )
318 parser.add_argument(
319 "--add-cwd",
320 action="store_true",
321 help="カレントディレクトリを sys.path 先頭に追加する",
322 )
323 parser.add_argument(
324 "--add-source",
325 action="store_true",
326 help="source ディレクトリを sys.path 先頭に追加する",
327 )
328 parser.add_argument(
329 "--mock",
330 default="",
331 help='conf.py import 前に手動でモックするモジュールを ; 区切りで指定 (例: "whisper;torch;numba")',
332 )
333 parser.add_argument(
334 "--show-skip",
335 action="store_true",
336 help="スキップしたファイルも表示する",
337 )
338 parser.add_argument(
339 "--keep-going",
340 action="store_true",
341 help="エラーが出ても終了せず、最後まで続行して失敗一覧を表示する",
342 )
343 parser.add_argument(
344 "--show-traceback-summary",
345 action="store_true",
346 help="最後の失敗一覧で traceback も全文表示する (--keep-going 向け)",
347 )
348 args = parser.parse_args()
349
350 source_dir = Path(args.source).resolve()
351 conf_py = source_dir / "conf.py"
352 errors: list[dict] = []
353
354 if not source_dir.is_dir():
355 print(f"ERROR: source ディレクトリが存在しません: {source_dir}", file=sys.stderr)
356 sys.exit(1)
357
358 if not conf_py.is_file():
359 print(f"ERROR: conf.py が存在しません: {conf_py}", file=sys.stderr)
360 sys.exit(1)
361
362 if args.add_cwd:
363 cwd = str(Path.cwd().resolve())
364 if cwd not in sys.path:
365 sys.path.insert(0, cwd)
366
367 if args.add_source:
368 sdir = str(source_dir)
369 if sdir not in sys.path:
370 sys.path.insert(0, sdir)
371
372 print(f"[INFO] source_dir = {source_dir}")
373 print(f"[INFO] conf.py = {conf_py}")
374
375 manual_mocks = parse_mock_arg(args.mock)
376 if manual_mocks:
377 install_mock_modules(manual_mocks)
378 print(f"[INFO] manual mock modules = {manual_mocks}")
379
380 conf_mod = None
381
382 print(f"[IMPORT] {conf_py}")
383 try:
384 conf_mod = load_module_from_file("sphinx_debug.conf", conf_py)
385 except Exception as e:
386 print("\n[ERROR] conf.py の import に失敗しました\n", file=sys.stderr)
387 traceback.print_exc()
388 record_error(errors, "conf.py", conf_py, e)
389 if not args.keep_going:
390 print_error_summary(errors)
391 sys.exit(1)
392
393 if conf_mod is not None:
394 conf_mocks = getattr(conf_mod, "autodoc_mock_imports", [])
395 if conf_mocks:
396 if not isinstance(conf_mocks, (list, tuple)):
397 print(
398 f"[WARN] autodoc_mock_imports が list/tuple ではありません: {type(conf_mocks).__name__}",
399 file=sys.stderr,
400 )
401 else:
402 conf_mocks = [str(x).strip() for x in conf_mocks if str(x).strip()]
403 install_mock_modules(conf_mocks)
404 print(f"[INFO] autodoc_mock_imports = {conf_mocks}")
405
406 for py_file in sorted(source_dir.rglob("*.py")):
407 skip, reason = should_skip(py_file)
408 if skip:
409 if args.show_skip:
410 print(f"[SKIP] {py_file} ({reason})")
411 continue
412
413 module_name = make_module_name(source_dir, py_file)
414 print(f"[IMPORT] {py_file}")
415 try:
416 load_module_from_file(module_name, py_file)
417 except Exception as e:
418 print(f"\n[ERROR] import に失敗しました: {py_file}\n", file=sys.stderr)
419 traceback.print_exc()
420 record_error(errors, "module", py_file, e)
421 if not args.keep_going:
422 print_error_summary(errors)
423 sys.exit(1)
424
425 print_error_summary(errors)
426
427 if args.keep_going and args.show_traceback_summary and errors:
428 print("\n" + "=" * 80)
429 print("[SUMMARY] traceback 詳細")
430 print("=" * 80)
431 for i, err in enumerate(errors, 1):
432 print(f"\n--- {i}. [{err['phase']}] {err['file']} ---")
433 print(err["traceback"])
434
435 if errors:
436 sys.exit(1)
437
438 print("\n[OK] すべての対象 .py ファイルを import できました")
439
440
441if __name__ == "__main__":
442 main()
files2md_tags.py (プログラム)
1#!/usr/bin/env python3
2"""
3画像ファイルからサムネイルを生成し、Markdown形式の画像タグやファイルリンクを生成するスクリプト。
4
5詳細説明:
6 位置引数で複数のワイルドカードを受け取り、指定されたファイルを処理します。
7 画像ファイルに対してはサムネイルを生成し、Markdownの画像一覧として出力します。
8 画像以外のファイルはデータファイルとして扱い、Markdownリンク一覧を出力します。
9 出力形式はSphinx / Markdownでそのまま利用しやすい構成になっています。
10
11関連リンク: :doc:`images2md_tags_usage`
12"""
13
14from __future__ import annotations
15
16import argparse
17import glob
18from pathlib import Path
19from typing import Iterable
20from PIL import Image
21
22
23IMAGE_EXTS = {
24 ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tif", ".tiff", ".webp"
25}
26
27TEXT_EXTS = {".txt", ".md", ".rst", ".html"}
28
29
30def is_image_file(path: Path) -> bool:
31 """
32 指定されたパスが画像ファイルであるかを判定します。
33
34 ファイルの拡張子が定義済みの画像拡張子セットに含まれるかをチェックします。
35
36 :param path: チェック対象のファイルパス。
37 :type path: Path
38 :returns: 画像ファイルであればTrue、そうでなければFalse。
39 :rtype: bool
40 """
41 return path.suffix.lower() in IMAGE_EXTS
42
43
44def is_thumbnail_file(path: Path) -> bool:
45 """
46 指定されたパスがサムネイルファイルであるかを判定します。
47
48 ファイル名が "-s" で終わるかをチェックします。
49
50 :param path: チェック対象のファイルパス。
51 :type path: Path
52 :returns: サムネイルファイルであればTrue、そうでなければFalse。
53 :rtype: bool
54 """
55 return path.stem.endswith("-s")
56
57
58def expand_patterns(patterns: Iterable[str]) -> list[Path]:
59 """
60 複数のワイルドカードパターンを展開し、重複を除いたファイルパスのリストを返します。
61
62 指定された各パターンにマッチするファイルを検索し、重複を排除してソートされたPathオブジェクトのリストとして返します。
63
64 :param patterns: 展開するワイルドカードパターンのリスト。
65 :type patterns: Iterable[str]
66 :returns: 重複を除いたPathオブジェクトのリスト。
67 :rtype: list[Path]
68 """
69 seen: set[Path] = set()
70 results: list[Path] = []
71
72 for pattern in patterns:
73 for item in sorted(glob.glob(pattern)):
74 p = Path(item)
75 if p in seen:
76 continue
77 seen.add(p)
78 results.append(p)
79
80 return results
81
82
83def make_thumbnail(src: Path, thumb: Path, width: int, overwrite: bool, update: bool) -> bool:
84 """
85 指定されたソース画像ファイルからサムネイル画像を生成します。
86
87 サムネイルが既に存在する場合、overwriteまたはupdateフラグに基づいて処理を決定します。
88 updateがTrueの場合、ソース画像が新しい場合にのみサムネイルを更新します。
89
90 :param src: 元となる画像ファイルのパス。
91 :type src: Path
92 :param thumb: 生成するサムネイルファイルのパス。
93 :type thumb: Path
94 :param width: サムネイルの幅(ピクセル単位)。高さはアスペクト比を維持して自動計算されます。
95 :type width: int
96 :param overwrite: 既存のサムネイルを強制的に上書きするかどうか。Trueの場合、既存ファイルは無条件に上書きされます。
97 :type overwrite: bool
98 :param update: 元画像がサムネイルよりも新しい場合にのみサムネイルを更新するかどうか。Trueの場合、更新日時を比較します。
99 :type update: bool
100 :returns: サムネイルが新しく生成または更新された場合はTrue、それ以外はFalse。
101 :rtype: bool
102 :raises ValueError: ソース画像のサイズが無効な場合(幅または高さが0以下)。
103 """
104 if thumb.exists():
105 if overwrite:
106 pass
107 elif update:
108 if thumb.stat().st_mtime >= src.stat().st_mtime:
109 return False
110 else:
111 return False
112
113 with Image.open(src) as img:
114 w, h = img.size
115 if w <= 0 or h <= 0:
116 raise ValueError(f"Invalid image size: {src}")
117 new_h = max(1, int(h * (width / w)))
118 img = img.resize((width, new_h), Image.LANCZOS)
119 img.save(thumb)
120
121 return True
122
123
124def format_data_section(files: list[Path]) -> str:
125 """
126 データファイルのリストからMarkdown形式のセクションを生成します。
127
128 各ファイルへのリンクを含む「## 生成されたデータファイル」セクションを作成します。
129
130 :param files: データファイルのPathオブジェクトのリスト。
131 :type files: list[Path]
132 :returns: 生成されたMarkdown文字列。ファイルがない場合は空文字列。
133 :rtype: str
134 """
135 if not files:
136 return ""
137
138 lines = ["## 生成されたデータファイル"]
139 for p in files:
140 rel = p.as_posix()
141# lines.append(f"[{p.name}]({p.name})")
142 lines.append(f"[{p.name}]({rel})")
143 lines.append("")
144 if p.suffix.lower() in TEXT_EXTS:
145 lines.append(f"""\
146<details>
147<summary>読み取り結果 {p.name}</summary>
148
149```{{eval-rst}}
150.. include:: {rel}
151```
152
153</details>
154""")
155
156 return "\n".join(lines).rstrip()
157
158
159def format_image_section(files: list[Path]) -> str:
160 """
161 画像ファイルのリストからMarkdown形式のセクションを生成します。
162
163 各画像に対するサムネイル付きのリンクと、元の画像へのリンクを含む
164 「## 生成された画像一覧」セクションを作成します。
165
166 :param files: 画像ファイルのPathオブジェクトのリスト。
167 :type files: list[Path]
168 :returns: 生成されたMarkdown文字列。ファイルがない場合は空文字列。
169 :rtype: str
170 """
171 if not files:
172 return ""
173
174 lines = ["## 生成された画像一覧", ""]
175
176 for p in files:
177 rel = p.as_posix()
178 desc = p.stem
179 thumb = p.with_name(p.stem + "-s" + p.suffix)
180 lines.append(f"### {desc}")
181 lines.append(f"[]({rel})")
182# lines.append(f"[]({p.name})")
183 lines.append(f"[link]({rel})")
184# lines.append(f"[link]({p.name})")
185 lines.append("")
186
187 return "\n".join(lines).rstrip()
188
189
190def main() -> None:
191 """
192 スクリプトのメイン処理を実行します。
193
194 コマンドライン引数を解析し、指定されたファイルパターンに基づいて、
195 画像ファイルのサムネイル生成およびMarkdown形式のデータファイル/画像一覧の出力を実行します。
196 """
197 parser = argparse.ArgumentParser()
198 parser.add_argument(
199 "patterns",
200 nargs="+",
201 help="検索するワイルドカードパターンを1つ以上指定"
202 )
203 parser.add_argument("--width", type=int, default=200, help="サムネイル幅 [px]")
204 parser.add_argument(
205 "--update",
206 type=int,
207 default=0,
208 help="1 なら元画像が新しい場合のみサムネイル更新"
209 )
210 parser.add_argument(
211 "--overwrite",
212 type=int,
213 default=0,
214 help="1 なら既存サムネイルを強制上書き"
215 )
216 args = parser.parse_args()
217
218 overwrite = bool(args.overwrite)
219 update = bool(args.update)
220
221 files = expand_patterns(args.patterns)
222
223 image_files: list[Path] = []
224 data_files: list[Path] = []
225
226 for p in files:
227 if not p.is_file():
228 continue
229
230 if is_thumbnail_file(p):
231 continue
232
233 if is_image_file(p):
234 thumb = p.with_name(p.stem + "-s" + p.suffix)
235 created = make_thumbnail(
236 src=p,
237 thumb=thumb,
238 width=args.width,
239 overwrite=overwrite,
240 update=update,
241 )
242 if created:
243 print(f"[生成] {thumb}")
244 else:
245 print(f"[skip] {thumb}")
246 image_files.append(p)
247 else:
248 data_files.append(p)
249
250 sections: list[str] = []
251
252 data_md = format_data_section(data_files)
253 if data_md:
254 sections.append(data_md)
255
256 image_md = format_image_section(image_files)
257 if image_md:
258 sections.append(image_md)
259
260 if sections:
261 print()
262 print("\n\n\n".join(sections))
263 else:
264 print("一致するファイルがありませんでした。")
265
266
267if __name__ == "__main__":
268 main()
burst_sphinx_indexes.py (プログラム)
burst_sphinx_indexes.py をダウンロード
1#!/usr/bin/env python3
2"""Sphinx toctree globパターン展開スクリプト。
3
4概要:
5 Sphinxのtoctreeにおけるglobパターンを展開し、指定されたファイル群を自動的に挿入します。
6
7詳細説明:
8 親のreStructuredTextファイル内の `.. toctree::` ディレクティブにおいて、
9 ファイル名パターン(glob形式)が指定された場合、そのパターンに合致するファイルを
10 自動的に見つけ出し、toctreeのエントリとして挿入します。
11 Markdown (`.md`) と reStructuredText (`.rst`) ファイルの両方に対応します。
12 重複エントリの除外、エントリのソート、および子ファイルのセクションタイトルを
13 ラベルとして使用するオプションをサポートします。
14
15関連リンク:
16 :doc:`burst_sphinx_index_usage`
17"""
18import argparse
19import re
20from pathlib import Path
21import shutil
22import sys
23from typing import List, Optional
24
25
26RST_UNDERLINE_RE = re.compile(r'^[=\-~`^"\'#*+]+$')
27MD_H1_RE = re.compile(r'^\s*#\s+(.+?)\s*$')
28
29
30def log(verbose: bool, message: str):
31 """verboseモードが有効な場合に標準エラー出力に情報メッセージを出力する。
32
33 :param verbose: bool - メッセージを出力するかどうかを決定するフラグ。
34 :param message: str - 出力する情報メッセージ。
35 :returns: None
36 """
37 if verbose:
38 print(f"[INFO] {message}", file=sys.stderr)
39
40
41def warn(message: str):
42 """標準エラー出力に警告メッセージを出力する。
43
44 :param message: str - 出力する警告メッセージ。
45 :returns: None
46 """
47 print(f"[WARN] {message}", file=sys.stderr)
48
49
50def indent_width(line: str) -> int:
51 """行頭のインデント幅を計算して返す。
52
53 詳細説明:
54 タブ文字 (`\t`) は4文字分のスペースとして扱って計算します。
55
56 :param line: str - インデント幅を計算する対象の行文字列。
57 :returns: int - 行頭のインデント幅。
58 """
59 expanded = line.expandtabs(4)
60 return len(expanded) - len(expanded.lstrip(" "))
61
62
63def extract_first_section_title(path: Path, verbose: bool = False) -> Optional[str]:
64 """指定されたファイルから最初のセクションタイトルを抽出する。
65
66 詳細説明:
67 reStructuredText形式の下線見出し (`----`, `====` など) または
68 Markdown形式のH1見出し (`# Title`) のいずれかをサポートします。
69
70 :param path: Path - タイトルを抽出する対象ファイルのパス。
71 :param verbose: bool - 詳細ログを出力するかどうか。デフォルトは `False`。
72 :returns: Optional[str] - 抽出されたタイトル文字列。タイトルが見つからない場合やエラーが発生した場合は `None`。
73 """
74 try:
75 with path.open(encoding="utf-8") as f:
76 lines = f.readlines()
77
78 # Markdown H1
79 for line in lines:
80 m = MD_H1_RE.match(line.rstrip("\n"))
81 if m:
82 title = m.group(1).strip()
83 if title:
84 return title
85
86 # reStructuredText underline style
87 for i in range(len(lines) - 1):
88 title = lines[i].rstrip()
89 underline = lines[i + 1].rstrip()
90 if title and RST_UNDERLINE_RE.match(underline):
91 return title
92
93 except Exception as e:
94 log(verbose, f"title extraction failed: {path} ({e})")
95
96 return None
97
98
99def glob_with_extensions(parent_dir: Path, pattern: str, verbose: bool = False) -> List[Path]:
100 """Sphinxのtoctreeのglobルールに沿ってファイルを検索する。
101
102 詳細説明:
103 パターンに拡張子がない場合、`.rst` と `.md` の両方を補完してファイルを検索します。
104 拡張子が明示されている場合は、その拡張子のみで検索します。
105
106 :param parent_dir: Path - 検索の基準となる親ディレクトリのパス。
107 :param pattern: str - 検索するファイル名のglobパターン。
108 :param verbose: bool - 詳細ログを出力するかどうか。デフォルトは `False`。
109 :returns: List[Path] - パターンにマッチしたファイルのパスのリスト。
110 """
111 p = Path(pattern)
112 if p.suffix:
113 paths = list(parent_dir.glob(pattern))
114 log(verbose, f"glob pattern={pattern!r} matched {len(paths)} path(s) with explicit suffix")
115 else:
116 paths = []
117 for ext in (".rst", ".md"):
118 matched = list(parent_dir.glob(pattern + ext))
119 log(verbose, f"glob pattern={pattern + ext!r} matched {len(matched)} path(s)")
120 paths.extend(matched)
121 return paths
122
123
124def expand_glob_in_toctree(parent_rst: Path, sort_flag: bool, label_flag: bool, verbose: bool = False) -> str:
125 """reStructuredTextファイル内のtoctreeにおけるglobパターンを展開して置換する。
126
127 詳細説明:
128 複数の `.. toctree::` ディレクティブに対応し、globパターンに合致する子ファイルを
129 toctreeエントリとして挿入した新しい内容を返します。
130 展開されたエントリのソートやラベル付けもサポートします。
131
132 :param parent_rst: Path - globパターンを展開する対象の親reStructuredTextファイルのパス。
133 :param sort_flag: bool - 展開されたエントリをファイル名でソートするかどうか。`True` の場合ソートされる。
134 :param label_flag: bool - 展開されたエントリに子ファイルの最初のセクションタイトルをラベルとして使用するかどうか。`True` の場合ラベルが使用される。
135 :param verbose: bool - 詳細ログを出力するかどうか。デフォルトは `False`。
136 :returns: str - globパターンが展開された後のreStructuredTextファイルの内容。
137 """
138 lines = parent_rst.read_text(encoding="utf-8").splitlines()
139 parent_dir = parent_rst.parent
140
141 out: List[str] = []
142 in_toctree = False
143 toctree_indent = 0
144 seen_paths_in_block = set()
145
146 for lineno, line in enumerate(lines, start=1):
147 stripped = line.strip()
148 current_indent = indent_width(line)
149
150 # toctree 開始
151 if re.match(r"\s*\.\.\s+toctree::", line):
152 in_toctree = True
153 toctree_indent = current_indent
154 seen_paths_in_block = set()
155 log(verbose, f"entered toctree at line {lineno}, indent={toctree_indent}")
156 out.append(line)
157 continue
158
159 if in_toctree:
160 # toctree の本文ブロックを抜けたか判定
161 # 空行はブロックの一部として許可する。
162 if stripped != "" and current_indent <= toctree_indent:
163 in_toctree = False
164 log(verbose, f"left toctree before line {lineno}")
165 # fall through to normal line handling
166 else:
167 # toctree 内のオプション行
168 if stripped.startswith(":"):
169 out.append(line)
170 continue
171
172 # toctree 内の空行
173 if stripped == "":
174 out.append(line)
175 continue
176
177 # toctree 内のエントリ行
178 pattern = stripped
179 paths = glob_with_extensions(parent_dir, pattern, verbose=verbose)
180 paths = [p for p in paths if p.suffix in [".rst", ".md"]]
181
182 # 重複除去(出現順保持)
183 unique_paths: List[Path] = []
184 local_seen = set()
185 for p in paths:
186 resolved = p.resolve()
187 if resolved not in local_seen:
188 local_seen.add(resolved)
189 unique_paths.append(p)
190 paths = unique_paths
191
192 if sort_flag:
193 paths = sorted(paths, key=lambda p: p.name)
194
195 if not paths:
196 warn(f"No matches for toctree entry {pattern!r} in {parent_rst} (line {lineno})")
197 continue
198
199 for p in paths:
200 rel = p.relative_to(parent_dir).as_posix()
201 if rel in seen_paths_in_block:
202 log(verbose, f"skip duplicated entry: {rel}")
203 continue
204 seen_paths_in_block.add(rel)
205
206 if label_flag:
207 title = extract_first_section_title(p, verbose=verbose)
208 if title:
209 out.append(f" {title} <{rel}>")
210 log(verbose, f"added labeled entry: {title} <{rel}>")
211 continue
212 log(verbose, f"title not found for {rel}, fallback to plain path")
213
214 out.append(f" {rel}")
215 log(verbose, f"added entry: {rel}")
216
217 # 元の glob/path 行は出力しない
218 continue
219
220 # 通常行
221 out.append(line)
222
223 if in_toctree:
224 log(verbose, "reached EOF while still inside toctree block")
225
226 return "\n".join(out) + "\n"
227
228
229def main():
230 """コマンドライン引数に基づいてSphinx toctreeのglobパターンを展開するメイン処理。
231
232 詳細説明:
233 指定された親reStructuredTextファイルを読み込み、`.. toctree::` ディレクティブ内の
234 globパターンを展開します。展開された内容は、元のファイルに書き戻すか、
235 標準出力に出力されます。バックアップ作成、ソート、ラベル付け、詳細ログの
236 オプションをサポートします。
237
238 :returns: None
239 """
240 parser = argparse.ArgumentParser(description="Expand Sphinx toctree glob patterns.")
241 parser.add_argument("parent", help="Parent .rst file")
242 parser.add_argument("--sort", type=int, default=1, help="1=sort entries, 0=keep original glob order")
243 parser.add_argument("--label", type=int, default=0, help="1=use section title labels, 0=paths only")
244 parser.add_argument("--backup", type=int, default=1, help="1=create backup, 0=do not create backup")
245 parser.add_argument("--verbose", type=int, default=0, help="1=verbose log to stderr, 0=quiet")
246 args = parser.parse_args()
247
248 parent_rst = Path(args.parent).resolve()
249
250 if not parent_rst.exists():
251 raise FileNotFoundError(f"Parent file not found: {parent_rst}")
252
253 if args.backup == 1:
254 backup_path = parent_rst.with_suffix(parent_rst.suffix + ".backup")
255 shutil.copy2(parent_rst, backup_path)
256 log(args.verbose == 1, f"backup created: {backup_path}")
257
258 original = parent_rst.read_text(encoding="utf-8")
259 result = expand_glob_in_toctree(
260 parent_rst,
261 sort_flag=(args.sort == 1),
262 label_flag=(args.label == 1),
263 verbose=(args.verbose == 1),
264 )
265
266 if result != original:
267 parent_rst.write_text(result, encoding="utf-8")
268 log(args.verbose == 1, f"updated file: {parent_rst}")
269 else:
270 log(args.verbose == 1, f"no changes detected: {parent_rst}")
271
272 print(result)
273
274
275if __name__ == "__main__":
276 main()
save_clipboard.py (プログラム)
1"""
2クリップボードの内容(テキストまたは画像)を取得し、ファイルに保存するスクリプト。
3
4このスクリプトはWindows API (User32, Ole32, Kernel32) を使用してクリップボードデータを処理し、
5PIL (Pillow) ライブラリを使って画像を保存します。
6テキストデータはUTF-8エンコーディングで保存され、DIB (Device Independent Bitmap) 形式の画像はPNG形式で保存されます。
7ファイル保存後、reStructuredText (rst) 形式でそのファイルを参照するためのスニペットを標準出力に表示します。
8
9Usage:
10 python save_clipboard.py [出力ファイル名のベース (拡張子なし)]
11
12例:
13 # クリップボードの内容を 'my_output.txt' または 'my_output.png' として保存
14 python save_clipboard.py my_output
15 # 引数なしの場合、'clipboard_output.txt' または 'clipboard_output.png' として保存
16 python save_clipboard.py
17
18:doc:`save_clipboard_usage`
19"""
20import ctypes
21import ctypes.wintypes as wintypes
22from ctypes import POINTER, byref, windll, c_void_p
23from PIL import Image
24import io
25import struct
26import sys
27import os
28
29# Windows APIの準備
30ole32 = windll.ole32
31kernel32 = windll.kernel32
32user32 = windll.user32 # テキスト取得用に追加
33
34# --- 64bitアドレスを正しく扱うための型定義 ---
35kernel32.GlobalLock.argtypes = [c_void_p]
36kernel32.GlobalLock.restype = c_void_p
37kernel32.GlobalUnlock.argtypes = [c_void_p]
38kernel32.GlobalSize.argtypes = [c_void_p]
39kernel32.GlobalSize.restype = ctypes.c_size_t
40
41# テキスト取得用API定義
42user32.OpenClipboard.argtypes = [wintypes.HWND]
43user32.OpenClipboard.restype = wintypes.BOOL
44user32.CloseClipboard.restype = wintypes.BOOL
45user32.GetClipboardData.argtypes = [wintypes.UINT]
46user32.GetClipboardData.restype = wintypes.HANDLE
47user32.IsClipboardFormatAvailable.argtypes = [wintypes.UINT]
48user32.IsClipboardFormatAvailable.restype = wintypes.BOOL
49
50# 定数定義
51TYMED_HGLOBAL = 1
52DVASPECT_CONTENT = 1
53CF_DIB = 8
54CF_UNICODETEXT = 13 # WindowsのUnicodeテキスト形式
55GMEM_MOVEABLE = 0x0002
56
57class FORMATETC(ctypes.Structure):
58 """
59 OLEデータ転送で使用される形式記述構造体。
60
61 クリップボードやOLEオブジェクトからのデータ取得時に、どのような形式
62 (クリップボード形式、メディアタイプ、表示側面など)のデータを要求するかを指定します。
63 """
64 _fields_ = [
65 ("cfFormat", wintypes.WORD),
66 ("ptd", c_void_p),
67 ("dwAspect", wintypes.DWORD),
68 ("lindex", wintypes.LONG),
69 ("tymed", wintypes.DWORD),
70 ]
71
72class STGMEDIUM(ctypes.Structure):
73 """
74 OLEデータ転送で使用されるストレージメディア構造体。
75
76 クリップボードやOLEオブジェクトからのデータ転送時に、実際のデータ
77 (ハンドル、ポインタ、ストリームなど)とそのタイプを保持します。
78 `u` メンバは、`tymed` の値に応じてHGLOBALハンドルなどを格納するために `c_void_p` として定義されています。
79 """
80 _fields_ = [
81 ("tymed", wintypes.DWORD),
82 ("u", c_void_p), # data handle (HGLOBAL)
83 ("pUnkForRelease", c_void_p),
84 ]
85
86class IDataObject(ctypes.Structure):
87 """
88 OLEデータ転送インターフェースの抽象基底クラス。
89
90 クリップボードやドラッグ&ドロップ操作など、データ転送オブジェクトが提供するデータを
91 管理するためのCOMインターフェースです。主に `GetData` メソッドを通じてデータを取得します。
92 """
93 pass
94IDataObject._fields_ = [("lpVtbl", POINTER(c_void_p))]
95
96def get_clipboard_text():
97 """
98 クリップボードからUnicodeテキストを取得します。
99
100 User32.dllのOpenClipboard, GetClipboardData, GlobalLock, GlobalUnlock, CloseClipboard関数を使用します。
101 CF_UNICODETEXT形式のデータを取得し、Pythonの文字列として返します。
102 クリップボードがロックされている場合や、テキストデータが見つからない場合はNoneを返します。
103
104 :returns: クリップボード上のテキストデータ (`str`)、またはNone。
105 :rtype: str | None
106 """
107 if not user32.OpenClipboard(None):
108 return None
109
110 try:
111 if not user32.IsClipboardFormatAvailable(CF_UNICODETEXT):
112 return None
113
114 h_global = user32.GetClipboardData(CF_UNICODETEXT)
115 if not h_global:
116 return None
117
118 ptr = kernel32.GlobalLock(c_void_p(h_global))
119 if not ptr:
120 return None
121
122 try:
123 # Unicode (UTF-16) としてデータを読み込む
124 text = ctypes.c_wchar_p(ptr).value
125 return text
126 finally:
127 kernel32.GlobalUnlock(c_void_p(h_global))
128 finally:
129 user32.CloseClipboard()
130
131def get_clipboard_image_fixed():
132 """
133 クリップボードからDIB (Device Independent Bitmap) 形式の画像を取得し、PIL Imageオブジェクトとして返します。
134
135 Ole32.dllのOleGetClipboardとReleaseStgMedium関数、およびIDataObjectインターフェースのGetDataメソッドを使用します。
136 取得したDIBデータは、BMPファイルヘッダを追加してPIL (Pillow) で読み込み可能な形式に変換されます。
137 この関数を呼び出す前に `ole32.OleInitialize(None)` が呼び出されている必要があります。
138
139 :returns: クリップボード上の画像データ (`PIL.Image.Image`)、またはNone。
140 :rtype: PIL.Image.Image | None
141 """
142 # 既にOleInitializeされている前提、または明示的に呼ぶ
143 # ole32.OleInitialize(None) # メイン側で制御
144
145 data_obj_ptr = POINTER(IDataObject)()
146 hr = ole32.OleGetClipboard(byref(data_obj_ptr))
147 if hr != 0:
148 return None
149
150 try:
151 # IDataObject::GetData のVtableインデックスは通常3
152 get_data_addr = data_obj_ptr.contents.lpVtbl[3]
153
154 GetDataProto = ctypes.WINFUNCTYPE(
155 ctypes.HRESULT,
156 POINTER(IDataObject),
157 POINTER(FORMATETC),
158 POINTER(STGMEDIUM)
159 )
160 get_data_func = GetDataProto(get_data_addr)
161 except (IndexError, AttributeError):
162 return None
163
164 fmt = FORMATETC()
165 fmt.cfFormat = CF_DIB
166 fmt.ptd = None
167 fmt.dwAspect = DVASPECT_CONTENT
168 fmt.lindex = -1
169 fmt.tymed = TYMED_HGLOBAL
170
171 stg = STGMEDIUM()
172 hr = get_data_func(data_obj_ptr, byref(fmt), byref(stg))
173
174 img = None
175 if hr == 0 and stg.tymed == TYMED_HGLOBAL:
176 h_global = stg.u
177 ptr = kernel32.GlobalLock(c_void_p(h_global))
178 size = kernel32.GlobalSize(c_void_p(h_global))
179
180 if ptr:
181 try:
182 dib_data = ctypes.string_at(ptr, size)
183 if len(dib_data) >= 40:
184 header_size = struct.unpack("<I", dib_data[0:4])[0]
185 offset_to_pixels = 14 + header_size
186 file_size = 14 + len(dib_data)
187
188 bmp_header = struct.pack("<2sIHHI", b"BM", file_size, 0, 0, offset_to_pixels)
189 img = Image.open(io.BytesIO(bmp_header + dib_data))
190 img.load() # メモリ上にデータをロード
191 finally:
192 kernel32.GlobalUnlock(c_void_p(h_global))
193 ole32.ReleaseStgMedium(byref(stg))
194
195 return img
196
197def print_rst_snippets_for_image(filepath):
198 """
199 指定された画像ファイル用のreStructuredText (rst) スニペットを表示します。
200
201 ファイルリンク、`image` ディレクティブ、`figure` ディレクティブの例を生成し、
202 標準出力に出力します。これはSphinxなどでドキュメントを作成する際に役立ちます。
203
204 :param filepath: スニペットを生成する画像ファイルのパス。
205 :type filepath: str
206 :returns: なし
207 :rtype: None
208 """
209 filename = os.path.basename(filepath)
210 rel_path = "./" + filename # 相対パスと仮定
211
212 print("\n" + "="*60)
213 print(f"--- reStructuredText (rst) 用表示コード ({filename}) ---")
214 print("="*60)
215
216 print("\n1. シンプルなファイルリンク:")
217 print(f"`{filename} <{rel_path}>`_")
218
219 print("\n2. 画像表示 (imageディレクティブ):")
220 print(f".. image:: {rel_path}\n :alt: {filename}")
221
222 print("\n3. 図表表示 (figureディレクティブ - キャプション付き):")
223 print(f".. figure:: {rel_path}\n :alt: {filename}\n :align: center\n\n {filename} の図面")
224 print("="*60 + "\n")
225
226def print_rst_snippets_for_text(filepath):
227 """
228 指定されたテキストファイル用のreStructuredText (rst) スニペットを表示します。
229
230 `dropdown` (sphinx-design拡張機能が必要)、`parsed-literal`、`literalinclude`
231 ディレクティブの例を生成し、標準出力に出力します。
232 言語ヒントはファイル拡張子から推測されます。
233
234 :param filepath: スニペットを生成するテキストファイルのパス。
235 :type filepath: str
236 :returns: なし
237 :rtype: None
238 """
239 filename = os.path.basename(filepath)
240 rel_path = "./" + filename # 相対パスと仮定
241
242 # ファイル拡張子から言語を推測(簡易版)
243 _, ext = os.path.splitext(filename)
244 lang_map = {'.py': 'python', '.js': 'javascript', '.bat': 'bat', '.sh': 'bash', '.cpp': 'cpp', '.h': 'cpp', '.txt': 'text'}
245 language = lang_map.get(ext.lower(), 'text')
246
247 print("\n" + "="*60)
248 print(f"--- reStructuredText (rst) 用表示コード ({filename}) ---")
249 print("="*60)
250
251 print("\n1. 折り畳み表示 (sphinx-design dropdown) ※要拡張機能:")
252 print(f".. dropdown:: {filename} の内容を表示(クリックで展開)\n :color: info\n :icon: file-code\n")
253 print(f" .. literalinclude:: {rel_path}\n :language: {language}\n :linenos:")
254
255 print("\n2. <pre>タグ相当での埋め込み (parsed-literal):")
256 print(".. parsed-literal::\n")
257 try:
258 with open(filepath, "r", encoding="utf-8") as f:
259 # 最初の数行だけサンプルとして表示
260 lines = f.readlines()
261 for line in lines[:5]:
262 print(f" {line.rstrip()}")
263 if len(lines) > 5:
264 print(" ...")
265 except Exception:
266 print(" (ファイル内容の読み込みに失敗しました)")
267
268 print("\n3. 通常の外部ファイル取り込み (literalinclude):")
269 print(f".. literalinclude:: {rel_path}\n :language: {language}")
270
271 print("="*60 + "\n")
272
273if __name__ == "__main__":
274 # OleInitializeはプロセス内で1回呼ぶ必要がある(OLE API使用のため)
275 ole32.OleInitialize(None)
276
277 # デフォルトのファイル名ベース
278 base_filename = "clipboard_output"
279
280 # 引数がある場合はそれをファイル名にする
281 if len(sys.argv) > 1:
282 # 安全なファイル名にするための簡易処理(パス区切り文字などを除去)
283 requested_name = sys.argv[1]
284 base_filename = os.path.splitext(os.path.basename(requested_name))[0]
285
286 try:
287 print(f"--- クリップボードの内容を確認中... ---")
288
289 # 1. まずテキストがあるか確認(優先度が高い場合が多い)
290 text_content = get_clipboard_text()
291 if text_content is not None:
292 print("[情報] クリップボード内にテキストが見つかりました。")
293
294 # テキストファイルとして保存
295 out_name = f"{base_filename}.txt"
296 # Windowsの改行コード(CRLF)で保存
297 with open(out_name, "w", encoding="utf-8", newline='\r\n') as f:
298 f.write(text_content)
299
300 print(f"成功: テキストを '{out_name}' として保存しました (UTF-8)。")
301 print(f"文字数: {len(text_content)}")
302
303 # rstスニペット表示
304 print_rst_snippets_for_text(out_name)
305 sys.exit(0)
306
307 # 2. 次に画像があるか確認
308 img = get_clipboard_image_fixed()
309 if img:
310 print("[情報] クリップボード内に画像(DIB)が見つかりました。")
311
312 # 画像ファイルとして保存 (PNG推奨)
313 out_name = f"{base_filename}.png"
314 img.save(out_name, "PNG")
315
316 print(f"成功: 画像を '{out_name}' として保存しました。")
317 print(f"画像サイズ: {img.size[0]}x{img.size[1]}, モード: {img.mode}")
318
319 # rstスニペット表示
320 print_rst_snippets_for_image(out_name)
321 sys.exit(0)
322
323 # 3. どちらもない場合
324 print("[情報] クリップボードにテキストまたは画像(CF_DIB)は見つかりませんでした。")
325 # 形式リストを取得して表示(デバッグ用)
326 if user32.OpenClipboard(None):
327 try:
328 formats = []
329 fnum = user32.EnumClipboardFormats(0)
330 while fnum:
331 formats.append(fnum)
332 fnum = user32.EnumClipboardFormats(fnum)
333 if formats:
334 print(f"利用可能な形式ID: {formats}")
335 finally:
336 user32.CloseClipboard()
337
338 except Exception as e:
339 import traceback
340 print(f"\n[エラー] 予期せぬエラーが発生しました。")
341 traceback.print_exc()
342 finally:
343 # OleUninitializeはプロセス終了時に呼ぶ
344 ole32.OleUninitialize()
compare_dir_files.py (プログラム)
1"""
2ディレクトリ間のファイル構成を比較するスクリプト。
3
4このスクリプトは、特定の除外パターンに合致するファイルを無視し、
52つのディレクトリツリー間でどちらか一方にのみ存在するファイルを報告します。
6主にソースコードの同期状態や、予期せぬファイルの有無を確認する際に役立ちます。
7
8:doc:`compare_dir_files_usage`
9"""
10import argparse
11import os
12import re
13from pathlib import Path
14
15
16# 除外パターンの定義 (パス全体に対して判定)
17exclude_patterns = [
18 r"/__init__\.py$",
19 r"/.*_docstring\.",
20 r"/.*_\d{8}\.",
21 r"/tklib/",
22 r"/cms/",
23 r"/jsap_crystal/",
24 r"/stastical_physics/",
25 ]
26
27def compare_directories(source_dir, target_dir, extension="*.py"):
28 """
29 2つのディレクトリの内容を比較し、一方にのみ存在するファイルを報告する。
30
31 この関数は、ネストされた ``is_excluded`` 関数と ``get_filtered_files`` 関数を使用して、
32 除外パターンにマッチしないファイルを収集します。収集したファイルセットの差分を計算し、
33 結果を標準出力に表示します。パスのセパレータはPOSIX形式 ('/') に統一されます。
34
35 :param source_dir: str 比較元のディレクトリパス。
36 :param target_dir: str 比較先のディレクトリパス。
37 :param extension: str 検索するファイルの拡張子パターン (例: "*.py")。
38 :returns: None 結果は標準出力に表示されるため、明示的な戻り値はない。
39 """
40 source_path = Path(source_dir)
41 target_path = Path(target_dir)
42
43 def is_excluded(rel_path_str):
44 """
45 相対パスが定義済み除外パターンにマッチするかを判定する。
46
47 相対パスの先頭に '/' を付与し、``exclude_patterns`` の各正規表現と照合します。
48 いずれかのパターンにマッチすれば除外対象と判断します。
49
50 :param rel_path_str: str 比較する相対パス文字列 (例: "path/to/file.py")。
51 :returns: bool 除外パターンにマッチした場合はTrue、そうでなければFalse。
52 """
53 # マッチング用に先頭に / をつける
54 test_path = "/" + rel_path_str
55 return any(re.search(pattern, test_path) for pattern in exclude_patterns)
56
57 def get_filtered_files(base_path):
58 """
59 指定されたベースパス以下のファイルをフィルタリングして取得する。
60
61 ``Path.rglob()`` を使用して指定された拡張子パターンにマッチするファイルを再帰的に検索します。
62 各ファイルの相対パスを ``is_excluded`` 関数でチェックし、
63 除外対象でないファイルのみをセットとして収集します。
64 相対パスはPOSIX形式 ('/') に統一されます。
65
66 :param base_path: Path 検索を開始するベースディレクトリの `Path` オブジェクト。
67 :returns: set[str] フィルタリングされたファイルの相対パスのセット。
68 """
69 file_set = set()
70 for p in base_path.rglob(extension):
71 # ベースディレクトリからの相対パスを取得し '/' 区切りに変換
72 rel_path = p.relative_to(base_path).as_posix()
73 if not is_excluded(rel_path):
74 file_set.add(rel_path)
75 return file_set
76
77 source_files = get_filtered_files(source_path)
78 target_files = get_filtered_files(target_path)
79
80 print(f"{'='*60}")
81 print(f" 比較レポート (Separator: '/')")
82 print(f"{'='*60}")
83 print(f"・[A] Source: {source_path.absolute()}")
84 print(f"・[B] Target: {target_path.absolute()}")
85 print(f"{'-'*60}\n")
86
87 # AにあってBにないもの
88 only_in_source = sorted(source_files - target_files)
89 print(f"【 状態: Source [A] にのみ存在 / Target [B] から欠落 】")
90 if only_in_source:
91 for f in only_in_source:
92 print(f" [MISSING in B] -> {f}")
93 else:
94 print(" 該当なし")
95
96 print("\n" + "-"*60 + "\n")
97
98 # BにあってAにないもの
99 only_in_target = sorted(target_files - source_files)
100 print(f"【 状態: Target [B] にのみ存在 / Source [A] から欠落 】")
101 if only_in_target:
102 for f in only_in_target:
103 print(f" [MISSING in A] -> {f}")
104 else:
105 print(" 該当なし")
106
107def main():
108 """
109 コマンドライン引数を解析し、ディレクトリ比較処理を実行する。
110
111 この関数は ``argparse`` を使用して、比較元ディレクトリ、比較先ディレクトリ、
112 検索拡張子パターンを引数として受け取ります。
113 デフォルト値も設定されており、指定されたパスが有効なディレクトリであるかを確認し、
114 ``compare_directories`` 関数を呼び出します。
115
116 :param None: コマンドライン引数は ``argparse`` によって処理されるため、明示的な引数はない。
117 :returns: None 処理結果は標準出力に表示される。
118 """
119 parser = argparse.ArgumentParser(description="特定のディレクトリやファイルパターンを除外して2つのツリーを比較します。")
120
121 parser.add_argument("source",
122 nargs="?",
123 default=r"D:\git\tkProg\tkprog_COE",
124 help="比較元ディレクトリ(A)")
125 parser.add_argument("target",
126 nargs="?",
127 default=r"D:\git\sphinx\tkProg\source",
128 help="比較先ディレクトリ(B)")
129 parser.add_argument("-e", "--ext",
130 default="*.py",
131 help="検索パターン (デフォルト: *.py)")
132
133 args = parser.parse_args()
134
135 if not os.path.isdir(args.source) or not os.path.isdir(args.target):
136 print("エラー: 指定されたパスが存在しないか、ディレクトリではありません。")
137 return
138
139 compare_directories(args.source, args.target, args.ext)
140
141if __name__ == "__main__":
142 main()