code2docs 3.0.4__tar.gz → 3.0.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. {code2docs-3.0.4 → code2docs-3.0.6}/PKG-INFO +3 -3
  2. {code2docs-3.0.4 → code2docs-3.0.6}/README.md +2 -2
  3. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/__init__.py +1 -1
  4. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/cli.py +4 -1
  5. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/config.py +1 -0
  6. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/generators/_registry_adapters.py +30 -0
  7. code2docs-3.0.6/code2docs/generators/org_readme_gen.py +227 -0
  8. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs.egg-info/PKG-INFO +3 -3
  9. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs.egg-info/SOURCES.txt +1 -0
  10. {code2docs-3.0.4 → code2docs-3.0.6}/pyproject.toml +1 -1
  11. code2docs-3.0.6/tests/test_cli.py +287 -0
  12. code2docs-3.0.4/tests/test_cli.py +0 -90
  13. {code2docs-3.0.4 → code2docs-3.0.6}/LICENSE +0 -0
  14. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/__main__.py +0 -0
  15. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/analyzers/__init__.py +0 -0
  16. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/analyzers/dependency_scanner.py +0 -0
  17. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/analyzers/docstring_extractor.py +0 -0
  18. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/analyzers/endpoint_detector.py +0 -0
  19. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/analyzers/project_scanner.py +0 -0
  20. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/base.py +0 -0
  21. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/examples/advanced_usage.py +0 -0
  22. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/examples/quickstart.py +0 -0
  23. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/formatters/__init__.py +0 -0
  24. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/formatters/badges.py +0 -0
  25. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/formatters/markdown.py +0 -0
  26. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/formatters/toc.py +0 -0
  27. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/generators/__init__.py +0 -0
  28. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/generators/_source_links.py +0 -0
  29. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/generators/api_changelog_gen.py +0 -0
  30. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/generators/api_reference_gen.py +0 -0
  31. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/generators/architecture_gen.py +0 -0
  32. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/generators/changelog_gen.py +0 -0
  33. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/generators/code2llm_gen.py +0 -0
  34. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/generators/config_docs_gen.py +0 -0
  35. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/generators/contributing_gen.py +0 -0
  36. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/generators/coverage_gen.py +0 -0
  37. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/generators/depgraph_gen.py +0 -0
  38. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/generators/examples_gen.py +0 -0
  39. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/generators/getting_started_gen.py +0 -0
  40. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/generators/mkdocs_gen.py +0 -0
  41. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/generators/module_docs_gen.py +0 -0
  42. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/generators/readme_gen.py +0 -0
  43. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/llm_helper.py +0 -0
  44. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/registry.py +0 -0
  45. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/sync/__init__.py +0 -0
  46. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/sync/differ.py +0 -0
  47. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/sync/updater.py +0 -0
  48. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/sync/watcher.py +0 -0
  49. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/templates/api_module.md.j2 +0 -0
  50. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/templates/architecture.md.j2 +0 -0
  51. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/templates/example_usage.py.j2 +0 -0
  52. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/templates/index.md.j2 +0 -0
  53. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/templates/module_doc.md.j2 +0 -0
  54. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs/templates/readme.md.j2 +0 -0
  55. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs.egg-info/dependency_links.txt +0 -0
  56. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs.egg-info/entry_points.txt +0 -0
  57. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs.egg-info/requires.txt +0 -0
  58. {code2docs-3.0.4 → code2docs-3.0.6}/code2docs.egg-info/top_level.txt +0 -0
  59. {code2docs-3.0.4 → code2docs-3.0.6}/setup.cfg +0 -0
  60. {code2docs-3.0.4 → code2docs-3.0.6}/tests/test_analyzers.py +0 -0
  61. {code2docs-3.0.4 → code2docs-3.0.6}/tests/test_code2docs.py +0 -0
  62. {code2docs-3.0.4 → code2docs-3.0.6}/tests/test_config.py +0 -0
  63. {code2docs-3.0.4 → code2docs-3.0.6}/tests/test_formatters.py +0 -0
  64. {code2docs-3.0.4 → code2docs-3.0.6}/tests/test_generators.py +0 -0
  65. {code2docs-3.0.4 → code2docs-3.0.6}/tests/test_llm_helper.py +0 -0
  66. {code2docs-3.0.4 → code2docs-3.0.6}/tests/test_registry.py +0 -0
  67. {code2docs-3.0.4 → code2docs-3.0.6}/tests/test_sync.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code2docs
3
- Version: 3.0.4
3
+ Version: 3.0.6
4
4
  Summary: Auto-generate and sync project documentation from source code analysis
5
5
  Author-email: Tom Sapletta <tom@sapletta.com>
6
6
  License-Expression: Apache-2.0
