code2docs 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
code2docs/cli.py ADDED
@@ -0,0 +1,226 @@
1
+ """CLI interface for code2docs."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ import click
8
+
9
+ from .config import Code2DocsConfig
10
+
11
+
12
+ @click.group(invoke_without_command=True)
13
+ @click.argument("project_path", default=".", type=click.Path(exists=True))
14
+ @click.option("--config", "-c", "config_path", default=None, help="Path to code2docs.yaml")
15
+ @click.option("--readme-only", is_flag=True, help="Generate only README.md")
16
+ @click.option("--sections", "-s", default=None, help="Comma-separated sections to generate")
17
+ @click.option("--output", "-o", default=None, help="Output directory for docs")
18
+ @click.option("--verbose", "-v", is_flag=True, help="Verbose output")
19
+ @click.option("--dry-run", is_flag=True, help="Show what would be generated without writing")
20
+ @click.pass_context
21
+ def main(ctx, project_path, config_path, readme_only, sections, output, verbose, dry_run):
22
+ """code2docs — Auto-generate project documentation from source code.
23
+
24
+ Analyzes PROJECT_PATH using code2llm and generates human-readable documentation.
25
+ """
26
+ if ctx.invoked_subcommand is not None:
27
+ ctx.ensure_object(dict)
28
+ ctx.obj["project_path"] = project_path
29
+ ctx.obj["config_path"] = config_path
30
+ ctx.obj["verbose"] = verbose
31
+ ctx.obj["dry_run"] = dry_run
32
+ return
33
+
34
+ # Default action: full generation
35
+ config = _load_config(project_path, config_path)
36
+ if verbose:
37
+ config.verbose = True
38
+ if output:
39
+ config.output = output
40
+ if sections:
41
+ config.readme.sections = [s.strip() for s in sections.split(",")]
42
+
43
+ _run_generate(project_path, config, readme_only=readme_only, dry_run=dry_run)
44
+
45
+
46
+ @main.command()
47
+ @click.argument("project_path", default=".", type=click.Path(exists=True))
48
+ @click.option("--config", "-c", "config_path", default=None, help="Path to code2docs.yaml")
49
+ @click.option("--verbose", "-v", is_flag=True, help="Verbose output")
50
+ @click.option("--dry-run", is_flag=True, help="Show what would change without writing")
51
+ def sync(project_path, config_path, verbose, dry_run):
52
+ """Synchronize documentation with source code changes."""
53
+ config = _load_config(project_path, config_path)
54
+ if verbose:
55
+ config.verbose = True
56
+
57
+ _run_sync(project_path, config, dry_run=dry_run)
58
+
59
+
60
+ @main.command()
61
+ @click.argument("project_path", default=".", type=click.Path(exists=True))
62
+ @click.option("--config", "-c", "config_path", default=None, help="Path to code2docs.yaml")
63
+ @click.option("--verbose", "-v", is_flag=True, help="Verbose output")
64
+ def watch(project_path, config_path, verbose):
65
+ """Watch for file changes and auto-regenerate docs."""
66
+ config = _load_config(project_path, config_path)
67
+ if verbose:
68
+ config.verbose = True
69
+
70
+ _run_watch(project_path, config)
71
+
72
+
73
+ @main.command()
74
+ @click.argument("project_path", default=".", type=click.Path(exists=True))
75
+ @click.option("--output", "-o", default="code2docs.yaml", help="Output config file path")
76
+ def init(project_path, output):
77
+ """Initialize code2docs.yaml configuration file."""
78
+ project_path = Path(project_path).resolve()
79
+ config = Code2DocsConfig(
80
+ project_name=project_path.name,
81
+ source="./",
82
+ )
83
+ out_path = project_path / output
84
+ config.to_yaml(str(out_path))
85
+ click.echo(f"Created {out_path}")
86
+
87
+
88
+ def _load_config(project_path: str, config_path: Optional[str] = None) -> Code2DocsConfig:
89
+ """Load configuration, auto-detecting code2docs.yaml if present."""
90
+ project = Path(project_path).resolve()
91
+
92
+ if config_path:
93
+ return Code2DocsConfig.from_yaml(config_path)
94
+
95
+ # Auto-detect
96
+ for name in ("code2docs.yaml", "code2docs.yml"):
97
+ candidate = project / name
98
+ if candidate.exists():
99
+ return Code2DocsConfig.from_yaml(str(candidate))
100
+
101
+ # Defaults
102
+ config = Code2DocsConfig(project_name=project.name, source="./")
103
+ return config
104
+
105
+
106
+ def _run_generate(project_path: str, config: Code2DocsConfig,
107
+ readme_only: bool = False, dry_run: bool = False):
108
+ """Run full documentation generation."""
109
+ from .analyzers.project_scanner import ProjectScanner
110
+ from .generators.readme_gen import ReadmeGenerator
111
+ from .generators.api_reference_gen import ApiReferenceGenerator
112
+ from .generators.module_docs_gen import ModuleDocsGenerator
113
+ from .generators.examples_gen import ExamplesGenerator
114
+ from .generators.architecture_gen import ArchitectureGenerator
115
+
116
+ project = Path(project_path).resolve()
117
+ click.echo(f"📖 code2docs: analyzing {project.name}...")
118
+
119
+ # Step 1: Analyze
120
+ scanner = ProjectScanner(config)
121
+ result = scanner.analyze(str(project))
122
+
123
+ if config.verbose:
124
+ click.echo(f" Functions: {len(result.functions)}")
125
+ click.echo(f" Classes: {len(result.classes)}")
126
+ click.echo(f" Modules: {len(result.modules)}")
127
+
128
+ # Step 2: Generate README
129
+ readme_gen = ReadmeGenerator(config, result)
130
+ readme_content = readme_gen.generate()
131
+
132
+ if dry_run:
133
+ click.echo(f"\n--- README.md ({len(readme_content)} chars) ---")
134
+ click.echo(readme_content[:500] + "..." if len(readme_content) > 500 else readme_content)
135
+ else:
136
+ readme_path = project / config.readme_output
137
+ readme_gen.write(str(readme_path), readme_content)
138
+ click.echo(f" ✅ {readme_path.relative_to(project)}")
139
+
140
+ if readme_only:
141
+ return
142
+
143
+ # Step 3: Generate docs/
144
+ docs_dir = project / config.output
145
+ docs_dir.mkdir(parents=True, exist_ok=True)
146
+
147
+ if config.docs.api_reference:
148
+ api_gen = ApiReferenceGenerator(config, result)
149
+ files = api_gen.generate_all()
150
+ if not dry_run:
151
+ api_gen.write_all(str(docs_dir / "api"), files)
152
+ click.echo(f" ✅ docs/api/ ({len(files)} files)")
153
+ else:
154
+ click.echo(f" [dry-run] docs/api/ ({len(files)} files)")
155
+
156
+ if config.docs.module_docs:
157
+ mod_gen = ModuleDocsGenerator(config, result)
158
+ files = mod_gen.generate_all()
159
+ if not dry_run:
160
+ mod_gen.write_all(str(docs_dir / "modules"), files)
161
+ click.echo(f" ✅ docs/modules/ ({len(files)} files)")
162
+ else:
163
+ click.echo(f" [dry-run] docs/modules/ ({len(files)} files)")
164
+
165
+ if config.docs.architecture:
166
+ arch_gen = ArchitectureGenerator(config, result)
167
+ content = arch_gen.generate()
168
+ if not dry_run:
169
+ arch_path = docs_dir / "architecture.md"
170
+ arch_path.write_text(content, encoding="utf-8")
171
+ click.echo(f" ✅ docs/architecture.md")
172
+ else:
173
+ click.echo(f" [dry-run] docs/architecture.md")
174
+
175
+ # Step 4: Generate examples/
176
+ if config.examples.auto_generate:
177
+ ex_gen = ExamplesGenerator(config, result)
178
+ files = ex_gen.generate_all()
179
+ if not dry_run:
180
+ examples_dir = project / "examples"
181
+ ex_gen.write_all(str(examples_dir), files)
182
+ click.echo(f" ✅ examples/ ({len(files)} files)")
183
+ else:
184
+ click.echo(f" [dry-run] examples/ ({len(files)} files)")
185
+
186
+ click.echo("📖 Done!")
187
+
188
+
189
+ def _run_sync(project_path: str, config: Code2DocsConfig, dry_run: bool = False):
190
+ """Run sync — regenerate only changed documentation."""
191
+ from .sync.differ import Differ
192
+ from .sync.updater import Updater
193
+
194
+ project = Path(project_path).resolve()
195
+ click.echo(f"🔄 code2docs sync: {project.name}...")
196
+
197
+ differ = Differ(config)
198
+ changes = differ.detect_changes(str(project))
199
+
200
+ if not changes:
201
+ click.echo(" No changes detected.")
202
+ return
203
+
204
+ click.echo(f" Changes detected in {len(changes)} modules")
205
+
206
+ if dry_run:
207
+ for change in changes:
208
+ click.echo(f" - {change}")
209
+ return
210
+
211
+ updater = Updater(config)
212
+ updater.apply(str(project), changes)
213
+ click.echo("🔄 Sync complete!")
214
+
215
+
216
+ def _run_watch(project_path: str, config: Code2DocsConfig):
217
+ """Run file watcher for auto-resync."""
218
+ try:
219
+ from .sync.watcher import start_watcher
220
+ except ImportError:
221
+ click.echo("Error: watchdog not installed. Install with: pip install code2docs[watch]")
222
+ sys.exit(1)
223
+
224
+ project = Path(project_path).resolve()
225
+ click.echo(f"👁 Watching {project.name} for changes... (Ctrl+C to stop)")
226
+ start_watcher(str(project), config)
code2docs/config.py ADDED
@@ -0,0 +1,158 @@
1
+ """Configuration for code2docs documentation generation."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+ from typing import Dict, List, Optional
6
+
7
+ import yaml
8
+
9
+
10
+ @dataclass
11
+ class ReadmeConfig:
12
+ """Configuration for README generation."""
13
+ sections: List[str] = field(default_factory=lambda: [
14
+ "overview", "install", "quickstart", "api", "structure", "endpoints",
15
+ ])
16
+ badges: List[str] = field(default_factory=lambda: [
17
+ "version", "python", "coverage", "complexity",
18
+ ])
19
+ sync_markers: bool = True
20
+
21
+
22
+ @dataclass
23
+ class DocsConfig:
24
+ """Configuration for docs/ generation."""
25
+ api_reference: bool = True
26
+ module_docs: bool = True
27
+ architecture: bool = True
28
+ changelog: bool = True
29
+
30
+
31
+ @dataclass
32
+ class ExamplesConfig:
33
+ """Configuration for examples/ generation."""
34
+ auto_generate: bool = True
35
+ from_entry_points: bool = True
36
+
37
+
38
+ @dataclass
39
+ class SyncConfig:
40
+ """Configuration for synchronization."""
41
+ strategy: str = "markers" # markers | full | git-diff
42
+ watch: bool = False
43
+ ignore: List[str] = field(default_factory=lambda: ["tests/", "__pycache__"])
44
+
45
+
46
+ @dataclass
47
+ class Code2DocsConfig:
48
+ """Main configuration for code2docs."""
49
+ project_name: str = ""
50
+ source: str = "./"
51
+ output: str = "./docs/"
52
+ readme_output: str = "./README.md"
53
+
54
+ readme: ReadmeConfig = field(default_factory=ReadmeConfig)
55
+ docs: DocsConfig = field(default_factory=DocsConfig)
56
+ examples: ExamplesConfig = field(default_factory=ExamplesConfig)
57
+ sync: SyncConfig = field(default_factory=SyncConfig)
58
+
59
+ # code2llm analysis options
60
+ verbose: bool = False
61
+ exclude_tests: bool = True
62
+ skip_private: bool = False
63
+
64
+ @classmethod
65
+ def from_yaml(cls, path: str) -> "Code2DocsConfig":
66
+ """Load configuration from code2docs.yaml."""
67
+ config_path = Path(path)
68
+ if not config_path.exists():
69
+ return cls()
70
+
71
+ with open(config_path, "r", encoding="utf-8") as f:
72
+ data = yaml.safe_load(f) or {}
73
+
74
+ config = cls()
75
+
76
+ # Project-level settings
77
+ project = data.get("project", {})
78
+ config.project_name = project.get("name", "")
79
+ config.source = project.get("source", "./")
80
+ config.output = project.get("output", "./docs/")
81
+ config.readme_output = project.get("readme_output", "./README.md")
82
+ config.verbose = project.get("verbose", False)
83
+ config.exclude_tests = project.get("exclude_tests", True)
84
+ config.skip_private = project.get("skip_private", False)
85
+
86
+ # Readme config
87
+ readme_data = data.get("readme", {})
88
+ if readme_data:
89
+ config.readme = ReadmeConfig(
90
+ sections=readme_data.get("sections", config.readme.sections),
91
+ badges=readme_data.get("badges", config.readme.badges),
92
+ sync_markers=readme_data.get("sync_markers", True),
93
+ )
94
+
95
+ # Docs config
96
+ docs_data = data.get("docs", {})
97
+ if docs_data:
98
+ config.docs = DocsConfig(
99
+ api_reference=docs_data.get("api_reference", True),
100
+ module_docs=docs_data.get("module_docs", True),
101
+ architecture=docs_data.get("architecture", True),
102
+ changelog=docs_data.get("changelog", True),
103
+ )
104
+
105
+ # Examples config
106
+ examples_data = data.get("examples", {})
107
+ if examples_data:
108
+ config.examples = ExamplesConfig(
109
+ auto_generate=examples_data.get("auto_generate", True),
110
+ from_entry_points=examples_data.get("from_entry_points", True),
111
+ )
112
+
113
+ # Sync config
114
+ sync_data = data.get("sync", {})
115
+ if sync_data:
116
+ config.sync = SyncConfig(
117
+ strategy=sync_data.get("strategy", "markers"),
118
+ watch=sync_data.get("watch", False),
119
+ ignore=sync_data.get("ignore", ["tests/", "__pycache__"]),
120
+ )
121
+
122
+ return config
123
+
124
+ def to_yaml(self, path: str) -> None:
125
+ """Save configuration to YAML file."""
126
+ data = {
127
+ "project": {
128
+ "name": self.project_name,
129
+ "source": self.source,
130
+ "output": self.output,
131
+ "readme_output": self.readme_output,
132
+ "verbose": self.verbose,
133
+ "exclude_tests": self.exclude_tests,
134
+ "skip_private": self.skip_private,
135
+ },
136
+ "readme": {
137
+ "sections": self.readme.sections,
138
+ "badges": self.readme.badges,
139
+ "sync_markers": self.readme.sync_markers,
140
+ },
141
+ "docs": {
142
+ "api_reference": self.docs.api_reference,
143
+ "module_docs": self.docs.module_docs,
144
+ "architecture": self.docs.architecture,
145
+ "changelog": self.docs.changelog,
146
+ },
147
+ "examples": {
148
+ "auto_generate": self.examples.auto_generate,
149
+ "from_entry_points": self.examples.from_entry_points,
150
+ },
151
+ "sync": {
152
+ "strategy": self.sync.strategy,
153
+ "watch": self.sync.watch,
154
+ "ignore": self.sync.ignore,
155
+ },
156
+ }
157
+ with open(path, "w", encoding="utf-8") as f:
158
+ yaml.dump(data, f, default_flow_style=False, sort_keys=False)
@@ -0,0 +1,7 @@
1
+ """Formatters — Markdown rendering, badges, TOC generation."""
2
+
3
+ from .markdown import MarkdownFormatter
4
+ from .badges import generate_badges
5
+ from .toc import generate_toc
6
+
7
+ __all__ = ["MarkdownFormatter", "generate_badges", "generate_toc"]
@@ -0,0 +1,52 @@
1
+ """Badge generation using shields.io URLs."""
2
+
3
+ from typing import Dict, List, Optional
4
+ from urllib.parse import quote
5
+
6
+
7
+ def generate_badges(project_name: str, badge_types: List[str],
8
+ stats: Dict = None, deps=None) -> str:
9
+ """Generate shields.io badge Markdown strings."""
10
+ stats = stats or {}
11
+ badges: List[str] = []
12
+
13
+ for badge_type in badge_types:
14
+ badge = _make_badge(badge_type, project_name, stats, deps)
15
+ if badge:
16
+ badges.append(badge)
17
+
18
+ return " ".join(badges)
19
+
20
+
21
+ def _make_badge(badge_type: str, project_name: str,
22
+ stats: Dict, deps) -> Optional[str]:
23
+ """Create a single badge Markdown string."""
24
+ name = quote(project_name)
25
+
26
+ if badge_type == "version":
27
+ return f"![version](https://img.shields.io/badge/version-0.1.0-blue)"
28
+
29
+ elif badge_type == "python":
30
+ py_version = ""
31
+ if deps and hasattr(deps, "python_version"):
32
+ py_version = deps.python_version
33
+ py_version = py_version or ">=3.9"
34
+ py_safe = quote(py_version)
35
+ return f"![python](https://img.shields.io/badge/python-{py_safe}-blue)"
36
+
37
+ elif badge_type == "coverage":
38
+ return f"![coverage](https://img.shields.io/badge/coverage-unknown-lightgrey)"
39
+
40
+ elif badge_type == "complexity":
41
+ funcs = stats.get("functions_found", 0)
42
+ if funcs:
43
+ return f"![functions](https://img.shields.io/badge/functions-{funcs}-green)"
44
+ return None
45
+
46
+ elif badge_type == "license":
47
+ return f"![license](https://img.shields.io/badge/license-Apache%202.0-green)"
48
+
49
+ elif badge_type == "docs":
50
+ return f"![docs](https://img.shields.io/badge/docs-auto--generated-blueviolet)"
51
+
52
+ return None
@@ -0,0 +1,73 @@
1
+ """Markdown formatting utilities."""
2
+
3
+ from typing import Dict, List, Optional
4
+
5
+
6
+ class MarkdownFormatter:
7
+ """Helper for constructing Markdown documents."""
8
+
9
+ def __init__(self):
10
+ self.lines: List[str] = []
11
+
12
+ def heading(self, text: str, level: int = 1) -> "MarkdownFormatter":
13
+ """Add a heading."""
14
+ self.lines.append(f"{'#' * level} {text}\n")
15
+ return self
16
+
17
+ def paragraph(self, text: str) -> "MarkdownFormatter":
18
+ """Add a paragraph."""
19
+ self.lines.append(f"{text}\n")
20
+ return self
21
+
22
+ def blockquote(self, text: str) -> "MarkdownFormatter":
23
+ """Add a blockquote."""
24
+ self.lines.append(f"> {text}\n")
25
+ return self
26
+
27
+ def code_block(self, code: str, language: str = "") -> "MarkdownFormatter":
28
+ """Add a fenced code block."""
29
+ self.lines.append(f"```{language}")
30
+ self.lines.append(code)
31
+ self.lines.append("```\n")
32
+ return self
33
+
34
+ def inline_code(self, text: str) -> str:
35
+ """Return inline code string."""
36
+ return f"`{text}`"
37
+
38
+ def bold(self, text: str) -> str:
39
+ """Return bold string."""
40
+ return f"**{text}**"
41
+
42
+ def link(self, text: str, url: str) -> str:
43
+ """Return a Markdown link."""
44
+ return f"[{text}]({url})"
45
+
46
+ def list_item(self, text: str, indent: int = 0) -> "MarkdownFormatter":
47
+ """Add a list item."""
48
+ prefix = " " * indent
49
+ self.lines.append(f"{prefix}- {text}")
50
+ return self
51
+
52
+ def table(self, headers: List[str], rows: List[List[str]]) -> "MarkdownFormatter":
53
+ """Add a Markdown table."""
54
+ self.lines.append("| " + " | ".join(headers) + " |")
55
+ self.lines.append("| " + " | ".join("---" for _ in headers) + " |")
56
+ for row in rows:
57
+ self.lines.append("| " + " | ".join(str(c) for c in row) + " |")
58
+ self.lines.append("")
59
+ return self
60
+
61
+ def separator(self) -> "MarkdownFormatter":
62
+ """Add a horizontal rule."""
63
+ self.lines.append("---\n")
64
+ return self
65
+
66
+ def blank(self) -> "MarkdownFormatter":
67
+ """Add a blank line."""
68
+ self.lines.append("")
69
+ return self
70
+
71
+ def render(self) -> str:
72
+ """Render accumulated Markdown to string."""
73
+ return "\n".join(self.lines)
@@ -0,0 +1,63 @@
1
+ """Table of contents generator from Markdown headings."""
2
+
3
+ import re
4
+ from typing import List, Tuple
5
+
6
+
7
+ def generate_toc(markdown_content: str, max_depth: int = 3) -> str:
8
+ """Generate a table of contents from Markdown headings.
9
+
10
+ Args:
11
+ markdown_content: Full Markdown document.
12
+ max_depth: Maximum heading level to include (1-6).
13
+
14
+ Returns:
15
+ TOC as Markdown string.
16
+ """
17
+ headings = extract_headings(markdown_content, max_depth)
18
+ if not headings:
19
+ return ""
20
+
21
+ lines = ["## Table of Contents\n"]
22
+ for level, text, anchor in headings:
23
+ indent = " " * (level - 1)
24
+ lines.append(f"{indent}- [{text}](#{anchor})")
25
+
26
+ lines.append("")
27
+ return "\n".join(lines)
28
+
29
+
30
+ def extract_headings(content: str, max_depth: int = 3) -> List[Tuple[int, str, str]]:
31
+ """Extract headings from Markdown content.
32
+
33
+ Returns list of (level, text, anchor) tuples.
34
+ """
35
+ headings: List[Tuple[int, str, str]] = []
36
+ in_code_block = False
37
+
38
+ for line in content.splitlines():
39
+ # Skip code blocks
40
+ if line.strip().startswith("```"):
41
+ in_code_block = not in_code_block
42
+ continue
43
+ if in_code_block:
44
+ continue
45
+
46
+ match = re.match(r'^(#{1,6})\s+(.+)$', line)
47
+ if match:
48
+ level = len(match.group(1))
49
+ if level <= max_depth:
50
+ text = match.group(2).strip()
51
+ anchor = _slugify(text)
52
+ headings.append((level, text, anchor))
53
+
54
+ return headings
55
+
56
+
57
+ def _slugify(text: str) -> str:
58
+ """Convert heading text to GitHub-compatible anchor slug."""
59
+ slug = text.lower()
60
+ slug = re.sub(r'[^\w\s-]', '', slug)
61
+ slug = re.sub(r'[\s]+', '-', slug)
62
+ slug = slug.strip('-')
63
+ return slug
@@ -0,0 +1,42 @@
1
+ """Documentation generators — produce Markdown, examples, and diagrams."""
2
+
3
+ from .readme_gen import ReadmeGenerator
4
+ from .api_reference_gen import ApiReferenceGenerator
5
+ from .module_docs_gen import ModuleDocsGenerator
6
+ from .examples_gen import ExamplesGenerator
7
+ from .architecture_gen import ArchitectureGenerator
8
+ from .changelog_gen import ChangelogGenerator
9
+
10
+ __all__ = [
11
+ "ReadmeGenerator",
12
+ "ApiReferenceGenerator",
13
+ "ModuleDocsGenerator",
14
+ "ExamplesGenerator",
15
+ "ArchitectureGenerator",
16
+ "ChangelogGenerator",
17
+ "generate_docs",
18
+ ]
19
+
20
+
21
+ def generate_docs(project_path: str, config=None):
22
+ """High-level function to generate all documentation."""
23
+ from ..analyzers.project_scanner import ProjectScanner
24
+ from ..config import Code2DocsConfig
25
+
26
+ config = config or Code2DocsConfig()
27
+ scanner = ProjectScanner(config)
28
+ result = scanner.analyze(project_path)
29
+
30
+ docs = {}
31
+ docs["readme"] = ReadmeGenerator(config, result).generate()
32
+
33
+ if config.docs.api_reference:
34
+ docs["api"] = ApiReferenceGenerator(config, result).generate_all()
35
+
36
+ if config.docs.module_docs:
37
+ docs["modules"] = ModuleDocsGenerator(config, result).generate_all()
38
+
39
+ if config.docs.architecture:
40
+ docs["architecture"] = ArchitectureGenerator(config, result).generate()
41
+
42
+ return docs