pystdoc 0.7.0__tar.gz → 0.7.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pystdoc-0.7.0/src/pystdoc.egg-info → pystdoc-0.7.2}/PKG-INFO +4 -4
- {pystdoc-0.7.0 → pystdoc-0.7.2}/README.md +3 -3
- {pystdoc-0.7.0 → pystdoc-0.7.2}/pyproject.toml +1 -1
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/__init__.py +1 -1
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/cli.py +8 -5
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/config.py +66 -1
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/doc_writer.py +21 -2
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/engine.py +1 -1
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/mcp_server.py +5 -3
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/parser_python.py +11 -1
- {pystdoc-0.7.0 → pystdoc-0.7.2/src/pystdoc.egg-info}/PKG-INFO +4 -4
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc.egg-info/SOURCES.txt +1 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_cli.py +40 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_config.py +39 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_coverage_boost.py +6 -7
- {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_final_100.py +1 -1
- {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_interrupt.py +6 -3
- {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_llm_client.py +6 -6
- pystdoc-0.7.2/tests/test_pep8.py +64 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_progress.py +3 -1
- {pystdoc-0.7.0 → pystdoc-0.7.2}/LICENSE +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/setup.cfg +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/__main__.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/adapters/__init__.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/adapters/base.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/adapters/clang_adapter.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/adapters/generic_adapter.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/adapters/java_adapter.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/adapters/kotlin_adapter.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/adapters/python_adapter.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/cache.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/call_graph.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/compilation_db.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/db.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/design_engine.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/hasher.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/interrupt.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/llm_client.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/parser_clang.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/parser_generic.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/parser_java.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/parser_kotlin.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/perf.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/progress.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/query.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/report_engine.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/scanner.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/symbols.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/ui_detector.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/watcher.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc.egg-info/dependency_links.txt +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc.egg-info/entry_points.txt +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc.egg-info/requires.txt +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc.egg-info/top_level.txt +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_adapters.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_design_engine_full.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_designgen.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_docgen.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_engine.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_engine_full.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_live_server_100.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_mcp_server.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_parser_generic.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_parser_java.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_parser_kotlin.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_perf.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_query.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_reportgen.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_ui_detector.py +0 -0
- {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_watcher.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pystdoc
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: Python Structural & Topological Documentation Engine (pystdoc): High-precision, LLM-powered hierarchical codebase and architecture documentation generator.
|
|
5
5
|
Author: tab4moji
|
|
6
6
|
License: MIT
|
|
@@ -94,7 +94,7 @@ your_project/
|
|
|
94
94
|
- Instant 360-degree dependency tracing: inspect where a symbol is called from (*Inbound Callers / References*) and what it uses (*Outbound Dependencies*) before making breaking code changes or deletions.
|
|
95
95
|
6. **Standard LLM Options & Multi-Language Support**:
|
|
96
96
|
- Works with **Ollama, LiteRT-LM, vLLM, and OpenAI API**.
|
|
97
|
-
- Supports `--host`, `--model`, `--token` / `--api-key`, `--context-size`, and `--language` (e.g. `
|
|
97
|
+
- Supports `--host`, `--model`, `--token` / `--api-key`, `--context-size`, and `--language` (defaults to terminal language, e.g. `Japanese`, `English`, `日本語`).
|
|
98
98
|
|
|
99
99
|
---
|
|
100
100
|
|
|
@@ -225,7 +225,7 @@ pystdoc sync --dir ./target_project/ --host 192.168.0.11:11434 --model gemma4-26
|
|
|
225
225
|
2. **Environment Variables** (`LLM_HOST`, `LLM_MODEL`, `OPENAI_API_KEY`, etc.)
|
|
226
226
|
3. **Project Config** (`<project>/.pystdoc.json`)
|
|
227
227
|
4. **User Config** (`~/.config/pystdoc/pystdoc.json`)
|
|
228
|
-
5. **Builtin Defaults** (`http://127.0.0.1:11434`, `gemma4-26b-a4b`, `English`)
|
|
228
|
+
5. **Builtin Defaults** (`http://127.0.0.1:11434`, `gemma4-26b-a4b`, Terminal language / `English`)
|
|
229
229
|
|
|
230
230
|
---
|
|
231
231
|
|
|
@@ -273,7 +273,7 @@ pystdoc sync --dir ./target_project/ --host 192.168.0.11:11434 --model gemma4-26
|
|
|
273
273
|
| :--- | :--- | :--- | :--- |
|
|
274
274
|
| `--dir` | | `./` | Target project directory path |
|
|
275
275
|
| `--fast` | | `false` | Fast bottom-up sync only (runs docgen symbol indexing, skips designgen and reportgen) |
|
|
276
|
-
| `--language` | `-l` |
|
|
276
|
+
| `--language` | `-l` | Terminal locale | Output documentation language (defaults to terminal language; e.g. `Japanese`, `English`, `日本語`) |
|
|
277
277
|
| `--host` | `-H`, `--base-url` | `http://127.0.0.1:11434` | LLM server host endpoint URL |
|
|
278
278
|
| `--model` | `-m`, `LLM_MODEL` | `gemma4-26b-a4b` | LLM model identifier |
|
|
279
279
|
| `--token` | `--api-key`, `OPENAI_API_KEY` | `None` | API Bearer token |
|
|
@@ -65,7 +65,7 @@ your_project/
|
|
|
65
65
|
- Instant 360-degree dependency tracing: inspect where a symbol is called from (*Inbound Callers / References*) and what it uses (*Outbound Dependencies*) before making breaking code changes or deletions.
|
|
66
66
|
6. **Standard LLM Options & Multi-Language Support**:
|
|
67
67
|
- Works with **Ollama, LiteRT-LM, vLLM, and OpenAI API**.
|
|
68
|
-
- Supports `--host`, `--model`, `--token` / `--api-key`, `--context-size`, and `--language` (e.g. `
|
|
68
|
+
- Supports `--host`, `--model`, `--token` / `--api-key`, `--context-size`, and `--language` (defaults to terminal language, e.g. `Japanese`, `English`, `日本語`).
|
|
69
69
|
|
|
70
70
|
---
|
|
71
71
|
|
|
@@ -196,7 +196,7 @@ pystdoc sync --dir ./target_project/ --host 192.168.0.11:11434 --model gemma4-26
|
|
|
196
196
|
2. **Environment Variables** (`LLM_HOST`, `LLM_MODEL`, `OPENAI_API_KEY`, etc.)
|
|
197
197
|
3. **Project Config** (`<project>/.pystdoc.json`)
|
|
198
198
|
4. **User Config** (`~/.config/pystdoc/pystdoc.json`)
|
|
199
|
-
5. **Builtin Defaults** (`http://127.0.0.1:11434`, `gemma4-26b-a4b`, `English`)
|
|
199
|
+
5. **Builtin Defaults** (`http://127.0.0.1:11434`, `gemma4-26b-a4b`, Terminal language / `English`)
|
|
200
200
|
|
|
201
201
|
---
|
|
202
202
|
|
|
@@ -244,7 +244,7 @@ pystdoc sync --dir ./target_project/ --host 192.168.0.11:11434 --model gemma4-26
|
|
|
244
244
|
| :--- | :--- | :--- | :--- |
|
|
245
245
|
| `--dir` | | `./` | Target project directory path |
|
|
246
246
|
| `--fast` | | `false` | Fast bottom-up sync only (runs docgen symbol indexing, skips designgen and reportgen) |
|
|
247
|
-
| `--language` | `-l` |
|
|
247
|
+
| `--language` | `-l` | Terminal locale | Output documentation language (defaults to terminal language; e.g. `Japanese`, `English`, `日本語`) |
|
|
248
248
|
| `--host` | `-H`, `--base-url` | `http://127.0.0.1:11434` | LLM server host endpoint URL |
|
|
249
249
|
| `--model` | `-m`, `LLM_MODEL` | `gemma4-26b-a4b` | LLM model identifier |
|
|
250
250
|
| `--token` | `--api-key`, `OPENAI_API_KEY` | `None` | API Bearer token |
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pystdoc"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.2"
|
|
8
8
|
description = "Python Structural & Topological Documentation Engine (pystdoc): High-precision, LLM-powered hierarchical codebase and architecture documentation generator."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
@@ -6,7 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
from typing import List, Optional, Tuple
|
|
7
7
|
|
|
8
8
|
import pystdoc
|
|
9
|
-
from pystdoc.config import load_config
|
|
9
|
+
from pystdoc.config import detect_terminal_language, load_config
|
|
10
10
|
from pystdoc.db import DocgenDB
|
|
11
11
|
from pystdoc.design_engine import run_design_generation
|
|
12
12
|
from pystdoc.engine import run_docgen
|
|
@@ -108,7 +108,7 @@ def docgen_main(argv: Optional[List[str]] = None) -> None:
|
|
|
108
108
|
model = args.model or cfg.get("model", "gemma4-26b-a4b")
|
|
109
109
|
token = args.token or args.api_key or cfg.get("token")
|
|
110
110
|
ctx_size = args.context_size or cfg.get("context_size", 16384)
|
|
111
|
-
lang = args.language or cfg.get("language"
|
|
111
|
+
lang = args.language or cfg.get("language") or detect_terminal_language()
|
|
112
112
|
concurrency = args.concurrency or cfg.get("concurrency", 1)
|
|
113
113
|
allow_fallback = (
|
|
114
114
|
args.allow_fallback
|
|
@@ -214,7 +214,7 @@ def designgen_main(argv: Optional[List[str]] = None) -> None:
|
|
|
214
214
|
model = args.model or cfg.get("model", "gemma4-26b-a4b")
|
|
215
215
|
token = args.token or args.api_key or cfg.get("token")
|
|
216
216
|
ctx_size = args.context_size or cfg.get("context_size", 16384)
|
|
217
|
-
lang = args.language or cfg.get("language"
|
|
217
|
+
lang = args.language or cfg.get("language") or detect_terminal_language()
|
|
218
218
|
allow_fallback = (
|
|
219
219
|
args.allow_fallback
|
|
220
220
|
if args.allow_fallback is not None
|
|
@@ -298,7 +298,7 @@ def run_watch(
|
|
|
298
298
|
token: Optional[str] = None,
|
|
299
299
|
context_size: int = 16384,
|
|
300
300
|
concurrency: int = 1,
|
|
301
|
-
language: str =
|
|
301
|
+
language: Optional[str] = None,
|
|
302
302
|
allow_fallback: bool = False,
|
|
303
303
|
compile_commands: Optional[str] = None,
|
|
304
304
|
interval: float = 1.0,
|
|
@@ -307,6 +307,9 @@ def run_watch(
|
|
|
307
307
|
"""Run continuous watcher with auto-sync on code change."""
|
|
308
308
|
from pystdoc.watcher import DNotifyWatcher
|
|
309
309
|
|
|
310
|
+
if language is None:
|
|
311
|
+
language = detect_terminal_language()
|
|
312
|
+
|
|
310
313
|
def _sync_action() -> None:
|
|
311
314
|
ret_docgen = run_docgen(
|
|
312
315
|
target_dir=target_dir,
|
|
@@ -649,7 +652,7 @@ def reportgen_main(argv: Optional[List[str]] = None) -> None:
|
|
|
649
652
|
model = args.model or cfg.get("model", "gemma4-26b-a4b")
|
|
650
653
|
token = args.token or args.api_key or cfg.get("token")
|
|
651
654
|
ctx_size = args.context_size or cfg.get("context_size", 16384)
|
|
652
|
-
lang = args.language or cfg.get("language"
|
|
655
|
+
lang = args.language or cfg.get("language") or detect_terminal_language()
|
|
653
656
|
concurrency = args.concurrency or cfg.get("concurrency", 1)
|
|
654
657
|
allow_fallback = (
|
|
655
658
|
args.allow_fallback
|
|
@@ -17,6 +17,71 @@ DEFAULT_CONFIG: Dict[str, Any] = {
|
|
|
17
17
|
"allow_fallback": False,
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
LANGUAGE_CODE_MAP: Dict[str, str] = {
|
|
21
|
+
"ja": "Japanese",
|
|
22
|
+
"en": "English",
|
|
23
|
+
"zh": "Chinese",
|
|
24
|
+
"es": "Spanish",
|
|
25
|
+
"fr": "French",
|
|
26
|
+
"de": "German",
|
|
27
|
+
"ko": "Korean",
|
|
28
|
+
"it": "Italian",
|
|
29
|
+
"ru": "Russian",
|
|
30
|
+
"pt": "Portuguese",
|
|
31
|
+
"nl": "Dutch",
|
|
32
|
+
"pl": "Polish",
|
|
33
|
+
"sv": "Swedish",
|
|
34
|
+
"da": "Danish",
|
|
35
|
+
"fi": "Finnish",
|
|
36
|
+
"no": "Norwegian",
|
|
37
|
+
"tr": "Turkish",
|
|
38
|
+
"ar": "Arabic",
|
|
39
|
+
"vi": "Vietnamese",
|
|
40
|
+
"th": "Thai",
|
|
41
|
+
"id": "Indonesian",
|
|
42
|
+
"hi": "Hindi",
|
|
43
|
+
"uk": "Ukrainian",
|
|
44
|
+
"cs": "Czech",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def detect_terminal_language() -> str:
|
|
49
|
+
"""Detect terminal/system language from locale environment variables."""
|
|
50
|
+
for env_var in ("LC_ALL", "LC_MESSAGES", "LANG", "LANGUAGE"):
|
|
51
|
+
val = os.environ.get(env_var)
|
|
52
|
+
if val:
|
|
53
|
+
val = val.strip()
|
|
54
|
+
if not val:
|
|
55
|
+
continue
|
|
56
|
+
if val.lower() in ("c", "posix", "c.utf-8", "c.utf8"):
|
|
57
|
+
return "English"
|
|
58
|
+
first_part = val.split(":")[0].strip()
|
|
59
|
+
lang_code = first_part.split(".")[0].split("@")[0].strip()
|
|
60
|
+
if not lang_code:
|
|
61
|
+
return "English"
|
|
62
|
+
l_lower = lang_code.lower()
|
|
63
|
+
if l_lower in ("japanese", "ja", "jp", "日本語"):
|
|
64
|
+
return "Japanese"
|
|
65
|
+
elif l_lower in ("english", "en"):
|
|
66
|
+
return "English"
|
|
67
|
+
elif l_lower in ("chinese", "zh"):
|
|
68
|
+
return "Chinese"
|
|
69
|
+
elif l_lower in ("spanish", "es"):
|
|
70
|
+
return "Spanish"
|
|
71
|
+
elif l_lower in ("french", "fr"):
|
|
72
|
+
return "French"
|
|
73
|
+
elif l_lower in ("german", "de"):
|
|
74
|
+
return "German"
|
|
75
|
+
elif l_lower in ("korean", "ko"):
|
|
76
|
+
return "Korean"
|
|
77
|
+
|
|
78
|
+
primary = l_lower.split("_")[0].split("-")[0]
|
|
79
|
+
if primary in LANGUAGE_CODE_MAP:
|
|
80
|
+
return LANGUAGE_CODE_MAP[primary]
|
|
81
|
+
return lang_code.capitalize()
|
|
82
|
+
|
|
83
|
+
return "English"
|
|
84
|
+
|
|
20
85
|
|
|
21
86
|
def get_user_config_path() -> Path:
|
|
22
87
|
"""Return user configuration path ~/.config/pystdoc/pystdoc.json."""
|
|
@@ -51,6 +116,7 @@ def load_config(target_dir: Optional[Path] = None) -> Dict[str, Any]:
|
|
|
51
116
|
Defaults < ~/.config/pystdoc/pystdoc.json < ./.pystdoc.json < Env vars
|
|
52
117
|
"""
|
|
53
118
|
config = dict(DEFAULT_CONFIG)
|
|
119
|
+
config["language"] = detect_terminal_language()
|
|
54
120
|
|
|
55
121
|
# 1. User config (~/.config/pystdoc/pystdoc.json)
|
|
56
122
|
user_cfg_path = get_user_config_path()
|
|
@@ -89,7 +155,6 @@ def load_config(target_dir: Optional[Path] = None) -> Dict[str, Any]:
|
|
|
89
155
|
env_lang = (
|
|
90
156
|
os.environ.get("DOCGEN_LANG")
|
|
91
157
|
or os.environ.get("PYSTDOC_LANG")
|
|
92
|
-
or os.environ.get("LANGUAGE")
|
|
93
158
|
)
|
|
94
159
|
if env_lang:
|
|
95
160
|
config["language"] = env_lang
|
|
@@ -34,7 +34,12 @@ def normalize_language(lang: Optional[str]) -> str:
|
|
|
34
34
|
"""Normalize language string into standard representation."""
|
|
35
35
|
if not lang:
|
|
36
36
|
return "English"
|
|
37
|
-
|
|
37
|
+
l_strip = lang.strip()
|
|
38
|
+
if not l_strip:
|
|
39
|
+
return "English"
|
|
40
|
+
l_lower = l_strip.lower()
|
|
41
|
+
if l_lower in ("c", "posix", "c.utf-8", "c.utf8"):
|
|
42
|
+
return "English"
|
|
38
43
|
if l_lower in ("japanese", "ja", "jp", "日本語"):
|
|
39
44
|
return "Japanese"
|
|
40
45
|
elif l_lower in ("english", "en"):
|
|
@@ -43,7 +48,21 @@ def normalize_language(lang: Optional[str]) -> str:
|
|
|
43
48
|
return "Chinese"
|
|
44
49
|
elif l_lower in ("spanish", "es"):
|
|
45
50
|
return "Spanish"
|
|
46
|
-
|
|
51
|
+
elif l_lower in ("french", "fr"):
|
|
52
|
+
return "French"
|
|
53
|
+
elif l_lower in ("german", "de"):
|
|
54
|
+
return "German"
|
|
55
|
+
elif l_lower in ("korean", "ko"):
|
|
56
|
+
return "Korean"
|
|
57
|
+
|
|
58
|
+
first_part = l_lower.split(":")[0].strip()
|
|
59
|
+
lang_tag = first_part.split(".")[0].split("@")[0].strip()
|
|
60
|
+
primary = lang_tag.split("_")[0].split("-")[0]
|
|
61
|
+
from pystdoc.config import LANGUAGE_CODE_MAP
|
|
62
|
+
|
|
63
|
+
if primary in LANGUAGE_CODE_MAP:
|
|
64
|
+
return LANGUAGE_CODE_MAP[primary]
|
|
65
|
+
return l_strip.capitalize()
|
|
47
66
|
|
|
48
67
|
|
|
49
68
|
def get_code_language(extension: str) -> str:
|
|
@@ -612,7 +612,7 @@ def run_docgen(
|
|
|
612
612
|
or (is_logic_changed and not is_trivial)
|
|
613
613
|
):
|
|
614
614
|
dirty_symbol_ids.add(node.unique_id)
|
|
615
|
-
elif is_trivial:
|
|
615
|
+
elif is_logic_changed and is_trivial:
|
|
616
616
|
trivial_changed_symbol_ids.add(node.unique_id)
|
|
617
617
|
|
|
618
618
|
perf_mgr = PerfProfileManager.get_instance()
|
|
@@ -13,7 +13,7 @@ except (ImportError, ModuleNotFoundError):
|
|
|
13
13
|
except (ImportError, ModuleNotFoundError):
|
|
14
14
|
FastMCP = None # type: ignore
|
|
15
15
|
|
|
16
|
-
from pystdoc.config import load_config
|
|
16
|
+
from pystdoc.config import detect_terminal_language, load_config
|
|
17
17
|
from pystdoc.db import DocgenDB
|
|
18
18
|
from pystdoc.design_engine import run_design_generation
|
|
19
19
|
from pystdoc.engine import run_docgen
|
|
@@ -53,7 +53,9 @@ class MCPWatcherManager:
|
|
|
53
53
|
def _on_change() -> None:
|
|
54
54
|
try:
|
|
55
55
|
cfg = load_config(target_dir)
|
|
56
|
-
lang =
|
|
56
|
+
lang = (
|
|
57
|
+
cfg.get("language") or detect_terminal_language()
|
|
58
|
+
)
|
|
57
59
|
host = cfg.get("host")
|
|
58
60
|
model = cfg.get("model")
|
|
59
61
|
token = cfg.get("token")
|
|
@@ -576,7 +578,7 @@ def create_mcp_server(
|
|
|
576
578
|
cfg = load_config(target_dir)
|
|
577
579
|
|
|
578
580
|
use_llm = not no_llm if no_llm is not None else True
|
|
579
|
-
lang = language or cfg.get("language"
|
|
581
|
+
lang = language or cfg.get("language") or detect_terminal_language()
|
|
580
582
|
host = cfg.get("host")
|
|
581
583
|
model = cfg.get("model")
|
|
582
584
|
token = cfg.get("token")
|
|
@@ -39,13 +39,23 @@ class PythonSymbolVisitor(ast.NodeVisitor):
|
|
|
39
39
|
callee_visitor = CalleeVisitor()
|
|
40
40
|
callee_visitor.visit(node)
|
|
41
41
|
|
|
42
|
+
param_strs = []
|
|
43
|
+
for p in params:
|
|
44
|
+
if p.type_hint:
|
|
45
|
+
param_strs.append(f"{p.name}: {p.type_hint}")
|
|
46
|
+
else:
|
|
47
|
+
param_strs.append(p.name)
|
|
48
|
+
ret_str = f" -> {return_type}" if return_type else ""
|
|
49
|
+
async_prefix = "async " if is_async else ""
|
|
50
|
+
sig_str = f"{async_prefix}def {name}({', '.join(param_strs)}){ret_str}"
|
|
51
|
+
|
|
42
52
|
sym = Symbol(
|
|
43
53
|
name=name,
|
|
44
54
|
kind="async_function" if is_async else "function",
|
|
45
55
|
line_start=line_start,
|
|
46
56
|
line_end=line_end,
|
|
47
57
|
fqdn=fqdn,
|
|
48
|
-
signature=
|
|
58
|
+
signature=sig_str,
|
|
49
59
|
doc=docstring,
|
|
50
60
|
parameters=params,
|
|
51
61
|
return_type=return_type,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pystdoc
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: Python Structural & Topological Documentation Engine (pystdoc): High-precision, LLM-powered hierarchical codebase and architecture documentation generator.
|
|
5
5
|
Author: tab4moji
|
|
6
6
|
License: MIT
|
|
@@ -94,7 +94,7 @@ your_project/
|
|
|
94
94
|
- Instant 360-degree dependency tracing: inspect where a symbol is called from (*Inbound Callers / References*) and what it uses (*Outbound Dependencies*) before making breaking code changes or deletions.
|
|
95
95
|
6. **Standard LLM Options & Multi-Language Support**:
|
|
96
96
|
- Works with **Ollama, LiteRT-LM, vLLM, and OpenAI API**.
|
|
97
|
-
- Supports `--host`, `--model`, `--token` / `--api-key`, `--context-size`, and `--language` (e.g. `
|
|
97
|
+
- Supports `--host`, `--model`, `--token` / `--api-key`, `--context-size`, and `--language` (defaults to terminal language, e.g. `Japanese`, `English`, `日本語`).
|
|
98
98
|
|
|
99
99
|
---
|
|
100
100
|
|
|
@@ -225,7 +225,7 @@ pystdoc sync --dir ./target_project/ --host 192.168.0.11:11434 --model gemma4-26
|
|
|
225
225
|
2. **Environment Variables** (`LLM_HOST`, `LLM_MODEL`, `OPENAI_API_KEY`, etc.)
|
|
226
226
|
3. **Project Config** (`<project>/.pystdoc.json`)
|
|
227
227
|
4. **User Config** (`~/.config/pystdoc/pystdoc.json`)
|
|
228
|
-
5. **Builtin Defaults** (`http://127.0.0.1:11434`, `gemma4-26b-a4b`, `English`)
|
|
228
|
+
5. **Builtin Defaults** (`http://127.0.0.1:11434`, `gemma4-26b-a4b`, Terminal language / `English`)
|
|
229
229
|
|
|
230
230
|
---
|
|
231
231
|
|
|
@@ -273,7 +273,7 @@ pystdoc sync --dir ./target_project/ --host 192.168.0.11:11434 --model gemma4-26
|
|
|
273
273
|
| :--- | :--- | :--- | :--- |
|
|
274
274
|
| `--dir` | | `./` | Target project directory path |
|
|
275
275
|
| `--fast` | | `false` | Fast bottom-up sync only (runs docgen symbol indexing, skips designgen and reportgen) |
|
|
276
|
-
| `--language` | `-l` |
|
|
276
|
+
| `--language` | `-l` | Terminal locale | Output documentation language (defaults to terminal language; e.g. `Japanese`, `English`, `日本語`) |
|
|
277
277
|
| `--host` | `-H`, `--base-url` | `http://127.0.0.1:11434` | LLM server host endpoint URL |
|
|
278
278
|
| `--model` | `-m`, `LLM_MODEL` | `gemma4-26b-a4b` | LLM model identifier |
|
|
279
279
|
| `--token` | `--api-key`, `OPENAI_API_KEY` | `None` | API Bearer token |
|
|
@@ -473,6 +473,46 @@ class TestCLI(unittest.TestCase):
|
|
|
473
473
|
self.assertEqual(cm.exception.code, 0)
|
|
474
474
|
mock_start.assert_called_once()
|
|
475
475
|
|
|
476
|
+
def test_cli_default_language_from_terminal(self):
|
|
477
|
+
# When --language is NOT specified, terminal language is used
|
|
478
|
+
test_args = [
|
|
479
|
+
"docgen",
|
|
480
|
+
"--dir", str(self.test_dir),
|
|
481
|
+
"--no-llm",
|
|
482
|
+
"--allow-fallback",
|
|
483
|
+
]
|
|
484
|
+
with patch.dict("os.environ", {"LANG": "ja_JP.UTF-8"}, clear=False):
|
|
485
|
+
with patch(
|
|
486
|
+
"pystdoc.cli.run_docgen", return_value=0
|
|
487
|
+
) as mock_docgen:
|
|
488
|
+
with patch("sys.argv", test_args):
|
|
489
|
+
with self.assertRaises(SystemExit) as cm:
|
|
490
|
+
docgen_main()
|
|
491
|
+
self.assertEqual(cm.exception.code, 0)
|
|
492
|
+
mock_docgen.assert_called_once()
|
|
493
|
+
_, kwargs = mock_docgen.call_args
|
|
494
|
+
self.assertEqual(kwargs.get("language"), "Japanese")
|
|
495
|
+
|
|
496
|
+
# When --language IS specified, the explicit argument should be used
|
|
497
|
+
test_args_explicit = [
|
|
498
|
+
"docgen",
|
|
499
|
+
"--dir", str(self.test_dir),
|
|
500
|
+
"--no-llm",
|
|
501
|
+
"--allow-fallback",
|
|
502
|
+
"--language", "French",
|
|
503
|
+
]
|
|
504
|
+
with patch.dict("os.environ", {"LANG": "ja_JP.UTF-8"}, clear=False):
|
|
505
|
+
with patch(
|
|
506
|
+
"pystdoc.cli.run_docgen", return_value=0
|
|
507
|
+
) as mock_docgen:
|
|
508
|
+
with patch("sys.argv", test_args_explicit):
|
|
509
|
+
with self.assertRaises(SystemExit) as cm:
|
|
510
|
+
docgen_main()
|
|
511
|
+
self.assertEqual(cm.exception.code, 0)
|
|
512
|
+
mock_docgen.assert_called_once()
|
|
513
|
+
_, kwargs = mock_docgen.call_args
|
|
514
|
+
self.assertEqual(kwargs.get("language"), "French")
|
|
515
|
+
|
|
476
516
|
|
|
477
517
|
if __name__ == "__main__":
|
|
478
518
|
unittest.main()
|
|
@@ -9,6 +9,7 @@ from unittest.mock import patch
|
|
|
9
9
|
|
|
10
10
|
from pystdoc.config import (
|
|
11
11
|
_read_json_file,
|
|
12
|
+
detect_terminal_language,
|
|
12
13
|
get_user_config_path,
|
|
13
14
|
load_config,
|
|
14
15
|
)
|
|
@@ -154,6 +155,44 @@ class TestConfig(unittest.TestCase):
|
|
|
154
155
|
p, Path.home() / ".config" / "pystdoc" / "pystdoc.json"
|
|
155
156
|
)
|
|
156
157
|
|
|
158
|
+
def test_detect_terminal_language_japanese(self):
|
|
159
|
+
with patch.dict(os.environ, {"LANG": "ja_JP.UTF-8"}, clear=True):
|
|
160
|
+
self.assertEqual(detect_terminal_language(), "Japanese")
|
|
161
|
+
|
|
162
|
+
def test_detect_terminal_language_priority(self):
|
|
163
|
+
# LC_ALL > LC_MESSAGES > LANG > LANGUAGE
|
|
164
|
+
env = {
|
|
165
|
+
"LANG": "ja_JP.UTF-8",
|
|
166
|
+
"LC_MESSAGES": "fr_FR.UTF-8",
|
|
167
|
+
"LC_ALL": "de_DE.UTF-8",
|
|
168
|
+
}
|
|
169
|
+
with patch.dict(os.environ, env, clear=True):
|
|
170
|
+
self.assertEqual(detect_terminal_language(), "German")
|
|
171
|
+
|
|
172
|
+
env2 = {
|
|
173
|
+
"LANG": "ja_JP.UTF-8",
|
|
174
|
+
"LC_MESSAGES": "fr_FR.UTF-8",
|
|
175
|
+
}
|
|
176
|
+
with patch.dict(os.environ, env2, clear=True):
|
|
177
|
+
self.assertEqual(detect_terminal_language(), "French")
|
|
178
|
+
|
|
179
|
+
def test_detect_terminal_language_c_posix(self):
|
|
180
|
+
with patch.dict(os.environ, {"LANG": "C"}, clear=True):
|
|
181
|
+
self.assertEqual(detect_terminal_language(), "English")
|
|
182
|
+
with patch.dict(os.environ, {"LANG": "POSIX"}, clear=True):
|
|
183
|
+
self.assertEqual(detect_terminal_language(), "English")
|
|
184
|
+
|
|
185
|
+
def test_load_config_defaults_to_terminal_language(self):
|
|
186
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
187
|
+
tmp_path = Path(tmpdir)
|
|
188
|
+
with patch.dict(os.environ, {"LANG": "ja_JP.UTF-8"}, clear=True):
|
|
189
|
+
with patch(
|
|
190
|
+
"pystdoc.config.get_user_config_path",
|
|
191
|
+
return_value=tmp_path / "not_found.json",
|
|
192
|
+
):
|
|
193
|
+
cfg = load_config(tmp_path)
|
|
194
|
+
self.assertEqual(cfg["language"], "Japanese")
|
|
195
|
+
|
|
157
196
|
|
|
158
197
|
if __name__ == "__main__":
|
|
159
198
|
unittest.main()
|
|
@@ -1021,11 +1021,10 @@ class TestCoverageBoost(unittest.TestCase):
|
|
|
1021
1021
|
mock_resp.readline.side_effect = (
|
|
1022
1022
|
lambda: lines_iter.pop(0) if lines_iter else b""
|
|
1023
1023
|
)
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
mock_cm.__exit__.return_value = None
|
|
1024
|
+
mock_resp.__enter__.return_value = mock_resp
|
|
1025
|
+
mock_resp.__exit__.return_value = None
|
|
1027
1026
|
|
|
1028
|
-
with patch("urllib.request.urlopen", return_value=
|
|
1027
|
+
with patch("urllib.request.urlopen", return_value=mock_resp):
|
|
1029
1028
|
with patch(
|
|
1030
1029
|
"pystdoc.llm_client.interruptible_sleep", return_value=None
|
|
1031
1030
|
):
|
|
@@ -1359,12 +1358,12 @@ class TestCoverageBoost(unittest.TestCase):
|
|
|
1359
1358
|
language="English",
|
|
1360
1359
|
)
|
|
1361
1360
|
|
|
1362
|
-
# 2. Modify helper implementation
|
|
1361
|
+
# 2. Modify helper implementation and signature
|
|
1363
1362
|
py_file.write_text(
|
|
1364
1363
|
"g_scale = 10\n\n"
|
|
1365
|
-
"def helper(x: int) -> int:\n"
|
|
1364
|
+
"def helper(x: int, step: int = 1) -> int:\n"
|
|
1366
1365
|
" # internal tweak\n"
|
|
1367
|
-
" y = x +
|
|
1366
|
+
" y = x + step\n"
|
|
1368
1367
|
" return y\n\n"
|
|
1369
1368
|
"def compute(a: int) -> int:\n"
|
|
1370
1369
|
" val = helper(a)\n"
|
|
@@ -491,7 +491,7 @@ class TestFinal100(unittest.TestCase):
|
|
|
491
491
|
use_llm=True,
|
|
492
492
|
allow_fallback=True,
|
|
493
493
|
)
|
|
494
|
-
self.assertEqual(ret_des_warn,
|
|
494
|
+
self.assertEqual(ret_des_warn, 1)
|
|
495
495
|
|
|
496
496
|
def test_engine_empty_dir_process_nodes_and_thread_exception(self):
|
|
497
497
|
from pystdoc.engine import run_docgen
|
|
@@ -56,9 +56,12 @@ class TestInterruptionState(unittest.TestCase):
|
|
|
56
56
|
|
|
57
57
|
def test_interruptible_sleep(self):
|
|
58
58
|
from pystdoc.interrupt import interruptible_sleep
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
with patch(
|
|
60
|
+
"pystdoc.interrupt.is_test_environment", return_value=False
|
|
61
|
+
):
|
|
62
|
+
start = time.time()
|
|
63
|
+
interruptible_sleep(0.05)
|
|
64
|
+
self.assertGreaterEqual(time.time() - start, 0.04)
|
|
62
65
|
|
|
63
66
|
InterruptionState.set_interrupted()
|
|
64
67
|
with self.assertRaises(KeyboardInterrupt):
|
|
@@ -175,12 +175,10 @@ class TestLLMClient(unittest.TestCase):
|
|
|
175
175
|
mock_resp_obj.readline.side_effect = (
|
|
176
176
|
lambda: lines_iter.pop(0) if lines_iter else b""
|
|
177
177
|
)
|
|
178
|
+
mock_resp_obj.__enter__.return_value = mock_resp_obj
|
|
179
|
+
mock_resp_obj.__exit__.return_value = None
|
|
178
180
|
|
|
179
|
-
|
|
180
|
-
mock_resp_ctx.__enter__.return_value = mock_resp_obj
|
|
181
|
-
mock_resp_ctx.__exit__.return_value = None
|
|
182
|
-
|
|
183
|
-
with patch("urllib.request.urlopen", return_value=mock_resp_ctx):
|
|
181
|
+
with patch("urllib.request.urlopen", return_value=mock_resp_obj):
|
|
184
182
|
res = client.chat_completion(
|
|
185
183
|
[{"role": "user", "content": "hello"}],
|
|
186
184
|
json_mode=True,
|
|
@@ -415,7 +413,9 @@ class TestLLMClient(unittest.TestCase):
|
|
|
415
413
|
)
|
|
416
414
|
|
|
417
415
|
with patch("urllib.request.urlopen", side_effect=http_err_500):
|
|
418
|
-
with patch(
|
|
416
|
+
with patch(
|
|
417
|
+
"pystdoc.llm_client.interruptible_sleep"
|
|
418
|
+
) as mock_sleep:
|
|
419
419
|
with self.assertRaises(LLMError):
|
|
420
420
|
client.chat_completion(
|
|
421
421
|
[{"role": "user", "content": "test"}], max_retries=2
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Unit test for PEP8 compliance using flake8 / pycodestyle."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import unittest
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestPEP8(unittest.TestCase):
|
|
10
|
+
"""Ensure all python source files adhere to PEP8 guidelines."""
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def setUpClass(cls):
|
|
14
|
+
cls.pystdoc_dir = Path(__file__).resolve().parent.parent
|
|
15
|
+
cls.src_dir = cls.pystdoc_dir / "src"
|
|
16
|
+
cls.tests_dir = cls.pystdoc_dir / "tests"
|
|
17
|
+
|
|
18
|
+
cls.linter_cmd = None
|
|
19
|
+
if shutil.which("flake8"):
|
|
20
|
+
cls.linter_cmd = ["flake8"]
|
|
21
|
+
elif shutil.which("pycodestyle"):
|
|
22
|
+
cls.linter_cmd = ["pycodestyle"]
|
|
23
|
+
else:
|
|
24
|
+
for candidate in (
|
|
25
|
+
Path.home() / ".local" / "bin" / "flake8",
|
|
26
|
+
Path.home() / ".local" / "bin" / "pycodestyle",
|
|
27
|
+
Path("/usr/local/bin/flake8"),
|
|
28
|
+
Path("/usr/bin/flake8"),
|
|
29
|
+
):
|
|
30
|
+
if candidate.exists() and candidate.is_file():
|
|
31
|
+
cls.linter_cmd = [str(candidate)]
|
|
32
|
+
break
|
|
33
|
+
|
|
34
|
+
def _run_linter(self, target_dir: Path):
|
|
35
|
+
if not self.linter_cmd:
|
|
36
|
+
self.skipTest(
|
|
37
|
+
"Neither flake8 nor pycodestyle found in environment."
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
cmd = self.linter_cmd + [str(target_dir)]
|
|
41
|
+
result = subprocess.run(
|
|
42
|
+
cmd,
|
|
43
|
+
stdout=subprocess.PIPE,
|
|
44
|
+
stderr=subprocess.PIPE,
|
|
45
|
+
text=True,
|
|
46
|
+
cwd=str(self.pystdoc_dir),
|
|
47
|
+
)
|
|
48
|
+
self.assertEqual(
|
|
49
|
+
result.returncode,
|
|
50
|
+
0,
|
|
51
|
+
f"PEP8 violations detected in {target_dir}:\n{result.stdout}",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def test_src_pep8_compliance(self):
|
|
55
|
+
"""Verify all files under src/ pass PEP8 compliance."""
|
|
56
|
+
self._run_linter(self.src_dir)
|
|
57
|
+
|
|
58
|
+
def test_tests_pep8_compliance(self):
|
|
59
|
+
"""Verify all test files under tests/ pass PEP8 compliance."""
|
|
60
|
+
self._run_linter(self.tests_dir)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
unittest.main()
|
|
@@ -201,7 +201,9 @@ def test_phase_progress_tracker():
|
|
|
201
201
|
assert "sym1" in l1
|
|
202
202
|
assert "\r" in stream.getvalue()
|
|
203
203
|
|
|
204
|
-
cur = tracker.render_current(
|
|
204
|
+
cur = tracker.render_current(
|
|
205
|
+
extra="sym1_current_very_long_extra_string", est_remaining=20.0
|
|
206
|
+
)
|
|
205
207
|
assert "1/5 ( 20.0%)" in cur
|
|
206
208
|
|
|
207
209
|
# Next render is shorter, triggering BS padding
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|