Sphinx作成支援プログラム

Sphinxマニュアル作成を支援するプログラムの使い方をまとめています。

  1. 生成AIを使うプログラムでは、API KEYを 環境変数 OPENAI_API_KEY あるいは GOOGLE_API_KEYに設定してください。また、AI modelのデフォルトは explain_program5.ini で設定されていますが、このファイルは無くても問題ありません。

  2. explain_program5.py: 引数で与えたpythonプログラムのマニュアル(usage) Markdownを作成します。プロンプトは explain_program5.ini で設定します。

  3. add_docstring.py: 引数で与えたpythonプログラムにsphinx互換のdocstringを追加します。プロンプトは add_docstring.ini で設定します。

  4. make_sphinx_files.py: 引数で与えたpythonプログラムに対して、上記プログラムを呼び出すとともに、関連するSphinxファイルを作成します

  5. save_clipboard.py: (Windowsのみ) クリップボードのテキストや画像をファイルに保存する

AI共通設定ファイル ai.env

ai.env をダウンロード

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 (プログラム)

explain_program5.py をダウンロード

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 (設定ファイル)

explain_program5.ini をダウンロード

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 (プログラム)

add_docstring.py をダウンロード

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 (設定ファイル)

add_docstring.ini をダウンロード

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 (プログラム)

make_sphinx_files.py をダウンロード

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"![{img}](./{img})\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)
save_clipboard.py (プログラム)

save_clipboard.py をダウンロード

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()