@@ -50,7 +50,7 @@ Dynamic: license-file
50
50
 
51
51
  # code2docs
52
52
 
53
- ![version](https://img.shields.io/badge/version-3.0.4-blue) ![python](https://img.shields.io/badge/python-%3E%3D3.9-blue) ![docs](https://img.shields.io/badge/docs-auto--generated-blueviolet)
53
+ ![version](https://img.shields.io/badge/version-3.0.6-blue) ![python](https://img.shields.io/badge/python-%3E%3D3.9-blue) ![docs](https://img.shields.io/badge/docs-auto--generated-blueviolet)
54
54
 
55
55
  > Auto-generate and sync project documentation from source code analysis.
56
56
 
@@ -190,7 +190,7 @@ code2docs can update only specific sections of an existing README using markers:
190
190
  ```markdown
191
191
  <!-- code2docs:start --># code2docs
192
192
 
193
- ![version](https://img.shields.io/badge/version-3.0.4-blue) ![python](https://img.shields.io/badge/python-%3E%3D3.9-blue) ![coverage](https://img.shields.io/badge/coverage-unknown-lightgrey) ![functions](https://img.shields.io/badge/functions-276-green)
193
+ ![version](https://img.shields.io/badge/version-3.0.6-blue) ![python](https://img.shields.io/badge/python-%3E%3D3.9-blue) ![coverage](https://img.shields.io/badge/coverage-unknown-lightgrey) ![functions](https://img.shields.io/badge/functions-276-green)
194
194
  > **276** functions | **57** classes | **51** files | CC̄ = 3.8
195
195
 
196
196
  > Auto-generated project documentation from source code analysis.
@@ -1,6 +1,6 @@
1
1
  # code2docs
2
2
 
3
- ![version](https://img.shields.io/badge/version-3.0.4-blue) ![python](https://img.shields.io/badge/python-%3E%3D3.9-blue) ![docs](https://img.shields.io/badge/docs-auto--generated-blueviolet)
3
+ ![version](https://img.shields.io/badge/version-3.0.6-blue) ![python](https://img.shields.io/badge/python-%3E%3D3.9-blue) ![docs](https://img.shields.io/badge/docs-auto--generated-blueviolet)
4
4
 
5
5
  > Auto-generate and sync project documentation from source code analysis.
6
6
 
@@ -140,7 +140,7 @@ code2docs can update only specific sections of an existing README using markers:
140
140
  ```markdown
141
141
  <!-- code2docs:start --># code2docs
142
142
 
143
- ![version](https://img.shields.io/badge/version-3.0.4-blue) ![python](https://img.shields.io/badge/python-%3E%3D3.9-blue) ![coverage](https://img.shields.io/badge/coverage-unknown-lightgrey) ![functions](https://img.shields.io/badge/functions-276-green)
143
+ ![version](https://img.shields.io/badge/version-3.0.6-blue) ![python](https://img.shields.io/badge/python-%3E%3D3.9-blue) ![coverage](https://img.shields.io/badge/coverage-unknown-lightgrey) ![functions](https://img.shields.io/badge/functions-276-green)
144
144
  > **276** functions | **57** classes | **51** files | CC̄ = 3.8
145
145
 
146
146
  > Auto-generated project documentation from source code analysis.
@@ -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.4"
8
+ __version__ = "3.0.6"
9
9
  __author__ = "Tom Sapletta"
10
10
 
11
11
  from .config import Code2DocsConfig
@@ -40,8 +40,9 @@ def main():
40
40
  @click.option("--dry-run", is_flag=True, help="Show what would be generated without writing")
41
41
  @click.option("--llm", "llm_model", default=None,
42
42
  help="Enable LLM-assisted generation (e.g. openai/gpt-4o-mini, ollama/llama3)")
43
+ @click.option("--org-name", default=None, help="Organization name for org-mode README generation")
43
44
  def generate(project_path, config_path, readme_only, sections, output, verbose, dry_run,
44
- llm_model):
45
+ llm_model, org_name):
45
46
  """Generate documentation (default command)."""
46
47
  config = _load_config(project_path, config_path)
47
48
  if verbose:
@@ -53,6 +54,8 @@ def generate(project_path, config_path, readme_only, sections, output, verbose,
53
54
  if llm_model:
54
55
  config.llm.enabled = True
55
56
  config.llm.model = llm_model
57
+ if org_name:
58
+ config.org_name = org_name
56
59
 
57
60
  _run_generate(project_path, config, readme_only=readme_only, dry_run=dry_run)
58
61
 
@@ -108,6 +108,7 @@ class Code2DocsConfig:
108
108
  output: str = "./docs/"
109
109
  readme_output: str = "./README.md"
110
110
  repo_url: str = "" # GitHub/GitLab URL for source links (auto-detected from git)
111
+ org_name: str = "" # Organization name for org-mode README generation
111
112
 
112
113
  readme: ReadmeConfig = field(default_factory=ReadmeConfig)
113
114
  docs: DocsConfig = field(default_factory=DocsConfig)
@@ -240,6 +240,35 @@ class Code2LlmAdapter(BaseGenerator):
240
240
  return "⚠️ project/ (no files generated)"
241
241
 
242
242
 
243
+ class OrgReadmeAdapter(BaseGenerator):
244
+ """Adapter for organization README generation."""
245
+ name = "org_readme"
246
+
247
+ def should_run(self, *, readme_only: bool = False) -> bool:
248
+ # Only run if org_name is set in config
249
+ return hasattr(self.config, 'org_name') and bool(self.config.org_name)
250
+
251
+ def run(self, ctx: GenerateContext) -> Optional[str]:
252
+ from .org_readme_gen import OrgReadmeGenerator
253
+
254
+ org_name = getattr(self.config, 'org_name', '')
255
+ if not org_name:
256
+ return None
257
+
258
+ gen = OrgReadmeGenerator(self.config, str(ctx.project), org_name)
259
+ content = gen.generate()
260
+
261
+ if ctx.dry_run:
262
+ click.echo(f"\n--- {org_name} README ({len(content)} chars) ---")
263
+ preview = content[:500] + "..." if len(content) > 500 else content
264
+ click.echo(preview)
265
+ return None
266
+
267
+ readme_path = ctx.docs_dir / "README.md"
268
+ gen.write(str(readme_path), content)
269
+ return f"✅ {readme_path.relative_to(ctx.project)}"
270
+
271
+
243
272
  ALL_ADAPTERS = [
244
273
  ReadmeGeneratorAdapter,
245
274
  ApiReferenceAdapter,
@@ -254,4 +283,5 @@ ALL_ADAPTERS = [
254
283
  ContributingAdapter,
255
284
  MkDocsAdapter,
256
285
  Code2LlmAdapter,
286
+ OrgReadmeAdapter,
257
287
  ]
@@ -0,0 +1,227 @@
1
+ """Organization README generator - generates overview of multiple projects."""
2
+
3
+ from pathlib import Path
4
+ from typing import Dict, List, Optional
5
+
6
+ from code2llm.api import AnalysisResult
7
+
8
+ from ..config import Code2DocsConfig
9
+ from ..analyzers.project_scanner import ProjectScanner
10
+
11
+
12
+ class OrgReadmeGenerator:
13
+ """Generate organization README with list of projects and brief descriptions."""
14
+
15
+ def __init__(self, config: Code2DocsConfig, org_path: str, org_name: str = ""):
16
+ self.config = config
17
+ self.org_path = Path(org_path).resolve()
18
+ self.org_name = org_name or self.org_path.name
19
+ self.scanner = ProjectScanner(config)
20
+
21
+ def generate(self) -> str:
22
+ """Generate organization README content."""
23
+ projects = self._discover_projects()
24
+
25
+ lines = [
26
+ f"# {self.org_name}\n",
27
+ f"Projects in the {self.org_name} organization.\n",
28
+ f"**{len(projects)}** projects discovered.\n",
29
+ "## Projects\n",
30
+ ]
31
+
32
+ for project_name, project_info in sorted(projects.items()):
33
+ lines.append(self._render_project_section(project_name, project_info))
34
+ lines.append("")
35
+
36
+ return "\n".join(lines)
37
+
38
+ def _discover_projects(self) -> Dict[str, Dict]:
39
+ """Discover all projects in organization directory."""
40
+ projects = {}
41
+
42
+ for item in self.org_path.iterdir():
43
+ if not item.is_dir():
44
+ continue
45
+ if item.name.startswith(".") or item.name.startswith("__"):
46
+ continue
47
+
48
+ project_info = self._analyze_project(item)
49
+ if project_info:
50
+ projects[item.name] = project_info
51
+
52
+ return projects
53
+
54
+ def _analyze_project(self, project_path: Path) -> Optional[Dict]:
55
+ """Analyze a single project and return summary info."""
56
+ try:
57
+ result = self.scanner.analyze(str(project_path))
58
+
59
+ # Extract description from first module docstring or pyproject.toml
60
+ description = self._extract_description(project_path, result)
61
+
62
+ # Count functions, classes, modules
63
+ func_count = len(result.functions)
64
+ class_count = len(result.classes)
65
+ module_count = len(result.modules)
66
+
67
+ # Get version from pyproject.toml if available
68
+ version = self._get_version(project_path)
69
+
70
+ # Get repo URL from git or config
71
+ repo_url = self._get_repo_url(project_path)
72
+
73
+ return {
74
+ "name": project_path.name,
75
+ "description": description,
76
+ "version": version,
77
+ "stats": {
78
+ "functions": func_count,
79
+ "classes": class_count,
80
+ "modules": module_count,
81
+ },
82
+ "repo_url": repo_url,
83
+ "path": str(project_path),
84
+ }
85
+ except Exception:
86
+ return None
87
+
88
+ def _extract_description(self, project_path: Path, result: AnalysisResult) -> str:
89
+ """Extract short description from project (max 5 lines)."""
90
+ # Try pyproject.toml first
91
+ try:
92
+ import tomllib
93
+ pyproject = project_path / "pyproject.toml"
94
+ if pyproject.exists():
95
+ with open(pyproject, "rb") as f:
96
+ data = tomllib.load(f)
97
+ desc = data.get("project", {}).get("description", "")
98
+ if desc:
99
+ # Limit to ~5 lines worth of content
100
+ return self._truncate_description(desc)
101
+ except Exception:
102
+ pass
103
+
104
+ # Try first package docstring
105
+ for mod in result.modules.values():
106
+ if mod.is_package and hasattr(mod, "docstring") and mod.docstring:
107
+ return self._truncate_description(mod.docstring)
108
+
109
+ # Try README.md first paragraph
110
+ readme = project_path / "README.md"
111
+ if readme.exists():
112
+ try:
113
+ content = readme.read_text(encoding="utf-8")
114
+ # Find first paragraph after title
115
+ lines = content.split("\n")
116
+ for i, line in enumerate(lines):
117
+ if line.startswith("# "):
118
+ # Get next non-empty lines
119
+ desc_lines = []
120
+ for j in range(i + 1, min(i + 10, len(lines))):
121
+ if lines[j].strip() and not lines[j].startswith("#"):
122
+ desc_lines.append(lines[j].strip())
123
+ if len(desc_lines) >= 5:
124
+ break
125
+ if desc_lines:
126
+ return " ".join(desc_lines)
127
+ except Exception:
128
+ pass
129
+
130
+ return "No description available."
131
+
132
+ def _truncate_description(self, desc: str, max_chars: int = 300) -> str:
133
+ """Truncate description to ~5 lines of content."""
134
+ lines = desc.strip().split("\n")
135
+ # Filter out empty lines and headers
136
+ content_lines = [l.strip() for l in lines if l.strip() and not l.startswith("#")]
137
+
138
+ result = []
139
+ char_count = 0
140
+ for line in content_lines[:5]:
141
+ if char_count + len(line) > max_chars:
142
+ remaining = max_chars - char_count
143
+ if remaining > 20:
144
+ result.append(line[:remaining] + "...")
145
+ break
146
+ result.append(line)
147
+ char_count += len(line)
148
+
149
+ return " ".join(result) if result else "No description available."
150
+
151
+ def _get_version(self, project_path: Path) -> str:
152
+ """Get version from pyproject.toml or VERSION file."""
153
+ try:
154
+ import tomllib
155
+ pyproject = project_path / "pyproject.toml"
156
+ if pyproject.exists():
157
+ with open(pyproject, "rb") as f:
158
+ data = tomllib.load(f)
159
+ return data.get("project", {}).get("version", "")
160
+ except Exception:
161
+ pass
162
+
163
+ version_file = project_path / "VERSION"
164
+ if version_file.exists():
165
+ return version_file.read_text(encoding="utf-8").strip()
166
+
167
+ return ""
168
+
169
+ def _get_repo_url(self, project_path: Path) -> str:
170
+ """Get repository URL from git or pyproject.toml."""
171
+ # Try pyproject.toml
172
+ try:
173
+ import tomllib
174
+ pyproject = project_path / "pyproject.toml"
175
+ if pyproject.exists():
176
+ with open(pyproject, "rb") as f:
177
+ data = tomllib.load(f)
178
+ urls = data.get("project", {}).get("urls", {})
179
+ if urls:
180
+ return urls.get("Repository", urls.get("Homepage", ""))
181
+ except Exception:
182
+ pass
183
+
184
+ # Try git remote
185
+ try:
186
+ import subprocess
187
+ result = subprocess.run(
188
+ ["git", "remote", "get-url", "origin"],
189
+ cwd=str(project_path),
190
+ capture_output=True, text=True, timeout=5,
191
+ )
192
+ if result.returncode == 0:
193
+ url = result.stdout.strip()
194
+ # Convert SSH to HTTPS
195
+ if url.startswith("git@"):
196
+ url = url.replace(":", "/", 1).replace("git@", "https://", 1)
197
+ return url.removesuffix(".git")
198
+ except Exception:
199
+ pass
200
+
201
+ return ""
202
+
203
+ def _render_project_section(self, name: str, info: Dict) -> str:
204
+ """Render a single project section (5 lines max)."""
205
+ lines = [f"### {name}"]
206
+
207
+ # Line 1: Description
208
+ lines.append(info["description"])
209
+
210
+ # Line 2: Stats
211
+ stats = info["stats"]
212
+ stats_line = f"📊 {stats['functions']} functions | {stats['classes']} classes | {stats['modules']} modules"
213
+ if info["version"]:
214
+ stats_line += f" | v{info['version']}"
215
+ lines.append(stats_line)
216
+
217
+ # Line 3: Repo link if available
218
+ if info["repo_url"]:
219
+ lines.append(f"🔗 [{info['repo_url']}]({info['repo_url']})")
220
+
221
+ return "\n".join(lines)
222
+
223
+ def write(self, output_path: str, content: str) -> None:
224
+ """Write README to output path."""
225
+ out_path = Path(output_path)
226
+ out_path.parent.mkdir(parents=True, exist_ok=True)
227
+ out_path.write_text(content, encoding="utf-8")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code2docs
3
- Version: 3.0.4
3
+ Version: 3.0.6
4
4
  Summary: Auto-generate and sync project documentation from source code analysis
5
5
  Author-email: Tom Sapletta <tom@sapletta.com>
6
6
  License-Expression: Apache-2.0
@@ -50,7 +50,7 @@ Dynamic: license-file
50
50
 
51
51
  # code2docs
52
52
 
53
- ![version](https://img.shields.io/badge/version-3.0.4-blue) ![python](https://img.shields.io/badge/python-%3E%3D3.9-blue) ![docs](https://img.shields.io/badge/docs-auto--generated-blueviolet)
53
+ ![version](https://img.shields.io/badge/version-3.0.6-blue) ![python](https://img.shields.io/badge/python-%3E%3D3.9-blue) ![docs](https://img.shields.io/badge/docs-auto--generated-blueviolet)
54
54
 
55
55
  > Auto-generate and sync project documentation from source code analysis.
56
56
 
@@ -190,7 +190,7 @@ code2docs can update only specific sections of an existing README using markers:
190
190
  ```markdown
191
191
  <!-- code2docs:start --># code2docs
192
192
 
193
- ![version](https://img.shields.io/badge/version-3.0.4-blue) ![python](https://img.shields.io/badge/python-%3E%3D3.9-blue) ![coverage](https://img.shields.io/badge/coverage-unknown-lightgrey) ![functions](https://img.shields.io/badge/functions-276-green)
193
+ ![version](https://img.shields.io/badge/version-3.0.6-blue) ![python](https://img.shields.io/badge/python-%3E%3D3.9-blue) ![coverage](https://img.shields.io/badge/coverage-unknown-lightgrey) ![functions](https://img.shields.io/badge/functions-276-green)
194
194
  > **276** functions | **57** classes | **51** files | CC̄ = 3.8
195
195
 
196
196
  > Auto-generated project documentation from source code analysis.
@@ -41,6 +41,7 @@ code2docs/generators/examples_gen.py
41
41
  code2docs/generators/getting_started_gen.py
42
42
  code2docs/generators/mkdocs_gen.py
43
43
  code2docs/generators/module_docs_gen.py
44
+ code2docs/generators/org_readme_gen.py
44
45
  code2docs/generators/readme_gen.py
45
46
  code2docs/sync/__init__.py
46
47
  code2docs/sync/differ.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "code2docs"
7
- version = "3.0.4"
7
+ version = "3.0.6"
8
8
  description = "Auto-generate and sync project documentation from source code analysis"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -0,0 +1,287 @@
1
+ """CLI smoke tests using Click's CliRunner and subprocess e2e tests."""
2
+
3
+ import subprocess
4
+ import sys
5
+ import tempfile
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+ from click.testing import CliRunner
10
+
11
+ from code2docs.cli import main
12
+
13
+
14
+ class TestCLI:
15
+ """Unit tests using Click's CliRunner."""
16
+
17
+ def setup_method(self):
18
+ self.runner = CliRunner()
19
+
20
+ def test_help(self):
21
+ result = self.runner.invoke(main, ["--help"])
22
+ assert result.exit_code == 0
23
+ assert "code2docs" in result.output
24
+ assert "generate" in result.output
25
+ assert "sync" in result.output
26
+ assert "init" in result.output
27
+
28
+ def test_generate_help(self):
29
+ result = self.runner.invoke(main, ["generate", "--help"])
30
+ assert result.exit_code == 0
31
+ assert "--readme-only" in result.output
32
+ assert "--dry-run" in result.output
33
+ assert "--verbose" in result.output
34
+
35
+ def test_sync_help(self):
36
+ result = self.runner.invoke(main, ["sync", "--help"])
37
+ assert result.exit_code == 0
38
+ assert "Synchronize" in result.output
39
+
40
+ def test_init_creates_config(self):
41
+ with tempfile.TemporaryDirectory() as tmpdir:
42
+ result = self.runner.invoke(main, ["init", tmpdir])
43
+ assert result.exit_code == 0
44
+ assert (Path(tmpdir) / "code2docs.yaml").exists()
45
+
46
+ def test_generate_dry_run(self):
47
+ result = self.runner.invoke(main, [".", "--dry-run"])
48
+ assert result.exit_code == 0
49
+ assert "code2docs" in result.output
50
+ assert "README.md" in result.output
51
+ assert "dry-run" in result.output
52
+ assert "Done!" in result.output
53
+
54
+ def test_readme_only_dry_run(self):
55
+ result = self.runner.invoke(main, [".", "--readme-only", "--dry-run"])
56
+ assert result.exit_code == 0
57
+ assert "README.md" in result.output
58
+ # Should NOT have docs/api since --readme-only
59
+ assert "docs/api" not in result.output
60
+
61
+ def test_default_group_routes_path(self):
62
+ """Ensure bare path argument is routed to generate."""
63
+ result = self.runner.invoke(main, [".", "--dry-run"])
64
+ assert result.exit_code == 0
65
+ assert "Done!" in result.output
66
+
67
+ def test_verbose(self):
68
+ result = self.runner.invoke(main, [".", "--verbose", "--dry-run"])
69
+ assert result.exit_code == 0
70
+ assert "Functions" in result.output
71
+ assert "Classes" in result.output
72
+ assert "Modules" in result.output
73
+
74
+ def test_check_help(self):
75
+ result = self.runner.invoke(main, ["check", "--help"])
76
+ assert result.exit_code == 0
77
+ assert "Health check" in result.output
78
+ assert "--target" in result.output
79
+
80
+ def test_check_runs(self):
81
+ result = self.runner.invoke(main, ["check", "."])
82
+ assert result.exit_code == 0
83
+ assert "check" in result.output
84
+ assert "Score:" in result.output
85
+
86
+ def test_diff_help(self):
87
+ result = self.runner.invoke(main, ["diff", "--help"])
88
+ assert result.exit_code == 0
89
+ assert "Preview" in result.output
90
+
91
+ def test_diff_runs(self):
92
+ result = self.runner.invoke(main, ["diff", "."])
93
+ assert result.exit_code == 0
94
+ assert "diff" in result.output
95
+ assert "dry-run" in result.output
96
+
97
+
98
+ class TestCLIShellE2E:
99
+ """E2E tests using actual shell subprocess calls."""
100
+
101
+ @pytest.fixture
102
+ def temp_project(self):
103
+ """Create a temporary project with Python files."""
104
+ with tempfile.TemporaryDirectory() as tmpdir:
105
+ project_dir = Path(tmpdir) / "test_project"
106
+ project_dir.mkdir()
107
+
108
+ # Create a simple Python package
109
+ pkg_dir = project_dir / "my_pkg"
110
+ pkg_dir.mkdir()
111
+ (pkg_dir / "__init__.py").write_text('"""My package."""\n')
112
+ (pkg_dir / "module.py").write_text(
113
+ '"""A module."""\n\ndef hello():\n """Say hello."""\n return "Hello"\n'
114
+ )
115
+
116
+ # Create pyproject.toml
117
+ (project_dir / "pyproject.toml").write_text(
118
+ '[project]\nname = "test-project"\nversion = "0.1.0"\n'
119
+ )
120
+
121
+ yield project_dir
122
+
123
+ def _run_shell(self, cmd: list, cwd: Path = None) -> subprocess.CompletedProcess:
124
+ """Run command in shell and return result."""
125
+ return subprocess.run(
126
+ cmd,
127
+ cwd=str(cwd) if cwd else None,
128
+ capture_output=True,
129
+ text=True,
130
+ timeout=30,
131
+ )
132
+
133
+ def test_shell_generate(self, temp_project):
134
+ """E2E: Generate docs via shell."""
135
+ result = self._run_shell(
136
+ [sys.executable, "-m", "code2docs", str(temp_project), "--dry-run"],
137
+ )
138
+ assert result.returncode == 0
139
+ assert "code2docs" in result.stdout
140
+
141
+ def test_shell_generate_creates_files(self, temp_project):
142
+ """E2E: Generate actually creates output files."""
143
+ output_dir = temp_project / "docs_output"
144
+
145
+ result = self._run_shell(
146
+ [sys.executable, "-m", "code2docs", str(temp_project),
147
+ "--output", str(output_dir)],
148
+ cwd=temp_project,
149
+ )
150
+ assert result.returncode == 0, f"stderr: {result.stderr}"
151
+ assert "Done!" in result.stdout
152
+
153
+ # Check files were created
154
+ assert (temp_project / "README.md").exists()
155
+
156
+ def test_shell_init(self, temp_project):
157
+ """E2E: Init command creates config file."""
158
+ result = self._run_shell(
159
+ [sys.executable, "-m", "code2docs", "init", str(temp_project)],
160
+ )
161
+ assert result.returncode == 0
162
+ assert (temp_project / "code2docs.yaml").exists()
163
+
164
+ def test_shell_check(self, temp_project):
165
+ """E2E: Check command runs successfully."""
166
+ # First generate some docs
167
+ self._run_shell(
168
+ [sys.executable, "-m", "code2docs", str(temp_project)],
169
+ cwd=temp_project,
170
+ )
171
+
172
+ result = self._run_shell(
173
+ [sys.executable, "-m", "code2docs", "check", str(temp_project)],
174
+ )
175
+ assert result.returncode == 0
176
+ assert "Health" in result.stdout or "check" in result.stdout
177
+ assert "Score:" in result.stdout
178
+
179
+ def test_shell_diff(self, temp_project):
180
+ """E2E: Diff command runs successfully."""
181
+ result = self._run_shell(
182
+ [sys.executable, "-m", "code2docs", "diff", str(temp_project)],
183
+ )
184
+ assert result.returncode == 0
185
+ assert "diff" in result.stdout.lower()
186
+
187
+ def test_shell_help(self):
188
+ """E2E: Help command works."""
189
+ result = self._run_shell(
190
+ [sys.executable, "-m", "code2docs", "--help"],
191
+ )
192
+ assert result.returncode == 0
193
+ assert "code2docs" in result.stdout
194
+ assert "generate" in result.stdout
195
+
196
+ def test_shell_generate_with_sections(self, temp_project):
197
+ """E2E: Generate with --sections option."""
198
+ result = self._run_shell(
199
+ [sys.executable, "-m", "code2docs", str(temp_project),
200
+ "--sections", "overview,install", "--dry-run"],
201
+ cwd=temp_project,
202
+ )
203
+ assert result.returncode == 0
204
+
205
+ def test_shell_version(self):
206
+ """E2E: Check version output."""
207
+ result = self._run_shell(
208
+ [sys.executable, "-c", "import code2docs; print(code2docs.__version__)"],
209
+ )
210
+ assert result.returncode == 0
211
+ assert "." in result.stdout.strip()
212
+
213
+
214
+ class TestCLINewFeaturesE2E:
215
+ """E2E tests for new CLI features (--org-name)."""
216
+
217
+ @pytest.fixture
218
+ def temp_org(self):
219
+ """Create a temporary organization with multiple projects."""
220
+ with tempfile.TemporaryDirectory() as tmpdir:
221
+ org_dir = Path(tmpdir) / "test_org"
222
+ org_dir.mkdir()
223
+
224
+ # Create project 1
225
+ proj1 = org_dir / "project1"
226
+ proj1.mkdir()
227
+ (proj1 / "pyproject.toml").write_text(
228
+ '[project]\nname = "project1"\nversion = "0.1.0"\n'
229
+ 'description = "First project"\n'
230
+ )
231
+ pkg1 = proj1 / "pkg1"
232
+ pkg1.mkdir()
233
+ (pkg1 / "__init__.py").write_text('"""Package 1."""\n')
234
+
235
+ # Create project 2
236
+ proj2 = org_dir / "project2"
237
+ proj2.mkdir()
238
+ (proj2 / "pyproject.toml").write_text(
239
+ '[project]\nname = "project2"\nversion = "0.2.0"\n'
240
+ 'description = "Second project"\n'
241
+ )
242
+ pkg2 = proj2 / "pkg2"
243
+ pkg2.mkdir()
244
+ (pkg2 / "__init__.py").write_text('"""Package 2."""\n')
245
+
246
+ yield org_dir
247
+
248
+ def _run_shell(self, cmd: list, cwd: Path = None) -> subprocess.CompletedProcess:
249
+ """Run command in shell and return result."""
250
+ return subprocess.run(
251
+ cmd,
252
+ cwd=str(cwd) if cwd else None,
253
+ capture_output=True,
254
+ text=True,
255
+ timeout=30,
256
+ )
257
+
258
+ def test_shell_org_mode(self, temp_org):
259
+ """E2E: Generate organization README with --org-name."""
260
+ output_dir = temp_org / "articles"
261
+
262
+ result = self._run_shell(
263
+ [sys.executable, "-m", "code2docs", str(temp_org),
264
+ "--output", str(output_dir),
265
+ "--org-name", "TestOrg"],
266
+ cwd=temp_org,
267
+ )
268
+ assert result.returncode == 0, f"stderr: {result.stderr}"
269
+ assert "Done!" in result.stdout
270
+
271
+ # Check org README was created
272
+ org_readme = output_dir / "README.md"
273
+ assert org_readme.exists(), f"Files in {output_dir}: {list(output_dir.iterdir())}"
274
+
275
+ content = org_readme.read_text()
276
+ assert "TestOrg" in content
277
+ assert "project1" in content
278
+ assert "project2" in content
279
+
280
+ def test_shell_org_mode_dry_run(self, temp_org):
281
+ """E2E: Organization mode dry run."""
282
+ result = self._run_shell(
283
+ [sys.executable, "-m", "code2docs", str(temp_org),
284
+ "--org-name", "TestOrg", "--dry-run"],
285
+ )
286
+ assert result.returncode == 0
287
+ assert "TestOrg" in result.stdout or "dry-run" in result.stdout
@@ -1,90 +0,0 @@
1
- """CLI smoke tests using Click's CliRunner."""
2
-
3
- import tempfile
4
- from pathlib import Path
5
-
6
- from click.testing import CliRunner
7
-
8
- from code2docs.cli import main
9
-
10
-
11
- class TestCLI:
12
- def setup_method(self):
13
- self.runner = CliRunner()
14
-
15
- def test_help(self):
16
- result = self.runner.invoke(main, ["--help"])
17
- assert result.exit_code == 0
18
- assert "code2docs" in result.output
19
- assert "generate" in result.output
20
- assert "sync" in result.output
21
- assert "init" in result.output
22
-
23
- def test_generate_help(self):
24
- result = self.runner.invoke(main, ["generate", "--help"])
25
- assert result.exit_code == 0
26
- assert "--readme-only" in result.output
27
- assert "--dry-run" in result.output
28
- assert "--verbose" in result.output
29
-
30
- def test_sync_help(self):
31
- result = self.runner.invoke(main, ["sync", "--help"])
32
- assert result.exit_code == 0
33
- assert "Synchronize" in result.output
34
-
35
- def test_init_creates_config(self):
36
- with tempfile.TemporaryDirectory() as tmpdir:
37
- result = self.runner.invoke(main, ["init", tmpdir])
38
- assert result.exit_code == 0
39
- assert (Path(tmpdir) / "code2docs.yaml").exists()
40
-
41
- def test_generate_dry_run(self):
42
- result = self.runner.invoke(main, [".", "--dry-run"])
43
- assert result.exit_code == 0
44
- assert "code2docs" in result.output
45
- assert "README.md" in result.output
46
- assert "dry-run" in result.output
47
- assert "Done!" in result.output
48
-
49
- def test_readme_only_dry_run(self):
50
- result = self.runner.invoke(main, [".", "--readme-only", "--dry-run"])
51
- assert result.exit_code == 0
52
- assert "README.md" in result.output
53
- # Should NOT have docs/api since --readme-only
54
- assert "docs/api" not in result.output
55
-
56
- def test_default_group_routes_path(self):
57
- """Ensure bare path argument is routed to generate."""
58
- result = self.runner.invoke(main, [".", "--dry-run"])
59
- assert result.exit_code == 0
60
- assert "Done!" in result.output
61
-
62
- def test_verbose(self):
63
- result = self.runner.invoke(main, [".", "--verbose", "--dry-run"])
64
- assert result.exit_code == 0
65
- assert "Functions" in result.output
66
- assert "Classes" in result.output
67
- assert "Modules" in result.output
68
-
69
- def test_check_help(self):
70
- result = self.runner.invoke(main, ["check", "--help"])
71
- assert result.exit_code == 0
72
- assert "Health check" in result.output
73
- assert "--target" in result.output
74
-
75
- def test_check_runs(self):
76
- result = self.runner.invoke(main, ["check", "."])
77
- assert result.exit_code == 0
78
- assert "check" in result.output
79
- assert "Score:" in result.output
80
-
81
- def test_diff_help(self):
82
- result = self.runner.invoke(main, ["diff", "--help"])
83
- assert result.exit_code == 0
84
- assert "Preview" in result.output
85
-
86
- def test_diff_runs(self):
87
- result = self.runner.invoke(main, ["diff", "."])
88
- assert result.exit_code == 0
89
- assert "diff" in result.output
90
- assert "dry-run" in result.output
File without changes
File without changes
File without changes
File without changes