make_sphinx_files.py ダウンロード/コピー
make_sphinx_files.py
make_sphinx_files.py
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3
4import os
5import sys
6import argparse
7import shutil
8import subprocess
9import traceback
10from pathlib import Path
11from datetime import datetime
12
13
14SCRIPT_FULLPATH = os.path.abspath(sys.argv[0])
15SCRIPT_DIR = os.path.dirname(SCRIPT_FULLPATH)
16SCRIPT_BASENAME = os.path.splitext(os.path.basename(SCRIPT_FULLPATH))[0]
17
18ADD_DOCSTRING_PATH = os.path.join(SCRIPT_DIR, "add_docstring.py")
19EXPLAIN_PROGRAM_PATH = os.path.join(SCRIPT_DIR, "explain_program5.py")
20
21
22def relative_from_source(argv0: str) -> str:
23 p = Path(argv0).resolve()
24 parts = p.parts
25
26 try:
27 idx = parts.index("source")
28 except ValueError:
29 return ""
30
31 rel_path = Path(*parts[idx + 1:])
32 return str(rel_path.parent)
33
34
35def run_step(message, cmd_list):
36 print(f"\n>>> {message}")
37 print(f" コマンド: {' '.join(cmd_list)}")
38 try:
39 result = subprocess.run(cmd_list, text=True, errors="ignore")
40 if result.returncode != 0:
41 print("!!! エラーが発生しました")
42 return False
43 return True
44 except Exception as e:
45 print(f"!!! 実行エラー: {e}")
46 return False
47
48
49def make_init_py(path):
50 if os.path.exists(path):
51 print(f">>> Step: {path} が見つかりました。")
52 else:
53 print(f">>> Step: {path} を作成中...")
54 with open(path, "w", encoding="utf-8") as f:
55 f.write("")
56 print(f" Done: {path}")
57
58
59def make_index_template(path, module_path):
60 if os.path.exists(path):
61 print(f">>> Step: {path} に追加中...")
62 with open(path, "a", encoding="utf-8") as f:
63 f.write(f" {module_path}_index\n")
64 print(f" Done: {path}")
65 else:
66 print(f">>> Step: {path} を作成中...")
67 content = f"""\
68プロジェクト全体ドキュメント
69============================
70
71.. toctree::
72 :maxdepth: 2
73 :caption: メインメニュー:
74 :hidden:
75 :glob:
76
77 {module_path}_index
78"""
79 with open(path, "w", encoding="utf-8") as f:
80 f.write(content)
81 print(f" Done: {path}")
82
83
84def make_index_rst(index_rst, base_name, package_path):
85 print(f">>> Step: {index_rst} を作成中...")
86 index_content = f"""{base_name} ドキュメント
87============================================================================
88
89.. toctree::
90 :maxdepth: 1
91
92 {base_name}_download
93 {base_name}_usage
94 {base_name}_quality
95 {base_name}_examples
96 {base_name}_api
97"""
98 with open(index_rst, "w", encoding="utf-8") as f:
99 f.write(index_content)
100 print(f" Done: {index_rst}")
101
102
103def make_download_rst(download_rst, base_name, script_rel_path):
104 print(f">>> Step: {download_rst} を作成中...")
105
106 script_name = os.path.basename(script_rel_path)
107
108 download_content = f"""{script_name} ダウンロード/コピー
109========================================================
110
111:download:`{script_name} をダウンロード <{script_rel_path}>`
112
113.. dropdown:: {script_name}
114 :color: primary
115 :icon: file-code
116
117 .. literalinclude:: {script_rel_path}
118 :language: python
119 :linenos:
120 :caption: {script_name}
121"""
122 with open(download_rst, "w", encoding="utf-8") as f:
123 f.write(download_content)
124
125 print(f" Done: {download_rst}")
126
127
128def make_api_rst(api_rst, base_name, package_path):
129 print(f">>> Step: {api_rst} を作成中...")
130 api_content = f"""{base_name} プログラム仕様
131============================================================================
132
133.. currentmodule:: {package_path}
134
135.. automodule:: {package_path}
136 :members:
137 :undoc-members:
138 :show-inheritance:
139"""
140 with open(api_rst, "w", encoding="utf-8") as f:
141 f.write(api_content)
142 print(f" Done: {api_rst}")
143
144
145def make_examples_md(examples_md, infile, base_name):
146 print(f">>> Step: {examples_md} テンプレートを作成中...")
147
148 image_files = sorted(
149 f for f in os.listdir(".")
150 if f.startswith(base_name) and f.lower().endswith((".png", ".jpg", ".jpeg"))
151 )
152
153 data_files = sorted(
154 f for f in os.listdir(".")
155 if f.startswith(base_name) and f.lower().endswith((".csv", ".xlsx", ".xls", ".txt"))
156 )
157
158 print("Image files:", image_files)
159 print("Data files:", data_files)
160
161 print(" help logを取得します")
162 result = subprocess.run(
163 [sys.executable, infile, "--help"],
164 text=True,
165 capture_output=True,
166 errors="ignore",
167 )
168
169 print(" return code:", result.returncode)
170
171 if result.returncode == 0:
172 help_log = result.stdout + "\n" + result.stderr
173 else:
174 help_log = "(ヘルプの自動取得に失敗しました。ここに実行ログを貼り付けてください)"
175
176 if data_files:
177 data_section = "## データファイル\n"
178 for df in data_files:
179 data_section += f"- [{df}](./{df})\n"
180 data_section += "\n"
181 else:
182 data_section = "## 生成されたデータファイル\n(データファイルが見つかりませんでした)\n\n"
183
184 if image_files:
185 image_section = "## 画像ファイル\n\n"
186 for img in image_files:
187 image_section += f"- [{img}](./{img})\n"
188 image_section += f"\n\n"
189 else:
190 image_section = "## 生成された画像一覧\n(画像ファイルが見つかりませんでした)\n\n"
191
192 examples_content = f"""# {base_name} 実行例
193
194## help出力 `{base_name}.py --help`
195
196<pre style="background-color: #f4f4f4; border: 1px solid #ccc; padding: 10px; border-radius: 5px; font-family: 'Courier New', Courier, monospace; overflow-x: auto;">
197{help_log}
198</pre>
199
200{data_section}
201
202{image_section}
203"""
204
205 with open(examples_md, "w", encoding="utf-8") as f:
206 f.write(examples_content)
207
208 print(f" Done: {examples_md}")
209
210
211def check_updated(file1, file2):
212 if not os.path.exists(file1):
213 return False
214 if not os.path.exists(file2):
215 return True
216
217 t1 = os.path.getmtime(file1)
218 dt1 = datetime.fromtimestamp(t1)
219 t2 = os.path.getmtime(file2)
220 dt2 = datetime.fromtimestamp(t2)
221
222 print(f"File stamps: {file1} : {dt1}")
223 print(f" {file2} : {dt2}")
224
225 if t1 >= t2:
226 return True
227 return False
228
229
230def main(args):
231 infile = args.infile
232
233 if not os.path.exists(infile):
234 print(f"エラー: 入力ファイル '{infile}' が見つかりません。")
235 return
236
237 base_name = os.path.splitext(os.path.basename(infile))[0]
238 date_str = datetime.now().strftime("%Y%m%d")
239
240 init_py = "__init__.py"
241 index_template = "index.template"
242 docstring_out = f"{base_name}_docstring.py"
243 backup_file = f"{base_name}_{date_str}.py"
244
245 usage_md = f"{base_name}_usage.md"
246 quality_md = f"{base_name}_quality.md"
247 examples_md = f"{base_name}_examples.md"
248 index_rst = f"{base_name}_index.rst"
249 download_rst = f"{base_name}_download.rst"
250 api_rst = f"{base_name}_api.rst"
251
252 if args.subdir is not None and args.subdir != "":
253 module_path = os.path.join(args.subdir, base_name)
254 package_path = f"{args.subdir}.{base_name}"
255 else:
256 module_path = base_name
257 package_path = module_path
258
259 print("=" * 60)
260 print(f" プロジェクト: {base_name} のSphinxファイル自動生成を開始します")
261 print(f" モジュールパス: {module_path}")
262 print(f" パッケージパス: {package_path}")
263 print("=" * 60)
264
265 make_init_py(init_py)
266
267 if os.path.exists(index_template):
268 print(f"** Warning: [{index_template}] exists. Skip to create.")
269 else:
270 make_index_template(index_template, module_path)
271
272 if args.update_index:
273 print(f"** Message: [Force update {index_rst} due to {args.update_index=}].")
274 make_index_rst(index_rst, base_name, package_path)
275 elif os.path.exists(index_rst):
276 print(f"** Warning: [{index_rst}] exists. Skip to create.")
277 else:
278 make_index_rst(index_rst, base_name, package_path)
279
280 if os.path.exists(download_rst):
281 print(f"** Warning: [{download_rst}] exists. Skip to create.")
282 else:
283 make_download_rst(download_rst, base_name, infile)
284
285 if os.path.exists(api_rst):
286 print(f"** Warning: [{api_rst}] exists. Skip to create.")
287 else:
288 make_api_rst(api_rst, base_name, package_path)
289
290 if os.path.exists(examples_md):
291 print(f"** Warning: [{examples_md}] exists. Skip to create.")
292 else:
293 make_examples_md(examples_md, infile, base_name)
294
295 args_list = [
296 "--api", args.api,
297 "--update", str(args.update),
298 "--overwrite", str(args.overwrite),
299 "--pause", str(args.pause),
300 ]
301
302 if not check_updated(infile, usage_md):
303 print(f"Step EXPLAIN_PROGRAM_PATH: {infile}が{usage_md}より古いのでスキップします")
304 else:
305 if not run_step(
306 "Step: EXPLAIN_PROGRAM_PATH を実行してusageを生成中...",
307 [
308 sys.executable,
309 EXPLAIN_PROGRAM_PATH,
310 infile,
311 usage_md,
312 "--inifile",
313 args.prompt_explain,
314 *args_list,
315 ],
316 ):
317 return
318
319 if not check_updated(infile, docstring_out):
320 print(f"Step ADD_DOCSTRING_PATH: {infile}が{docstring_out}より古いのでスキップします")
321 else:
322 if not run_step(
323 "Step: ADD_DOCSTRING_PATH を実行してDocstringを追加中...",
324 [
325 sys.executable,
326 ADD_DOCSTRING_PATH,
327 infile,
328 "--inifile",
329 args.prompt_add_docstring,
330 *args_list,
331 ],
332 ):
333 return
334
335 print(">>> Step: オリジナルファイルのバックアップを作成中...")
336 if os.path.exists(infile):
337 shutil.copy2(infile, backup_file)
338 print(f" Done: {infile} -> {backup_file}")
339 else:
340 print("!!! バックアップ対象のファイルが見つかりません。")
341 return
342
343 if os.path.exists(docstring_out) and os.path.exists(infile):
344 t_doc = os.path.getmtime(docstring_out)
345 size_byte = os.path.getsize(docstring_out)
346 t_in = os.path.getmtime(infile)
347
348 dt_doc = datetime.fromtimestamp(t_doc)
349 dt_in = datetime.fromtimestamp(t_in)
350
351 print(f"File stamps: {docstring_out} : {dt_doc} ({size_byte} bytes)")
352 print(f" {infile} : {dt_in}")
353
354 if size_byte == 0:
355 print(f" {docstring_out} はダミーファイルのため、ファイルコピーは行いません")
356 elif t_doc <= t_in:
357 print(" infile の方が新しいのでファイルコピーは行いません")
358 else:
359 print(f">>> Step: 生成されたDocstring版ファイルを {infile} にリネーム中...")
360 if os.path.exists(docstring_out):
361 os.replace(docstring_out, infile)
362 print(f" Done: {docstring_out} -> {infile}")
363
364 with open(docstring_out, "w", encoding="utf-8") as fp:
365 fp.write("")
366
367 print(f" ダミーの空ファイル {docstring_out} を作りました")
368 else:
369 print("!!! Docstring版ファイルが生成されていなかったため、リネームをスキップします。")
370 return
371
372 if args.prompt_quality:
373 if not run_step(
374 "Step: EXPLAIN_PROGRAM_PATH を実行してコード品質評価を生成中...",
375 [
376 sys.executable,
377 EXPLAIN_PROGRAM_PATH,
378 infile,
379 quality_md,
380 "--inifile",
381 args.prompt_quality,
382 *args_list,
383 ],
384 ):
385 return
386 else:
387 print("** Warning: --prompt_quality が指定されていないため、quality生成をスキップします。")
388
389 print("\n" + "=" * 60)
390 print(" 全ての自動生成プロセスが正常に終了しました。")
391 print("=" * 60)
392
393
394def initialize():
395 parser = argparse.ArgumentParser(
396 description="Sphinxドキュメント生成の一連のルーチン(Docstring追加、バックアップ、解説生成、RST作成)を自動化します。"
397 )
398
399 parser.add_argument("infile", help="対象となるPythonスクリプトファイル名 (例: mu_fit.py)")
400 parser.add_argument("--subdir", default=None, help="sourceディレクトリからの相対パス")
401
402 parser.add_argument(
403 "--prompt_explain",
404 default="explain_program5.ini",
405 help="usage生成用プロンプトファイルのパス",
406 )
407 parser.add_argument(
408 "--prompt_quality",
409 default="prompt_quality.ini",
410 help="quality生成用プロンプトファイルのパス",
411 )
412 parser.add_argument(
413 "--prompt_add_docstring",
414 default="",
415 help="add_docstring.pyに渡すプロンプトファイルのパス",
416 )
417
418 parser.add_argument("--api", choices=["openai", "openai5", "google", "gemini"], default="google")
419 parser.add_argument("--update_index", type=int, default=0)
420 parser.add_argument("-u", "--update", type=int, default=1)
421 parser.add_argument("-w", "--overwrite", type=int, default=0)
422 parser.add_argument("-p", "--pause", type=int, default=0)
423
424 return parser
425
426
427def read_args(parser):
428 args = parser.parse_args()
429
430 if args.subdir is None:
431 args.subdir = relative_from_source(args.infile)
432
433 return args
434
435
436if __name__ == "__main__":
437 print()
438 print(f"=== {sys.argv[0]} ===")
439 print(f"{EXPLAIN_PROGRAM_PATH=}")
440 print(f"{ADD_DOCSTRING_PATH=}")
441
442 parser = initialize()
443# args = read_args(parser)
444 args = parser.parse_args()
445 print()
446 print("Args:")
447 print(f" {args.infile=}")
448 print(f" {args.subdir=}")
449 print(f" {args.api=}")
450 print(f" {args.update=}")
451 print(f" {args.overwrite=}")
452 print(f" {args.pause=}")
453 print(f" {args.prompt_explain=}")
454 print(f" {args.prompt_quality=}")
455 print(f" {args.prompt_add_docstring=}")
456 print(f" {args.update=}")
457 print(f" {args.overwrite=}")
458 print(f" {args.update_index=}")
459 print(f" {args.pause=}")
460
461 try:
462 main(args)
463 except Exception:
464 print("\n" + "!" * 60)
465 print(" 予期せぬ致命的なエラーが発生しました。")
466 traceback.print_exc()
467 print("!" * 60)
468 sys.exit(1)