Skip to content

Commit 113c566

Browse files
refactoring
1 parent 2561085 commit 113c566

15 files changed

+1720
-124
lines changed

code2docs/analyzers/docstring_extractor.py

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ def _classify_section(line: str) -> Optional[str]:
6767
return "examples"
6868
return None
6969

70+
_SECTION_PARSERS = {
71+
"params": "_parse_param_line",
72+
"returns": "_parse_returns_line",
73+
"raises": "_parse_raises_line",
74+
"examples": "_parse_examples_line",
75+
}
76+
7077
def _parse_sections(self, lines: List[str], info: DocstringInfo) -> None:
7178
"""Walk remaining lines, dispatching content to the right section."""
7279
current_section = "description"
@@ -84,19 +91,33 @@ def _parse_sections(self, lines: List[str], info: DocstringInfo) -> None:
8491

8592
if current_section == "description":
8693
desc_lines.append(stripped)
87-
elif current_section == "params" and stripped:
88-
if ":" in stripped:
89-
pname, pdesc = stripped.split(":", 1)
90-
info.params[pname.strip()] = pdesc.strip()
91-
elif current_section == "returns" and stripped:
92-
info.returns = stripped
93-
elif current_section == "raises" and stripped:
94-
info.raises.append(stripped)
95-
elif current_section == "examples" and stripped:
96-
info.examples.append(stripped)
94+
elif stripped and current_section in self._SECTION_PARSERS:
95+
getattr(self, self._SECTION_PARSERS[current_section])(info, stripped)
9796

9897
info.description = "\n".join(desc_lines).strip()
9998

99+
@staticmethod
100+
def _parse_param_line(info: DocstringInfo, line: str) -> None:
101+
"""Parse a single param line: 'name: description'."""
102+
if ":" in line:
103+
pname, pdesc = line.split(":", 1)
104+
info.params[pname.strip()] = pdesc.strip()
105+
106+
@staticmethod
107+
def _parse_returns_line(info: DocstringInfo, line: str) -> None:
108+
"""Parse a returns line."""
109+
info.returns = line
110+
111+
@staticmethod
112+
def _parse_raises_line(info: DocstringInfo, line: str) -> None:
113+
"""Parse a raises line."""
114+
info.raises.append(line)
115+
116+
@staticmethod
117+
def _parse_examples_line(info: DocstringInfo, line: str) -> None:
118+
"""Parse an examples line."""
119+
info.examples.append(line)
120+
100121
def coverage_report(self, result: AnalysisResult) -> Dict[str, float]:
101122
"""Calculate docstring coverage statistics."""
102123
total_funcs = len(result.functions)

