code2docs 0.1.1__py3-none-any.whl
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.
- code2docs/__init__.py +32 -0
- code2docs/__main__.py +6 -0
- code2docs/analyzers/__init__.py +13 -0
- code2docs/analyzers/dependency_scanner.py +159 -0
- code2docs/analyzers/docstring_extractor.py +111 -0
- code2docs/analyzers/endpoint_detector.py +116 -0
- code2docs/analyzers/project_scanner.py +45 -0
- code2docs/cli.py +226 -0
- code2docs/config.py +158 -0
- code2docs/formatters/__init__.py +7 -0
- code2docs/formatters/badges.py +52 -0
- code2docs/formatters/markdown.py +73 -0
- code2docs/formatters/toc.py +63 -0
- code2docs/generators/__init__.py +42 -0
- code2docs/generators/api_reference_gen.py +150 -0
- code2docs/generators/architecture_gen.py +192 -0
- code2docs/generators/changelog_gen.py +121 -0
- code2docs/generators/examples_gen.py +194 -0
- code2docs/generators/module_docs_gen.py +204 -0
- code2docs/generators/readme_gen.py +229 -0
- code2docs/sync/__init__.py +6 -0
- code2docs/sync/differ.py +125 -0
- code2docs/sync/updater.py +77 -0
- code2docs/sync/watcher.py +75 -0
- code2docs/templates/api_module.md.j2 +62 -0
- code2docs/templates/architecture.md.j2 +45 -0
- code2docs/templates/example_usage.py.j2 +12 -0
- code2docs/templates/index.md.j2 +31 -0
- code2docs/templates/readme.md.j2 +85 -0
- code2docs-0.1.1.dist-info/METADATA +228 -0
- code2docs-0.1.1.dist-info/RECORD +35 -0
- code2docs-0.1.1.dist-info/WHEEL +5 -0
- code2docs-0.1.1.dist-info/entry_points.txt +2 -0
- code2docs-0.1.1.dist-info/licenses/LICENSE +201 -0
- code2docs-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""API reference documentation generator — per-module API docs."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from jinja2 import Environment, PackageLoader, select_autoescape
|
|
7
|
+
|
|
8
|
+
from code2llm.core.models import AnalysisResult, FunctionInfo, ClassInfo, ModuleInfo
|
|
9
|
+
|
|
10
|
+
from ..config import Code2DocsConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ApiReferenceGenerator:
|
|
14
|
+
"""Generate docs/api/ — per-module API reference from signatures."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, config: Code2DocsConfig, result: AnalysisResult):
|
|
17
|
+
self.config = config
|
|
18
|
+
self.result = result
|
|
19
|
+
self.env = Environment(
|
|
20
|
+
loader=PackageLoader("code2docs", "templates"),
|
|
21
|
+
autoescape=select_autoescape([]),
|
|
22
|
+
trim_blocks=True,
|
|
23
|
+
lstrip_blocks=True,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def generate_all(self) -> Dict[str, str]:
|
|
27
|
+
"""Generate API reference for all modules. Returns {filename: content}."""
|
|
28
|
+
files: Dict[str, str] = {}
|
|
29
|
+
|
|
30
|
+
# Index page
|
|
31
|
+
files["index.md"] = self._generate_index()
|
|
32
|
+
|
|
33
|
+
# Per-module pages
|
|
34
|
+
for mod_name, mod_info in sorted(self.result.modules.items()):
|
|
35
|
+
safe_name = mod_name.replace(".", "_").replace("/", "_")
|
|
36
|
+
filename = f"module_{safe_name}.md"
|
|
37
|
+
files[filename] = self._generate_module_api(mod_name, mod_info)
|
|
38
|
+
|
|
39
|
+
return files
|
|
40
|
+
|
|
41
|
+
def _generate_index(self) -> str:
|
|
42
|
+
"""Generate API index page."""
|
|
43
|
+
lines = [
|
|
44
|
+
"# API Reference\n",
|
|
45
|
+
f"> Auto-generated from {len(self.result.modules)} modules | "
|
|
46
|
+
f"{len(self.result.functions)} functions | "
|
|
47
|
+
f"{len(self.result.classes)} classes\n",
|
|
48
|
+
"## Modules\n",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
for mod_name in sorted(self.result.modules.keys()):
|
|
52
|
+
mod = self.result.modules[mod_name]
|
|
53
|
+
safe_name = mod_name.replace(".", "_").replace("/", "_")
|
|
54
|
+
func_count = len(mod.functions)
|
|
55
|
+
class_count = len(mod.classes)
|
|
56
|
+
lines.append(
|
|
57
|
+
f"- [`{mod_name}`](module_{safe_name}.md) — "
|
|
58
|
+
f"{func_count} functions, {class_count} classes"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return "\n".join(lines) + "\n"
|
|
62
|
+
|
|
63
|
+
def _generate_module_api(self, mod_name: str, mod_info: ModuleInfo) -> str:
|
|
64
|
+
"""Generate API reference for a single module."""
|
|
65
|
+
lines = [f"# `{mod_name}`\n"]
|
|
66
|
+
|
|
67
|
+
# Source info
|
|
68
|
+
lines.append(f"> Source: `{mod_info.file}`\n")
|
|
69
|
+
|
|
70
|
+
# Classes in this module
|
|
71
|
+
module_classes = {
|
|
72
|
+
k: v for k, v in self.result.classes.items()
|
|
73
|
+
if v.module == mod_name or k.startswith(mod_name + ".")
|
|
74
|
+
}
|
|
75
|
+
if module_classes:
|
|
76
|
+
lines.append("## Classes\n")
|
|
77
|
+
for cls_name, cls_info in sorted(module_classes.items()):
|
|
78
|
+
lines.append(f"### `{cls_info.name}`\n")
|
|
79
|
+
if cls_info.bases:
|
|
80
|
+
lines.append(f"Inherits from: {', '.join(f'`{b}`' for b in cls_info.bases)}\n")
|
|
81
|
+
if cls_info.docstring:
|
|
82
|
+
lines.append(f"{cls_info.docstring.strip()}\n")
|
|
83
|
+
|
|
84
|
+
# Methods of this class
|
|
85
|
+
methods = self._get_class_methods(cls_info)
|
|
86
|
+
if methods:
|
|
87
|
+
lines.append("#### Methods\n")
|
|
88
|
+
for method in methods:
|
|
89
|
+
sig = self._format_signature(method)
|
|
90
|
+
doc_line = ""
|
|
91
|
+
if method.docstring:
|
|
92
|
+
doc_line = f" — {method.docstring.splitlines()[0]}"
|
|
93
|
+
cc = method.complexity.get("cyclomatic", 0)
|
|
94
|
+
cc_badge = f" ⚠️ CC={cc}" if cc > 10 else ""
|
|
95
|
+
lines.append(f"- `{sig}`{doc_line}{cc_badge}")
|
|
96
|
+
lines.append("")
|
|
97
|
+
|
|
98
|
+
# Standalone functions in this module
|
|
99
|
+
module_functions = {
|
|
100
|
+
k: v for k, v in self.result.functions.items()
|
|
101
|
+
if (v.module == mod_name or k.startswith(mod_name + "."))
|
|
102
|
+
and not v.is_method
|
|
103
|
+
}
|
|
104
|
+
if module_functions:
|
|
105
|
+
lines.append("## Functions\n")
|
|
106
|
+
for func_name, func_info in sorted(module_functions.items()):
|
|
107
|
+
sig = self._format_signature(func_info)
|
|
108
|
+
lines.append(f"### `{sig}`\n")
|
|
109
|
+
if func_info.docstring:
|
|
110
|
+
lines.append(f"{func_info.docstring.strip()}\n")
|
|
111
|
+
cc = func_info.complexity.get("cyclomatic", 0)
|
|
112
|
+
if cc:
|
|
113
|
+
lines.append(f"- Complexity: {cc}")
|
|
114
|
+
if func_info.calls:
|
|
115
|
+
lines.append(f"- Calls: {', '.join(f'`{c}`' for c in func_info.calls[:10])}")
|
|
116
|
+
lines.append("")
|
|
117
|
+
|
|
118
|
+
# Imports
|
|
119
|
+
if mod_info.imports:
|
|
120
|
+
lines.append("## Imports\n")
|
|
121
|
+
for imp in sorted(mod_info.imports):
|
|
122
|
+
lines.append(f"- `{imp}`")
|
|
123
|
+
lines.append("")
|
|
124
|
+
|
|
125
|
+
return "\n".join(lines)
|
|
126
|
+
|
|
127
|
+
def _get_class_methods(self, cls_info: ClassInfo) -> List[FunctionInfo]:
|
|
128
|
+
"""Get FunctionInfo objects for class methods."""
|
|
129
|
+
methods = []
|
|
130
|
+
for method_name in cls_info.methods:
|
|
131
|
+
# Try various qualified name patterns
|
|
132
|
+
for key in [method_name, f"{cls_info.qualified_name}.{method_name}"]:
|
|
133
|
+
if key in self.result.functions:
|
|
134
|
+
methods.append(self.result.functions[key])
|
|
135
|
+
break
|
|
136
|
+
return methods
|
|
137
|
+
|
|
138
|
+
@staticmethod
|
|
139
|
+
def _format_signature(func: FunctionInfo) -> str:
|
|
140
|
+
"""Format a function signature string."""
|
|
141
|
+
args_str = ", ".join(func.args)
|
|
142
|
+
ret = f" → {func.returns}" if func.returns else ""
|
|
143
|
+
return f"{func.name}({args_str}){ret}"
|
|
144
|
+
|
|
145
|
+
def write_all(self, output_dir: str, files: Dict[str, str]) -> None:
|
|
146
|
+
"""Write all generated API reference files."""
|
|
147
|
+
out = Path(output_dir)
|
|
148
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
149
|
+
for filename, content in files.items():
|
|
150
|
+
(out / filename).write_text(content, encoding="utf-8")
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Architecture documentation generator with Mermaid diagrams."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Dict, List, Set
|
|
5
|
+
|
|
6
|
+
from code2llm.core.models import AnalysisResult, ModuleInfo
|
|
7
|
+
|
|
8
|
+
from ..config import Code2DocsConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ArchitectureGenerator:
|
|
12
|
+
"""Generate docs/architecture.md — architecture overview with diagrams."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, config: Code2DocsConfig, result: AnalysisResult):
|
|
15
|
+
self.config = config
|
|
16
|
+
self.result = result
|
|
17
|
+
|
|
18
|
+
def generate(self) -> str:
|
|
19
|
+
"""Generate architecture documentation."""
|
|
20
|
+
project_name = self.config.project_name or Path(self.result.project_path).name
|
|
21
|
+
|
|
22
|
+
lines = [
|
|
23
|
+
f"# {project_name} — Architecture\n",
|
|
24
|
+
f"> Auto-generated from {len(self.result.modules)} modules, "
|
|
25
|
+
f"{len(self.result.functions)} functions, "
|
|
26
|
+
f"{len(self.result.classes)} classes\n",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
# Module dependency graph
|
|
30
|
+
lines.append("## Module Dependency Graph\n")
|
|
31
|
+
lines.append(self._generate_module_graph())
|
|
32
|
+
lines.append("")
|
|
33
|
+
|
|
34
|
+
# Layered architecture
|
|
35
|
+
layers = self._detect_layers()
|
|
36
|
+
if layers:
|
|
37
|
+
lines.append("## Architecture Layers\n")
|
|
38
|
+
for layer_name, modules in layers.items():
|
|
39
|
+
lines.append(f"### {layer_name}\n")
|
|
40
|
+
for mod in modules:
|
|
41
|
+
lines.append(f"- `{mod}`")
|
|
42
|
+
lines.append("")
|
|
43
|
+
|
|
44
|
+
# Key classes
|
|
45
|
+
lines.append("## Key Classes\n")
|
|
46
|
+
lines.append(self._generate_class_diagram())
|
|
47
|
+
lines.append("")
|
|
48
|
+
|
|
49
|
+
# Patterns detected
|
|
50
|
+
if self.result.patterns:
|
|
51
|
+
lines.append("## Detected Patterns\n")
|
|
52
|
+
for pattern in self.result.patterns:
|
|
53
|
+
lines.append(
|
|
54
|
+
f"- **{pattern.name}** ({pattern.type}) — "
|
|
55
|
+
f"confidence: {pattern.confidence:.0%}, "
|
|
56
|
+
f"functions: {', '.join(f'`{f}`' for f in pattern.functions[:5])}"
|
|
57
|
+
)
|
|
58
|
+
lines.append("")
|
|
59
|
+
|
|
60
|
+
# Entry points
|
|
61
|
+
if self.result.entry_points:
|
|
62
|
+
lines.append("## Entry Points\n")
|
|
63
|
+
for ep in self.result.entry_points:
|
|
64
|
+
func = self.result.functions.get(ep)
|
|
65
|
+
if func:
|
|
66
|
+
doc = f" — {func.docstring.splitlines()[0]}" if func.docstring else ""
|
|
67
|
+
lines.append(f"- `{ep}`{doc}")
|
|
68
|
+
else:
|
|
69
|
+
lines.append(f"- `{ep}`")
|
|
70
|
+
lines.append("")
|
|
71
|
+
|
|
72
|
+
# Metrics summary
|
|
73
|
+
lines.append("## Metrics Summary\n")
|
|
74
|
+
lines.append(self._generate_metrics_table())
|
|
75
|
+
|
|
76
|
+
return "\n".join(lines)
|
|
77
|
+
|
|
78
|
+
def _generate_module_graph(self) -> str:
|
|
79
|
+
"""Generate Mermaid module dependency graph."""
|
|
80
|
+
lines = ["```mermaid", "graph LR"]
|
|
81
|
+
|
|
82
|
+
# Build dependency edges from module imports
|
|
83
|
+
edges: Set[tuple] = set()
|
|
84
|
+
for mod_name, mod_info in self.result.modules.items():
|
|
85
|
+
short_name = mod_name.split(".")[-1]
|
|
86
|
+
for imp in mod_info.imports:
|
|
87
|
+
# Only show internal dependencies
|
|
88
|
+
imp_module = imp.split(".")[0] if "." in imp else imp
|
|
89
|
+
for other_mod in self.result.modules:
|
|
90
|
+
other_short = other_mod.split(".")[-1]
|
|
91
|
+
if imp_module in other_mod and mod_name != other_mod:
|
|
92
|
+
edges.add((short_name, other_short))
|
|
93
|
+
|
|
94
|
+
for src, tgt in sorted(edges):
|
|
95
|
+
lines.append(f" {src} --> {tgt}")
|
|
96
|
+
|
|
97
|
+
if not edges:
|
|
98
|
+
lines.append(" note[No internal dependencies detected]")
|
|
99
|
+
|
|
100
|
+
lines.append("```")
|
|
101
|
+
return "\n".join(lines)
|
|
102
|
+
|
|
103
|
+
def _generate_class_diagram(self) -> str:
|
|
104
|
+
"""Generate Mermaid class diagram for key classes."""
|
|
105
|
+
lines = ["```mermaid", "classDiagram"]
|
|
106
|
+
|
|
107
|
+
# Show top classes by method count
|
|
108
|
+
top_classes = sorted(
|
|
109
|
+
self.result.classes.values(),
|
|
110
|
+
key=lambda c: len(c.methods),
|
|
111
|
+
reverse=True,
|
|
112
|
+
)[:15]
|
|
113
|
+
|
|
114
|
+
for cls in top_classes:
|
|
115
|
+
lines.append(f" class {cls.name} {{")
|
|
116
|
+
methods = cls.methods[:8]
|
|
117
|
+
for method_name in methods:
|
|
118
|
+
func = self.result.functions.get(
|
|
119
|
+
f"{cls.qualified_name}.{method_name}"
|
|
120
|
+
) or self.result.functions.get(method_name)
|
|
121
|
+
if func:
|
|
122
|
+
args = ", ".join(func.args[:3])
|
|
123
|
+
ret = func.returns or "None"
|
|
124
|
+
prefix = "-" if func.is_private else "+"
|
|
125
|
+
lines.append(f" {prefix}{func.name}({args}) {ret}")
|
|
126
|
+
else:
|
|
127
|
+
lines.append(f" +{method_name}()")
|
|
128
|
+
if len(cls.methods) > 8:
|
|
129
|
+
lines.append(f" ... +{len(cls.methods) - 8} more")
|
|
130
|
+
lines.append(" }")
|
|
131
|
+
|
|
132
|
+
# Inheritance
|
|
133
|
+
for base in cls.bases:
|
|
134
|
+
if base in [c.name for c in top_classes]:
|
|
135
|
+
lines.append(f" {base} <|-- {cls.name}")
|
|
136
|
+
|
|
137
|
+
lines.append("```")
|
|
138
|
+
return "\n".join(lines)
|
|
139
|
+
|
|
140
|
+
def _detect_layers(self) -> Dict[str, List[str]]:
|
|
141
|
+
"""Detect architectural layers from module names."""
|
|
142
|
+
layers: Dict[str, List[str]] = {}
|
|
143
|
+
layer_keywords = {
|
|
144
|
+
"Core": ["core", "base", "common", "utils", "lib"],
|
|
145
|
+
"API / CLI": ["cli", "api", "rest", "routes", "views", "endpoints"],
|
|
146
|
+
"Analysis": ["analysis", "analyzer", "parser", "scanner", "detector"],
|
|
147
|
+
"Export / Output": ["export", "output", "format", "render", "template"],
|
|
148
|
+
"Config": ["config", "settings", "constants"],
|
|
149
|
+
"Tests": ["test", "tests", "spec"],
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for mod_name in sorted(self.result.modules.keys()):
|
|
153
|
+
lower = mod_name.lower()
|
|
154
|
+
assigned = False
|
|
155
|
+
for layer, keywords in layer_keywords.items():
|
|
156
|
+
if any(kw in lower for kw in keywords):
|
|
157
|
+
layers.setdefault(layer, []).append(mod_name)
|
|
158
|
+
assigned = True
|
|
159
|
+
break
|
|
160
|
+
if not assigned:
|
|
161
|
+
layers.setdefault("Other", []).append(mod_name)
|
|
162
|
+
|
|
163
|
+
return {k: v for k, v in layers.items() if v}
|
|
164
|
+
|
|
165
|
+
def _generate_metrics_table(self) -> str:
|
|
166
|
+
"""Generate metrics summary table."""
|
|
167
|
+
stats = self.result.stats or {}
|
|
168
|
+
lines = [
|
|
169
|
+
"| Metric | Value |",
|
|
170
|
+
"|--------|-------|",
|
|
171
|
+
f"| Modules | {len(self.result.modules)} |",
|
|
172
|
+
f"| Functions | {len(self.result.functions)} |",
|
|
173
|
+
f"| Classes | {len(self.result.classes)} |",
|
|
174
|
+
f"| CFG Nodes | {stats.get('nodes_created', len(self.result.nodes))} |",
|
|
175
|
+
f"| Patterns | {len(self.result.patterns)} |",
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
# Average complexity
|
|
179
|
+
complexities = [
|
|
180
|
+
f.complexity.get("cyclomatic", 0)
|
|
181
|
+
for f in self.result.functions.values()
|
|
182
|
+
if f.complexity.get("cyclomatic", 0) > 0
|
|
183
|
+
]
|
|
184
|
+
if complexities:
|
|
185
|
+
avg = round(sum(complexities) / len(complexities), 1)
|
|
186
|
+
lines.append(f"| Avg Complexity | {avg} |")
|
|
187
|
+
|
|
188
|
+
if stats.get("analysis_time_seconds"):
|
|
189
|
+
lines.append(f"| Analysis Time | {stats['analysis_time_seconds']}s |")
|
|
190
|
+
|
|
191
|
+
lines.append("")
|
|
192
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Changelog generator from git log and API diff."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from code2llm.core.models import AnalysisResult
|
|
9
|
+
|
|
10
|
+
from ..config import Code2DocsConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ChangelogEntry:
|
|
15
|
+
"""A single changelog entry."""
|
|
16
|
+
date: str
|
|
17
|
+
hash: str
|
|
18
|
+
author: str
|
|
19
|
+
message: str
|
|
20
|
+
type: str = "other" # feat, fix, refactor, docs, test, chore, other
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ChangelogGenerator:
|
|
24
|
+
"""Generate CHANGELOG.md from git log and analysis diff."""
|
|
25
|
+
|
|
26
|
+
CONVENTIONAL_TYPES = {
|
|
27
|
+
"feat": "Features",
|
|
28
|
+
"fix": "Bug Fixes",
|
|
29
|
+
"refactor": "Refactoring",
|
|
30
|
+
"docs": "Documentation",
|
|
31
|
+
"test": "Tests",
|
|
32
|
+
"perf": "Performance",
|
|
33
|
+
"chore": "Chores",
|
|
34
|
+
"ci": "CI/CD",
|
|
35
|
+
"style": "Style",
|
|
36
|
+
"build": "Build",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
def __init__(self, config: Code2DocsConfig, result: AnalysisResult):
|
|
40
|
+
self.config = config
|
|
41
|
+
self.result = result
|
|
42
|
+
|
|
43
|
+
def generate(self, project_path: Optional[str] = None, max_entries: int = 100) -> str:
|
|
44
|
+
"""Generate changelog content from git log."""
|
|
45
|
+
project = Path(project_path or self.result.project_path)
|
|
46
|
+
|
|
47
|
+
entries = self._get_git_log(project, max_entries)
|
|
48
|
+
if not entries:
|
|
49
|
+
return "# Changelog\n\nNo git history available.\n"
|
|
50
|
+
|
|
51
|
+
grouped = self._group_by_type(entries)
|
|
52
|
+
return self._render(grouped)
|
|
53
|
+
|
|
54
|
+
def _get_git_log(self, project_path: Path, max_entries: int) -> List[ChangelogEntry]:
|
|
55
|
+
"""Extract git log entries."""
|
|
56
|
+
try:
|
|
57
|
+
result = subprocess.run(
|
|
58
|
+
["git", "log", f"--max-count={max_entries}",
|
|
59
|
+
"--format=%H|%ad|%an|%s", "--date=short"],
|
|
60
|
+
cwd=str(project_path),
|
|
61
|
+
capture_output=True, text=True, timeout=10,
|
|
62
|
+
)
|
|
63
|
+
if result.returncode != 0:
|
|
64
|
+
return []
|
|
65
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
entries = []
|
|
69
|
+
for line in result.stdout.strip().splitlines():
|
|
70
|
+
parts = line.split("|", 3)
|
|
71
|
+
if len(parts) == 4:
|
|
72
|
+
entry = ChangelogEntry(
|
|
73
|
+
hash=parts[0][:8],
|
|
74
|
+
date=parts[1],
|
|
75
|
+
author=parts[2],
|
|
76
|
+
message=parts[3],
|
|
77
|
+
type=self._classify_message(parts[3]),
|
|
78
|
+
)
|
|
79
|
+
entries.append(entry)
|
|
80
|
+
|
|
81
|
+
return entries
|
|
82
|
+
|
|
83
|
+
def _classify_message(self, message: str) -> str:
|
|
84
|
+
"""Classify commit message by conventional commit type."""
|
|
85
|
+
lower = message.lower()
|
|
86
|
+
for prefix in self.CONVENTIONAL_TYPES:
|
|
87
|
+
if lower.startswith(f"{prefix}:") or lower.startswith(f"{prefix}("):
|
|
88
|
+
return prefix
|
|
89
|
+
return "other"
|
|
90
|
+
|
|
91
|
+
def _group_by_type(self, entries: List[ChangelogEntry]) -> Dict[str, List[ChangelogEntry]]:
|
|
92
|
+
"""Group entries by type."""
|
|
93
|
+
grouped: Dict[str, List[ChangelogEntry]] = {}
|
|
94
|
+
for entry in entries:
|
|
95
|
+
grouped.setdefault(entry.type, []).append(entry)
|
|
96
|
+
return grouped
|
|
97
|
+
|
|
98
|
+
def _render(self, grouped: Dict[str, List[ChangelogEntry]]) -> str:
|
|
99
|
+
"""Render grouped changelog to Markdown."""
|
|
100
|
+
lines = ["# Changelog\n"]
|
|
101
|
+
|
|
102
|
+
# Show by type
|
|
103
|
+
for type_key, type_label in self.CONVENTIONAL_TYPES.items():
|
|
104
|
+
entries = grouped.get(type_key, [])
|
|
105
|
+
if entries:
|
|
106
|
+
lines.append(f"## {type_label}\n")
|
|
107
|
+
for entry in entries:
|
|
108
|
+
lines.append(
|
|
109
|
+
f"- {entry.message} (`{entry.hash}` — {entry.date})"
|
|
110
|
+
)
|
|
111
|
+
lines.append("")
|
|
112
|
+
|
|
113
|
+
# Other
|
|
114
|
+
other = grouped.get("other", [])
|
|
115
|
+
if other:
|
|
116
|
+
lines.append("## Other\n")
|
|
117
|
+
for entry in other:
|
|
118
|
+
lines.append(f"- {entry.message} (`{entry.hash}` — {entry.date})")
|
|
119
|
+
lines.append("")
|
|
120
|
+
|
|
121
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Auto-generate usage examples from public signatures and entry points."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Dict, List
|
|
5
|
+
|
|
6
|
+
from code2llm.core.models import AnalysisResult, FunctionInfo, ClassInfo
|
|
7
|
+
|
|
8
|
+
from ..config import Code2DocsConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ExamplesGenerator:
|
|
12
|
+
"""Generate examples/ — usage examples from public API signatures."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, config: Code2DocsConfig, result: AnalysisResult):
|
|
15
|
+
self.config = config
|
|
16
|
+
self.result = result
|
|
17
|
+
|
|
18
|
+
def generate_all(self) -> Dict[str, str]:
|
|
19
|
+
"""Generate all example files. Returns {filename: content}."""
|
|
20
|
+
files: Dict[str, str] = {}
|
|
21
|
+
|
|
22
|
+
# Basic usage example
|
|
23
|
+
files["basic_usage.py"] = self._generate_basic_usage()
|
|
24
|
+
|
|
25
|
+
# Entry-point examples
|
|
26
|
+
if self.config.examples.from_entry_points and self.result.entry_points:
|
|
27
|
+
files["entry_points.py"] = self._generate_entry_point_examples()
|
|
28
|
+
|
|
29
|
+
# Class-based examples for major classes
|
|
30
|
+
major_classes = self._get_major_classes()
|
|
31
|
+
if major_classes:
|
|
32
|
+
files["class_examples.py"] = self._generate_class_examples(major_classes)
|
|
33
|
+
|
|
34
|
+
return files
|
|
35
|
+
|
|
36
|
+
def _generate_basic_usage(self) -> str:
|
|
37
|
+
"""Generate basic_usage.py example."""
|
|
38
|
+
project_name = self.config.project_name or Path(self.result.project_path).name
|
|
39
|
+
lines: List[str] = [
|
|
40
|
+
f'"""Basic usage of {project_name}."""',
|
|
41
|
+
"",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
# Find importable public classes and functions
|
|
45
|
+
public_classes = [
|
|
46
|
+
c for c in self.result.classes.values()
|
|
47
|
+
if not c.name.startswith("_")
|
|
48
|
+
]
|
|
49
|
+
public_functions = [
|
|
50
|
+
f for f in self.result.functions.values()
|
|
51
|
+
if not f.is_private and not f.is_method and not f.name.startswith("_")
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
# Generate import statements
|
|
55
|
+
imported = set()
|
|
56
|
+
for cls in public_classes[:5]:
|
|
57
|
+
mod = cls.module or project_name
|
|
58
|
+
lines.append(f"from {mod} import {cls.name}")
|
|
59
|
+
imported.add(cls.name)
|
|
60
|
+
|
|
61
|
+
for func in public_functions[:5]:
|
|
62
|
+
if func.name not in imported:
|
|
63
|
+
mod = func.module or project_name
|
|
64
|
+
lines.append(f"from {mod} import {func.name}")
|
|
65
|
+
imported.add(func.name)
|
|
66
|
+
|
|
67
|
+
if not imported:
|
|
68
|
+
lines.append(f"import {project_name}")
|
|
69
|
+
|
|
70
|
+
lines.append("")
|
|
71
|
+
lines.append("")
|
|
72
|
+
|
|
73
|
+
# Generate usage examples
|
|
74
|
+
if public_classes:
|
|
75
|
+
cls = public_classes[0]
|
|
76
|
+
lines.append(f"# Create {cls.name} instance")
|
|
77
|
+
init_args = self._get_init_args(cls)
|
|
78
|
+
if init_args:
|
|
79
|
+
args_str = ", ".join(f"{a}=..." for a in init_args[:3])
|
|
80
|
+
lines.append(f"obj = {cls.name}({args_str})")
|
|
81
|
+
else:
|
|
82
|
+
lines.append(f"obj = {cls.name}()")
|
|
83
|
+
lines.append("")
|
|
84
|
+
|
|
85
|
+
# Show method calls
|
|
86
|
+
methods = self._get_public_methods(cls)
|
|
87
|
+
for method in methods[:3]:
|
|
88
|
+
args_str = ", ".join(f"{a}=..." for a in method.args if a != "self")
|
|
89
|
+
ret_comment = f" # → {method.returns}" if method.returns else ""
|
|
90
|
+
lines.append(f"result = obj.{method.name}({args_str}){ret_comment}")
|
|
91
|
+
|
|
92
|
+
if public_functions:
|
|
93
|
+
lines.append("")
|
|
94
|
+
lines.append("# Standalone functions")
|
|
95
|
+
for func in public_functions[:5]:
|
|
96
|
+
args_str = ", ".join(f"{a}=..." for a in func.args[:4])
|
|
97
|
+
ret_comment = f" # → {func.returns}" if func.returns else ""
|
|
98
|
+
lines.append(f"result = {func.name}({args_str}){ret_comment}")
|
|
99
|
+
|
|
100
|
+
lines.append("")
|
|
101
|
+
return "\n".join(lines)
|
|
102
|
+
|
|
103
|
+
def _generate_entry_point_examples(self) -> str:
|
|
104
|
+
"""Generate examples based on entry points."""
|
|
105
|
+
project_name = self.config.project_name or Path(self.result.project_path).name
|
|
106
|
+
lines = [
|
|
107
|
+
f'"""Entry point examples for {project_name}."""',
|
|
108
|
+
"",
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
for ep in self.result.entry_points[:5]:
|
|
112
|
+
func = self.result.functions.get(ep)
|
|
113
|
+
if func:
|
|
114
|
+
mod = func.module or project_name
|
|
115
|
+
lines.append(f"from {mod} import {func.name}")
|
|
116
|
+
lines.append("")
|
|
117
|
+
args_str = ", ".join(f"{a}=..." for a in func.args[:4])
|
|
118
|
+
lines.append(f"# Call entry point: {func.name}")
|
|
119
|
+
if func.docstring:
|
|
120
|
+
lines.append(f"# {func.docstring.splitlines()[0]}")
|
|
121
|
+
lines.append(f"result = {func.name}({args_str})")
|
|
122
|
+
lines.append("")
|
|
123
|
+
|
|
124
|
+
return "\n".join(lines)
|
|
125
|
+
|
|
126
|
+
def _generate_class_examples(self, classes: List[ClassInfo]) -> str:
|
|
127
|
+
"""Generate examples for major classes."""
|
|
128
|
+
project_name = self.config.project_name or Path(self.result.project_path).name
|
|
129
|
+
lines = [
|
|
130
|
+
f'"""Class usage examples for {project_name}."""',
|
|
131
|
+
"",
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
for cls in classes[:5]:
|
|
135
|
+
mod = cls.module or project_name
|
|
136
|
+
lines.append(f"from {mod} import {cls.name}")
|
|
137
|
+
|
|
138
|
+
lines.append("")
|
|
139
|
+
lines.append("")
|
|
140
|
+
|
|
141
|
+
for cls in classes[:5]:
|
|
142
|
+
lines.append(f"# --- {cls.name} ---")
|
|
143
|
+
if cls.docstring:
|
|
144
|
+
lines.append(f"# {cls.docstring.splitlines()[0]}")
|
|
145
|
+
|
|
146
|
+
init_args = self._get_init_args(cls)
|
|
147
|
+
if init_args:
|
|
148
|
+
args_str = ", ".join(f"{a}=..." for a in init_args[:3])
|
|
149
|
+
lines.append(f"instance = {cls.name}({args_str})")
|
|
150
|
+
else:
|
|
151
|
+
lines.append(f"instance = {cls.name}()")
|
|
152
|
+
|
|
153
|
+
methods = self._get_public_methods(cls)
|
|
154
|
+
for m in methods[:3]:
|
|
155
|
+
args = [a for a in m.args if a != "self"]
|
|
156
|
+
args_str = ", ".join(f"{a}=..." for a in args[:3])
|
|
157
|
+
lines.append(f"instance.{m.name}({args_str})")
|
|
158
|
+
|
|
159
|
+
lines.append("")
|
|
160
|
+
|
|
161
|
+
return "\n".join(lines)
|
|
162
|
+
|
|
163
|
+
def _get_major_classes(self) -> List[ClassInfo]:
|
|
164
|
+
"""Get classes with most methods (likely most important)."""
|
|
165
|
+
classes = [c for c in self.result.classes.values() if not c.name.startswith("_")]
|
|
166
|
+
return sorted(classes, key=lambda c: len(c.methods), reverse=True)[:5]
|
|
167
|
+
|
|
168
|
+
def _get_init_args(self, cls: ClassInfo) -> List[str]:
|
|
169
|
+
"""Get __init__ args for a class."""
|
|
170
|
+
for method_name in cls.methods:
|
|
171
|
+
if "__init__" in method_name:
|
|
172
|
+
for key in [method_name, f"{cls.qualified_name}.__init__"]:
|
|
173
|
+
func = self.result.functions.get(key)
|
|
174
|
+
if func:
|
|
175
|
+
return [a for a in func.args if a != "self"]
|
|
176
|
+
return []
|
|
177
|
+
|
|
178
|
+
def _get_public_methods(self, cls: ClassInfo) -> List[FunctionInfo]:
|
|
179
|
+
"""Get public methods of a class."""
|
|
180
|
+
methods = []
|
|
181
|
+
for method_name in cls.methods:
|
|
182
|
+
for key in [method_name, f"{cls.qualified_name}.{method_name}"]:
|
|
183
|
+
func = self.result.functions.get(key)
|
|
184
|
+
if func and not func.is_private and func.name != "__init__":
|
|
185
|
+
methods.append(func)
|
|
186
|
+
break
|
|
187
|
+
return methods
|
|
188
|
+
|
|
189
|
+
def write_all(self, output_dir: str, files: Dict[str, str]) -> None:
|
|
190
|
+
"""Write all generated example files."""
|
|
191
|
+
out = Path(output_dir)
|
|
192
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
193
|
+
for filename, content in files.items():
|
|
194
|
+
(out / filename).write_text(content, encoding="utf-8")
|