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
code2docs/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
code2docs - Auto-generate and sync project documentation from source code.
|
|
3
|
+
|
|
4
|
+
Uses code2llm's AnalysisResult to produce human-readable documentation:
|
|
5
|
+
README.md, API references, module docs, examples, and architecture diagrams.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.1"
|
|
9
|
+
__author__ = "Tom Sapletta"
|
|
10
|
+
|
|
11
|
+
from .config import Code2DocsConfig
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"Code2DocsConfig",
|
|
15
|
+
"generate_readme",
|
|
16
|
+
"generate_docs",
|
|
17
|
+
"analyze_and_document",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def __getattr__(name):
|
|
22
|
+
"""Lazy import heavy modules on first access."""
|
|
23
|
+
if name == "generate_readme":
|
|
24
|
+
from .generators.readme_gen import generate_readme
|
|
25
|
+
return generate_readme
|
|
26
|
+
if name == "generate_docs":
|
|
27
|
+
from .generators import generate_docs
|
|
28
|
+
return generate_docs
|
|
29
|
+
if name == "analyze_and_document":
|
|
30
|
+
from .analyzers.project_scanner import analyze_and_document
|
|
31
|
+
return analyze_and_document
|
|
32
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
code2docs/__main__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Analyzers — adapters to code2llm and custom detectors."""
|
|
2
|
+
|
|
3
|
+
from .project_scanner import ProjectScanner
|
|
4
|
+
from .endpoint_detector import EndpointDetector
|
|
5
|
+
from .docstring_extractor import DocstringExtractor
|
|
6
|
+
from .dependency_scanner import DependencyScanner
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"ProjectScanner",
|
|
10
|
+
"EndpointDetector",
|
|
11
|
+
"DocstringExtractor",
|
|
12
|
+
"DependencyScanner",
|
|
13
|
+
]
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Scan project dependencies from requirements.txt, pyproject.toml, setup.py."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import tomllib
|
|
10
|
+
except ImportError:
|
|
11
|
+
try:
|
|
12
|
+
import tomli as tomllib
|
|
13
|
+
except ImportError:
|
|
14
|
+
tomllib = None # type: ignore
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class DependencyInfo:
|
|
19
|
+
"""Information about a project dependency."""
|
|
20
|
+
name: str
|
|
21
|
+
version_spec: str = ""
|
|
22
|
+
optional: bool = False
|
|
23
|
+
group: str = "main"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class ProjectDependencies:
|
|
28
|
+
"""All detected project dependencies."""
|
|
29
|
+
python_version: str = ""
|
|
30
|
+
dependencies: List[DependencyInfo] = field(default_factory=list)
|
|
31
|
+
dev_dependencies: List[DependencyInfo] = field(default_factory=list)
|
|
32
|
+
optional_groups: Dict[str, List[DependencyInfo]] = field(default_factory=dict)
|
|
33
|
+
install_command: str = "pip install ."
|
|
34
|
+
source_file: str = ""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class DependencyScanner:
|
|
38
|
+
"""Scan and parse project dependency files."""
|
|
39
|
+
|
|
40
|
+
def scan(self, project_path: str) -> ProjectDependencies:
|
|
41
|
+
"""Scan project for dependency information."""
|
|
42
|
+
project = Path(project_path)
|
|
43
|
+
deps = ProjectDependencies()
|
|
44
|
+
|
|
45
|
+
# Priority: pyproject.toml > setup.py > requirements.txt
|
|
46
|
+
pyproject = project / "pyproject.toml"
|
|
47
|
+
if pyproject.exists():
|
|
48
|
+
deps = self._parse_pyproject(pyproject)
|
|
49
|
+
deps.source_file = "pyproject.toml"
|
|
50
|
+
return deps
|
|
51
|
+
|
|
52
|
+
setup_py = project / "setup.py"
|
|
53
|
+
if setup_py.exists():
|
|
54
|
+
deps = self._parse_setup_py(setup_py)
|
|
55
|
+
deps.source_file = "setup.py"
|
|
56
|
+
return deps
|
|
57
|
+
|
|
58
|
+
req_txt = project / "requirements.txt"
|
|
59
|
+
if req_txt.exists():
|
|
60
|
+
deps = self._parse_requirements_txt(req_txt)
|
|
61
|
+
deps.source_file = "requirements.txt"
|
|
62
|
+
return deps
|
|
63
|
+
|
|
64
|
+
return deps
|
|
65
|
+
|
|
66
|
+
def _parse_pyproject(self, path: Path) -> ProjectDependencies:
|
|
67
|
+
"""Parse pyproject.toml for dependencies."""
|
|
68
|
+
deps = ProjectDependencies()
|
|
69
|
+
|
|
70
|
+
if tomllib is None:
|
|
71
|
+
# Fallback: regex-based parsing
|
|
72
|
+
return self._parse_pyproject_regex(path)
|
|
73
|
+
|
|
74
|
+
with open(path, "rb") as f:
|
|
75
|
+
data = tomllib.load(f)
|
|
76
|
+
|
|
77
|
+
project = data.get("project", {})
|
|
78
|
+
deps.python_version = project.get("requires-python", "")
|
|
79
|
+
|
|
80
|
+
# Main dependencies
|
|
81
|
+
for dep_str in project.get("dependencies", []):
|
|
82
|
+
deps.dependencies.append(self._parse_dep_string(dep_str))
|
|
83
|
+
|
|
84
|
+
# Optional dependencies
|
|
85
|
+
for group, dep_list in project.get("optional-dependencies", {}).items():
|
|
86
|
+
group_deps = [self._parse_dep_string(d) for d in dep_list]
|
|
87
|
+
for d in group_deps:
|
|
88
|
+
d.optional = True
|
|
89
|
+
d.group = group
|
|
90
|
+
deps.optional_groups[group] = group_deps
|
|
91
|
+
if group == "dev":
|
|
92
|
+
deps.dev_dependencies = group_deps
|
|
93
|
+
|
|
94
|
+
# Install command
|
|
95
|
+
name = project.get("name", "")
|
|
96
|
+
if name:
|
|
97
|
+
deps.install_command = f"pip install {name}"
|
|
98
|
+
|
|
99
|
+
return deps
|
|
100
|
+
|
|
101
|
+
def _parse_pyproject_regex(self, path: Path) -> ProjectDependencies:
|
|
102
|
+
"""Fallback regex-based pyproject.toml parser."""
|
|
103
|
+
deps = ProjectDependencies()
|
|
104
|
+
content = path.read_text(encoding="utf-8")
|
|
105
|
+
|
|
106
|
+
# Extract dependencies array
|
|
107
|
+
dep_match = re.search(r'dependencies\s*=\s*\[(.*?)\]', content, re.DOTALL)
|
|
108
|
+
if dep_match:
|
|
109
|
+
for dep_str in re.findall(r'"([^"]+)"', dep_match.group(1)):
|
|
110
|
+
deps.dependencies.append(self._parse_dep_string(dep_str))
|
|
111
|
+
|
|
112
|
+
# Extract python version
|
|
113
|
+
py_match = re.search(r'requires-python\s*=\s*"([^"]+)"', content)
|
|
114
|
+
if py_match:
|
|
115
|
+
deps.python_version = py_match.group(1)
|
|
116
|
+
|
|
117
|
+
return deps
|
|
118
|
+
|
|
119
|
+
def _parse_setup_py(self, path: Path) -> ProjectDependencies:
|
|
120
|
+
"""Parse setup.py for dependencies (regex-based, no exec)."""
|
|
121
|
+
deps = ProjectDependencies()
|
|
122
|
+
content = path.read_text(encoding="utf-8")
|
|
123
|
+
|
|
124
|
+
# install_requires
|
|
125
|
+
match = re.search(r'install_requires\s*=\s*\[(.*?)\]', content, re.DOTALL)
|
|
126
|
+
if match:
|
|
127
|
+
for dep_str in re.findall(r'"([^"]+)"', match.group(1)):
|
|
128
|
+
deps.dependencies.append(self._parse_dep_string(dep_str))
|
|
129
|
+
|
|
130
|
+
# python_requires
|
|
131
|
+
py_match = re.search(r'python_requires\s*=\s*"([^"]+)"', content)
|
|
132
|
+
if py_match:
|
|
133
|
+
deps.python_version = py_match.group(1)
|
|
134
|
+
|
|
135
|
+
return deps
|
|
136
|
+
|
|
137
|
+
def _parse_requirements_txt(self, path: Path) -> ProjectDependencies:
|
|
138
|
+
"""Parse requirements.txt."""
|
|
139
|
+
deps = ProjectDependencies()
|
|
140
|
+
|
|
141
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
142
|
+
line = line.strip()
|
|
143
|
+
if not line or line.startswith("#") or line.startswith("-"):
|
|
144
|
+
continue
|
|
145
|
+
deps.dependencies.append(self._parse_dep_string(line))
|
|
146
|
+
|
|
147
|
+
deps.install_command = "pip install -r requirements.txt"
|
|
148
|
+
return deps
|
|
149
|
+
|
|
150
|
+
@staticmethod
|
|
151
|
+
def _parse_dep_string(dep_str: str) -> DependencyInfo:
|
|
152
|
+
"""Parse a dependency string like 'package>=1.0'."""
|
|
153
|
+
match = re.match(r'^([a-zA-Z0-9_-]+)\s*(.*)', dep_str.strip())
|
|
154
|
+
if match:
|
|
155
|
+
return DependencyInfo(
|
|
156
|
+
name=match.group(1),
|
|
157
|
+
version_spec=match.group(2).strip(),
|
|
158
|
+
)
|
|
159
|
+
return DependencyInfo(name=dep_str.strip())
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Extract and analyze docstrings from source code."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, List, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
from code2llm.core.models import AnalysisResult, FunctionInfo, ClassInfo
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class DocstringInfo:
|
|
13
|
+
"""Parsed docstring with sections."""
|
|
14
|
+
raw: str
|
|
15
|
+
summary: str = ""
|
|
16
|
+
description: str = ""
|
|
17
|
+
params: Dict[str, str] = field(default_factory=dict)
|
|
18
|
+
returns: str = ""
|
|
19
|
+
raises: List[str] = field(default_factory=list)
|
|
20
|
+
examples: List[str] = field(default_factory=list)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DocstringExtractor:
|
|
24
|
+
"""Extract and parse docstrings from AnalysisResult."""
|
|
25
|
+
|
|
26
|
+
def extract_all(self, result: AnalysisResult) -> Dict[str, DocstringInfo]:
|
|
27
|
+
"""Extract docstrings for all functions and classes."""
|
|
28
|
+
docs: Dict[str, DocstringInfo] = {}
|
|
29
|
+
|
|
30
|
+
for name, func in result.functions.items():
|
|
31
|
+
if func.docstring:
|
|
32
|
+
docs[name] = self.parse(func.docstring)
|
|
33
|
+
|
|
34
|
+
for name, cls in result.classes.items():
|
|
35
|
+
if cls.docstring:
|
|
36
|
+
docs[name] = self.parse(cls.docstring)
|
|
37
|
+
|
|
38
|
+
return docs
|
|
39
|
+
|
|
40
|
+
def parse(self, docstring: str) -> DocstringInfo:
|
|
41
|
+
"""Parse a docstring into structured sections."""
|
|
42
|
+
if not docstring:
|
|
43
|
+
return DocstringInfo(raw="")
|
|
44
|
+
|
|
45
|
+
lines = docstring.strip().splitlines()
|
|
46
|
+
info = DocstringInfo(raw=docstring)
|
|
47
|
+
|
|
48
|
+
if lines:
|
|
49
|
+
info.summary = lines[0].strip()
|
|
50
|
+
|
|
51
|
+
# Simple parser for Google/Numpy/Sphinx styles
|
|
52
|
+
current_section = "description"
|
|
53
|
+
desc_lines: List[str] = []
|
|
54
|
+
param_lines: List[Tuple[str, str]] = []
|
|
55
|
+
|
|
56
|
+
for line in lines[1:]:
|
|
57
|
+
stripped = line.strip()
|
|
58
|
+
lower = stripped.lower()
|
|
59
|
+
|
|
60
|
+
if lower.startswith(("args:", "parameters:", "params:")):
|
|
61
|
+
current_section = "params"
|
|
62
|
+
continue
|
|
63
|
+
elif lower.startswith(("returns:", "return:")):
|
|
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(">>>"):
|
|
72
|
+
info.examples.append(stripped)
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
if current_section == "description":
|
|
76
|
+
desc_lines.append(stripped)
|
|
77
|
+
elif current_section == "params" and stripped:
|
|
78
|
+
# Parse "name: description" or "name (type): description"
|
|
79
|
+
if ":" in stripped:
|
|
80
|
+
pname, pdesc = stripped.split(":", 1)
|
|
81
|
+
info.params[pname.strip()] = pdesc.strip()
|
|
82
|
+
elif current_section == "returns" and stripped:
|
|
83
|
+
info.returns = stripped
|
|
84
|
+
elif current_section == "raises" and stripped:
|
|
85
|
+
info.raises.append(stripped)
|
|
86
|
+
elif current_section == "examples" and stripped:
|
|
87
|
+
info.examples.append(stripped)
|
|
88
|
+
|
|
89
|
+
info.description = "\n".join(desc_lines).strip()
|
|
90
|
+
return info
|
|
91
|
+
|
|
92
|
+
def coverage_report(self, result: AnalysisResult) -> Dict[str, float]:
|
|
93
|
+
"""Calculate docstring coverage statistics."""
|
|
94
|
+
total_funcs = len(result.functions)
|
|
95
|
+
total_classes = len(result.classes)
|
|
96
|
+
documented_funcs = sum(1 for f in result.functions.values() if f.docstring)
|
|
97
|
+
documented_classes = sum(1 for c in result.classes.values() if c.docstring)
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
"functions_total": total_funcs,
|
|
101
|
+
"functions_documented": documented_funcs,
|
|
102
|
+
"functions_coverage": (documented_funcs / total_funcs * 100) if total_funcs else 0,
|
|
103
|
+
"classes_total": total_classes,
|
|
104
|
+
"classes_documented": documented_classes,
|
|
105
|
+
"classes_coverage": (documented_classes / total_classes * 100) if total_classes else 0,
|
|
106
|
+
"overall_coverage": (
|
|
107
|
+
(documented_funcs + documented_classes)
|
|
108
|
+
/ (total_funcs + total_classes)
|
|
109
|
+
* 100
|
|
110
|
+
) if (total_funcs + total_classes) else 0,
|
|
111
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Detect web framework endpoints (Flask, FastAPI, Django) from AST analysis."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import re
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from code2llm.core.models import AnalysisResult, FunctionInfo
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Endpoint:
|
|
14
|
+
"""Represents a detected web endpoint."""
|
|
15
|
+
method: str # GET, POST, PUT, DELETE, etc.
|
|
16
|
+
path: str
|
|
17
|
+
function_name: str
|
|
18
|
+
file: str
|
|
19
|
+
line: int
|
|
20
|
+
framework: str # flask, fastapi, django
|
|
21
|
+
docstring: Optional[str] = None
|
|
22
|
+
params: List[str] = field(default_factory=list)
|
|
23
|
+
return_type: Optional[str] = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class EndpointDetector:
|
|
27
|
+
"""Detects web endpoints from decorator patterns in source code."""
|
|
28
|
+
|
|
29
|
+
FLASK_PATTERNS = re.compile(
|
|
30
|
+
r'@(?:app|blueprint|bp)\.(route|get|post|put|delete|patch)\s*\(\s*["\']([^"\']+)["\']'
|
|
31
|
+
)
|
|
32
|
+
FASTAPI_PATTERNS = re.compile(
|
|
33
|
+
r'@(?:app|router)\.(get|post|put|delete|patch|options|head)\s*\(\s*["\']([^"\']+)["\']'
|
|
34
|
+
)
|
|
35
|
+
DJANGO_URL_PATTERN = re.compile(
|
|
36
|
+
r'(?:path|re_path|url)\s*\(\s*["\']([^"\']+)["\']'
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def detect(self, result: AnalysisResult, project_path: str) -> List[Endpoint]:
|
|
40
|
+
"""Detect all endpoints from the analysis result."""
|
|
41
|
+
endpoints: List[Endpoint] = []
|
|
42
|
+
|
|
43
|
+
for qualified_name, func_info in result.functions.items():
|
|
44
|
+
file_path = func_info.file
|
|
45
|
+
if not file_path:
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
# Check decorators for route patterns
|
|
49
|
+
for decorator in func_info.decorators:
|
|
50
|
+
endpoint = self._parse_decorator(decorator, func_info)
|
|
51
|
+
if endpoint:
|
|
52
|
+
endpoints.append(endpoint)
|
|
53
|
+
|
|
54
|
+
# Also scan for Django URL patterns in urls.py files
|
|
55
|
+
endpoints.extend(self._scan_django_urls(project_path))
|
|
56
|
+
|
|
57
|
+
return endpoints
|
|
58
|
+
|
|
59
|
+
def _parse_decorator(self, decorator: str, func: FunctionInfo) -> Optional[Endpoint]:
|
|
60
|
+
"""Try to parse a route decorator string."""
|
|
61
|
+
# Flask patterns
|
|
62
|
+
match = self.FLASK_PATTERNS.search(decorator)
|
|
63
|
+
if match:
|
|
64
|
+
method = match.group(1).upper()
|
|
65
|
+
if method == "ROUTE":
|
|
66
|
+
method = "GET"
|
|
67
|
+
return Endpoint(
|
|
68
|
+
method=method,
|
|
69
|
+
path=match.group(2),
|
|
70
|
+
function_name=func.name,
|
|
71
|
+
file=func.file,
|
|
72
|
+
line=func.line,
|
|
73
|
+
framework="flask",
|
|
74
|
+
docstring=func.docstring,
|
|
75
|
+
params=func.args,
|
|
76
|
+
return_type=func.returns,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# FastAPI patterns
|
|
80
|
+
match = self.FASTAPI_PATTERNS.search(decorator)
|
|
81
|
+
if match:
|
|
82
|
+
return Endpoint(
|
|
83
|
+
method=match.group(1).upper(),
|
|
84
|
+
path=match.group(2),
|
|
85
|
+
function_name=func.name,
|
|
86
|
+
file=func.file,
|
|
87
|
+
line=func.line,
|
|
88
|
+
framework="fastapi",
|
|
89
|
+
docstring=func.docstring,
|
|
90
|
+
params=func.args,
|
|
91
|
+
return_type=func.returns,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
def _scan_django_urls(self, project_path: str) -> List[Endpoint]:
|
|
97
|
+
"""Scan urls.py files for Django URL patterns."""
|
|
98
|
+
endpoints: List[Endpoint] = []
|
|
99
|
+
project = Path(project_path)
|
|
100
|
+
|
|
101
|
+
for urls_file in project.rglob("urls.py"):
|
|
102
|
+
try:
|
|
103
|
+
source = urls_file.read_text(encoding="utf-8")
|
|
104
|
+
for match in self.DJANGO_URL_PATTERN.finditer(source):
|
|
105
|
+
endpoints.append(Endpoint(
|
|
106
|
+
method="GET",
|
|
107
|
+
path=match.group(1),
|
|
108
|
+
function_name="",
|
|
109
|
+
file=str(urls_file),
|
|
110
|
+
line=source[:match.start()].count("\n") + 1,
|
|
111
|
+
framework="django",
|
|
112
|
+
))
|
|
113
|
+
except (OSError, UnicodeDecodeError):
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
return endpoints
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Wrapper around code2llm's ProjectAnalyzer for documentation purposes."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from code2llm import Config, FAST_CONFIG
|
|
7
|
+
from code2llm.core.analyzer import ProjectAnalyzer
|
|
8
|
+
from code2llm.core.models import AnalysisResult
|
|
9
|
+
|
|
10
|
+
from ..config import Code2DocsConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ProjectScanner:
|
|
14
|
+
"""Wraps code2llm's ProjectAnalyzer with code2docs-specific defaults."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, config: Optional[Code2DocsConfig] = None):
|
|
17
|
+
self.config = config or Code2DocsConfig()
|
|
18
|
+
self._llm_config = self._build_llm_config()
|
|
19
|
+
|
|
20
|
+
def _build_llm_config(self) -> Config:
|
|
21
|
+
"""Create code2llm Config tuned for documentation generation."""
|
|
22
|
+
config = Config(
|
|
23
|
+
mode="static",
|
|
24
|
+
verbose=self.config.verbose,
|
|
25
|
+
)
|
|
26
|
+
config.filters.exclude_tests = self.config.exclude_tests
|
|
27
|
+
config.filters.skip_private = self.config.skip_private
|
|
28
|
+
# Keep properties and accessors visible for docs
|
|
29
|
+
config.filters.skip_properties = False
|
|
30
|
+
config.filters.skip_accessors = False
|
|
31
|
+
# Enable pattern detection for architecture docs
|
|
32
|
+
config.performance.skip_pattern_detection = False
|
|
33
|
+
config.performance.parallel_enabled = True
|
|
34
|
+
return config
|
|
35
|
+
|
|
36
|
+
def analyze(self, project_path: str) -> AnalysisResult:
|
|
37
|
+
"""Analyze a project and return AnalysisResult for doc generation."""
|
|
38
|
+
analyzer = ProjectAnalyzer(self._llm_config)
|
|
39
|
+
return analyzer.analyze_project(project_path)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def analyze_and_document(project_path: str, config: Optional[Code2DocsConfig] = None) -> AnalysisResult:
|
|
43
|
+
"""Convenience function: analyze a project in one call."""
|
|
44
|
+
scanner = ProjectScanner(config)
|
|
45
|
+
return scanner.analyze(project_path)
|