code2docs 0.1.2__tar.gz → 0.1.4__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.
- {code2docs-0.1.2 → code2docs-0.1.4}/PKG-INFO +2 -2
- {code2docs-0.1.2 → code2docs-0.1.4}/README.md +1 -1
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs/__init__.py +1 -1
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs/analyzers/docstring_extractor.py +31 -23
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs/analyzers/endpoint_detector.py +1 -1
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs/analyzers/project_scanner.py +2 -5
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs/cli.py +19 -16
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs/generators/api_reference_gen.py +61 -49
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs/generators/architecture_gen.py +1 -1
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs/generators/changelog_gen.py +1 -1
- code2docs-0.1.4/code2docs/generators/coverage_gen.py +85 -0
- code2docs-0.1.4/code2docs/generators/depgraph_gen.py +112 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs/generators/examples_gen.py +48 -44
- code2docs-0.1.4/code2docs/generators/mkdocs_gen.py +79 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs/generators/module_docs_gen.py +90 -74
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs/generators/readme_gen.py +91 -60
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs.egg-info/PKG-INFO +2 -2
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs.egg-info/SOURCES.txt +4 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/pyproject.toml +1 -1
- code2docs-0.1.4/tests/test_generators.py +366 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/LICENSE +0 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs/__main__.py +0 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs/analyzers/__init__.py +0 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs/analyzers/dependency_scanner.py +0 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs/config.py +0 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs/formatters/__init__.py +0 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs/formatters/badges.py +0 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs/formatters/markdown.py +0 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs/formatters/toc.py +0 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs/generators/__init__.py +0 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs/sync/__init__.py +0 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs/sync/differ.py +0 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs/sync/updater.py +0 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs/sync/watcher.py +0 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs/templates/api_module.md.j2 +0 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs/templates/architecture.md.j2 +0 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs/templates/example_usage.py.j2 +0 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs/templates/index.md.j2 +0 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs/templates/readme.md.j2 +0 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs.egg-info/dependency_links.txt +0 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs.egg-info/entry_points.txt +0 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs.egg-info/requires.txt +0 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/code2docs.egg-info/top_level.txt +0 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/setup.cfg +0 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/tests/test_analyzers.py +0 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/tests/test_code2docs.py +0 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/tests/test_config.py +0 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/tests/test_formatters.py +0 -0
- {code2docs-0.1.2 → code2docs-0.1.4}/tests/test_sync.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code2docs
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Auto-generate and sync project documentation from source code analysis
|
|
5
5
|
Author-email: Tom Sapletta <tom@sapletta.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -40,7 +40,7 @@ Dynamic: license-file
|
|
|
40
40
|
|
|
41
41
|
# code2docs
|
|
42
42
|
|
|
43
|
-
  
|
|
44
44
|
|
|
45
45
|
> Auto-generate and sync project documentation from source code analysis.
|
|
46
46
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# code2docs
|
|
2
2
|
|
|
3
|
-
  
