code2docs 3.0.26__py3-none-any.whl → 3.0.28__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 +1 -1
- code2docs/analyzers/docstring_extractor.py +2 -4
- code2docs/analyzers/endpoint_detector.py +1 -2
- code2docs/analyzers/project_scanner.py +1 -2
- code2docs/cli.py +1 -1
- code2docs/examples/advanced_usage.py +0 -1
- code2docs/examples/quickstart.py +0 -2
- code2docs/formatters/markdown.py +1 -1
- code2docs/generators/_registry_adapters.py +37 -25
- code2docs/generators/api_changelog_gen.py +2 -2
- code2docs/generators/api_reference_gen.py +60 -42
- code2docs/generators/architecture_gen.py +1 -1
- code2docs/generators/changelog_gen.py +1 -1
- code2docs/generators/code2llm_gen.py +99 -57
- code2docs/generators/config_docs_gen.py +1 -2
- code2docs/generators/contributing_gen.py +44 -29
- code2docs/generators/depgraph_gen.py +1 -1
- code2docs/generators/examples_gen.py +144 -108
- code2docs/generators/getting_started_gen.py +62 -40
- code2docs/generators/module_docs_gen.py +124 -76
- code2docs/generators/org_readme_gen.py +52 -27
- code2docs/generators/readme_gen.py +48 -31
- code2docs/sync/differ.py +1 -1
- {code2docs-3.0.26.dist-info → code2docs-3.0.28.dist-info}/METADATA +8 -272
- code2docs-3.0.28.dist-info/RECORD +52 -0
- code2docs-3.0.26.dist-info/RECORD +0 -52
- {code2docs-3.0.26.dist-info → code2docs-3.0.28.dist-info}/WHEEL +0 -0
- {code2docs-3.0.26.dist-info → code2docs-3.0.28.dist-info}/entry_points.txt +0 -0
- {code2docs-3.0.26.dist-info → code2docs-3.0.28.dist-info}/licenses/LICENSE +0 -0
- {code2docs-3.0.26.dist-info → code2docs-3.0.28.dist-info}/top_level.txt +0 -0
code2docs/__init__.py
CHANGED
|
@@ -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__ = '3.0.
|
|
8
|
+
__version__ = '3.0.28'
|
|
9
9
|
__author__ = 'Tom Sapletta'
|
|
10
10
|
__all__ = ['Code2DocsConfig', 'generate_readme', 'generate_docs', 'analyze_and_document']
|
|
11
11
|
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
"""Extract and analyze docstrings from source code."""
|
|
2
2
|
|
|
3
|
-
import ast
|
|
4
3
|
from dataclasses import dataclass, field
|
|
5
|
-
from
|
|
6
|
-
from typing import Dict, List, Optional, Tuple
|
|
4
|
+
from typing import Dict, List, Optional
|
|
7
5
|
|
|
8
|
-
from code2llm.api import AnalysisResult
|
|
6
|
+
from code2llm.api import AnalysisResult
|
|
9
7
|
|
|
10
8
|
|
|
11
9
|
@dataclass
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
"""Detect web framework endpoints (Flask, FastAPI, Django) from AST analysis."""
|
|
2
2
|
|
|
3
|
-
import ast
|
|
4
3
|
import re
|
|
5
4
|
from dataclasses import dataclass, field
|
|
6
5
|
from pathlib import Path
|
|
7
|
-
from typing import
|
|
6
|
+
from typing import List, Optional
|
|
8
7
|
|
|
9
8
|
from code2llm.api import AnalysisResult, FunctionInfo
|
|
10
9
|
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
"""Wrapper around code2llm's ProjectAnalyzer for documentation purposes."""
|
|
2
2
|
|
|
3
|
-
from pathlib import Path
|
|
4
3
|
from typing import Optional
|
|
5
4
|
|
|
6
|
-
from code2llm.api import Config,
|
|
5
|
+
from code2llm.api import Config, AnalysisResult, analyze
|
|
7
6
|
|
|
8
7
|
from ..config import Code2DocsConfig
|
|
9
8
|
|
code2docs/cli.py
CHANGED
|
@@ -32,7 +32,7 @@ def main() -> None:
|
|
|
32
32
|
@click.option('--output', '-o', default=None, help='Output directory for docs')
|
|
33
33
|
@click.option('--verbose', '-v', is_flag=True, help='Verbose output')
|
|
34
34
|
@click.option('--dry-run', is_flag=True, help='Show what would be generated without writing')
|
|
35
|
-
@click.option('--llm', 'llm_model', default=None, help='Enable LLM-assisted generation (e.g. openai/gpt-
|
|
35
|
+
@click.option('--llm', 'llm_model', default=None, help='Enable LLM-assisted generation (e.g. openai/gpt-5.4-mini, ollama/llama3)')
|
|
36
36
|
@click.option('--org-name', default=None, help='Organization name for org-mode README generation')
|
|
37
37
|
def generate(project_path, config_path, readme_only, sections, output, verbose, dry_run, llm_model, org_name) -> None:
|
|
38
38
|
"""Generate documentation (default command)."""
|
code2docs/examples/quickstart.py
CHANGED
|
@@ -5,10 +5,8 @@ Minimal working examples for the most common use cases.
|
|
|
5
5
|
Run: python examples/quickstart.py
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from pathlib import Path
|
|
9
8
|
|
|
10
9
|
from code2docs import Code2DocsConfig
|
|
11
|
-
from code2docs import analyze_and_document
|
|
12
10
|
from code2docs import generate_docs
|
|
13
11
|
from code2docs import generate_readme
|
|
14
12
|
|
code2docs/formatters/markdown.py
CHANGED
|
@@ -260,31 +260,43 @@ class IndexHtmlAdapter(BaseGenerator):
|
|
|
260
260
|
|
|
261
261
|
def _generate_html(self, ctx: GenerateContext) -> str:
|
|
262
262
|
project_name = self.config.project_name or ctx.project.name
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
if (
|
|
284
|
-
files.append(('CONTRIBUTING.md', 'Contributing Guide', '🤝'))
|
|
285
|
-
if (ctx.docs_dir / 'examples').is_dir():
|
|
263
|
+
files = self._collect_doc_files(ctx.docs_dir)
|
|
264
|
+
github_link = self._build_github_link(self.config.repo_url)
|
|
265
|
+
files_html = self._build_files_html(files)
|
|
266
|
+
return self._build_html_template(project_name, github_link, files_html)
|
|
267
|
+
|
|
268
|
+
def _collect_doc_files(self, docs_dir: Path) -> list:
|
|
269
|
+
"""Collect existing documentation files with their display info."""
|
|
270
|
+
doc_files = [
|
|
271
|
+
('README.md', 'Project Overview', '📖'),
|
|
272
|
+
('getting-started.md', 'Getting Started', '🚀'),
|
|
273
|
+
('api.md', 'API Reference', '📚'),
|
|
274
|
+
('modules.md', 'Module Documentation', '📦'),
|
|
275
|
+
('architecture.md', 'Architecture', '🏗️'),
|
|
276
|
+
('dependency-graph.md', 'Dependency Graph', '🔗'),
|
|
277
|
+
('coverage.md', 'Code Coverage', '📊'),
|
|
278
|
+
('api-changelog.md', 'API Changelog', '📝'),
|
|
279
|
+
('configuration.md', 'Configuration', '⚙️'),
|
|
280
|
+
('CONTRIBUTING.md', 'Contributing Guide', '🤝'),
|
|
281
|
+
]
|
|
282
|
+
files = [(href, title, icon) for href, title, icon in doc_files if (docs_dir / href).exists()]
|
|
283
|
+
if (docs_dir / 'examples').is_dir():
|
|
286
284
|
files.append(('examples/', 'Examples', '💡'))
|
|
287
|
-
|
|
288
|
-
|
|
285
|
+
return files
|
|
286
|
+
|
|
287
|
+
def _build_github_link(self, repo_url: str) -> str:
|
|
288
|
+
"""Build GitHub link HTML if repo_url is available."""
|
|
289
|
+
if not repo_url:
|
|
290
|
+
return ''
|
|
291
|
+
return f'<a href="{repo_url}" class="github-link" target="_blank" rel="noopener">View on GitHub</a>'
|
|
292
|
+
|
|
293
|
+
def _build_files_html(self, files: list) -> str:
|
|
294
|
+
"""Build HTML for file cards."""
|
|
295
|
+
return '\n'.join(
|
|
296
|
+
f'<a href="{href}" class="doc-card"><span class="icon">{icon}</span><span class="title">{title}</span></a>'
|
|
297
|
+
for href, title, icon in files
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
def _build_html_template(self, project_name: str, github_link: str, files_html: str) -> str:
|
|
289
301
|
return f'<!DOCTYPE html>\n<html lang="en">\n<head>\n <meta charset="UTF-8">\n <meta name="viewport" content="width=device-width, initial-scale=1.0">\n <title>{project_name} - Documentation</title>\n <style>\n * {{ margin: 0; padding: 0; box-sizing: border-box; }}\n body {{\n font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, sans-serif;\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n min-height: 100vh;\n padding: 40px 20px;\n }}\n .container {{\n max-width: 900px;\n margin: 0 auto;\n }}\n .header {{\n text-align: center;\n margin-bottom: 40px;\n color: white;\n }}\n .header h1 {{\n font-size: 2.5rem;\n margin-bottom: 10px;\n text-shadow: 0 2px 4px rgba(0,0,0,0.2);\n }}\n .header p {{\n font-size: 1.1rem;\n opacity: 0.9;\n }}\n .github-link {{\n display: inline-block;\n margin-top: 15px;\n padding: 10px 20px;\n background: rgba(255,255,255,0.2);\n color: white;\n text-decoration: none;\n border-radius: 25px;\n transition: background 0.3s;\n }}\n .github-link:hover {{ background: rgba(255,255,255,0.3); }}\n .docs-grid {{\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));\n gap: 20px;\n }}\n .doc-card {{\n background: white;\n border-radius: 12px;\n padding: 25px;\n text-decoration: none;\n color: #333;\n box-shadow: 0 4px 15px rgba(0,0,0,0.1);\n transition: transform 0.2s, box-shadow 0.2s;\n display: flex;\n align-items: center;\n gap: 15px;\n }}\n .doc-card:hover {{\n transform: translateY(-3px);\n box-shadow: 0 8px 25px rgba(0,0,0,0.15);\n }}\n .doc-card .icon {{\n font-size: 2rem;\n flex-shrink: 0;\n }}\n .doc-card .title {{\n font-size: 1.1rem;\n font-weight: 600;\n }}\n .footer {{\n text-align: center;\n margin-top: 40px;\n color: rgba(255,255,255,0.7);\n font-size: 0.9rem;\n }}\n @media (max-width: 600px) {{\n .header h1 {{ font-size: 1.8rem; }}\n .docs-grid {{ grid-template-columns: 1fr; }}\n }}\n </style>\n</head>\n<body>\n <div class="container">\n <div class="header">\n <h1>{project_name}</h1>\n <p>Generated Documentation</p>\n {github_link}\n </div>\n <div class="docs-grid">\n {files_html}\n </div>\n <div class="footer">\n Generated with <a href="https://github.com/wronai/code2docs" style="color: rgba(255,255,255,0.9);">code2docs</a>\n </div>\n </div>\n</body>\n</html>'
|
|
290
302
|
ALL_ADAPTERS = [ReadmeGeneratorAdapter, ApiReferenceAdapter, ModuleDocsAdapter, ArchitectureAdapter, DepGraphAdapter, CoverageAdapter, ApiChangelogAdapter, ExamplesAdapter, GettingStartedAdapter, ConfigDocsAdapter, ContributingAdapter, MkDocsAdapter, Code2LlmAdapter, OrgReadmeAdapter, IndexHtmlAdapter]
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""API changelog generator — diff function/class signatures between versions."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
from dataclasses import dataclass
|
|
4
|
+
from dataclasses import dataclass
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Dict, List, Optional
|
|
6
|
+
from typing import Dict, List, Optional
|
|
7
7
|
|
|
8
8
|
from code2llm.api import AnalysisResult
|
|
9
9
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from collections import defaultdict
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Dict, List
|
|
5
|
+
from typing import Dict, List
|
|
6
6
|
|
|
7
7
|
from code2llm.api import AnalysisResult, FunctionInfo, ClassInfo, ModuleInfo
|
|
8
8
|
|
|
@@ -88,56 +88,74 @@ class ApiReferenceGenerator:
|
|
|
88
88
|
heading = f"### `{mod_name}` {src}" if src else f"### `{mod_name}`"
|
|
89
89
|
lines = [f"{heading}\n"]
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
module_classes
|
|
91
|
+
module_classes = self._get_module_classes(mod_name)
|
|
92
|
+
if module_classes:
|
|
93
|
+
lines.extend(self._render_classes_table(module_classes))
|
|
94
|
+
lines.extend(self._render_class_methods(module_classes))
|
|
95
|
+
|
|
96
|
+
module_functions = self._get_module_functions(mod_name)
|
|
97
|
+
if module_functions:
|
|
98
|
+
lines.extend(self._render_functions_table(module_functions))
|
|
99
|
+
|
|
100
|
+
return "\n".join(lines)
|
|
101
|
+
|
|
102
|
+
def _get_module_classes(self, mod_name: str) -> Dict[str, ClassInfo]:
|
|
103
|
+
"""Get all public classes for a module."""
|
|
104
|
+
return {
|
|
93
105
|
k: v for k, v in self.result.classes.items()
|
|
94
106
|
if (v.module == mod_name or k.startswith(mod_name + "."))
|
|
95
107
|
and not v.name.startswith("_")
|
|
96
108
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
doc = cls_info.docstring.splitlines()[0] if cls_info.docstring else "—"
|
|
102
|
-
public_methods = [m for m in cls_info.methods
|
|
103
|
-
if not m.split(".")[-1].startswith("_")]
|
|
104
|
-
src = self._linker.source_link(cls_info.file, cls_info.line)
|
|
105
|
-
lines.append(f"| `{cls_info.name}` | {len(public_methods)} | {doc} | {src} |")
|
|
106
|
-
lines.append("")
|
|
107
|
-
|
|
108
|
-
# Expand methods for important classes (>2 public methods)
|
|
109
|
-
for cls_name, cls_info in sorted(module_classes.items()):
|
|
110
|
-
methods = self._get_public_methods(cls_info)
|
|
111
|
-
if len(methods) >= 2:
|
|
112
|
-
lines.append(f"**`{cls_info.name}` methods:**\n")
|
|
113
|
-
for m in methods:
|
|
114
|
-
sig = self._format_signature(m)
|
|
115
|
-
doc = f" — {m.docstring.splitlines()[0]}" if m.docstring else ""
|
|
116
|
-
lines.append(f"- `{sig}`{doc}")
|
|
117
|
-
lines.append("")
|
|
118
|
-
|
|
119
|
-
# Functions table
|
|
120
|
-
module_functions = {
|
|
109
|
+
|
|
110
|
+
def _get_module_functions(self, mod_name: str) -> Dict[str, FunctionInfo]:
|
|
111
|
+
"""Get all public functions for a module."""
|
|
112
|
+
return {
|
|
121
113
|
k: v for k, v in self.result.functions.items()
|
|
122
114
|
if (v.module == mod_name or k.startswith(mod_name + "."))
|
|
123
115
|
and not v.is_method and not v.name.startswith("_")
|
|
124
116
|
}
|
|
125
|
-
if module_functions:
|
|
126
|
-
lines.append("| Function | Signature | CC | Description | Source |")
|
|
127
|
-
lines.append("|----------|-----------|----|----------- |--------|")
|
|
128
|
-
for func_name, func_info in sorted(module_functions.items()):
|
|
129
|
-
sig = self._format_signature(func_info)
|
|
130
|
-
cc = func_info.complexity.get(
|
|
131
|
-
"cyclomatic_complexity",
|
|
132
|
-
func_info.complexity.get("cyclomatic", "—"),
|
|
133
|
-
)
|
|
134
|
-
doc = func_info.docstring.splitlines()[0] if func_info.docstring else "—"
|
|
135
|
-
warn = " ⚠️" if isinstance(cc, (int, float)) and cc > 10 else ""
|
|
136
|
-
src = self._linker.source_link(func_info.file, func_info.line)
|
|
137
|
-
lines.append(f"| `{func_info.name}` | `{sig}` | {cc}{warn} | {doc} | {src} |")
|
|
138
|
-
lines.append("")
|
|
139
117
|
|
|
140
|
-
|
|
118
|
+
def _render_classes_table(self, module_classes: Dict[str, ClassInfo]) -> List[str]:
|
|
119
|
+
"""Render the classes summary table."""
|
|
120
|
+
lines = ["| Class | Methods | Description | Source |", "|-------|---------|-------------|--------|"]
|
|
121
|
+
for cls_name, cls_info in sorted(module_classes.items()):
|
|
122
|
+
doc = cls_info.docstring.splitlines()[0] if cls_info.docstring else "—"
|
|
123
|
+
public_methods = [m for m in cls_info.methods
|
|
124
|
+
if not m.split(".")[-1].startswith("_")]
|
|
125
|
+
src = self._linker.source_link(cls_info.file, cls_info.line)
|
|
126
|
+
lines.append(f"| `{cls_info.name}` | {len(public_methods)} | {doc} | {src} |")
|
|
127
|
+
lines.append("")
|
|
128
|
+
return lines
|
|
129
|
+
|
|
130
|
+
def _render_class_methods(self, module_classes: Dict[str, ClassInfo]) -> List[str]:
|
|
131
|
+
"""Render expanded methods for classes with >=2 public methods."""
|
|
132
|
+
lines = []
|
|
133
|
+
for cls_name, cls_info in sorted(module_classes.items()):
|
|
134
|
+
methods = self._get_public_methods(cls_info)
|
|
135
|
+
if len(methods) >= 2:
|
|
136
|
+
lines.append(f"**`{cls_info.name}` methods:**\n")
|
|
137
|
+
for m in methods:
|
|
138
|
+
sig = self._format_signature(m)
|
|
139
|
+
doc = f" — {m.docstring.splitlines()[0]}" if m.docstring else ""
|
|
140
|
+
lines.append(f"- `{sig}`{doc}")
|
|
141
|
+
lines.append("")
|
|
142
|
+
return lines
|
|
143
|
+
|
|
144
|
+
def _render_functions_table(self, module_functions: Dict[str, FunctionInfo]) -> List[str]:
|
|
145
|
+
"""Render the functions summary table."""
|
|
146
|
+
lines = ["| Function | Signature | CC | Description | Source |", "|----------|-----------|----|----------- |--------|"]
|
|
147
|
+
for func_name, func_info in sorted(module_functions.items()):
|
|
148
|
+
sig = self._format_signature(func_info)
|
|
149
|
+
cc = func_info.complexity.get(
|
|
150
|
+
"cyclomatic_complexity",
|
|
151
|
+
func_info.complexity.get("cyclomatic", "—"),
|
|
152
|
+
)
|
|
153
|
+
doc = func_info.docstring.splitlines()[0] if func_info.docstring else "—"
|
|
154
|
+
warn = " ⚠️" if isinstance(cc, (int, float)) and cc > 10 else ""
|
|
155
|
+
src = self._linker.source_link(func_info.file, func_info.line)
|
|
156
|
+
lines.append(f"| `{func_info.name}` | `{sig}` | {cc}{warn} | {doc} | {src} |")
|
|
157
|
+
lines.append("")
|
|
158
|
+
return lines
|
|
141
159
|
|
|
142
160
|
def _get_public_methods(self, cls_info: ClassInfo) -> List[FunctionInfo]:
|
|
143
161
|
"""Get public (non-dunder) methods of a class."""
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import subprocess
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import List, Optional, Dict
|
|
5
|
+
from typing import List, Optional, Dict
|
|
6
6
|
|
|
7
7
|
from code2llm.api import AnalysisResult
|
|
8
8
|
|
|
@@ -11,54 +11,79 @@ from ..config import Code2DocsConfig
|
|
|
11
11
|
|
|
12
12
|
def parse_gitignore(project_path: Path) -> List[str]:
|
|
13
13
|
"""Parse .gitignore file and return list of patterns to exclude.
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
Filters out:
|
|
16
16
|
- Empty lines
|
|
17
17
|
- Comments (lines starting with #)
|
|
18
18
|
- Negation patterns (starting with !)
|
|
19
19
|
- Complex patterns with ** or regex
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
Returns simple directory/file patterns that can be passed to --skip-subprojects.
|
|
22
22
|
"""
|
|
23
23
|
gitignore_path = project_path / ".gitignore"
|
|
24
24
|
if not gitignore_path.exists():
|
|
25
25
|
return []
|
|
26
|
-
|
|
27
|
-
patterns = []
|
|
26
|
+
|
|
28
27
|
try:
|
|
29
28
|
content = gitignore_path.read_text(encoding="utf-8")
|
|
30
|
-
|
|
31
|
-
line = line.strip()
|
|
32
|
-
# Skip empty lines and comments
|
|
33
|
-
if not line or line.startswith("#"):
|
|
34
|
-
continue
|
|
35
|
-
# Skip negation patterns (too complex)
|
|
36
|
-
if line.startswith("!"):
|
|
37
|
-
continue
|
|
38
|
-
# Skip patterns with ** (globstar - too complex)
|
|
39
|
-
if "**" in line:
|
|
40
|
-
continue
|
|
41
|
-
# Skip file-specific patterns (with wildcards)
|
|
42
|
-
if "*" in line and "/" not in line:
|
|
43
|
-
# Wildcard without path - likely file pattern, skip
|
|
44
|
-
continue
|
|
45
|
-
# Clean up the pattern
|
|
46
|
-
pattern = line.rstrip("/") # Remove trailing slash
|
|
47
|
-
# Skip patterns starting with / (root-only patterns)
|
|
48
|
-
if pattern.startswith("/"):
|
|
49
|
-
pattern = pattern[1:]
|
|
50
|
-
# Skip if still has special characters
|
|
51
|
-
if any(c in pattern for c in "[]?*"):
|
|
52
|
-
continue
|
|
53
|
-
# Valid directory pattern
|
|
54
|
-
if pattern and len(pattern) > 1:
|
|
55
|
-
patterns.append(pattern)
|
|
29
|
+
return _extract_patterns(content)
|
|
56
30
|
except Exception:
|
|
57
|
-
|
|
58
|
-
|
|
31
|
+
return []
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _extract_patterns(content: str) -> List[str]:
|
|
35
|
+
"""Extract valid patterns from gitignore content."""
|
|
36
|
+
patterns = []
|
|
37
|
+
for line in content.split("\n"):
|
|
38
|
+
pattern = _process_line(line)
|
|
39
|
+
if pattern:
|
|
40
|
+
patterns.append(pattern)
|
|
59
41
|
return patterns
|
|
60
42
|
|
|
61
43
|
|
|
44
|
+
def _process_line(line: str) -> str:
|
|
45
|
+
"""Process a single gitignore line, returning valid pattern or empty string."""
|
|
46
|
+
line = line.strip()
|
|
47
|
+
|
|
48
|
+
if _should_skip_line(line):
|
|
49
|
+
return ""
|
|
50
|
+
|
|
51
|
+
pattern = _clean_pattern(line)
|
|
52
|
+
if _is_valid_pattern(pattern):
|
|
53
|
+
return pattern
|
|
54
|
+
return ""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _should_skip_line(line: str) -> bool:
|
|
58
|
+
"""Check if line should be skipped (empty, comment, negation, globstar)."""
|
|
59
|
+
if not line or line.startswith("#"):
|
|
60
|
+
return True
|
|
61
|
+
if line.startswith("!"):
|
|
62
|
+
return True
|
|
63
|
+
if "**" in line:
|
|
64
|
+
return True
|
|
65
|
+
if "*" in line and "/" not in line:
|
|
66
|
+
return True
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _clean_pattern(line: str) -> str:
|
|
71
|
+
"""Clean up the pattern by removing trailing slashes and leading slashes."""
|
|
72
|
+
pattern = line.rstrip("/")
|
|
73
|
+
if pattern.startswith("/"):
|
|
74
|
+
pattern = pattern[1:]
|
|
75
|
+
return pattern
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _is_valid_pattern(pattern: str) -> bool:
|
|
79
|
+
"""Check if pattern is valid (no special chars, length > 1)."""
|
|
80
|
+
if not pattern or len(pattern) <= 1:
|
|
81
|
+
return False
|
|
82
|
+
if any(c in pattern for c in "[]?*"):
|
|
83
|
+
return False
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
|
|
62
87
|
class Code2LlmGenerator:
|
|
63
88
|
"""Generate code2llm analysis files in project/ directory.
|
|
64
89
|
|
|
@@ -124,8 +149,15 @@ class Code2LlmGenerator:
|
|
|
124
149
|
def _run_code2llm(self, project_path: Path, output_dir: Path) -> None:
|
|
125
150
|
"""Execute code2llm CLI with appropriate options."""
|
|
126
151
|
cfg = self.config.code2llm
|
|
127
|
-
|
|
128
|
-
cmd =
|
|
152
|
+
|
|
153
|
+
cmd = self._build_base_cmd(project_path, output_dir, cfg)
|
|
154
|
+
self._add_config_options(cmd, cfg)
|
|
155
|
+
self._add_exclude_patterns(cmd, cfg, project_path)
|
|
156
|
+
self._execute_command(cmd, project_path)
|
|
157
|
+
|
|
158
|
+
def _build_base_cmd(self, project_path: Path, output_dir: Path, cfg) -> List[str]:
|
|
159
|
+
"""Build base command with required options."""
|
|
160
|
+
return [
|
|
129
161
|
"python", "-m", "code2llm",
|
|
130
162
|
str(project_path),
|
|
131
163
|
"-f", ",".join(cfg.formats),
|
|
@@ -133,41 +165,51 @@ class Code2LlmGenerator:
|
|
|
133
165
|
"--strategy", cfg.strategy,
|
|
134
166
|
"--max-depth", str(cfg.max_depth),
|
|
135
167
|
]
|
|
136
|
-
|
|
137
|
-
|
|
168
|
+
|
|
169
|
+
def _add_config_options(self, cmd: List[str], cfg) -> None:
|
|
170
|
+
"""Add optional flags based on config settings."""
|
|
138
171
|
if not cfg.chunk:
|
|
139
172
|
cmd.append("--no-chunk")
|
|
140
173
|
if cfg.no_png:
|
|
141
174
|
cmd.append("--no-png")
|
|
142
175
|
if self.config.verbose:
|
|
143
176
|
cmd.append("-v")
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
skip_dirs = [p for p in cfg.exclude_patterns if not p.startswith(".") and not p.startswith("*")]
|
|
149
|
-
if skip_dirs:
|
|
150
|
-
cmd.extend(["--skip-subprojects"] + skip_dirs[:10]) # Limit to 10
|
|
151
|
-
|
|
152
|
-
# Add patterns from .gitignore
|
|
177
|
+
|
|
178
|
+
def _add_exclude_patterns(self, cmd: List[str], cfg, project_path: Path) -> None:
|
|
179
|
+
"""Add exclude patterns from config and .gitignore."""
|
|
180
|
+
skip_dirs = self._get_config_skip_dirs(cfg)
|
|
153
181
|
gitignore_patterns = parse_gitignore(project_path)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
182
|
+
|
|
183
|
+
all_patterns = self._merge_patterns(skip_dirs, gitignore_patterns)
|
|
184
|
+
if all_patterns:
|
|
185
|
+
cmd.append("--skip-subprojects")
|
|
186
|
+
cmd.extend(all_patterns[:10])
|
|
187
|
+
|
|
188
|
+
def _get_config_skip_dirs(self, cfg) -> List[str]:
|
|
189
|
+
"""Get skip directories from config exclude patterns."""
|
|
190
|
+
if not cfg.exclude_patterns:
|
|
191
|
+
return []
|
|
192
|
+
return [p for p in cfg.exclude_patterns if not p.startswith(".") and not p.startswith("*")]
|
|
193
|
+
|
|
194
|
+
def _merge_patterns(self, config_patterns: List[str], gitignore_patterns: List[str]) -> List[str]:
|
|
195
|
+
"""Merge config and gitignore patterns, removing duplicates."""
|
|
196
|
+
existing = set(config_patterns)
|
|
197
|
+
merged = list(config_patterns)
|
|
198
|
+
for p in gitignore_patterns:
|
|
199
|
+
if p not in existing:
|
|
200
|
+
merged.append(p)
|
|
201
|
+
existing.add(p)
|
|
202
|
+
return merged
|
|
203
|
+
|
|
204
|
+
def _execute_command(self, cmd: List[str], project_path: Path) -> None:
|
|
205
|
+
"""Run the subprocess command and handle errors."""
|
|
164
206
|
result = subprocess.run(
|
|
165
207
|
cmd,
|
|
166
208
|
capture_output=True,
|
|
167
209
|
text=True,
|
|
168
210
|
cwd=str(project_path),
|
|
169
211
|
)
|
|
170
|
-
|
|
212
|
+
|
|
171
213
|
# Don't raise on mmdc/png errors (optional dependencies)
|
|
172
214
|
if result.returncode != 0 and "mmdc" not in result.stderr:
|
|
173
215
|
raise RuntimeError(f"code2llm failed: {result.stderr}")
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Configuration documentation generator."""
|
|
2
2
|
|
|
3
3
|
from dataclasses import fields, MISSING
|
|
4
|
-
from typing import Any
|
|
5
4
|
|
|
6
5
|
from code2llm.api import AnalysisResult
|
|
7
6
|
|
|
@@ -55,7 +54,7 @@ class ConfigDocsGenerator:
|
|
|
55
54
|
"watch": "Enable file watcher for auto-resync",
|
|
56
55
|
"ignore": "Glob patterns to ignore during sync",
|
|
57
56
|
"enabled": "Enable LLM-assisted documentation generation",
|
|
58
|
-
"model": "LLM model identifier (litellm format, e.g. `openai/gpt-
|
|
57
|
+
"model": "LLM model identifier (litellm format, e.g. `openai/gpt-5.4-mini`, `ollama/llama3`)",
|
|
59
58
|
"api_key": "API key for the LLM provider (use `.env` or env var `CODE2DOCS_LLM_API_KEY`)",
|
|
60
59
|
"api_base": "Custom API base URL (for self-hosted or proxy endpoints)",
|
|
61
60
|
"max_tokens": "Maximum tokens per LLM call",
|