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.
Files changed (70) hide show
  1. {pystdoc-0.7.0/src/pystdoc.egg-info → pystdoc-0.7.2}/PKG-INFO +4 -4
  2. {pystdoc-0.7.0 → pystdoc-0.7.2}/README.md +3 -3
  3. {pystdoc-0.7.0 → pystdoc-0.7.2}/pyproject.toml +1 -1
  4. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/__init__.py +1 -1
  5. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/cli.py +8 -5
  6. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/config.py +66 -1
  7. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/doc_writer.py +21 -2
  8. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/engine.py +1 -1
  9. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/mcp_server.py +5 -3
  10. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/parser_python.py +11 -1
  11. {pystdoc-0.7.0 → pystdoc-0.7.2/src/pystdoc.egg-info}/PKG-INFO +4 -4
  12. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc.egg-info/SOURCES.txt +1 -0
  13. {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_cli.py +40 -0
  14. {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_config.py +39 -0
  15. {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_coverage_boost.py +6 -7
  16. {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_final_100.py +1 -1
  17. {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_interrupt.py +6 -3
  18. {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_llm_client.py +6 -6
  19. pystdoc-0.7.2/tests/test_pep8.py +64 -0
  20. {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_progress.py +3 -1
  21. {pystdoc-0.7.0 → pystdoc-0.7.2}/LICENSE +0 -0
  22. {pystdoc-0.7.0 → pystdoc-0.7.2}/setup.cfg +0 -0
  23. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/__main__.py +0 -0
  24. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/adapters/__init__.py +0 -0
  25. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/adapters/base.py +0 -0
  26. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/adapters/clang_adapter.py +0 -0
  27. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/adapters/generic_adapter.py +0 -0
  28. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/adapters/java_adapter.py +0 -0
  29. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/adapters/kotlin_adapter.py +0 -0
  30. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/adapters/python_adapter.py +0 -0
  31. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/cache.py +0 -0
  32. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/call_graph.py +0 -0
  33. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/compilation_db.py +0 -0
  34. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/db.py +0 -0
  35. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/design_engine.py +0 -0
  36. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/hasher.py +0 -0
  37. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/interrupt.py +0 -0
  38. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/llm_client.py +0 -0
  39. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/parser_clang.py +0 -0
  40. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/parser_generic.py +0 -0
  41. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/parser_java.py +0 -0
  42. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/parser_kotlin.py +0 -0
  43. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/perf.py +0 -0
  44. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/progress.py +0 -0
  45. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/query.py +0 -0
  46. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/report_engine.py +0 -0
  47. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/scanner.py +0 -0
  48. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/symbols.py +0 -0
  49. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/ui_detector.py +0 -0
  50. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc/watcher.py +0 -0
  51. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc.egg-info/dependency_links.txt +0 -0
  52. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc.egg-info/entry_points.txt +0 -0
  53. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc.egg-info/requires.txt +0 -0
  54. {pystdoc-0.7.0 → pystdoc-0.7.2}/src/pystdoc.egg-info/top_level.txt +0 -0
  55. {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_adapters.py +0 -0
  56. {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_design_engine_full.py +0 -0
  57. {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_designgen.py +0 -0
  58. {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_docgen.py +0 -0
  59. {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_engine.py +0 -0
  60. {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_engine_full.py +0 -0
  61. {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_live_server_100.py +0 -0
  62. {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_mcp_server.py +0 -0
  63. {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_parser_generic.py +0 -0
  64. {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_parser_java.py +0 -0
  65. {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_parser_kotlin.py +0 -0
  66. {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_perf.py +0 -0
  67. {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_query.py +0 -0
  68. {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_reportgen.py +0 -0
  69. {pystdoc-0.7.0 → pystdoc-0.7.2}/tests/test_ui_detector.py +0 -0
  70. {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.0
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. `English`, `Japanese`, `日本語`).
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` | `English` | Output documentation language (`English`, `Japanese`, `日本語`) |
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. `English`, `Japanese`, `日本語`).
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` | `English` | Output documentation language (`English`, `Japanese`, `日本語`) |
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.0"
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 = [
@@ -1,3 +1,3 @@
1
1
  """pystdoc: Python Structural & Topological Documentation Engine (pystdoc)."""
2
2
 
3
- __version__ = "0.7.0"
3
+ __version__ = "0.7.2"
@@ -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", "English")
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", "English")
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 = "English",
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", "English")
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
- l_lower = lang.strip().lower()
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
- return lang.strip().capitalize()
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 = cfg.get("language", "English")
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", "English")
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=f"def {name}(...)",
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.0
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. `English`, `Japanese`, `日本語`).
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` | `English` | Output documentation language (`English`, `Japanese`, `日本語`) |
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 |
@@ -59,6 +59,7 @@ tests/test_mcp_server.py
59
59
  tests/test_parser_generic.py
60
60
  tests/test_parser_java.py
61
61
  tests/test_parser_kotlin.py
62
+ tests/test_pep8.py
62
63
  tests/test_perf.py
63
64
  tests/test_progress.py
64
65
  tests/test_query.py
@@ -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
- mock_cm = MagicMock()
1025
- mock_cm.__enter__.return_value = mock_resp
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=mock_cm):
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 (internal tweak)
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 + 1\n"
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, 0)
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
- start = time.time()
60
- interruptible_sleep(0.05)
61
- self.assertGreaterEqual(time.time() - start, 0.04)
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
- mock_resp_ctx = MagicMock()
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("time.sleep") as mock_sleep:
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(extra="sym1_current_very_long_extra_string", est_remaining=20.0)
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