|
|
4
4
|
|
|
5
5
|
> Auto-generate and sync project documentation from source code analysis.
|
|
6
6
|
|
|
@@ -5,7 +5,7 @@ Uses code2llm's AnalysisResult to produce human-readable documentation:
|
|
|
5
5
|
README.md, API references, module docs, examples, and architecture diagrams.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
__version__ = "0.1.
|
|
8
|
+
__version__ = "0.1.4"
|
|
9
9
|
__author__ = "Tom Sapletta"
|
|
10
10
|
|
|
11
11
|
from .config import Code2DocsConfig
|
|
@@ -5,7 +5,7 @@ from dataclasses import dataclass, field
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Dict, List, Optional, Tuple
|
|
7
7
|
|
|
8
|
-
from code2llm.
|
|
8
|
+
from code2llm.api import AnalysisResult, FunctionInfo, ClassInfo
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
@dataclass
|
|
@@ -38,44 +38,53 @@ class DocstringExtractor:
|
|
|
38
38
|
return docs
|
|
39
39
|
|
|
40
40
|
def parse(self, docstring: str) -> DocstringInfo:
|
|
41
|
-
"""Parse a docstring into structured sections."""
|
|
41
|
+
"""Parse a docstring into structured sections (orchestrator)."""
|
|
42
42
|
if not docstring:
|
|
43
43
|
return DocstringInfo(raw="")
|
|
44
44
|
|
|
45
45
|
lines = docstring.strip().splitlines()
|
|
46
46
|
info = DocstringInfo(raw=docstring)
|
|
47
|
+
info.summary = self._extract_summary(lines)
|
|
48
|
+
self._parse_sections(lines[1:], info)
|
|
49
|
+
return info
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
@staticmethod
|
|
52
|
+
def _extract_summary(lines: List[str]) -> str:
|
|
53
|
+
"""Extract the first-line summary."""
|
|
54
|
+
return lines[0].strip() if lines else ""
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def _classify_section(line: str) -> Optional[str]:
|
|
58
|
+
"""Classify a line as a section header, or return None."""
|
|
59
|
+
lower = line.strip().lower()
|
|
60
|
+
if lower.startswith(("args:", "parameters:", "params:")):
|
|
61
|
+
return "params"
|
|
62
|
+
if lower.startswith(("returns:", "return:")):
|
|
63
|
+
return "returns"
|
|
64
|
+
if lower.startswith(("raises:", "raise:")):
|
|
65
|
+
return "raises"
|
|
66
|
+
if lower.startswith(("example:", "examples:", ">>>")):
|
|
67
|
+
return "examples"
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
def _parse_sections(self, lines: List[str], info: DocstringInfo) -> None:
|
|
71
|
+
"""Walk remaining lines, dispatching content to the right section."""
|
|
52
72
|
current_section = "description"
|
|
53
73
|
desc_lines: List[str] = []
|
|
54
|
-
param_lines: List[Tuple[str, str]] = []
|
|
55
74
|
|
|
56
|
-
for line in lines
|
|
75
|
+
for line in lines:
|
|
57
76
|
stripped = line.strip()
|
|
58
|
-
lower = stripped.lower()
|
|
59
77
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
current_section = "returns"
|
|
65
|
-
continue
|
|
66
|
-
elif lower.startswith(("raises:", "raise:")):
|
|
67
|
-
current_section = "raises"
|
|
68
|
-
continue
|
|
69
|
-
elif lower.startswith(("example:", "examples:", ">>>")):
|
|
70
|
-
current_section = "examples"
|
|
71
|
-
if stripped.startswith(">>>"):
|
|
78
|
+
new_section = self._classify_section(stripped)
|
|
79
|
+
if new_section is not None:
|
|
80
|
+
current_section = new_section
|
|
81
|
+
if current_section == "examples" and stripped.startswith(">>>"):
|
|
72
82
|
info.examples.append(stripped)
|
|
73
83
|
continue
|
|
74
84
|
|
|
75
85
|
if current_section == "description":
|
|
76
86
|
desc_lines.append(stripped)
|
|
77
87
|
elif current_section == "params" and stripped:
|
|
78
|
-
# Parse "name: description" or "name (type): description"
|
|
79
88
|
if ":" in stripped:
|
|
80
89
|
pname, pdesc = stripped.split(":", 1)
|
|
81
90
|
info.params[pname.strip()] = pdesc.strip()
|
|
@@ -87,7 +96,6 @@ class DocstringExtractor:
|
|
|
87
96
|
info.examples.append(stripped)
|
|
88
97
|
|
|
89
98
|
info.description = "\n".join(desc_lines).strip()
|
|
90
|
-
return info
|
|
91
99
|
|
|
92
100
|
def coverage_report(self, result: AnalysisResult) -> Dict[str, float]:
|
|
93
101
|
"""Calculate docstring coverage statistics."""
|
|
@@ -3,9 +3,7 @@
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import Optional
|
|
5
5
|
|
|
6
|
-
from code2llm import Config, FAST_CONFIG
|
|
7
|
-
from code2llm.core.analyzer import ProjectAnalyzer
|
|
8
|
-
from code2llm.core.models import AnalysisResult
|
|
6
|
+
from code2llm.api import Config, FAST_CONFIG, AnalysisResult, analyze
|
|
9
7
|
|
|
10
8
|
from ..config import Code2DocsConfig
|
|
11
9
|
|
|
@@ -35,8 +33,7 @@ class ProjectScanner:
|
|
|
35
33
|
|
|
36
34
|
def analyze(self, project_path: str) -> AnalysisResult:
|
|
37
35
|
"""Analyze a project and return AnalysisResult for doc generation."""
|
|
38
|
-
|
|
39
|
-
return analyzer.analyze_project(project_path)
|
|
36
|
+
return analyze(project_path, self._llm_config)
|
|
40
37
|
|
|
41
38
|
|
|
42
39
|
def analyze_and_document(project_path: str, config: Optional[Code2DocsConfig] = None) -> AnalysisResult:
|
|
@@ -9,7 +9,23 @@ import click
|
|
|
9
9
|
from .config import Code2DocsConfig
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
class DefaultGroup(click.Group):
|
|
13
|
+
"""Click Group that routes unknown subcommands to 'generate'."""
|
|
14
|
+
|
|
15
|
+
def parse_args(self, ctx, args):
|
|
16
|
+
if not args:
|
|
17
|
+
args = ["generate"]
|
|
18
|
+
elif args[0] not in self.commands and args[0] not in ("--help", "-h"):
|
|
19
|
+
args = ["generate"] + args
|
|
20
|
+
return super().parse_args(ctx, args)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@click.group(cls=DefaultGroup)
|
|
24
|
+
def main():
|
|
25
|
+
"""code2docs — Auto-generate project documentation from source code."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@main.command()
|
|
13
29
|
@click.argument("project_path", default=".", type=click.Path(exists=True))
|
|
14
30
|
@click.option("--config", "-c", "config_path", default=None, help="Path to code2docs.yaml")
|
|
15
31
|
@click.option("--readme-only", is_flag=True, help="Generate only README.md")
|
|
@@ -17,21 +33,8 @@ from .config import Code2DocsConfig
|
|
|
17
33
|
@click.option("--output", "-o", default=None, help="Output directory for docs")
|
|
18
34
|
@click.option("--verbose", "-v", is_flag=True, help="Verbose output")
|
|
19
35
|
@click.option("--dry-run", is_flag=True, help="Show what would be generated without writing")
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"""code2docs — Auto-generate project documentation from source code.
|
|
23
|
-
|
|
24
|
-
Analyzes PROJECT_PATH using code2llm and generates human-readable documentation.
|
|
25
|
-
"""
|
|
26
|
-
if ctx.invoked_subcommand is not None:
|
|
27
|
-
ctx.ensure_object(dict)
|
|
28
|
-
ctx.obj["project_path"] = project_path
|
|
29
|
-
ctx.obj["config_path"] = config_path
|
|
30
|
-
ctx.obj["verbose"] = verbose
|
|
31
|
-
ctx.obj["dry_run"] = dry_run
|
|
32
|
-
return
|
|
33
|
-
|
|
34
|
-
# Default action: full generation
|
|
36
|
+
def generate(project_path, config_path, readme_only, sections, output, verbose, dry_run):
|
|
37
|
+
"""Generate documentation (default command)."""
|
|
35
38
|
config = _load_config(project_path, config_path)
|
|
36
39
|
if verbose:
|
|
37
40
|
config.verbose = True
|
|
@@ -5,7 +5,7 @@ from typing import Dict, List, Optional
|
|
|
5
5
|
|
|
6
6
|
from jinja2 import Environment, PackageLoader, select_autoescape
|
|
7
7
|
|
|
8
|
-
from code2llm.
|
|
8
|
+
from code2llm.api import AnalysisResult, FunctionInfo, ClassInfo, ModuleInfo
|
|
9
9
|
|
|
10
10
|
from ..config import Code2DocsConfig
|
|
11
11
|
|
|
@@ -61,67 +61,79 @@ class ApiReferenceGenerator:
|
|
|
61
61
|
return "\n".join(lines) + "\n"
|
|
62
62
|
|
|
63
63
|
def _generate_module_api(self, mod_name: str, mod_info: ModuleInfo) -> str:
|
|
64
|
-
"""Generate API reference for a single module."""
|
|
65
|
-
|
|
64
|
+
"""Generate API reference for a single module (orchestrator)."""
|
|
65
|
+
parts: List[str] = [
|
|
66
|
+
self._render_api_header(mod_name, mod_info),
|
|
67
|
+
self._render_api_classes(mod_name),
|
|
68
|
+
self._render_api_functions(mod_name),
|
|
69
|
+
self._render_api_imports(mod_info),
|
|
70
|
+
]
|
|
71
|
+
return "\n".join(p for p in parts if p)
|
|
66
72
|
|
|
67
|
-
|
|
68
|
-
|
|
73
|
+
def _render_api_header(self, mod_name: str, mod_info: ModuleInfo) -> str:
|
|
74
|
+
"""Render module header with source info."""
|
|
75
|
+
return f"# `{mod_name}`\n\n> Source: `{mod_info.file}`\n"
|
|
69
76
|
|
|
70
|
-
|
|
77
|
+
def _render_api_classes(self, mod_name: str) -> str:
|
|
78
|
+
"""Render classes with their method signatures."""
|
|
71
79
|
module_classes = {
|
|
72
80
|
k: v for k, v in self.result.classes.items()
|
|
73
81
|
if v.module == mod_name or k.startswith(mod_name + ".")
|
|
74
82
|
}
|
|
75
|
-
if module_classes:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
83
|
+
if not module_classes:
|
|
84
|
+
return ""
|
|
85
|
+
lines = ["## Classes\n"]
|
|
86
|
+
for cls_name, cls_info in sorted(module_classes.items()):
|
|
87
|
+
lines.append(f"### `{cls_info.name}`\n")
|
|
88
|
+
if cls_info.bases:
|
|
89
|
+
lines.append(f"Inherits from: {', '.join(f'`{b}`' for b in cls_info.bases)}\n")
|
|
90
|
+
if cls_info.docstring:
|
|
91
|
+
lines.append(f"{cls_info.docstring.strip()}\n")
|
|
92
|
+
methods = self._get_class_methods(cls_info)
|
|
93
|
+
if methods:
|
|
94
|
+
lines.append("#### Methods\n")
|
|
95
|
+
for method in methods:
|
|
96
|
+
sig = self._format_signature(method)
|
|
97
|
+
doc_line = ""
|
|
98
|
+
if method.docstring:
|
|
99
|
+
doc_line = f" — {method.docstring.splitlines()[0]}"
|
|
100
|
+
cc = method.complexity.get("cyclomatic", 0)
|
|
101
|
+
cc_badge = f" ⚠️ CC={cc}" if cc > 10 else ""
|
|
102
|
+
lines.append(f"- `{sig}`{doc_line}{cc_badge}")
|
|
103
|
+
lines.append("")
|
|
104
|
+
return "\n".join(lines)
|
|
105
|
+
|
|
106
|
+
def _render_api_functions(self, mod_name: str) -> str:
|
|
107
|
+
"""Render standalone functions with signatures and complexity."""
|
|
99
108
|
module_functions = {
|
|
100
109
|
k: v for k, v in self.result.functions.items()
|
|
101
110
|
if (v.module == mod_name or k.startswith(mod_name + "."))
|
|
102
111
|
and not v.is_method
|
|
103
112
|
}
|
|
104
|
-
if module_functions:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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}`")
|
|
113
|
+
if not module_functions:
|
|
114
|
+
return ""
|
|
115
|
+
lines = ["## Functions\n"]
|
|
116
|
+
for func_name, func_info in sorted(module_functions.items()):
|
|
117
|
+
sig = self._format_signature(func_info)
|
|
118
|
+
lines.append(f"### `{sig}`\n")
|
|
119
|
+
if func_info.docstring:
|
|
120
|
+
lines.append(f"{func_info.docstring.strip()}\n")
|
|
121
|
+
cc = func_info.complexity.get("cyclomatic", 0)
|
|
122
|
+
if cc:
|
|
123
|
+
lines.append(f"- Complexity: {cc}")
|
|
124
|
+
if func_info.calls:
|
|
125
|
+
lines.append(f"- Calls: {', '.join(f'`{c}`' for c in func_info.calls[:10])}")
|
|
123
126
|
lines.append("")
|
|
127
|
+
return "\n".join(lines)
|
|
124
128
|
|
|
129
|
+
def _render_api_imports(self, mod_info: ModuleInfo) -> str:
|
|
130
|
+
"""Render module imports list."""
|
|
131
|
+
if not mod_info.imports:
|
|
132
|
+
return ""
|
|
133
|
+
lines = ["## Imports\n"]
|
|
134
|
+
for imp in sorted(mod_info.imports):
|
|
135
|
+
lines.append(f"- `{imp}`")
|
|
136
|
+
lines.append("")
|
|
125
137
|
return "\n".join(lines)
|
|
126
138
|
|
|
127
139
|
def _get_class_methods(self, cls_info: ClassInfo) -> List[FunctionInfo]:
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Docstring coverage report generator."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List
|
|
4
|
+
|
|
5
|
+
from code2llm.api import AnalysisResult
|
|
6
|
+
|
|
7
|
+
from ..config import Code2DocsConfig
|
|
8
|
+
from ..analyzers.docstring_extractor import DocstringExtractor
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CoverageGenerator:
|
|
12
|
+
"""Generate docs/coverage.md — docstring coverage report."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, config: Code2DocsConfig, result: AnalysisResult):
|
|
15
|
+
self.config = config
|
|
16
|
+
self.result = result
|
|
17
|
+
self._extractor = DocstringExtractor()
|
|
18
|
+
|
|
19
|
+
def generate(self) -> str:
|
|
20
|
+
"""Generate coverage.md content."""
|
|
21
|
+
project_name = self.config.project_name or "Project"
|
|
22
|
+
report = self._extractor.coverage_report(self.result)
|
|
23
|
+
|
|
24
|
+
lines = [
|
|
25
|
+
f"# {project_name} — Docstring Coverage\n",
|
|
26
|
+
self._render_summary(report),
|
|
27
|
+
"",
|
|
28
|
+
"## Per-Module Breakdown\n",
|
|
29
|
+
self._render_per_module(),
|
|
30
|
+
"",
|
|
31
|
+
"## Undocumented Items\n",
|
|
32
|
+
self._render_undocumented(),
|
|
33
|
+
"",
|
|
34
|
+
]
|
|
35
|
+
return "\n".join(lines)
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def _render_summary(report: Dict[str, float]) -> str:
|
|
39
|
+
"""Render overall coverage summary."""
|
|
40
|
+
overall = report.get("overall_coverage", 0)
|
|
41
|
+
badge = "🟢" if overall >= 80 else "🟡" if overall >= 50 else "🔴"
|
|
42
|
+
lines = [
|
|
43
|
+
f"{badge} **Overall coverage: {overall:.1f}%**\n",
|
|
44
|
+
"| Category | Documented | Total | Coverage |",
|
|
45
|
+
"|----------|-----------|-------|----------|",
|
|
46
|
+
f"| Functions | {report['functions_documented']} | {report['functions_total']} | {report['functions_coverage']:.1f}% |",
|
|
47
|
+
f"| Classes | {report['classes_documented']} | {report['classes_total']} | {report['classes_coverage']:.1f}% |",
|
|
48
|
+
]
|
|
49
|
+
return "\n".join(lines)
|
|
50
|
+
|
|
51
|
+
def _render_per_module(self) -> str:
|
|
52
|
+
"""Render per-module coverage table."""
|
|
53
|
+
rows: List[str] = [
|
|
54
|
+
"| Module | Functions | Classes | Coverage |",
|
|
55
|
+
"|--------|-----------|---------|----------|",
|
|
56
|
+
]
|
|
57
|
+
for mod_name in sorted(self.result.modules.keys()):
|
|
58
|
+
funcs = [f for f in self.result.functions.values()
|
|
59
|
+
if f.module == mod_name and not f.is_method]
|
|
60
|
+
classes = [c for c in self.result.classes.values()
|
|
61
|
+
if c.module == mod_name]
|
|
62
|
+
total = len(funcs) + len(classes)
|
|
63
|
+
documented = (sum(1 for f in funcs if f.docstring) +
|
|
64
|
+
sum(1 for c in classes if c.docstring))
|
|
65
|
+
pct = (documented / total * 100) if total else 100.0
|
|
66
|
+
badge = "🟢" if pct >= 80 else "🟡" if pct >= 50 else "🔴"
|
|
67
|
+
rows.append(
|
|
68
|
+
f"| `{mod_name}` | {sum(1 for f in funcs if f.docstring)}/{len(funcs)} "
|
|
69
|
+
f"| {sum(1 for c in classes if c.docstring)}/{len(classes)} "
|
|
70
|
+
f"| {badge} {pct:.0f}% |"
|
|
71
|
+
)
|
|
72
|
+
return "\n".join(rows)
|
|
73
|
+
|
|
74
|
+
def _render_undocumented(self) -> str:
|
|
75
|
+
"""List all undocumented public functions and classes."""
|
|
76
|
+
items: List[str] = []
|
|
77
|
+
for name, func in sorted(self.result.functions.items()):
|
|
78
|
+
if not func.docstring and not func.is_private and not func.is_method:
|
|
79
|
+
items.append(f"- `{name}` ({func.file}:{func.line})")
|
|
80
|
+
for name, cls in sorted(self.result.classes.items()):
|
|
81
|
+
if not cls.docstring:
|
|
82
|
+
items.append(f"- `{name}` ({cls.file}:{cls.line})")
|
|
83
|
+
if not items:
|
|
84
|
+
return "_All public items are documented._ ✅"
|
|
85
|
+
return "\n".join(items)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Dependency graph generator — Mermaid diagram from coupling matrix."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Set, Tuple
|
|
4
|
+
|
|
5
|
+
from code2llm.api import AnalysisResult, ModuleInfo
|
|
6
|
+
|
|
7
|
+
from ..config import Code2DocsConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DepGraphGenerator:
|
|
11
|
+
"""Generate docs/dependency-graph.md with Mermaid diagrams."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, config: Code2DocsConfig, result: AnalysisResult):
|
|
14
|
+
self.config = config
|
|
15
|
+
self.result = result
|
|
16
|
+
|
|
17
|
+
def generate(self) -> str:
|
|
18
|
+
"""Generate dependency-graph.md content."""
|
|
19
|
+
project_name = self.config.project_name or "Project"
|
|
20
|
+
edges = self._collect_edges()
|
|
21
|
+
in_degree, out_degree = self._calc_degrees(edges)
|
|
22
|
+
|
|
23
|
+
lines = [
|
|
24
|
+
f"# {project_name} — Dependency Graph\n",
|
|
25
|
+
f"> {len(self.result.modules)} modules, "
|
|
26
|
+
f"{len(edges)} dependency edges\n",
|
|
27
|
+
"## Module Dependencies\n",
|
|
28
|
+
self._render_mermaid(edges),
|
|
29
|
+
"",
|
|
30
|
+
"## Coupling Matrix\n",
|
|
31
|
+
self._render_matrix(edges),
|
|
32
|
+
"",
|
|
33
|
+
"## Fan-in / Fan-out\n",
|
|
34
|
+
self._render_degree_table(in_degree, out_degree),
|
|
35
|
+
"",
|
|
36
|
+
]
|
|
37
|
+
return "\n".join(lines)
|
|
38
|
+
|
|
39
|
+
def _collect_edges(self) -> List[Tuple[str, str]]:
|
|
40
|
+
"""Build directed edges from module imports."""
|
|
41
|
+
edges: List[Tuple[str, str]] = []
|
|
42
|
+
module_names = set(self.result.modules.keys())
|
|
43
|
+
|
|
44
|
+
for mod_name, mod_info in self.result.modules.items():
|
|
45
|
+
for imp in mod_info.imports:
|
|
46
|
+
for other in module_names:
|
|
47
|
+
if mod_name != other and self._import_matches(imp, other):
|
|
48
|
+
edges.append((mod_name, other))
|
|
49
|
+
return sorted(set(edges))
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def _import_matches(imp: str, module: str) -> bool:
|
|
53
|
+
"""Check if an import string refers to a known module."""
|
|
54
|
+
return imp == module or imp.startswith(module + ".")
|
|
55
|
+
|
|
56
|
+
def _render_mermaid(self, edges: List[Tuple[str, str]]) -> str:
|
|
57
|
+
"""Render Mermaid graph from edges."""
|
|
58
|
+
lines = ["```mermaid", "graph LR"]
|
|
59
|
+
for src, tgt in edges:
|
|
60
|
+
s = src.split(".")[-1]
|
|
61
|
+
t = tgt.split(".")[-1]
|
|
62
|
+
lines.append(f" {s} --> {t}")
|
|
63
|
+
if not edges:
|
|
64
|
+
lines.append(" note[No internal dependencies]")
|
|
65
|
+
lines.append("```")
|
|
66
|
+
return "\n".join(lines)
|
|
67
|
+
|
|
68
|
+
def _render_matrix(self, edges: List[Tuple[str, str]]) -> str:
|
|
69
|
+
"""Render a coupling matrix as a Markdown table."""
|
|
70
|
+
mods = sorted(self.result.modules.keys())
|
|
71
|
+
if not mods:
|
|
72
|
+
return "_No modules._"
|
|
73
|
+
short = {m: m.split(".")[-1] for m in mods}
|
|
74
|
+
edge_set = set(edges)
|
|
75
|
+
|
|
76
|
+
header = "| | " + " | ".join(short[m] for m in mods) + " |"
|
|
77
|
+
sep = "| --- | " + " | ".join("---" for _ in mods) + " |"
|
|
78
|
+
rows = [header, sep]
|
|
79
|
+
for src in mods:
|
|
80
|
+
cells = []
|
|
81
|
+
for tgt in mods:
|
|
82
|
+
if src == tgt:
|
|
83
|
+
cells.append("·")
|
|
84
|
+
elif (src, tgt) in edge_set:
|
|
85
|
+
cells.append("→")
|
|
86
|
+
else:
|
|
87
|
+
cells.append("")
|
|
88
|
+
rows.append(f"| **{short[src]}** | " + " | ".join(cells) + " |")
|
|
89
|
+
return "\n".join(rows)
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def _calc_degrees(edges: List[Tuple[str, str]]) -> Tuple[Dict[str, int], Dict[str, int]]:
|
|
93
|
+
"""Calculate in-degree and out-degree per module."""
|
|
94
|
+
in_deg: Dict[str, int] = {}
|
|
95
|
+
out_deg: Dict[str, int] = {}
|
|
96
|
+
for src, tgt in edges:
|
|
97
|
+
out_deg[src] = out_deg.get(src, 0) + 1
|
|
98
|
+
in_deg[tgt] = in_deg.get(tgt, 0) + 1
|
|
99
|
+
return in_deg, out_deg
|
|
100
|
+
|
|
101
|
+
def _render_degree_table(self, in_deg: Dict[str, int], out_deg: Dict[str, int]) -> str:
|
|
102
|
+
"""Render fan-in/fan-out table."""
|
|
103
|
+
mods = sorted(self.result.modules.keys())
|
|
104
|
+
lines = [
|
|
105
|
+
"| Module | Fan-in | Fan-out |",
|
|
106
|
+
"|--------|--------|---------|",
|
|
107
|
+
]
|
|
108
|
+
for m in mods:
|
|
109
|
+
fi = in_deg.get(m, 0)
|
|
110
|
+
fo = out_deg.get(m, 0)
|
|
111
|
+
lines.append(f"| `{m}` | {fi} | {fo} |")
|
|
112
|
+
return "\n".join(lines)
|