make_sphinx_files.py ダウンロード/コピー

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