Skip to content

Commit d01f395

Browse files
refactor(docs): code analysis engine
changes: - file: dependency_scanner.py area: analyzer added: [_detect_version] modified: [ProjectDependencies, DependencyScanner, _parse_pyproject] - file: examples_gen.py area: docs added: [_get_example_value] modified: [_generate_advanced, _generate_quickstart, _example_from_type, _build_realistic_args, ExamplesGenerator] - file: mkdocs_gen.py area: docs added: [_read_pyproject_mkdocs] modified: [MkDocsGenerator, generate] stats: lines: "+8650/-8248 (net +402)" files: 17 complexity: "Large structural change (normalized)"
1 parent 794ecc0 commit d01f395

24 files changed

Lines changed: 8678 additions & 8253 deletions

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [3.0.4] - 2026-03-08
11+
12+
### Docs
13+
- Update project/README.md
14+
- Update project/context.md
15+
16+
### Test
17+
- Update tests/project/dashboard.html
18+
- Update tests/project/project.yaml
19+
20+
### Other
21+
- Update code2docs/analyzers/dependency_scanner.py
22+
- Update code2docs/generators/examples_gen.py
23+
- Update code2docs/generators/mkdocs_gen.py
24+
- Update project/analysis.json
25+
- Update project/analysis.toon
26+
- Update project/analysis.yaml
27+
- Update project/calls.mmd
28+
- Update project/compact_flow.mmd
29+
- Update project/dashboard.html
30+
- Update project/evolution.toon
31+
- ... and 5 more files
32+
1033
## [3.0.3] - 2026-03-08
1134

1235
### Docs

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# code2docs
22

