code2docs 3.0.27__py3-none-any.whl → 3.0.29__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/generators/_registry_adapters.py +37 -25
- code2docs/generators/api_reference_gen.py +59 -41
- code2docs/generators/code2llm_gen.py +98 -56
- code2docs/generators/contributing_gen.py +43 -28
- code2docs/generators/examples_gen.py +116 -100
- code2docs/generators/getting_started_gen.py +63 -41
- code2docs/generators/module_docs_gen.py +124 -76
- code2docs/generators/org_readme_gen.py +51 -26
- code2docs/generators/readme_gen.py +42 -53
- {code2docs-3.0.27.dist-info → code2docs-3.0.29.dist-info}/METADATA +8 -272
- {code2docs-3.0.27.dist-info → code2docs-3.0.29.dist-info}/RECORD +16 -16
- {code2docs-3.0.27.dist-info → code2docs-3.0.29.dist-info}/WHEEL +0 -0
- {code2docs-3.0.27.dist-info → code2docs-3.0.29.dist-info}/entry_points.txt +0 -0
- {code2docs-3.0.27.dist-info → code2docs-3.0.29.dist-info}/licenses/LICENSE +0 -0
- {code2docs-3.0.27.dist-info → code2docs-3.0.29.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.29'
|
|
9
9
|
__author__ = 'Tom Sapletta'
|
|
10
10
|
__all__ = ['Code2DocsConfig', 'generate_readme', 'generate_docs', 'analyze_and_document']
|
|
11
11
|
|
|
@@ -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]
|
|
@@ -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."""
|
|
@@ -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}")
|
|
@@ -186,36 +186,51 @@ class ContributingGenerator:
|
|
|
186
186
|
"""Render code style guidelines."""
|
|
187
187
|
lang = tools.get("language", "python")
|
|
188
188
|
lines = ["## Code Style\n"]
|
|
189
|
+
lines.extend(ContributingGenerator._get_style_lines(lang, tools))
|
|
190
|
+
return "\n".join(lines)
|
|
189
191
|
|
|
192
|
+
@staticmethod
|
|
193
|
+
def _get_style_lines(lang: str, tools: Dict[str, bool]) -> list:
|
|
194
|
+
"""Get style guideline lines for the language."""
|
|
190
195
|
if lang in ("javascript", "typescript"):
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
lines.append("- **
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
196
|
+
return ContributingGenerator._get_jsts_style_lines(tools)
|
|
197
|
+
if lang == "rust":
|
|
198
|
+
return ["- **Formatting:** `cargo fmt`", "- **Linting:** `cargo clippy`"]
|
|
199
|
+
if lang == "go":
|
|
200
|
+
return ["- **Formatting:** `gofmt -w .`", "- **Linting:** `go vet ./...`"]
|
|
201
|
+
return ContributingGenerator._get_python_style_lines(tools)
|
|
202
|
+
|
|
203
|
+
@staticmethod
|
|
204
|
+
def _get_jsts_style_lines(tools: Dict[str, bool]) -> list:
|
|
205
|
+
"""Get JavaScript/TypeScript style lines."""
|
|
206
|
+
lines = []
|
|
207
|
+
if tools.get("eslint"):
|
|
208
|
+
lines.append("- **Linting:** [ESLint](https://eslint.org/) — `npx eslint .`")
|
|
209
|
+
if tools.get("prettier"):
|
|
210
|
+
lines.append("- **Formatting:** [Prettier](https://prettier.io/) — `npx prettier --write .`")
|
|
211
|
+
if tools.get("typescript"):
|
|
212
|
+
lines.append("- **Type checking:** TypeScript — `npx tsc --noEmit`")
|
|
213
|
+
if not lines:
|
|
214
|
+
lines.append("Follow the project's ESLint/Prettier configuration.")
|
|
215
|
+
return lines
|
|
216
|
+
|
|
217
|
+
@staticmethod
|
|
218
|
+
def _get_python_style_lines(tools: Dict[str, bool]) -> list:
|
|
219
|
+
"""Get Python style lines."""
|
|
220
|
+
lines = []
|
|
221
|
+
if tools.get("black"):
|
|
222
|
+
lines.append("- **Formatting:** [Black](https://black.readthedocs.io/) — `black .`")
|
|
223
|
+
if tools.get("ruff"):
|
|
224
|
+
lines.append("- **Linting:** [Ruff](https://docs.astral.sh/ruff/) — `ruff check .`")
|
|
225
|
+
if tools.get("mypy"):
|
|
226
|
+
lines.append("- **Type checking:** [mypy](https://mypy.readthedocs.io/) — `mypy .`")
|
|
227
|
+
if tools.get("flake8"):
|
|
228
|
+
lines.append("- **Linting:** [flake8](https://flake8.pycqa.org/) — `flake8 .`")
|
|
229
|
+
if tools.get("isort"):
|
|
230
|
+
lines.append("- **Imports:** [isort](https://pycqa.github.io/isort/) — `isort .`")
|
|
231
|
+
if not lines:
|
|
232
|
+
lines.append("Follow PEP 8 conventions.")
|
|
233
|
+
return lines
|
|
219
234
|
|
|
220
235
|
@staticmethod
|
|
221
236
|
def _render_pull_request() -> str:
|