code2docs/base.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Base generator interface and generation context."""
2+
3+
from abc import ABC, abstractmethod
4+
from dataclasses import dataclass
5+
from pathlib import Path
6+
from typing import Optional
7+
8+
from code2llm.api import AnalysisResult
9+
10+
from .config import Code2DocsConfig
11+
12+
13+
@dataclass
14+
class GenerateContext:
15+
"""Shared context passed to all generators during a run."""
16+
project: Path
17+
docs_dir: Path
18+
dry_run: bool = False
19+
verbose: bool = False
20+
21+
22+
class BaseGenerator(ABC):
23+
"""Abstract base for all documentation generators.
24+
25+
Subclasses must define ``name`` and implement ``should_run`` / ``run``.
26+
Adding a new generator requires only creating a subclass and registering it
27+
with the :class:`GeneratorRegistry`; no changes to ``cli.py`` needed.
28+
"""
29+
30+
name: str = ""
31+
32+
def __init__(self, config: Code2DocsConfig, result: AnalysisResult):
33+
self.config = config
34+
self.result = result
35+
36+
@abstractmethod
37+
def should_run(self, *, readme_only: bool = False) -> bool:
38+
"""Return True if this generator should execute."""
39+
40+
@abstractmethod
41+
def run(self, ctx: GenerateContext) -> Optional[str]:
42+
"""Execute generation and write output.
43+
44+
Returns a short status message (e.g. '✅ docs/api/ (12 files)')
45+
or None if nothing was produced.
46+
"""

code2docs/cli.py

Lines changed: 18 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -108,17 +108,11 @@ def _load_config(project_path: str, config_path: Optional[str] = None) -> Code2D
108108

109109
def _run_generate(project_path: str, config: Code2DocsConfig,
110110
readme_only: bool = False, dry_run: bool = False):
111-
"""Run full documentation generation."""
111+
"""Run full documentation generation via the generator registry."""
112112
from .analyzers.project_scanner import ProjectScanner
113-
from .generators.readme_gen import ReadmeGenerator
114-
from .generators.api_reference_gen import ApiReferenceGenerator
115-
from .generators.module_docs_gen import ModuleDocsGenerator
116-
from .generators.examples_gen import ExamplesGenerator
117-
from .generators.architecture_gen import ArchitectureGenerator
118-
from .generators.depgraph_gen import DepGraphGenerator
119-
from .generators.coverage_gen import CoverageGenerator
120-
from .generators.mkdocs_gen import MkDocsGenerator
121-
from .generators.api_changelog_gen import ApiChangelogGenerator
113+
from .base import GenerateContext
114+
from .registry import GeneratorRegistry
115+
from .generators._registry_adapters import ALL_ADAPTERS
122116

123117
project = Path(project_path).resolve()
124118
click.echo(f"📖 code2docs: analyzing {project.name}...")
@@ -132,99 +126,22 @@ def _run_generate(project_path: str, config: Code2DocsConfig,
132126
click.echo(f" Classes: {len(result.classes)}")
133127
click.echo(f" Modules: {len(result.modules)}")
134128

135-
# Step 2: Generate README
136-
readme_gen = ReadmeGenerator(config, result)
137-
readme_content = readme_gen.generate()
129+
# Step 2: Build context and registry
130+
docs_dir = project / config.output
131+
if not dry_run and not readme_only:
132+
docs_dir.mkdir(parents=True, exist_ok=True)
138133

139-
if dry_run:
140-
click.echo(f"\n--- README.md ({len(readme_content)} chars) ---")
141-
click.echo(readme_content[:500] + "..." if len(readme_content) > 500 else readme_content)
142-
else:
143-
readme_path = project / config.readme_output
144-
readme_gen.write(str(readme_path), readme_content)
145-
click.echo(f" ✅ {readme_path.relative_to(project)}")
146-
147-
if readme_only:
148-
return
134+
ctx = GenerateContext(
135+
project=project, docs_dir=docs_dir,
136+
dry_run=dry_run, verbose=config.verbose,
137+
)
149138

150-
# Step 3: Generate docs/
151-
docs_dir = project / config.output
152-
docs_dir.mkdir(parents=True, exist_ok=True)
153-
154-
if config.docs.api_reference:
155-
api_gen = ApiReferenceGenerator(config, result)
156-
files = api_gen.generate_all()
157-
if not dry_run:
158-
api_gen.write_all(str(docs_dir / "api"), files)
159-
click.echo(f" ✅ docs/api/ ({len(files)} files)")
160-
else:
161-
click.echo(f" [dry-run] docs/api/ ({len(files)} files)")
162-
163-
if config.docs.module_docs:
164-
mod_gen = ModuleDocsGenerator(config, result)
165-
files = mod_gen.generate_all()
166-
if not dry_run:
167-
mod_gen.write_all(str(docs_dir / "modules"), files)
168-
click.echo(f" ✅ docs/modules/ ({len(files)} files)")
169-
else:
170-
click.echo(f" [dry-run] docs/modules/ ({len(files)} files)")
171-
172-
if config.docs.architecture:
173-
arch_gen = ArchitectureGenerator(config, result)
174-
content = arch_gen.generate()
175-
if not dry_run:
176-
(docs_dir / "architecture.md").write_text(content, encoding="utf-8")
177-
click.echo(f" ✅ docs/architecture.md")
178-
else:
179-
click.echo(f" [dry-run] docs/architecture.md")
180-
181-
# Step 4: Dependency graph
182-
depgraph_gen = DepGraphGenerator(config, result)
183-
content = depgraph_gen.generate()
184-
if not dry_run:
185-
(docs_dir / "dependency-graph.md").write_text(content, encoding="utf-8")
186-
click.echo(f" ✅ docs/dependency-graph.md")
187-
else:
188-
click.echo(f" [dry-run] docs/dependency-graph.md")
189-
190-
# Step 5: Docstring coverage
191-
cov_gen = CoverageGenerator(config, result)
192-
content = cov_gen.generate()
193-
if not dry_run:
194-
(docs_dir / "coverage.md").write_text(content, encoding="utf-8")
195-
click.echo(f" ✅ docs/coverage.md")
196-
else:
197-
click.echo(f" [dry-run] docs/coverage.md")
198-
199-
# Step 6: API changelog (diff with previous snapshot)
200-
api_cl_gen = ApiChangelogGenerator(config, result)
201-
content = api_cl_gen.generate(str(project))
202-
if not dry_run:
203-
(docs_dir / "api-changelog.md").write_text(content, encoding="utf-8")
204-
api_cl_gen.save_snapshot(str(project))
205-
click.echo(f" ✅ docs/api-changelog.md")
206-
else:
207-
click.echo(f" [dry-run] docs/api-changelog.md")
208-
209-
# Step 7: Generate examples/
210-
if config.examples.auto_generate:
211-
ex_gen = ExamplesGenerator(config, result)
212-
files = ex_gen.generate_all()
213-
if not dry_run:
214-
examples_dir = project / "examples"
215-
ex_gen.write_all(str(examples_dir), files)
216-
click.echo(f" ✅ examples/ ({len(files)} files)")
217-
else:
218-
click.echo(f" [dry-run] examples/ ({len(files)} files)")
219-
220-
# Step 8: mkdocs.yml
221-
mkdocs_gen = MkDocsGenerator(config, result)
222-
content = mkdocs_gen.generate(str(docs_dir))
223-
if not dry_run:
224-
mkdocs_gen.write(str(project / "mkdocs.yml"), content)
225-
click.echo(f" ✅ mkdocs.yml")
226-
else:
227-
click.echo(f" [dry-run] mkdocs.yml")
139+
registry = GeneratorRegistry()
140+
for adapter_cls in ALL_ADAPTERS:
141+
registry.add(adapter_cls(config, result))
142+
143+
# Step 3: Run all generators
144+
registry.run_all(ctx, readme_only=readme_only)
228145

229146
click.echo("📖 Done!")
230147

0 commit comments

Comments
 (0)