markdownizer 0.2.1__tar.gz → 0.3.0__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.
- {markdownizer-0.2.1 → markdownizer-0.3.0}/PKG-INFO +66 -5
- {markdownizer-0.2.1 → markdownizer-0.3.0}/README.md +65 -4
- {markdownizer-0.2.1 → markdownizer-0.3.0}/markdownizer/__init__.py +3 -2
- markdownizer-0.3.0/markdownizer/backends/__init__.py +26 -0
- markdownizer-0.3.0/markdownizer/backends/base.py +36 -0
- markdownizer-0.3.0/markdownizer/backends/compact.py +75 -0
- markdownizer-0.3.0/markdownizer/backends/json.py +19 -0
- markdownizer-0.3.0/markdownizer/backends/markdown.py +62 -0
- {markdownizer-0.2.1 → markdownizer-0.3.0}/markdownizer/classifier.py +3 -3
- {markdownizer-0.2.1 → markdownizer-0.3.0}/markdownizer/cli.py +63 -16
- markdownizer-0.3.0/markdownizer/extractor.py +88 -0
- markdownizer-0.3.0/markdownizer/imports.py +146 -0
- markdownizer-0.3.0/markdownizer/ir.py +426 -0
- {markdownizer-0.2.1 → markdownizer-0.3.0}/markdownizer/parser.py +66 -4
- {markdownizer-0.2.1 → markdownizer-0.3.0}/markdownizer/renderer.py +57 -19
- {markdownizer-0.2.1 → markdownizer-0.3.0}/markdownizer.egg-info/PKG-INFO +66 -5
- {markdownizer-0.2.1 → markdownizer-0.3.0}/markdownizer.egg-info/SOURCES.txt +10 -0
- markdownizer-0.3.0/tests/test_backends.py +241 -0
- markdownizer-0.3.0/tests/test_cli.py +161 -0
- markdownizer-0.3.0/tests/test_imports.py +146 -0
- markdownizer-0.3.0/tests/test_ir.py +247 -0
- {markdownizer-0.2.1 → markdownizer-0.3.0}/tests/test_packaging.py +11 -2
- markdownizer-0.2.1/markdownizer/extractor.py +0 -96
- markdownizer-0.2.1/tests/test_cli.py +0 -83
- {markdownizer-0.2.1 → markdownizer-0.3.0}/LICENSE +0 -0
- {markdownizer-0.2.1 → markdownizer-0.3.0}/markdownizer/__main__.py +0 -0
- {markdownizer-0.2.1 → markdownizer-0.3.0}/markdownizer/py.typed +0 -0
- {markdownizer-0.2.1 → markdownizer-0.3.0}/markdownizer/scanner.py +0 -0
- {markdownizer-0.2.1 → markdownizer-0.3.0}/markdownizer.egg-info/dependency_links.txt +0 -0
- {markdownizer-0.2.1 → markdownizer-0.3.0}/markdownizer.egg-info/entry_points.txt +0 -0
- {markdownizer-0.2.1 → markdownizer-0.3.0}/markdownizer.egg-info/requires.txt +0 -0
- {markdownizer-0.2.1 → markdownizer-0.3.0}/markdownizer.egg-info/top_level.txt +0 -0
- {markdownizer-0.2.1 → markdownizer-0.3.0}/pyproject.toml +0 -0
- {markdownizer-0.2.1 → markdownizer-0.3.0}/setup.cfg +0 -0
- {markdownizer-0.2.1 → markdownizer-0.3.0}/tests/test_classifier.py +0 -0
- {markdownizer-0.2.1 → markdownizer-0.3.0}/tests/test_extractor.py +0 -0
- {markdownizer-0.2.1 → markdownizer-0.3.0}/tests/test_parser.py +0 -0
- {markdownizer-0.2.1 → markdownizer-0.3.0}/tests/test_renderer.py +0 -0
- {markdownizer-0.2.1 → markdownizer-0.3.0}/tests/test_scanner.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: markdownizer
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Extract documentation from Python projects into Markdown without rewriting it
|
|
5
5
|
Author-email: Mohammad Hasan Khoddami <mohammadh.khoddami@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -89,8 +89,17 @@ Requires Python 3.9+. No runtime dependencies.
|
|
|
89
89
|
markdownizer /path/to/project -o ./docs
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
This recursively scans the project, parses every Python file with the AST,
|
|
93
|
-
writes one Markdown file per package
|
|
92
|
+
This recursively scans the project, parses every Python file with the AST,
|
|
93
|
+
builds a deterministic Project IR, and writes one Markdown file per package
|
|
94
|
+
into `./docs`.
|
|
95
|
+
|
|
96
|
+
The compiler-style form is equivalent and supports format selection:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
markdownizer build /path/to/project -o ./docs --format markdown
|
|
100
|
+
markdownizer build /path/to/project -o ./docs --format json
|
|
101
|
+
markdownizer build /path/to/project -o ./docs --format compact
|
|
102
|
+
```
|
|
94
103
|
|
|
95
104
|
Common options:
|
|
96
105
|
|
|
@@ -105,6 +114,7 @@ markdownizer . -o ./docs \
|
|
|
105
114
|
| `-o, --output DIR` | Output directory (default: `./docs`) |
|
|
106
115
|
| `--root-name NAME` | Filename for files at the project root (default: `_root`) |
|
|
107
116
|
| `--exclude GLOB` | Skip matching paths; may be repeated |
|
|
117
|
+
| `--format FMT` | Output backend: `markdown`, `json`, or `compact` |
|
|
108
118
|
| `--no-source` | Omit the `## Source Code` section |
|
|
109
119
|
| `--no-comments` | Omit the `## Comments` section |
|
|
110
120
|
| `--only-documented` | Only include objects with a docstring |
|
|
@@ -118,7 +128,7 @@ Run `markdownizer --help` for the full list.
|
|
|
118
128
|
|
|
119
129
|
```python
|
|
120
130
|
from pathlib import Path
|
|
121
|
-
from markdownizer import extract_project
|
|
131
|
+
from markdownizer import extract_project, build_project_ir
|
|
122
132
|
|
|
123
133
|
written = extract_project(
|
|
124
134
|
Path("."),
|
|
@@ -126,9 +136,60 @@ written = extract_project(
|
|
|
126
136
|
exclude=["tests/*"],
|
|
127
137
|
include_source=False,
|
|
128
138
|
)
|
|
129
|
-
print(written) # list of written
|
|
139
|
+
print(written) # list of written output files
|
|
140
|
+
|
|
141
|
+
# Or build the Project IR directly:
|
|
142
|
+
ir = build_project_ir(Path("."), exclude=["tests/*"])
|
|
143
|
+
print(ir.ir_version, ir.hash, ir.stats.symbol_count)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Output formats
|
|
147
|
+
|
|
148
|
+
The pipeline builds a deterministic **Project IR** (packages → modules →
|
|
149
|
+
symbols, plus import/inherit/define edges) and renders it through a backend:
|
|
150
|
+
|
|
151
|
+
| Format | Command | Output |
|
|
152
|
+
| --- | --- | --- |
|
|
153
|
+
| `markdown` (default) | `markdownizer build . -o ./docs` | One `.md` file per package |
|
|
154
|
+
| `json` | `markdownizer build . -o ./docs --format json` | `project.json` — full IR serialization |
|
|
155
|
+
| `compact` | `markdownizer build . -o ./docs --format compact` | `context.compact.md` — signatures, docstrings, inheritance, decorators (no bodies) |
|
|
156
|
+
|
|
157
|
+
The legacy invocation `markdownizer <project> -o <out>` is kept as a
|
|
158
|
+
compatibility alias for `markdownizer build <project> --format markdown`.
|
|
159
|
+
|
|
160
|
+
### Signature mode
|
|
161
|
+
|
|
162
|
+
Instead of full source or no source, `extract_project()` accepts
|
|
163
|
+
`include_source="signature"` to emit only declaration lines:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
extract_project(Path("."), Path("docs"), include_source="signature")
|
|
130
167
|
```
|
|
131
168
|
|
|
169
|
+
Functions render as `def foo(x: int = 1) -> str:`, async functions as
|
|
170
|
+
`async def ...`, classes as `class User(models.Model):` (with base classes),
|
|
171
|
+
and methods with their parameters. Modules render without source. The
|
|
172
|
+
boolean modes (`True`/`False`) are unchanged.
|
|
173
|
+
|
|
174
|
+
### Project IR
|
|
175
|
+
|
|
176
|
+
`build_project_ir(project_root, exclude=None)` returns a `ProjectIR` with:
|
|
177
|
+
|
|
178
|
+
- `ir_version` — schema version (currently `1`), independent of the package version
|
|
179
|
+
- `packages`, `modules`, `symbols` — the project hierarchy
|
|
180
|
+
- `imports`, `inherits`, `defines` — relationship edges
|
|
181
|
+
- `stats` — file/module/symbol counts
|
|
182
|
+
- `hash` — deterministic `blake2b` of the canonical IR content
|
|
183
|
+
|
|
184
|
+
The hash and JSON serialization are deterministic: the same repository
|
|
185
|
+
content always produces the same hash and the same `project.json`, making
|
|
186
|
+
the output suitable for version control and caching. Machine-specific
|
|
187
|
+
metadata (`root`, `python_version`, `git`) is excluded from the hash.
|
|
188
|
+
|
|
189
|
+
Import resolution is conservative and fully static: project code is never
|
|
190
|
+
imported or executed. Imports that cannot be resolved to a project module
|
|
191
|
+
are marked `external`.
|
|
192
|
+
|
|
132
193
|
## What is extracted
|
|
133
194
|
|
|
134
195
|
For every documented object (modules, packages, classes, dataclasses, enums,
|
|
@@ -33,8 +33,17 @@ Requires Python 3.9+. No runtime dependencies.
|
|
|
33
33
|
markdownizer /path/to/project -o ./docs
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
This recursively scans the project, parses every Python file with the AST,
|
|
37
|
-
writes one Markdown file per package
|
|
36
|
+
This recursively scans the project, parses every Python file with the AST,
|
|
37
|
+
builds a deterministic Project IR, and writes one Markdown file per package
|
|
38
|
+
into `./docs`.
|
|
39
|
+
|
|
40
|
+
The compiler-style form is equivalent and supports format selection:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
markdownizer build /path/to/project -o ./docs --format markdown
|
|
44
|
+
markdownizer build /path/to/project -o ./docs --format json
|
|
45
|
+
markdownizer build /path/to/project -o ./docs --format compact
|
|
46
|
+
```
|
|
38
47
|
|
|
39
48
|
Common options:
|
|
40
49
|
|
|
@@ -49,6 +58,7 @@ markdownizer . -o ./docs \
|
|
|
49
58
|
| `-o, --output DIR` | Output directory (default: `./docs`) |
|
|
50
59
|
| `--root-name NAME` | Filename for files at the project root (default: `_root`) |
|
|
51
60
|
| `--exclude GLOB` | Skip matching paths; may be repeated |
|
|
61
|
+
| `--format FMT` | Output backend: `markdown`, `json`, or `compact` |
|
|
52
62
|
| `--no-source` | Omit the `## Source Code` section |
|
|
53
63
|
| `--no-comments` | Omit the `## Comments` section |
|
|
54
64
|
| `--only-documented` | Only include objects with a docstring |
|
|
@@ -62,7 +72,7 @@ Run `markdownizer --help` for the full list.
|
|
|
62
72
|
|
|
63
73
|
```python
|
|
64
74
|
from pathlib import Path
|
|
65
|
-
from markdownizer import extract_project
|
|
75
|
+
from markdownizer import extract_project, build_project_ir
|
|
66
76
|
|
|
67
77
|
written = extract_project(
|
|
68
78
|
Path("."),
|
|
@@ -70,9 +80,60 @@ written = extract_project(
|
|
|
70
80
|
exclude=["tests/*"],
|
|
71
81
|
include_source=False,
|
|
72
82
|
)
|
|
73
|
-
print(written) # list of written
|
|
83
|
+
print(written) # list of written output files
|
|
84
|
+
|
|
85
|
+
# Or build the Project IR directly:
|
|
86
|
+
ir = build_project_ir(Path("."), exclude=["tests/*"])
|
|
87
|
+
print(ir.ir_version, ir.hash, ir.stats.symbol_count)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Output formats
|
|
91
|
+
|
|
92
|
+
The pipeline builds a deterministic **Project IR** (packages → modules →
|
|
93
|
+
symbols, plus import/inherit/define edges) and renders it through a backend:
|
|
94
|
+
|
|
95
|
+
| Format | Command | Output |
|
|
96
|
+
| --- | --- | --- |
|
|
97
|
+
| `markdown` (default) | `markdownizer build . -o ./docs` | One `.md` file per package |
|
|
98
|
+
| `json` | `markdownizer build . -o ./docs --format json` | `project.json` — full IR serialization |
|
|
99
|
+
| `compact` | `markdownizer build . -o ./docs --format compact` | `context.compact.md` — signatures, docstrings, inheritance, decorators (no bodies) |
|
|
100
|
+
|
|
101
|
+
The legacy invocation `markdownizer <project> -o <out>` is kept as a
|
|
102
|
+
compatibility alias for `markdownizer build <project> --format markdown`.
|
|
103
|
+
|
|
104
|
+
### Signature mode
|
|
105
|
+
|
|
106
|
+
Instead of full source or no source, `extract_project()` accepts
|
|
107
|
+
`include_source="signature"` to emit only declaration lines:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
extract_project(Path("."), Path("docs"), include_source="signature")
|
|
74
111
|
```
|
|
75
112
|
|
|
113
|
+
Functions render as `def foo(x: int = 1) -> str:`, async functions as
|
|
114
|
+
`async def ...`, classes as `class User(models.Model):` (with base classes),
|
|
115
|
+
and methods with their parameters. Modules render without source. The
|
|
116
|
+
boolean modes (`True`/`False`) are unchanged.
|
|
117
|
+
|
|
118
|
+
### Project IR
|
|
119
|
+
|
|
120
|
+
`build_project_ir(project_root, exclude=None)` returns a `ProjectIR` with:
|
|
121
|
+
|
|
122
|
+
- `ir_version` — schema version (currently `1`), independent of the package version
|
|
123
|
+
- `packages`, `modules`, `symbols` — the project hierarchy
|
|
124
|
+
- `imports`, `inherits`, `defines` — relationship edges
|
|
125
|
+
- `stats` — file/module/symbol counts
|
|
126
|
+
- `hash` — deterministic `blake2b` of the canonical IR content
|
|
127
|
+
|
|
128
|
+
The hash and JSON serialization are deterministic: the same repository
|
|
129
|
+
content always produces the same hash and the same `project.json`, making
|
|
130
|
+
the output suitable for version control and caching. Machine-specific
|
|
131
|
+
metadata (`root`, `python_version`, `git`) is excluded from the hash.
|
|
132
|
+
|
|
133
|
+
Import resolution is conservative and fully static: project code is never
|
|
134
|
+
imported or executed. Imports that cannot be resolved to a project module
|
|
135
|
+
are marked `external`.
|
|
136
|
+
|
|
76
137
|
## What is extracted
|
|
77
138
|
|
|
78
139
|
For every documented object (modules, packages, classes, dataclasses, enums,
|
|
@@ -5,6 +5,7 @@ It only extracts what already exists in the source code.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from markdownizer.extractor import extract_project
|
|
8
|
+
from markdownizer.ir import IR_VERSION, ProjectIR, build_project_ir
|
|
8
9
|
|
|
9
|
-
__version__ = "0.
|
|
10
|
-
__all__ = ["extract_project"]
|
|
10
|
+
__version__ = "0.3.0"
|
|
11
|
+
__all__ = ["extract_project", "build_project_ir", "IR_VERSION", "ProjectIR"]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Output backends for the Project IR."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from markdownizer.backends.base import Backend, RenderOptions
|
|
6
|
+
from markdownizer.backends.compact import CompactBackend
|
|
7
|
+
from markdownizer.backends.json import JSONBackend
|
|
8
|
+
from markdownizer.backends.markdown import MarkdownBackend
|
|
9
|
+
|
|
10
|
+
_BACKENDS: dict[str, type[Backend]] = {
|
|
11
|
+
"markdown": MarkdownBackend,
|
|
12
|
+
"json": JSONBackend,
|
|
13
|
+
"compact": CompactBackend,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_backend(name: str) -> Backend:
|
|
18
|
+
"""Instantiate a backend by name (``markdown``, ``json``, or ``compact``)."""
|
|
19
|
+
try:
|
|
20
|
+
return _BACKENDS[name]()
|
|
21
|
+
except KeyError:
|
|
22
|
+
choices = ", ".join(sorted(_BACKENDS))
|
|
23
|
+
raise ValueError(f"unknown format: {name!r} (expected one of: {choices})") from None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ = ["Backend", "RenderOptions", "get_backend"]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Backend abstraction: deterministic rendering of a ProjectIR into output files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Literal, Protocol, Union
|
|
7
|
+
|
|
8
|
+
from markdownizer.ir import ProjectIR
|
|
9
|
+
|
|
10
|
+
SourceMode = Union[bool, Literal["signature"]]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class RenderOptions:
|
|
15
|
+
"""Options controlling how a backend renders an IR.
|
|
16
|
+
|
|
17
|
+
``include_source`` may be ``True`` (full source), ``False`` (no source),
|
|
18
|
+
or ``"signature"`` (declaration line only).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
root_name: str = "_root"
|
|
22
|
+
include_source: SourceMode = True
|
|
23
|
+
include_comments: bool = True
|
|
24
|
+
include_undocumented: bool = True
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Backend(Protocol):
|
|
28
|
+
"""A backend renders a ProjectIR into ``{filename: content}`` pairs.
|
|
29
|
+
|
|
30
|
+
Implementations must be deterministic: the same IR always produces the
|
|
31
|
+
same output.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
name: str
|
|
35
|
+
|
|
36
|
+
def render(self, ir: ProjectIR, options: RenderOptions) -> dict[str, str]: ...
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Compact backend: a low-volume, structure-oriented representation.
|
|
2
|
+
|
|
3
|
+
Contains packages, modules, public symbols, signatures, inheritance,
|
|
4
|
+
decorators, and docstrings — without full source bodies. The output is
|
|
5
|
+
deterministic and intended as a high-signal overview for AI consumption.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from markdownizer.backends.base import RenderOptions
|
|
11
|
+
from markdownizer.ir import ModuleInfo, ProjectIR, Symbol
|
|
12
|
+
from markdownizer.renderer import format_signature
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _symbol_line(symbol: Symbol, options: RenderOptions) -> list[str]:
|
|
16
|
+
lines = [f"# {symbol.framework}: {symbol.qualified_name} — {symbol.file_path}:{symbol.lineno}"]
|
|
17
|
+
if options.include_source is not False:
|
|
18
|
+
signature = format_signature(symbol)
|
|
19
|
+
if signature:
|
|
20
|
+
lines.append(f"```{signature}```")
|
|
21
|
+
if symbol.base_classes:
|
|
22
|
+
lines.append(f"inherits: {', '.join(symbol.base_classes)}")
|
|
23
|
+
if symbol.decorators:
|
|
24
|
+
lines.append(f"decorators: {', '.join(symbol.decorators)}")
|
|
25
|
+
if symbol.docstring not in (None, ""):
|
|
26
|
+
lines.append("")
|
|
27
|
+
lines.append(symbol.docstring.rstrip())
|
|
28
|
+
lines.append("")
|
|
29
|
+
return lines
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CompactBackend:
|
|
33
|
+
"""Render a single structure-oriented ``context.compact.md`` file."""
|
|
34
|
+
|
|
35
|
+
name = "compact"
|
|
36
|
+
|
|
37
|
+
def render(self, ir: ProjectIR, options: RenderOptions) -> dict[str, str]:
|
|
38
|
+
lines = [
|
|
39
|
+
f"# Project: {ir.name} (IR v{ir.ir_version})",
|
|
40
|
+
"",
|
|
41
|
+
(
|
|
42
|
+
f"# Packages: {ir.stats.package_count} | "
|
|
43
|
+
f"Modules: {ir.stats.module_count} | "
|
|
44
|
+
f"Symbols: {ir.stats.symbol_count} | "
|
|
45
|
+
f"Lines: {ir.stats.line_count}"
|
|
46
|
+
),
|
|
47
|
+
"",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
modules_by_package: dict[str, list[ModuleInfo]] = {}
|
|
51
|
+
symbols_by_package: dict[str, list[Symbol]] = {}
|
|
52
|
+
for module in ir.modules:
|
|
53
|
+
modules_by_package.setdefault(module.package, []).append(module)
|
|
54
|
+
for symbol in ir.symbols:
|
|
55
|
+
package = symbol.file_path.rsplit("/", 1)[0] if "/" in symbol.file_path else ""
|
|
56
|
+
symbols_by_package.setdefault(package, []).append(symbol)
|
|
57
|
+
|
|
58
|
+
for package in sorted(set(modules_by_package) | set(symbols_by_package)):
|
|
59
|
+
lines.append(f"# Package: {package if package else options.root_name}")
|
|
60
|
+
lines.append("")
|
|
61
|
+
for module in modules_by_package.get(package, []):
|
|
62
|
+
lines.append(f"# Module: {module.path}")
|
|
63
|
+
if module.docstring not in (None, ""):
|
|
64
|
+
lines.append("")
|
|
65
|
+
lines.append(module.docstring.rstrip())
|
|
66
|
+
lines.append("")
|
|
67
|
+
symbols = symbols_by_package.get(package, [])
|
|
68
|
+
if not options.include_undocumented:
|
|
69
|
+
symbols = [s for s in symbols if s.docstring not in (None, "")]
|
|
70
|
+
for symbol in symbols:
|
|
71
|
+
if not symbol.is_public:
|
|
72
|
+
continue
|
|
73
|
+
lines.extend(_symbol_line(symbol, options))
|
|
74
|
+
|
|
75
|
+
return {"context.compact.md": "\n".join(lines).rstrip() + "\n"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""JSON backend: deterministic serialization of the Project IR."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from markdownizer.backends.base import RenderOptions
|
|
6
|
+
from markdownizer.ir import ProjectIR
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class JSONBackend:
|
|
10
|
+
"""Render the full ProjectIR as ``project.json``.
|
|
11
|
+
|
|
12
|
+
This backend serializes the complete IR regardless of ``RenderOptions``;
|
|
13
|
+
it is a faithful machine-readable representation of the analysis.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
name = "json"
|
|
17
|
+
|
|
18
|
+
def render(self, ir: ProjectIR, options: RenderOptions) -> dict[str, str]:
|
|
19
|
+
return {"project.json": ir.to_json() + "\n"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Markdown backend: one Markdown file per package (the historical output).
|
|
2
|
+
|
|
3
|
+
The rendering logic lives in :mod:`markdownizer.renderer`; this backend
|
|
4
|
+
groups the IR by package and drives it.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from markdownizer.backends.base import RenderOptions
|
|
10
|
+
from markdownizer.ir import ModuleInfo, ProjectIR, Symbol
|
|
11
|
+
from markdownizer.parser import DocObject
|
|
12
|
+
from markdownizer.renderer import render_package_markdown
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _module_docobject(module: ModuleInfo) -> DocObject:
|
|
16
|
+
"""Adapt an IR module back into a DocObject for the renderer."""
|
|
17
|
+
return DocObject(
|
|
18
|
+
name=module.path,
|
|
19
|
+
qualified_name=module.path,
|
|
20
|
+
kind="module",
|
|
21
|
+
lineno=1,
|
|
22
|
+
end_lineno=module.line_count,
|
|
23
|
+
anchor_lineno=1,
|
|
24
|
+
docstring=module.docstring,
|
|
25
|
+
source=module.source,
|
|
26
|
+
inline_comments=list(module.inline_comments),
|
|
27
|
+
file_path=module.path,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MarkdownBackend:
|
|
32
|
+
"""Render one ``{package}.md`` file per package."""
|
|
33
|
+
|
|
34
|
+
name = "markdown"
|
|
35
|
+
|
|
36
|
+
def render(self, ir: ProjectIR, options: RenderOptions) -> dict[str, str]:
|
|
37
|
+
modules_by_package: dict[str, list[ModuleInfo]] = {}
|
|
38
|
+
symbols_by_package: dict[str, list[Symbol]] = {}
|
|
39
|
+
|
|
40
|
+
for module in ir.modules:
|
|
41
|
+
modules_by_package.setdefault(module.package, []).append(module)
|
|
42
|
+
for symbol in ir.symbols:
|
|
43
|
+
package = symbol.file_path.rsplit("/", 1)[0] if "/" in symbol.file_path else ""
|
|
44
|
+
symbols_by_package.setdefault(package, []).append(symbol)
|
|
45
|
+
|
|
46
|
+
out: dict[str, str] = {}
|
|
47
|
+
for package in sorted(set(modules_by_package) | set(symbols_by_package)):
|
|
48
|
+
label = package.replace("/", ".") if package else options.root_name
|
|
49
|
+
modules = modules_by_package.get(package, [])
|
|
50
|
+
symbols = symbols_by_package.get(package, [])
|
|
51
|
+
if not options.include_undocumented:
|
|
52
|
+
symbols = [s for s in symbols if s.docstring not in (None, "")]
|
|
53
|
+
markdown = render_package_markdown(
|
|
54
|
+
label,
|
|
55
|
+
[_module_docobject(m) for m in modules],
|
|
56
|
+
symbols, # Symbols duck-type DocObject for the renderer
|
|
57
|
+
None,
|
|
58
|
+
include_source=options.include_source,
|
|
59
|
+
include_comments=options.include_comments,
|
|
60
|
+
)
|
|
61
|
+
out[f"{label}.md"] = markdown
|
|
62
|
+
return out
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
"""Classify a
|
|
1
|
+
"""Classify a renderable object into a specialized documentation header kind."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from markdownizer.parser import
|
|
5
|
+
from markdownizer.parser import Renderable
|
|
6
6
|
|
|
7
7
|
_DJANGO_MODEL_BASES = {"Model", "AbstractUser", "AbstractBaseUser", "PermissionsMixin"}
|
|
8
8
|
|
|
@@ -69,7 +69,7 @@ def _decorator_base(dec: str) -> str:
|
|
|
69
69
|
return _rightmost(head)
|
|
70
70
|
|
|
71
71
|
|
|
72
|
-
def classify(obj:
|
|
72
|
+
def classify(obj: Renderable) -> str:
|
|
73
73
|
"""Return a human-readable header label like 'DRF Serializer' or 'Async Function'."""
|
|
74
74
|
if obj.kind == "module":
|
|
75
75
|
if obj.file_path.endswith("__init__.py"):
|
|
@@ -13,22 +13,13 @@ from markdownizer.extractor import extract_project
|
|
|
13
13
|
logger = logging.getLogger(__name__)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
def
|
|
17
|
-
parser = argparse.ArgumentParser(
|
|
18
|
-
prog="markdownizer",
|
|
19
|
-
description="Extract existing documentation from a Python project into Markdown files.",
|
|
20
|
-
)
|
|
21
|
-
parser.add_argument(
|
|
22
|
-
"project",
|
|
23
|
-
type=Path,
|
|
24
|
-
help="Path to the Python project root to scan.",
|
|
25
|
-
)
|
|
16
|
+
def _add_common_arguments(parser: argparse.ArgumentParser) -> None:
|
|
26
17
|
parser.add_argument(
|
|
27
18
|
"-o",
|
|
28
19
|
"--output",
|
|
29
20
|
type=Path,
|
|
30
21
|
default=Path("docs"),
|
|
31
|
-
help="Directory where generated
|
|
22
|
+
help="Directory where generated files will be written (default: ./docs).",
|
|
32
23
|
)
|
|
33
24
|
parser.add_argument(
|
|
34
25
|
"--root-name",
|
|
@@ -59,6 +50,15 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
59
50
|
action="store_true",
|
|
60
51
|
help="Only include objects that have a docstring.",
|
|
61
52
|
)
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--format",
|
|
55
|
+
choices=["markdown", "json", "compact"],
|
|
56
|
+
default="markdown",
|
|
57
|
+
help="Output backend (default: markdown).",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _add_verbose_arguments(parser: argparse.ArgumentParser) -> None:
|
|
62
62
|
parser.add_argument(
|
|
63
63
|
"-v",
|
|
64
64
|
"--verbose",
|
|
@@ -72,6 +72,25 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
72
72
|
action="store_true",
|
|
73
73
|
help="Suppress non-error output.",
|
|
74
74
|
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
78
|
+
"""Parser for the legacy invocation ``markdownizer <project> -o <out>``."""
|
|
79
|
+
parser = argparse.ArgumentParser(
|
|
80
|
+
prog="markdownizer",
|
|
81
|
+
description=(
|
|
82
|
+
"Extract existing documentation from a Python project into "
|
|
83
|
+
"Markdown files. Use 'markdownizer build <project>' for format "
|
|
84
|
+
"selection; the legacy form below is kept as a compatibility alias."
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
parser.add_argument(
|
|
88
|
+
"project",
|
|
89
|
+
type=Path,
|
|
90
|
+
help="Path to the Python project root to scan.",
|
|
91
|
+
)
|
|
92
|
+
_add_common_arguments(parser)
|
|
93
|
+
_add_verbose_arguments(parser)
|
|
75
94
|
parser.add_argument(
|
|
76
95
|
"--version",
|
|
77
96
|
action="version",
|
|
@@ -80,10 +99,26 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
80
99
|
return parser
|
|
81
100
|
|
|
82
101
|
|
|
83
|
-
def
|
|
84
|
-
|
|
85
|
-
|
|
102
|
+
def build_build_parser() -> argparse.ArgumentParser:
|
|
103
|
+
"""Parser for ``markdownizer build <project>``."""
|
|
104
|
+
parser = argparse.ArgumentParser(
|
|
105
|
+
prog="markdownizer build",
|
|
106
|
+
description=(
|
|
107
|
+
"Scan a project and emit a deterministic representation: "
|
|
108
|
+
"Markdown (default), JSON project IR, or compact context."
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
parser.add_argument(
|
|
112
|
+
"project",
|
|
113
|
+
type=Path,
|
|
114
|
+
help="Path to the Python project root to scan.",
|
|
115
|
+
)
|
|
116
|
+
_add_common_arguments(parser)
|
|
117
|
+
_add_verbose_arguments(parser)
|
|
118
|
+
return parser
|
|
86
119
|
|
|
120
|
+
|
|
121
|
+
def _run(args: argparse.Namespace) -> int:
|
|
87
122
|
if args.quiet:
|
|
88
123
|
level = logging.ERROR
|
|
89
124
|
elif args.verbose >= 2:
|
|
@@ -113,17 +148,29 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
113
148
|
include_source=not args.no_source,
|
|
114
149
|
include_comments=not args.no_comments,
|
|
115
150
|
include_undocumented=not args.only_documented,
|
|
151
|
+
format=args.format,
|
|
116
152
|
)
|
|
117
|
-
except OSError as exc:
|
|
153
|
+
except (OSError, ValueError) as exc:
|
|
118
154
|
print(f"error: {exc}", file=sys.stderr)
|
|
119
155
|
return 1
|
|
120
156
|
|
|
121
157
|
if not args.quiet:
|
|
122
|
-
print(f"Wrote {len(written)}
|
|
158
|
+
print(f"Wrote {len(written)} file(s) to {output}")
|
|
123
159
|
for path in written:
|
|
124
160
|
print(f" - {path}")
|
|
125
161
|
return 0
|
|
126
162
|
|
|
127
163
|
|
|
164
|
+
def main(argv: list[str] | None = None) -> int:
|
|
165
|
+
args_list = list(sys.argv[1:] if argv is None else argv)
|
|
166
|
+
if args_list and args_list[0] == "build":
|
|
167
|
+
parser = build_build_parser()
|
|
168
|
+
args = parser.parse_args(args_list[1:])
|
|
169
|
+
else:
|
|
170
|
+
parser = build_parser()
|
|
171
|
+
args = parser.parse_args(args_list)
|
|
172
|
+
return _run(args)
|
|
173
|
+
|
|
174
|
+
|
|
128
175
|
if __name__ == "__main__":
|
|
129
176
|
sys.exit(main())
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Orchestrates scanning, parsing, IR building, and backend output writing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from markdownizer.backends import RenderOptions, get_backend
|
|
9
|
+
from markdownizer.ir import build_project_ir
|
|
10
|
+
from markdownizer.renderer import SourceMode
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _package_key(file_path: Path, project_root: Path) -> str:
|
|
16
|
+
try:
|
|
17
|
+
rel = file_path.resolve().relative_to(project_root.resolve())
|
|
18
|
+
except ValueError:
|
|
19
|
+
rel = Path(file_path)
|
|
20
|
+
parent = rel.parent
|
|
21
|
+
if parent == Path("."):
|
|
22
|
+
return ""
|
|
23
|
+
return str(parent)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _package_label(key: str, root_name: str) -> str:
|
|
27
|
+
if key == "":
|
|
28
|
+
return root_name
|
|
29
|
+
return key.replace("/", ".").replace("\\", ".")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def extract_project(
|
|
33
|
+
project_root: Path,
|
|
34
|
+
output_dir: Path,
|
|
35
|
+
root_name: str = "_root",
|
|
36
|
+
exclude: list[str] | None = None,
|
|
37
|
+
include_source: SourceMode = True,
|
|
38
|
+
include_comments: bool = True,
|
|
39
|
+
include_undocumented: bool = True,
|
|
40
|
+
format: str = "markdown",
|
|
41
|
+
) -> list[Path]:
|
|
42
|
+
"""Scan the project, build a Project IR, and write backend output.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
project_root: Directory to scan recursively.
|
|
46
|
+
output_dir: Directory where generated files will be written.
|
|
47
|
+
root_name: Filename (without extension) for files that live at the
|
|
48
|
+
project root.
|
|
49
|
+
exclude: Optional glob patterns (e.g. ``"tests/*"``, ``"migrations"``)
|
|
50
|
+
matched against paths relative to ``project_root``.
|
|
51
|
+
include_source: Include source: ``True`` (full), ``False`` (none), or
|
|
52
|
+
``"signature"`` (declaration line only).
|
|
53
|
+
include_comments: Include the ``## Comments`` section.
|
|
54
|
+
include_undocumented: Keep objects that have no docstring.
|
|
55
|
+
format: Output backend: ``"markdown"`` (default), ``"json"``, or
|
|
56
|
+
``"compact"``.
|
|
57
|
+
"""
|
|
58
|
+
project_root = project_root.resolve()
|
|
59
|
+
output_dir = output_dir.resolve()
|
|
60
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
|
|
62
|
+
ir = build_project_ir(project_root, exclude=exclude)
|
|
63
|
+
backend = get_backend(format)
|
|
64
|
+
options = RenderOptions(
|
|
65
|
+
root_name=root_name,
|
|
66
|
+
include_source=include_source,
|
|
67
|
+
include_comments=include_comments,
|
|
68
|
+
include_undocumented=include_undocumented,
|
|
69
|
+
)
|
|
70
|
+
files = backend.render(ir, options)
|
|
71
|
+
|
|
72
|
+
written: list[Path] = []
|
|
73
|
+
for filename, content in files.items():
|
|
74
|
+
out_path = output_dir / filename
|
|
75
|
+
try:
|
|
76
|
+
out_path.write_text(content, encoding="utf-8")
|
|
77
|
+
except OSError as exc:
|
|
78
|
+
logger.error("could not write %s: %s", out_path, exc)
|
|
79
|
+
raise
|
|
80
|
+
written.append(out_path)
|
|
81
|
+
|
|
82
|
+
logger.info(
|
|
83
|
+
"Wrote %d file(s) to %s (%d Python file(s) scanned)",
|
|
84
|
+
len(written),
|
|
85
|
+
output_dir,
|
|
86
|
+
len(ir.modules),
|
|
87
|
+
)
|
|
88
|
+
return written
|