3-
![version](https://img.shields.io/badge/version-3.0.3-blue) ![python](https://img.shields.io/badge/python-%3E%3D3.9-blue) ![docs](https://img.shields.io/badge/docs-auto--generated-blueviolet)
3+
![version](https://img.shields.io/badge/version-3.0.4-blue) ![python](https://img.shields.io/badge/python-%3E%3D3.9-blue) ![docs](https://img.shields.io/badge/docs-auto--generated-blueviolet)
44

55
> Auto-generate and sync project documentation from source code analysis.
66
@@ -140,7 +140,7 @@ code2docs can update only specific sections of an existing README using markers:
140140
```markdown
141141
<!-- code2docs:start --># code2docs
142142

143-
![version](https://img.shields.io/badge/version-3.0.3-blue) ![python](https://img.shields.io/badge/python-%3E%3D3.9-blue) ![coverage](https://img.shields.io/badge/coverage-unknown-lightgrey) ![functions](https://img.shields.io/badge/functions-276-green)
143+
![version](https://img.shields.io/badge/version-3.0.4-blue) ![python](https://img.shields.io/badge/python-%3E%3D3.9-blue) ![coverage](https://img.shields.io/badge/coverage-unknown-lightgrey) ![functions](https://img.shields.io/badge/functions-276-green)
144144
> **276** functions | **57** classes | **51** files | CC̄ = 3.8
145145

146146
> Auto-generated project documentation from source code analysis.

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.0.3
1+
3.0.4

code2docs/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
README.md, API references, module docs, examples, and architecture diagrams.
66
"""
77

8-
__version__ = "3.0.3"
8+
__version__ = "3.0.4"
99
__author__ = "Tom Sapletta"
1010

1111
from .config import Code2DocsConfig

code2docs/analyzers/dependency_scanner.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ class ProjectDependencies:
3232
optional_groups: Dict[str, List[DependencyInfo]] = field(default_factory=dict)
3333
install_command: str = "pip install ."
3434
source_file: str = ""
35+
# Additional metadata from pyproject.toml
36+
keywords: List[str] = field(default_factory=list)
37+
classifiers: List[str] = field(default_factory=list)
38+
urls: Dict[str, str] = field(default_factory=dict)
39+
version: str = ""
3540

3641

3742
class DependencyScanner:
@@ -76,6 +81,10 @@ def _parse_pyproject(self, path: Path) -> ProjectDependencies:
7681

7782
project = data.get("project", {})
7883
deps.python_version = project.get("requires-python", "")
84+
deps.version = project.get("version", "")
85+
deps.keywords = project.get("keywords", [])
86+
deps.classifiers = project.get("classifiers", [])
87+
deps.urls = project.get("urls", {})
7988

8089
# Main dependencies
8190
for dep_str in project.get("dependencies", []):
@@ -96,6 +105,9 @@ def _parse_pyproject(self, path: Path) -> ProjectDependencies:
96105
if name:
97106
deps.install_command = f"pip install {name}"
98107

108+
# Detect version with fallback to git tags or VERSION file
109+
deps.version = self._detect_version(path.parent, deps.version)
110+
99111
return deps
100112

101113
def _parse_pyproject_regex(self, path: Path) -> ProjectDependencies:
@@ -157,3 +169,29 @@ def _parse_dep_string(dep_str: str) -> DependencyInfo:
157169
version_spec=match.group(2).strip(),
158170
)
159171
return DependencyInfo(name=dep_str.strip())
172+
173+
def _detect_version(self, project_path: Path, pyproject_version: str = "") -> str:
174+
"""Detect version from pyproject.toml, git tags, or VERSION file."""
175+
# Priority 1: pyproject.toml version
176+
if pyproject_version:
177+
return pyproject_version
178+
179+
# Priority 2: VERSION file
180+
version_file = project_path / "VERSION"
181+
if version_file.exists():
182+
return version_file.read_text(encoding="utf-8").strip()
183+
184+
# Priority 3: git tags (latest tag)
185+
try:
186+
import subprocess
187+
result = subprocess.run(
188+
["git", "describe", "--tags", "--abbrev=0"],
189+
cwd=str(project_path),
190+
capture_output=True, text=True, timeout=5,
191+
)
192+
if result.returncode == 0:
193+
return result.stdout.strip().lstrip("v")
194+
except (subprocess.TimeoutExpired, FileNotFoundError):
195+
pass
196+
197+
return ""

code2docs/generators/examples_gen.py

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,44 @@ def __init__(self, config: Code2DocsConfig, result: AnalysisResult):
5959
self.result = result
6060
self._pkg = self._detect_package_name()
6161

62+
def _get_example_value(self, arg_name: str) -> str:
63+
"""Get realistic example value based on actual project config."""
64+
project_name = self.config.project_name or Path(self.result.project_path).name
65+
66+
# Map argument names to actual config values
67+
arg_mappings = {
68+
"project_path": f'"./{project_name}"',
69+
"path": f'"./{project_name}"',
70+
"source": f'"{self.config.source}"' if self.config.source else '"./src"',
71+
"output": f'"{self.config.output}"' if self.config.output else '"./docs"',
72+
"output_dir": f'"{self.config.output}"' if self.config.output else '"./docs"',
73+
"output_path": f'"{self.config.readme_output}"' if self.config.readme_output else '"./docs/README.md"',
74+
"config": "config",
75+
"config_path": '"code2docs.yaml"',
76+
"result": "result",
77+
"name": f'"{project_name}"',
78+
"project_name": f'"{project_name}"',
79+
"verbose": "True",
80+
"dry_run": "False",
81+
"readme_only": "False",
82+
"sections": '["overview", "install", "quickstart"]',
83+
"content": '"# My Doc\\n## Section"',
84+
"markdown_content": '"# My Doc\\n## Section"',
85+
"max_depth": "3",
86+
"target": "80",
87+
"badge_types": '["version", "python"]',
88+
"stats": "{}",
89+
"deps": "[]",
90+
"sync_markers": "True",
91+
"docstring": '"""My function docstring."""',
92+
}
93+
94+
if arg_name in arg_mappings:
95+
return arg_mappings[arg_name]
96+
97+
# Fallback to original static examples
98+
return _ARG_EXAMPLES.get(arg_name, '"..."')
99+
62100
def generate_all(self) -> Dict[str, str]:
63101
"""Generate all example files. Returns {filename: content}."""
64102
files: Dict[str, str] = {}
@@ -100,15 +138,18 @@ def _generate_quickstart(self) -> str:
100138

101139
# --- Example 1: Config (define first so later examples can use it) ---
102140
config_cls = self._find_class_by_name("Code2DocsConfig")
141+
project_name = self.config.project_name or Path(self.result.project_path).name
142+
source = self.config.source or "./"
143+
output = self.config.output or "./docs"
103144
if config_cls:
104145
lines.append('# ' + '=' * 50)
105146
lines.append("# Example 1: Configuration")
106147
lines.append('# ' + '=' * 50)
107148
lines.append("")
108149
lines.append(f"config = {config_cls.name}(")
109-
lines.append(' project_name="my-project",')
110-
lines.append(' source="./src",')
111-
lines.append(' output="./docs",')
150+
lines.append(f' project_name="{project_name}",')
151+
lines.append(f' source="{source}",')
152+
lines.append(f' output="{output}",')
112153
lines.append(" verbose=True,")
113154
lines.append(")")
114155
lines.append("")
@@ -119,20 +160,22 @@ def _generate_quickstart(self) -> str:
119160
lines.append("# Example 2: Generate documentation")
120161
lines.append('# ' + '=' * 50)
121162
lines.append("")
163+
164+
project_path = f'"./{project_name}"' if project_name != "." else '"./"'
122165

123166
gen_func = self._find_function_by_name("generate_readme")
124167
if gen_func:
125168
lines.append("# Generate a README for your project")
126-
lines.append('generate_readme("./my-project", output="README.md")')
169+
lines.append(f'generate_readme({project_path}, output="README.md")')
127170
lines.append("")
128171

129172
docs_func = self._find_function_by_name("generate_docs")
130173
if docs_func:
131174
lines.append("# Generate all documentation")
132175
if config_cls:
133-
lines.append('docs = generate_docs("./my-project", config=config)')
176+
lines.append(f'docs = generate_docs({project_path}, config=config)')
134177
else:
135-
lines.append('docs = generate_docs("./my-project")')
178+
lines.append(f'docs = generate_docs({project_path})')
136179
lines.append('print(f"Generated {len(docs)} documentation sections")')
137180
lines.append("")
138181

@@ -147,7 +190,7 @@ def _generate_quickstart(self) -> str:
147190
lines.append(f"from {pkg}.analyzers.project_scanner import ProjectScanner")
148191
lines.append("")
149192
lines.append("scanner = ProjectScanner(config)")
150-
lines.append('result = scanner.analyze("./my-project")')
193+
lines.append(f'result = scanner.analyze({project_path})')
151194
lines.append("")
152195
lines.append('print(f"Found {len(result.functions)} functions")')
153196
lines.append('print(f"Found {len(result.classes)} classes")')
@@ -191,7 +234,7 @@ def _generate_advanced(self) -> str:
191234
lines.append("# Step 1: Analyze the project")
192235
lines.append("config = Code2DocsConfig(project_name=\"my-project\")")
193236
lines.append("scanner = ProjectScanner(config)")
194-
lines.append('result = scanner.analyze("./my-project")')
237+
lines.append('result = scanner.analyze(f"./${project_name_adv}") if project_name_adv != "." else result = scanner.analyze("./")')
195238
lines.append("")
196239

197240
for i, cls in enumerate(gen_classes[:4], start=2):
@@ -351,26 +394,27 @@ def _build_realistic_args(self, func: FunctionInfo) -> str:
351394
args = [a for a in func.args if a not in ("self", "cls")]
352395
parts = []
353396
for arg in args[:5]:
354-
val = _ARG_EXAMPLES.get(arg)
355-
if not val:
397+
val = self._get_example_value(arg)
398+
if not val or val == '"..."':
356399
# Try type hint
357400
val = self._example_from_type(func, arg)
358401
if not val:
359402
val = '"..."'
360403
parts.append(f"{arg}={val}" if len(args) > 1 else val)
361404
return ", ".join(parts)
362405

363-
@staticmethod
364-
def _example_from_type(func: FunctionInfo, arg: str) -> Optional[str]:
406+
def _example_from_type(self, func: FunctionInfo, arg: str) -> Optional[str]:
365407
"""Try to infer example value from type annotation."""
408+
project_name = self.config.project_name or Path(self.result.project_path).name
409+
366410
# FunctionInfo.returns gives return type, but arg types
367411
# are not always available; use naming heuristics
368412
if "path" in arg.lower():
369-
return '"./my-project"'
413+
return f'"./{project_name}"'
370414
if "name" in arg.lower():
371-
return '"my-project"'
415+
return f'"{project_name}"'
372416
if "dir" in arg.lower():
373-
return '"./docs"'
417+
return f'"{self.config.output}"' if self.config.output else '"./docs"'
374418
if arg.startswith("is_") or arg.startswith("enable"):
375419
return "True"
376420
if "count" in arg.lower() or "max" in arg.lower() or "min" in arg.lower():

code2docs/generators/mkdocs_gen.py

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""MkDocs configuration generator — auto-generate mkdocs.yml from docs tree."""
22

33
from pathlib import Path
4-
from typing import Dict, List, Optional
4+
from typing import Any, Dict, List, Optional
55

66
import yaml
77

@@ -22,11 +22,14 @@ def generate(self, docs_dir: Optional[str] = None) -> str:
2222
project_name = self.config.project_name or "Project"
2323
nav = self._build_nav(docs_dir)
2424

25+
# Read MkDocs config from pyproject.toml if available
26+
mkdocs_config = self._read_pyproject_mkdocs()
27+
2528
data = {
2629
"site_name": f"{project_name} Documentation",
27-
"theme": {"name": "material"},
30+
"theme": mkdocs_config.get("theme", {"name": "material"}),
2831
"nav": nav,
29-
"markdown_extensions": [
32+
"markdown_extensions": mkdocs_config.get("markdown_extensions", [
3033
"admonition",
3134
"pymdownx.highlight",
3235
"pymdownx.superfences",
@@ -37,10 +40,47 @@ def generate(self, docs_dir: Optional[str] = None) -> str:
3740
"format": "!!python/name:pymdownx.superfences.fence_code_format",
3841
}]
3942
}},
40-
],
43+
]),
4144
}
45+
46+
# Add extra fields from pyproject.toml if present
47+
for key in ["extra_css", "extra_javascript", "plugins", "copyright"]:
48+
if key in mkdocs_config:
49+
data[key] = mkdocs_config[key]
50+
4251
return yaml.dump(data, default_flow_style=False, sort_keys=False)
4352

53+
def _read_pyproject_mkdocs(self) -> Dict[str, Any]:
54+
"""Read MkDocs configuration from [tool.mkdocs] in pyproject.toml."""
55+
project_path = Path(self.result.project_path)
56+
pyproject_path = project_path / "pyproject.toml"
57+
58+
if not pyproject_path.exists():
59+
# Also check parent directory (for nested packages)
60+
pyproject_path = project_path.parent / "pyproject.toml"
61+
62+
if not pyproject_path.exists():
63+
return {}
64+
65+
try:
66+
import tomllib
67+
with open(pyproject_path, "rb") as f:
68+
data = tomllib.load(f)
69+
70+
# Support both [tool.mkdocs] and [tool.poetry.plugins.mkdocs] formats
71+
tool_data = data.get("tool", {})
72+
mkdocs_data = tool_data.get("mkdocs", {})
73+
74+
# Also check for poetry-style config
75+
if not mkdocs_data:
76+
poetry = tool_data.get("poetry", {})
77+
plugins = poetry.get("plugins", {})
78+
mkdocs_data = plugins.get("mkdocs", {})
79+
80+
return mkdocs_data
81+
except Exception:
82+
return {}
83+
4484
def _build_nav(self, docs_dir: Optional[str] = None) -> List:
4585
"""Build navigation structure from docs tree and analysis."""
4686
nav: List = [{"Home": "index.md"}]

project/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ code2llm ./ -f yaml --separate-orphans
338338

339339
**Generated by**: `code2llm ./ -f all --readme`
340340
**Analysis Date**: 2026-03-08
341-
**Total Functions**: 277
341+
**Total Functions**: 278
342342
**Total Classes**: 57
343343
**Modules**: 51
344344

project/analysis.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)