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 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.27'
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
- repo_url = self.config.repo_url
264
- files = []
265
- if (ctx.docs_dir / 'README.md').exists():
266
- files.append(('README.md', 'Project Overview', '📖'))
267
- if (ctx.docs_dir / 'getting-started.md').exists():
268
- files.append(('getting-started.md', 'Getting Started', '🚀'))
269
- if (ctx.docs_dir / 'api.md').exists():
270
- files.append(('api.md', 'API Reference', '📚'))
271
- if (ctx.docs_dir / 'modules.md').exists():
272
- files.append(('modules.md', 'Module Documentation', '📦'))
273
- if (ctx.docs_dir / 'architecture.md').exists():
274
- files.append(('architecture.md', 'Architecture', '🏗️'))
275
- if (ctx.docs_dir / 'dependency-graph.md').exists():
276
- files.append(('dependency-graph.md', 'Dependency Graph', '🔗'))
277
- if (ctx.docs_dir / 'coverage.md').exists():
278
- files.append(('coverage.md', 'Code Coverage', '📊'))
279
- if (ctx.docs_dir / 'api-changelog.md').exists():
280
- files.append(('api-changelog.md', 'API Changelog', '📝'))
281
- if (ctx.docs_dir / 'configuration.md').exists():
282
- files.append(('configuration.md', 'Configuration', '⚙️'))
283
- if (ctx.docs_dir / 'CONTRIBUTING.md').exists():
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
- github_link = f'<a href="{repo_url}" class="github-link" target="_blank" rel="noopener">View on GitHub</a>' if repo_url else ''
288
- files_html = '\n'.join((f'<a href="{href}" class="doc-card"><span class="icon">{icon}</span><span class="title">{title}</span></a>' for href, title, icon in files))
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
- # Classes table
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
- if module_classes:
98
- lines.append("| Class | Methods | Description | Source |")
99
- lines.append("|-------|---------|-------------|--------|")
100
- for cls_name, cls_info in sorted(module_classes.items()):
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
- return "\n".join(lines)
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
- for line in content.split("\n"):
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
- pass # Silently fail if can't read gitignore
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
- # Add options based on config
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
- # Add exclude patterns as skip-subprojects if specified
146
- if cfg.exclude_patterns:
147
- # Convert patterns to skip-subprojects (folders to skip)
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
- if gitignore_patterns:
155
- # Merge with existing patterns, remove duplicates
156
- existing = set(skip_dirs) if cfg.exclude_patterns else set()
157
- new_patterns = [p for p in gitignore_patterns if p not in existing][:10]
158
- if new_patterns:
159
- if "--skip-subprojects" not in cmd:
160
- cmd.append("--skip-subprojects")
161
- cmd.extend(new_patterns)
162
-
163
- # Run the command
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
- if tools.get("eslint"):
192
- lines.append("- **Linting:** [ESLint](https://eslint.org/) — `npx eslint .`")
193
- if tools.get("prettier"):
194
- lines.append("- **Formatting:** [Prettier](https://prettier.io/) — `npx prettier --write .`")
195
- if tools.get("typescript"):
196
- lines.append("- **Type checking:** TypeScript — `npx tsc --noEmit`")
197
- if not any(tools.get(t) for t in ("eslint", "prettier")):
198
- lines.append("Follow the project's ESLint/Prettier configuration.")
199
- elif lang == "rust":
200
- lines.append("- **Formatting:** `cargo fmt`")
201
- lines.append("- **Linting:** `cargo clippy`")
202
- elif lang == "go":
203
- lines.append("- **Formatting:** `gofmt -w .`")
204
- lines.append("- **Linting:** `go vet ./...`")
205
- else:
206
- if tools.get("black"):
207
- lines.append("- **Formatting:** [Black](https://black.readthedocs.io/) — `black .`")
208
- if tools.get("ruff"):
209
- lines.append("- **Linting:** [Ruff](https://docs.astral.sh/ruff/) — `ruff check .`")
210
- if tools.get("mypy"):
211
- lines.append("- **Type checking:** [mypy](https://mypy.readthedocs.io/) — `mypy .`")
212
- if tools.get("flake8"):
213
- lines.append("- **Linting:** [flake8](https://flake8.pycqa.org/) — `flake8 .`")
214
- if tools.get("isort"):
215
- lines.append("- **Imports:** [isort](https://pycqa.github.io/isort/) — `isort .`")
216
- if not any(tools.get(t) for t in ("black", "ruff", "mypy", "flake8", "isort")):
217
- lines.append("Follow PEP 8 conventions.")
218
- return "\n".join(lines)
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: