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.
@@ -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")