pydoc2markdown 0.1.0__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.
@@ -0,0 +1,26 @@
1
+ """PyDoc2Markdown - Convert Python docstrings to Markdown documentation."""
2
+
3
+ __version__ = "0.1.0"
4
+ __author__ = "f1sherFM"
5
+
6
+ from pydoc2markdown.core.generator import MarkdownGenerator
7
+ from pydoc2markdown.core.parser import (
8
+ ClassDoc,
9
+ DocstringParser,
10
+ FunctionDoc,
11
+ ModuleDoc,
12
+ Parameter,
13
+ RaisesInfo,
14
+ ReturnsInfo,
15
+ )
16
+
17
+ __all__ = [
18
+ "ClassDoc",
19
+ "DocstringParser",
20
+ "FunctionDoc",
21
+ "MarkdownGenerator",
22
+ "ModuleDoc",
23
+ "Parameter",
24
+ "RaisesInfo",
25
+ "ReturnsInfo",
26
+ ]
pydoc2markdown/cli.py ADDED
@@ -0,0 +1,131 @@
1
+ """Command-line interface for PyDoc2Markdown."""
2
+
3
+ import argparse
4
+ import logging
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from pydoc2markdown.core.config import load_config
9
+ from pydoc2markdown.core.generator import MarkdownGenerator
10
+ from pydoc2markdown.core.parser import DocstringParser
11
+ from pydoc2markdown.core.watcher import watch_and_generate
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def create_parser() -> argparse.ArgumentParser:
17
+ """Create and configure the argument parser."""
18
+ defaults = load_config()
19
+ parser = argparse.ArgumentParser(
20
+ prog="pydoc2markdown",
21
+ description="Convert Python docstrings to Markdown documentation.",
22
+ )
23
+ parser.add_argument(
24
+ "source",
25
+ type=Path,
26
+ help="Path to a Python file or directory to process.",
27
+ )
28
+ parser.add_argument(
29
+ "-o",
30
+ "--output",
31
+ type=Path,
32
+ default=Path(str(defaults.get("output", "docs"))),
33
+ help="Output directory for generated Markdown files (default: docs).",
34
+ )
35
+ parser.add_argument(
36
+ "--recursive",
37
+ action="store_true",
38
+ default=defaults.get("recursive", False),
39
+ help="Recursively process subdirectories.",
40
+ )
41
+ parser.add_argument(
42
+ "--template",
43
+ type=Path,
44
+ default=None,
45
+ help="Path to a custom Jinja2 template for Markdown generation.",
46
+ )
47
+ parser.add_argument(
48
+ "--theme",
49
+ choices=["default", "minimal"],
50
+ default=defaults.get("theme", "default"),
51
+ help="Built-in theme/template to use (default: default).",
52
+ )
53
+ parser.add_argument(
54
+ "--watch",
55
+ action="store_true",
56
+ help="Watch source files and regenerate docs on change.",
57
+ )
58
+ parser.add_argument(
59
+ "-v",
60
+ "--verbose",
61
+ action="count",
62
+ default=0,
63
+ help="Increase verbosity (-v for INFO, -vv for DEBUG).",
64
+ )
65
+ parser.add_argument(
66
+ "--version",
67
+ action="version",
68
+ version="%(prog)s 0.1.0",
69
+ )
70
+ return parser
71
+
72
+
73
+ def _setup_logging(verbosity: int) -> None:
74
+ """Configure logging level based on verbosity count."""
75
+ level = logging.WARNING
76
+ if verbosity >= 2:
77
+ level = logging.DEBUG
78
+ elif verbosity == 1:
79
+ level = logging.INFO
80
+ logging.basicConfig(
81
+ level=level,
82
+ format="%(name)s - %(levelname)s - %(message)s",
83
+ )
84
+
85
+
86
+ def main(args: list[str] | None = None) -> int:
87
+ """Main entry point for the CLI."""
88
+ parser = create_parser()
89
+ parsed_args = parser.parse_args(args)
90
+ _setup_logging(parsed_args.verbose)
91
+
92
+ if not parsed_args.source.exists():
93
+ logger.error("Source path does not exist: %s", parsed_args.source)
94
+ return 1
95
+
96
+ if parsed_args.watch:
97
+ return watch_and_generate(
98
+ source=parsed_args.source,
99
+ output_dir=parsed_args.output,
100
+ recursive=parsed_args.recursive,
101
+ theme=parsed_args.theme,
102
+ template_path=parsed_args.template,
103
+ )
104
+
105
+ logger.info("Parsing source: %s (recursive=%s)", parsed_args.source, parsed_args.recursive)
106
+ doc_parser = DocstringParser()
107
+ md_generator = MarkdownGenerator(
108
+ template_path=parsed_args.template,
109
+ theme=parsed_args.theme,
110
+ )
111
+
112
+ try:
113
+ modules = doc_parser.parse(
114
+ source=parsed_args.source,
115
+ recursive=parsed_args.recursive,
116
+ )
117
+ logger.info("Parsed %d module(s)", len(modules))
118
+ generated = md_generator.generate(
119
+ modules=modules,
120
+ output_dir=parsed_args.output,
121
+ )
122
+ logger.info("Generated %d Markdown file(s) in %s", len(generated), parsed_args.output)
123
+ except Exception:
124
+ logger.exception("Documentation generation failed")
125
+ return 1
126
+
127
+ return 0
128
+
129
+
130
+ if __name__ == "__main__":
131
+ sys.exit(main())
@@ -0,0 +1 @@
1
+ """Core logic for PyDoc2Markdown."""
@@ -0,0 +1,39 @@
1
+ """Configuration loader for PyDoc2Markdown."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import Any, cast
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ def load_config(
11
+ cwd: Path | None = None,
12
+ ) -> dict[str, Any]:
13
+ """Load [tool.pydoc2markdown] from pyproject.toml.
14
+
15
+ Args:
16
+ cwd: Directory to search for pyproject.toml. Defaults to current dir.
17
+
18
+ Returns:
19
+ Dictionary with parsed config values.
20
+ """
21
+ try:
22
+ import tomllib
23
+ except ModuleNotFoundError:
24
+ import tomli as tomllib
25
+
26
+ if cwd is None:
27
+ cwd = Path.cwd()
28
+
29
+ pyproject = cwd / "pyproject.toml"
30
+ if not pyproject.exists():
31
+ return {}
32
+
33
+ try:
34
+ with pyproject.open("rb") as f:
35
+ data = tomllib.load(f)
36
+ return cast(dict[str, Any], data.get("tool", {}).get("pydoc2markdown", {}))
37
+ except Exception:
38
+ logger.warning("Failed to load pyproject.toml config")
39
+ return {}
@@ -0,0 +1,271 @@
1
+ """Markdown documentation generator."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+
6
+ from jinja2 import Environment, FileSystemLoader, PackageLoader, select_autoescape
7
+
8
+ from pydoc2markdown.core.parser import ModuleDoc
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class MarkdownGenerator:
14
+ """Generate Markdown files from parsed Python documentation."""
15
+
16
+ DEFAULT_TEMPLATE = """# {{ module.name }}
17
+
18
+ {% if module.docstring %}
19
+ {{ module.docstring }}
20
+ {% endif %}
21
+
22
+ {% if module.classes or module.functions %}
23
+ ## Table of Contents
24
+
25
+ {% if module.classes %}
26
+ - [Classes](#classes)
27
+ {% for class in module.classes %}
28
+ - [`{{ class.name }}`](#{{ class.name | lower | replace(' ', '-') }})
29
+ {% if class.methods %}
30
+ {% for method in class.methods %}
31
+ - [
32
+ {% if method.is_property %}@property {% elif method.is_classmethod %}@classmethod
33
+ {% elif method.is_staticmethod %}@staticmethod {% endif %}
34
+ `{{ method.name }}`](#{{ method.name | lower | replace(' ', '-') }})
35
+ {% endfor %}
36
+ {% endif %}
37
+ {% endfor %}
38
+ {% endif %}
39
+ {% if module.functions %}
40
+ - [Functions](#functions)
41
+ {% for func in module.functions %}
42
+ - [`{{ func.name }}`](#{{ func.name | lower | replace(' ', '-') }})
43
+ {% endfor %}
44
+ {% endif %}
45
+ {% endif %}
46
+
47
+ {% if module.public_api %}
48
+ **Public API:**
49
+ {% for name in module.public_api %}
50
+ - `{{ name }}`
51
+ {% endfor %}
52
+ {% endif %}
53
+
54
+ {% if module.classes %}
55
+ ## Classes
56
+
57
+ {% for class in module.classes %}
58
+ ### `{{ class.name }}`{% if class.class_type != "class" %} ({{ class.class_type }}){% endif %}
59
+
60
+ {% if class.bases %}
61
+ **Bases:** `{{ class.bases | join(", ") }}`
62
+ {% endif %}
63
+
64
+ {% if class.docstring %}
65
+ {{ class.docstring }}
66
+ {% endif %}
67
+
68
+ {% if class.attributes %}
69
+ #### Attributes
70
+
71
+ | Name | Type | Description |
72
+ |------|------|-------------|
73
+ {% for attr in class.attributes %}
74
+ | `{{ attr.name }}` |
75
+ {% if attr.type_hint %}`{{ attr.type_hint }}`{% else %}-{% endif %} |
76
+ {% if attr.description %}{{ attr.description }}{% else %}-{% endif %} |
77
+ {% endfor %}
78
+ {% endif %}
79
+
80
+ {% if class.methods %}
81
+ #### Methods
82
+
83
+ {% for method in class.methods %}
84
+ {% if method.is_property %}##### @property `{{ method.name }}`
85
+ {% elif method.is_classmethod %}##### @classmethod `{{ method.name }}`
86
+ {% elif method.is_staticmethod %}##### @staticmethod `{{ method.name }}`
87
+ {% else %}##### `{{ method.name }}`
88
+ {% endif %}
89
+
90
+ {% if method.docstring %}
91
+ {{ method.docstring }}
92
+ {% endif %}
93
+
94
+ {% if method.params %}
95
+ **Parameters:**
96
+
97
+ | Name | Type | Description |
98
+ |------|------|-------------|
99
+ {% for param in method.params %}
100
+ | `{{ param.name }}` |
101
+ {% if param.type_hint %}`{{ param.type_hint }}`{% else %}-{% endif %} |
102
+ {% if param.description %}{{ param.description }}{% else %}-{% endif %} |
103
+ {% endfor %}
104
+ {% endif %}
105
+
106
+ {% if method.returns %}
107
+ **Returns:**{% if method.returns.type_hint %} `{{ method.returns.type_hint }}`{% endif %}
108
+
109
+ {% if method.returns.description %}{{ method.returns.description }}{% endif %}
110
+ {% endif %}
111
+
112
+ {% if method.raises %}
113
+ **Raises:**
114
+ {% for raise in method.raises %}
115
+ - {% if raise.type_name %}`{{ raise.type_name }}`{% endif %}
116
+ {% if raise.description %}: {{ raise.description }}{% endif %}
117
+ {% endfor %}
118
+ {% endif %}
119
+
120
+ {% endfor %}
121
+ {% endif %}
122
+
123
+ ---
124
+ {% endfor %}
125
+ {% endif %}
126
+
127
+ {% if module.functions %}
128
+ ## Functions
129
+
130
+ {% for func in module.functions %}
131
+ ### `{{ func.name }}`
132
+
133
+ {% if func.docstring %}
134
+ {{ func.docstring }}
135
+ {% endif %}
136
+
137
+ {% if func.params %}
138
+ **Parameters:**
139
+
140
+ | Name | Type | Description |
141
+ |------|------|-------------|
142
+ {% for param in func.params %}
143
+ | `{{ param.name }}` |
144
+ {% if param.type_hint %}`{{ param.type_hint }}`{% else %}-{% endif %} |
145
+ {% if param.description %}{{ param.description }}{% else %}-{% endif %} |
146
+ {% endfor %}
147
+ {% endif %}
148
+
149
+ {% if func.returns %}
150
+ **Returns:**{% if func.returns.type_hint %} `{{ func.returns.type_hint }}`{% endif %}
151
+
152
+ {% if func.returns.description %}{{ func.returns.description }}{% endif %}
153
+ {% endif %}
154
+
155
+ {% if func.raises %}
156
+ **Raises:**
157
+ {% for raise in func.raises %}
158
+ - {% if raise.type_name %}`{{ raise.type_name }}`{% endif %}
159
+ {% if raise.description %}: {{ raise.description }}{% endif %}
160
+ {% endfor %}
161
+ {% endif %}
162
+
163
+ {% endfor %}
164
+ {% endif %}
165
+ """
166
+
167
+ def __init__(
168
+ self,
169
+ template_path: Path | None = None,
170
+ theme: str = "default",
171
+ ) -> None:
172
+ """Initialize the generator with an optional custom template or theme."""
173
+ self._template_path = template_path
174
+ self._theme = theme
175
+ self._env = self._create_environment()
176
+
177
+ def _create_environment(self) -> Environment:
178
+ """Create and configure the Jinja2 environment."""
179
+ if self._template_path and self._template_path.exists():
180
+ return Environment(
181
+ loader=FileSystemLoader(str(self._template_path.parent)),
182
+ autoescape=select_autoescape(),
183
+ )
184
+ return Environment(
185
+ loader=PackageLoader("pydoc2markdown", "templates"),
186
+ autoescape=select_autoescape(),
187
+ )
188
+
189
+ def _resolve_template_name(self) -> str:
190
+ """Resolve the template file name based on theme or custom path."""
191
+ if self._template_path:
192
+ return self._template_path.name
193
+ if self._theme == "default":
194
+ return "module.md"
195
+ return f"{self._theme}.md"
196
+
197
+ def generate(
198
+ self,
199
+ modules: list[ModuleDoc],
200
+ output_dir: Path,
201
+ ) -> list[Path]:
202
+ """Generate Markdown files for the given modules."""
203
+ output_dir.mkdir(parents=True, exist_ok=True)
204
+ generated: list[Path] = []
205
+
206
+ template_name = self._resolve_template_name()
207
+ logger.debug("Using template: %s", template_name)
208
+ template = self._env.get_template(template_name)
209
+
210
+ for module in modules:
211
+ if module.package:
212
+ module_dir = output_dir / module.package.replace(".", "/")
213
+ module_dir.mkdir(parents=True, exist_ok=True)
214
+ output_path = module_dir / f"{module.name}.md"
215
+ else:
216
+ output_path = output_dir / f"{module.name}.md"
217
+ logger.debug("Generating %s", output_path)
218
+ content = template.render(module=module)
219
+ output_path.write_text(content, encoding="utf-8")
220
+ generated.append(output_path)
221
+
222
+ index_path = self._generate_index(modules, output_dir)
223
+ if index_path:
224
+ generated.append(index_path)
225
+
226
+ return generated
227
+
228
+ def _generate_index(
229
+ self,
230
+ modules: list[ModuleDoc],
231
+ output_dir: Path,
232
+ ) -> Path | None:
233
+ """Generate an index.md with links to all module docs, grouped by package."""
234
+ if not modules:
235
+ return None
236
+
237
+ from collections import OrderedDict
238
+
239
+ groups: OrderedDict[str, list[ModuleDoc]] = OrderedDict()
240
+ for module in sorted(modules, key=lambda m: (m.package, m.name)):
241
+ groups.setdefault(module.package, []).append(module)
242
+
243
+ lines = ["# Index", ""]
244
+ for package, mods in groups.items():
245
+ if package:
246
+ lines.append(f"## {package}")
247
+ else:
248
+ lines.append("## Modules")
249
+ for module in mods:
250
+ rel_path = (
251
+ f"{module.package.replace('.', '/')}/{module.name}.md"
252
+ if module.package
253
+ else f"{module.name}.md"
254
+ )
255
+ lines.append(f"- [{module.name}]({rel_path})")
256
+ if module.docstring:
257
+ first_line = module.docstring.split("\n")[0]
258
+ lines.append(f" - {first_line}")
259
+ lines.append("")
260
+
261
+ index_path = output_dir / "index.md"
262
+ logger.debug("Generating %s", index_path)
263
+ index_path.write_text("\n".join(lines), encoding="utf-8")
264
+ return index_path
265
+
266
+ def generate_string(self, module: ModuleDoc) -> str:
267
+ """Generate Markdown content as a string for a single module."""
268
+ from jinja2 import Template
269
+
270
+ template = Template(self.DEFAULT_TEMPLATE)
271
+ return template.render(module=module)
@@ -0,0 +1,396 @@
1
+ """Python docstring parser with structured docstring support."""
2
+
3
+ import ast
4
+ import contextlib
5
+ import logging
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path
8
+
9
+ import docstring_parser
10
+ from docstring_parser import Docstring as ParsedDocstring
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ @dataclass
16
+ class Parameter:
17
+ """Represents a function/method parameter."""
18
+
19
+ name: str
20
+ type_hint: str | None = None
21
+ default: str | None = None
22
+ description: str | None = None
23
+
24
+
25
+ @dataclass
26
+ class ReturnsInfo:
27
+ """Structured return type and description."""
28
+
29
+ type_hint: str | None = None
30
+ description: str | None = None
31
+
32
+
33
+ @dataclass
34
+ class RaisesInfo:
35
+ """Structured exception type and description."""
36
+
37
+ type_name: str | None = None
38
+ description: str | None = None
39
+
40
+
41
+ @dataclass
42
+ class FunctionDoc:
43
+ """Represents extracted documentation for a function or method."""
44
+
45
+ name: str
46
+ docstring: str | None = None
47
+ params: list[Parameter] = field(default_factory=list)
48
+ returns: ReturnsInfo | None = None
49
+ raises: list[RaisesInfo] = field(default_factory=list)
50
+ is_method: bool = False
51
+ is_async: bool = False
52
+ is_property: bool = False
53
+ is_classmethod: bool = False
54
+ is_staticmethod: bool = False
55
+
56
+
57
+ @dataclass
58
+ class ClassDoc:
59
+ """Represents extracted documentation for a class."""
60
+
61
+ name: str
62
+ docstring: str | None = None
63
+ methods: list[FunctionDoc] = field(default_factory=list)
64
+ attributes: list[Parameter] = field(default_factory=list)
65
+ bases: list[str] = field(default_factory=list)
66
+ class_type: str = "class"
67
+
68
+
69
+ @dataclass
70
+ class ModuleDoc:
71
+ """Represents extracted documentation for a module."""
72
+
73
+ name: str
74
+ path: Path
75
+ docstring: str | None = None
76
+ classes: list[ClassDoc] = field(default_factory=list)
77
+ functions: list[FunctionDoc] = field(default_factory=list)
78
+ public_api: list[str] = field(default_factory=list)
79
+ package: str = ""
80
+
81
+
82
+ class DocstringParser:
83
+ """Parse Python source files and extract structured docstrings."""
84
+
85
+ def __init__(self) -> None:
86
+ self._modules: list[ModuleDoc] = []
87
+ self._source: Path = Path()
88
+
89
+ def parse(
90
+ self,
91
+ source: Path,
92
+ recursive: bool = False,
93
+ ) -> list[ModuleDoc]:
94
+ """Parse a file or directory and return extracted documentation."""
95
+ self._modules.clear()
96
+ self._source = source
97
+
98
+ if source.is_file() and source.suffix == ".py":
99
+ self._parse_file(source)
100
+ elif source.is_dir():
101
+ pattern = "**/*.py" if recursive else "*.py"
102
+ for file_path in sorted(source.glob(pattern)):
103
+ self._parse_file(file_path)
104
+ else:
105
+ raise ValueError(f"Invalid source: {source}")
106
+
107
+ return self._modules
108
+
109
+ def _parse_file(self, path: Path) -> None:
110
+ """Parse a single Python file."""
111
+ logger.debug("Parsing file: %s", path)
112
+ try:
113
+ source_text = path.read_text(encoding="utf-8")
114
+ except UnicodeDecodeError:
115
+ source_text = path.read_text(encoding="latin-1")
116
+
117
+ tree = ast.parse(source_text)
118
+ module_doc = self._extract_module(path, tree)
119
+ logger.debug(
120
+ "Extracted %d classes and %d functions from %s",
121
+ len(module_doc.classes),
122
+ len(module_doc.functions),
123
+ path,
124
+ )
125
+ self._modules.append(module_doc)
126
+
127
+ def _extract_module(self, path: Path, tree: ast.AST) -> ModuleDoc:
128
+ """Extract module-level documentation."""
129
+ assert isinstance(tree, ast.Module)
130
+ package = ""
131
+ if self._source.is_dir():
132
+ try:
133
+ rel = path.parent.relative_to(self._source)
134
+ if rel != Path("."):
135
+ package = str(rel).replace("\\", ".").replace("/", ".")
136
+ except ValueError:
137
+ package = ""
138
+ module = ModuleDoc(
139
+ name=path.stem,
140
+ path=path,
141
+ docstring=ast.get_docstring(tree),
142
+ package=package,
143
+ )
144
+
145
+ for node in ast.iter_child_nodes(tree):
146
+ if isinstance(node, ast.ClassDef):
147
+ module.classes.append(self._extract_class(node))
148
+ elif isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
149
+ module.functions.append(self._extract_function(node))
150
+ elif isinstance(node, ast.Assign | ast.AnnAssign):
151
+ module.public_api.extend(self._extract_public_api(node))
152
+
153
+ return module
154
+
155
+ def _extract_class(self, node: ast.ClassDef) -> ClassDoc:
156
+ """Extract documentation from a class definition."""
157
+ bases = [self._format_base(base) for base in node.bases]
158
+ class_doc = ClassDoc(
159
+ name=node.name,
160
+ docstring=ast.get_docstring(node),
161
+ bases=bases,
162
+ class_type=self._resolve_class_type(node, bases),
163
+ )
164
+
165
+ for item in node.body:
166
+ if isinstance(item, ast.FunctionDef | ast.AsyncFunctionDef):
167
+ if item.name == "__init__":
168
+ class_doc.attributes.extend(self._extract_attributes(item))
169
+ else:
170
+ class_doc.methods.append(self._extract_function(item, is_method=True))
171
+
172
+ return class_doc
173
+
174
+ def _extract_function(
175
+ self,
176
+ node: ast.FunctionDef | ast.AsyncFunctionDef,
177
+ is_method: bool = False,
178
+ ) -> FunctionDoc:
179
+ """Extract documentation from a function definition."""
180
+ raw_docstring = ast.get_docstring(node)
181
+ params = self._extract_ast_params(node)
182
+ returns: ReturnsInfo | None = None
183
+ raises: list[RaisesInfo] = []
184
+
185
+ if raw_docstring:
186
+ with contextlib.suppress(Exception):
187
+ parsed = docstring_parser.parse(raw_docstring)
188
+ self._merge_param_descriptions(params, parsed)
189
+ returns = self._extract_returns(parsed)
190
+ raises = self._extract_raises(parsed)
191
+
192
+ # Fallback to AST return annotation if docstring did not provide type
193
+ ast_return_type = ast.unparse(node.returns) if node.returns else None
194
+ if returns is None:
195
+ returns = ReturnsInfo(type_hint=ast_return_type)
196
+ elif ast_return_type and not returns.type_hint:
197
+ returns.type_hint = ast_return_type
198
+
199
+ decorators = self._extract_decorator_names(node)
200
+ is_property = any(d in ("property", "cached_property") for d in decorators)
201
+ is_classmethod = "classmethod" in decorators
202
+ is_staticmethod = "staticmethod" in decorators
203
+
204
+ return FunctionDoc(
205
+ name=node.name,
206
+ docstring=raw_docstring,
207
+ params=params,
208
+ returns=returns,
209
+ raises=raises,
210
+ is_method=is_method,
211
+ is_async=isinstance(node, ast.AsyncFunctionDef),
212
+ is_property=is_property,
213
+ is_classmethod=is_classmethod,
214
+ is_staticmethod=is_staticmethod,
215
+ )
216
+
217
+ def _extract_ast_params(
218
+ self,
219
+ node: ast.FunctionDef | ast.AsyncFunctionDef,
220
+ ) -> list[Parameter]:
221
+ """Extract parameter names and type hints from AST."""
222
+ params: list[Parameter] = []
223
+ args = node.args
224
+
225
+ # Determine start index (skip self/cls for methods)
226
+ start = 0
227
+ if args.args and args.args[0].arg in ("self", "cls"):
228
+ start = 1
229
+
230
+ # Positional args
231
+ for _idx, arg in enumerate(args.args[start:], start=start):
232
+ param = Parameter(
233
+ name=arg.arg,
234
+ type_hint=ast.unparse(arg.annotation) if arg.annotation else None,
235
+ )
236
+ params.append(param)
237
+
238
+ # *args
239
+ if args.vararg:
240
+ params.append(
241
+ Parameter(
242
+ name=f"*{args.vararg.arg}",
243
+ type_hint=ast.unparse(args.vararg.annotation)
244
+ if args.vararg.annotation
245
+ else None,
246
+ ),
247
+ )
248
+
249
+ # Keyword-only args
250
+ for arg in args.kwonlyargs:
251
+ params.append(
252
+ Parameter(
253
+ name=arg.arg,
254
+ type_hint=ast.unparse(arg.annotation) if arg.annotation else None,
255
+ ),
256
+ )
257
+
258
+ # **kwargs
259
+ if args.kwarg:
260
+ params.append(
261
+ Parameter(
262
+ name=f"**{args.kwarg.arg}",
263
+ type_hint=ast.unparse(args.kwarg.annotation) if args.kwarg.annotation else None,
264
+ ),
265
+ )
266
+
267
+ return params
268
+
269
+ def _merge_param_descriptions(
270
+ self,
271
+ params: list[Parameter],
272
+ parsed: ParsedDocstring,
273
+ ) -> None:
274
+ """Merge docstring parameter descriptions into AST-extracted parameters."""
275
+ for param in params:
276
+ doc_param = next(
277
+ (p for p in parsed.params if p.arg_name == param.name.lstrip("*")),
278
+ None,
279
+ )
280
+ if doc_param and doc_param.description:
281
+ param.description = doc_param.description
282
+
283
+ def _extract_returns(self, parsed: ParsedDocstring) -> ReturnsInfo | None:
284
+ """Extract structured return info from parsed docstring."""
285
+ if parsed.returns:
286
+ return ReturnsInfo(
287
+ type_hint=parsed.returns.type_name,
288
+ description=parsed.returns.description,
289
+ )
290
+ return None
291
+
292
+ def _extract_raises(self, parsed: ParsedDocstring) -> list[RaisesInfo]:
293
+ """Extract structured raises info from parsed docstring."""
294
+ return [
295
+ RaisesInfo(
296
+ type_name=r.type_name,
297
+ description=r.description,
298
+ )
299
+ for r in parsed.raises
300
+ ]
301
+
302
+ def _extract_attributes(
303
+ self,
304
+ init_node: ast.FunctionDef | ast.AsyncFunctionDef,
305
+ ) -> list[Parameter]:
306
+ """Extract instance attributes from __init__ method."""
307
+ attributes: list[Parameter] = []
308
+
309
+ # Try to enrich with __init__ docstring parameter descriptions
310
+ init_docstring = ast.get_docstring(init_node)
311
+ parsed_init: ParsedDocstring | None = None
312
+ if init_docstring:
313
+ with contextlib.suppress(Exception):
314
+ parsed_init = docstring_parser.parse(init_docstring)
315
+
316
+ for node in ast.walk(init_node):
317
+ if (
318
+ isinstance(node, ast.AnnAssign)
319
+ and isinstance(node.target, ast.Attribute)
320
+ and isinstance(node.target.value, ast.Name)
321
+ and node.target.value.id == "self"
322
+ ):
323
+ attr_name = node.target.attr
324
+ attr = Parameter(
325
+ name=attr_name,
326
+ type_hint=ast.unparse(node.annotation) if node.annotation else None,
327
+ )
328
+ if parsed_init:
329
+ doc_attr = next(
330
+ (p for p in parsed_init.params if p.arg_name == attr_name),
331
+ None,
332
+ )
333
+ if doc_attr and doc_attr.description:
334
+ attr.description = doc_attr.description
335
+ attributes.append(attr)
336
+
337
+ return attributes
338
+
339
+ def _format_base(self, base: ast.expr) -> str:
340
+ """Format a base class expression as a string."""
341
+ return ast.unparse(base)
342
+
343
+ def _extract_decorator_names(
344
+ self,
345
+ node: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef,
346
+ ) -> list[str]:
347
+ """Extract decorator names from a function or class definition."""
348
+ names: list[str] = []
349
+ for dec in node.decorator_list:
350
+ if isinstance(dec, ast.Name):
351
+ names.append(dec.id)
352
+ elif isinstance(dec, ast.Attribute):
353
+ names.append(f"{ast.unparse(dec.value)}.{dec.attr}")
354
+ elif isinstance(dec, ast.Call):
355
+ if isinstance(dec.func, ast.Name):
356
+ names.append(dec.func.id)
357
+ elif isinstance(dec.func, ast.Attribute):
358
+ names.append(f"{ast.unparse(dec.func.value)}.{dec.func.attr}")
359
+ return names
360
+
361
+ def _resolve_class_type(
362
+ self,
363
+ node: ast.ClassDef,
364
+ bases: list[str],
365
+ ) -> str:
366
+ """Determine if a class is a dataclass, enum, typeddict, or plain class."""
367
+ decorators = self._extract_decorator_names(node)
368
+ if any(d in ("dataclass", "dataclasses.dataclass") for d in decorators):
369
+ return "dataclass"
370
+ if any(base.rsplit(".", 1)[-1] in ("Enum", "IntEnum", "Flag", "IntFlag") for base in bases):
371
+ return "enum"
372
+ if any(base.rsplit(".", 1)[-1] in ("TypedDict", "typing.TypedDict") for base in bases):
373
+ return "typeddict"
374
+ return "class"
375
+
376
+ def _extract_public_api(
377
+ self,
378
+ node: ast.Assign | ast.AnnAssign,
379
+ ) -> list[str]:
380
+ """Extract names from __all__ assignment."""
381
+ target = node.targets[0] if isinstance(node, ast.Assign) else node.target
382
+ if not (isinstance(target, ast.Name) and target.id == "__all__"):
383
+ return []
384
+
385
+ value = node.value
386
+ if value is None:
387
+ return []
388
+
389
+ names: list[str] = []
390
+ if isinstance(value, (ast.List, ast.Tuple)):
391
+ for elt in value.elts:
392
+ if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
393
+ names.append(elt.value)
394
+ elif hasattr(ast, "Str") and isinstance(elt, ast.Str): # noqa: UP023
395
+ names.append(str(elt.s))
396
+ return names
@@ -0,0 +1,88 @@
1
+ """File watcher for auto-regenerating documentation."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ def watch_and_generate(
10
+ source: Path,
11
+ output_dir: Path,
12
+ recursive: bool,
13
+ theme: str,
14
+ template_path: Path | None,
15
+ ) -> int:
16
+ """Watch source files and regenerate docs on change.
17
+
18
+ Args:
19
+ source: Path to Python file or directory.
20
+ output_dir: Output directory for Markdown files.
21
+ recursive: Whether to scan subdirectories recursively.
22
+ theme: Built-in theme name.
23
+ template_path: Optional custom template path.
24
+
25
+ Returns:
26
+ Exit code (0 for success, 1 for error).
27
+ """
28
+ try:
29
+ from watchdog.events import FileSystemEvent, FileSystemEventHandler
30
+ from watchdog.observers import Observer
31
+ except ImportError:
32
+ logger.error(
33
+ "watchdog is required for --watch. Install it with: pip install pydoc2markdown[watch]"
34
+ )
35
+ return 1
36
+
37
+ from pydoc2markdown.core.generator import MarkdownGenerator
38
+ from pydoc2markdown.core.parser import DocstringParser
39
+
40
+ generator = MarkdownGenerator(
41
+ template_path=template_path,
42
+ theme=theme,
43
+ )
44
+
45
+ class _RebuildHandler(FileSystemEventHandler):
46
+ def __init__(self) -> None:
47
+ self._parser = DocstringParser()
48
+
49
+ def on_any_event(self, event: FileSystemEvent) -> None:
50
+ if event.is_directory:
51
+ return
52
+ src = getattr(event, "src_path", "")
53
+ if isinstance(src, str) and not src.endswith(".py"):
54
+ return
55
+ logger.info("Change detected, regenerating docs...")
56
+ try:
57
+ modules = self._parser.parse(source, recursive=recursive)
58
+ generator.generate(modules, output_dir)
59
+ logger.info("Docs regenerated in %s", output_dir)
60
+ except Exception:
61
+ logger.exception("Regeneration failed")
62
+
63
+ handler = _RebuildHandler()
64
+ watch_path = source if source.is_dir() else source.parent
65
+ observer = Observer()
66
+ observer.schedule(handler, str(watch_path), recursive=True)
67
+ observer.start()
68
+
69
+ logger.info("Watching %s for changes. Press Ctrl+C to stop.", watch_path)
70
+
71
+ # Generate once before watching
72
+ try:
73
+ modules = handler._parser.parse(source, recursive=recursive)
74
+ generator.generate(modules, output_dir)
75
+ logger.info("Initial docs generated in %s", output_dir)
76
+ except Exception:
77
+ logger.exception("Initial generation failed")
78
+
79
+ try:
80
+ while observer.is_alive():
81
+ observer.join(1)
82
+ except KeyboardInterrupt:
83
+ logger.info("Stopping watcher...")
84
+ finally:
85
+ observer.stop()
86
+ observer.join()
87
+
88
+ return 0
@@ -0,0 +1,31 @@
1
+ # {{ module.name }}
2
+
3
+ {% if module.docstring %}
4
+ {{ module.docstring }}
5
+ {% endif %}
6
+
7
+ {% if module.classes %}
8
+ ## Classes
9
+
10
+ {% for class in module.classes %}
11
+ ### `{{ class.name }}`{% if class.class_type != "class" %} ({{ class.class_type }}){% endif %}
12
+
13
+ {% if class.docstring %}
14
+ {{ class.docstring }}
15
+ {% endif %}
16
+
17
+ {% endfor %}
18
+ {% endif %}
19
+
20
+ {% if module.functions %}
21
+ ## Functions
22
+
23
+ {% for func in module.functions %}
24
+ ### `{{ func.name }}`
25
+
26
+ {% if func.docstring %}
27
+ {{ func.docstring }}
28
+ {% endif %}
29
+
30
+ {% endfor %}
31
+ {% endif %}
@@ -0,0 +1,137 @@
1
+ # {{ module.name }}
2
+
3
+ {% if module.docstring %}
4
+ {{ module.docstring }}
5
+ {% endif %}
6
+
7
+ {% if module.classes or module.functions %}
8
+ ## Table of Contents
9
+
10
+ {% if module.classes %}
11
+ - [Classes](#classes)
12
+ {% for class in module.classes %}
13
+ - [`{{ class.name }}`](#{{ class.name | lower | replace(' ', '-') }})
14
+ {% if class.methods %}
15
+ {% for method in class.methods %}
16
+ - [
17
+ {% if method.is_property %}@property {% elif method.is_classmethod %}@classmethod
18
+ {% elif method.is_staticmethod %}@staticmethod {% endif %}
19
+ `{{ method.name }}`](#{{ method.name | lower | replace(' ', '-') }})
20
+ {% endfor %}
21
+ {% endif %}
22
+ {% endfor %}
23
+ {% endif %}
24
+ {% if module.functions %}
25
+ - [Functions](#functions)
26
+ {% for func in module.functions %}
27
+ - [`{{ func.name }}`](#{{ func.name | lower | replace(' ', '-') }})
28
+ {% endfor %}
29
+ {% endif %}
30
+ {% endif %}
31
+
32
+ {% if module.public_api %}
33
+ **Public API:**
34
+ {% for name in module.public_api %}
35
+ - `{{ name }}`
36
+ {% endfor %}
37
+ {% endif %}
38
+
39
+ {% if module.classes %}
40
+ ## Classes
41
+
42
+ {% for class in module.classes %}
43
+ ### `{{ class.name }}`{% if class.class_type != "class" %} ({{ class.class_type }}){% endif %}
44
+
45
+ {% if class.bases %}
46
+ **Bases:** `{{ class.bases | join(", ") }}`
47
+ {% endif %}
48
+
49
+ {% if class.docstring %}
50
+ {{ class.docstring }}
51
+ {% endif %}
52
+
53
+ {% if class.attributes %}
54
+ #### Attributes
55
+
56
+ | Name | Type | Description |
57
+ |------|------|-------------|
58
+ {% for attr in class.attributes %}
59
+ | `{{ attr.name }}` | {% if attr.type_hint %}`{{ attr.type_hint }}`{% else %}-{% endif %} | {% if attr.description %}{{ attr.description }}{% else %}-{% endif %} |
60
+ {% endfor %}
61
+ {% endif %}
62
+
63
+ {% if class.methods %}
64
+ #### Methods
65
+
66
+ {% for method in class.methods %}
67
+ ##### {% if method.is_property %}@property {% elif method.is_classmethod %}@classmethod {% elif method.is_staticmethod %}@staticmethod {% endif %}`{{ method.name }}`
68
+
69
+ {% if method.docstring %}
70
+ {{ method.docstring }}
71
+ {% endif %}
72
+
73
+ {% if method.params %}
74
+ **Parameters:**
75
+
76
+ | Name | Type | Description |
77
+ |------|------|-------------|
78
+ {% for param in method.params %}
79
+ | `{{ param.name }}` | {% if param.type_hint %}`{{ param.type_hint }}`{% else %}-{% endif %} | {% if param.description %}{{ param.description }}{% else %}-{% endif %} |
80
+ {% endfor %}
81
+ {% endif %}
82
+
83
+ {% if method.returns %}
84
+ **Returns:**{% if method.returns.type_hint %} `{{ method.returns.type_hint }}`{% endif %}
85
+
86
+ {% if method.returns.description %}{{ method.returns.description }}{% endif %}
87
+ {% endif %}
88
+
89
+ {% if method.raises %}
90
+ **Raises:**
91
+ {% for raise in method.raises %}
92
+ - {% if raise.type_name %}`{{ raise.type_name }}`{% endif %}{% if raise.description %}: {{ raise.description }}{% endif %}
93
+ {% endfor %}
94
+ {% endif %}
95
+
96
+ {% endfor %}
97
+ {% endif %}
98
+
99
+ ---
100
+ {% endfor %}
101
+ {% endif %}
102
+
103
+ {% if module.functions %}
104
+ ## Functions
105
+
106
+ {% for func in module.functions %}
107
+ ### `{{ func.name }}`
108
+
109
+ {% if func.docstring %}
110
+ {{ func.docstring }}
111
+ {% endif %}
112
+
113
+ {% if func.params %}
114
+ **Parameters:**
115
+
116
+ | Name | Type | Description |
117
+ |------|------|-------------|
118
+ {% for param in func.params %}
119
+ | `{{ param.name }}` | {% if param.type_hint %}`{{ param.type_hint }}`{% else %}-{% endif %} | {% if param.description %}{{ param.description }}{% else %}-{% endif %} |
120
+ {% endfor %}
121
+ {% endif %}
122
+
123
+ {% if func.returns %}
124
+ **Returns:**{% if func.returns.type_hint %} `{{ func.returns.type_hint }}`{% endif %}
125
+
126
+ {% if func.returns.description %}{{ func.returns.description }}{% endif %}
127
+ {% endif %}
128
+
129
+ {% if func.raises %}
130
+ **Raises:**
131
+ {% for raise in func.raises %}
132
+ - {% if raise.type_name %}`{{ raise.type_name }}`{% endif %}{% if raise.description %}: {{ raise.description }}{% endif %}
133
+ {% endfor %}
134
+ {% endif %}
135
+
136
+ {% endfor %}
137
+ {% endif %}
@@ -0,0 +1 @@
1
+ """Utility modules for PyDoc2Markdown."""
@@ -0,0 +1,34 @@
1
+ """Helper utilities for PyDoc2Markdown."""
2
+
3
+ import re
4
+
5
+
6
+ def clean_docstring(docstring: str | None) -> str | None:
7
+ """Clean and normalize a docstring."""
8
+ if not docstring:
9
+ return None
10
+ lines = docstring.expandtabs().splitlines()
11
+ if not lines:
12
+ return None
13
+
14
+ # Find minimum indentation
15
+ indent = None
16
+ for line in lines[1:]:
17
+ stripped = line.lstrip()
18
+ if stripped:
19
+ line_indent = len(line) - len(stripped)
20
+ if indent is None or line_indent < indent:
21
+ indent = line_indent
22
+
23
+ if indent:
24
+ lines[1:] = [line[indent:] for line in lines[1:]]
25
+
26
+ return "\n".join(lines).strip()
27
+
28
+
29
+ def slugify(text: str) -> str:
30
+ """Convert text to a URL-friendly slug."""
31
+ text = text.lower().strip()
32
+ text = re.sub(r"[^\w\s-]", "", text)
33
+ text = re.sub(r"[\s_]+", "-", text)
34
+ return re.sub(r"-+", "-", text)
@@ -0,0 +1,104 @@
1
+ Metadata-Version: 2.4
2
+ Name: pydoc2markdown
3
+ Version: 0.1.0
4
+ Summary: Convert Python docstrings to Markdown documentation.
5
+ Project-URL: Homepage, https://github.com/f1sherFM/PyDoc2Markdown
6
+ Project-URL: Repository, https://github.com/f1sherFM/PyDoc2Markdown
7
+ Project-URL: Issues, https://github.com/f1sherFM/PyDoc2Markdown/issues
8
+ Author-email: f1sherFM <f1sherFM@example.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: docstring,documentation,markdown,python
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Documentation
20
+ Classifier: Topic :: Software Development :: Documentation
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: docstring-parser>=0.15
24
+ Requires-Dist: jinja2>=3.1.0
25
+ Requires-Dist: tomli>=1.2.0; python_version < '3.11'
26
+ Provides-Extra: dev
27
+ Requires-Dist: mypy>=1.5.0; extra == 'dev'
28
+ Requires-Dist: pre-commit>=3.5.0; extra == 'dev'
29
+ Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
30
+ Requires-Dist: pytest>=7.4.0; extra == 'dev'
31
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
32
+ Requires-Dist: watchdog>=3.0; extra == 'dev'
33
+ Provides-Extra: watch
34
+ Requires-Dist: watchdog>=3.0; extra == 'watch'
35
+ Description-Content-Type: text/markdown
36
+
37
+ # PyDoc2Markdown
38
+
39
+ [![CI](https://github.com/f1sherFM/PyDoc2Markdown/actions/workflows/ci.yml/badge.svg)](https://github.com/f1sherFM/PyDoc2Markdown/actions/workflows/ci.yml)
40
+ [![codecov](https://codecov.io/gh/f1sherFM/PyDoc2Markdown/branch/main/graph/badge.svg)](https://codecov.io/gh/f1sherFM/PyDoc2Markdown)
41
+ [![PyPI](https://img.shields.io/pypi/v/pydoc2markdown)](https://pypi.org/project/pydoc2markdown/)
42
+
43
+ > Convert Python docstrings into clean, structured Markdown documentation.
44
+
45
+ ## Features
46
+
47
+ - **Docstring parsing** — Extract Google, NumPy, and reStructuredText style docstrings with structured params, returns, and raises.
48
+ - **Markdown generation** — Produce beautiful Markdown files with customizable Jinja2 templates.
49
+ - **Auto-generated index & TOC** — Each module gets a Table of Contents; an `index.md` with package grouping is created automatically.
50
+ - **Package grouping** — Output files are organized into subdirectories matching the package structure.
51
+ - **Built-in themes** — Choose between `default` (detailed) and `minimal` themes, or supply your own template.
52
+ - **CLI & API** — Use via command line or import as a Python library.
53
+ - **Recursive processing** — Scan entire packages in one command.
54
+ - **Type-aware** — Respects type hints and annotations.
55
+ - **Advanced constructs** — Supports `property`, `classmethod`, `staticmethod`, `dataclass`, `Enum`, `TypedDict`, and `__all__`.
56
+
57
+ ## Installation
58
+
59
+ ```bash
60
+ pip install pydoc2markdown
61
+ ```
62
+
63
+ ## Quick Start
64
+
65
+ ### CLI Usage
66
+
67
+ ```bash
68
+ # Generate docs for a single file
69
+ pydoc2markdown my_module.py -o docs
70
+
71
+ # Recursively process a package
72
+ pydoc2markdown src/my_package --recursive -o docs
73
+
74
+ # Use a built-in theme
75
+ pydoc2markdown src/my_package --recursive --theme minimal -o docs
76
+
77
+ # Use a custom template
78
+ pydoc2markdown src/my_package --recursive --template custom.md.j2 -o docs
79
+
80
+ # Enable verbose logging
81
+ pydoc2markdown src/my_package --recursive -vv -o docs
82
+ ```
83
+
84
+ ### Library Usage
85
+
86
+ ```python
87
+ from pathlib import Path
88
+ from pydoc2markdown import DocstringParser, MarkdownGenerator
89
+
90
+ parser = DocstringParser()
91
+ modules = parser.parse(Path("src/my_package"), recursive=True)
92
+
93
+ generator = MarkdownGenerator(theme="default")
94
+ generator.generate(modules, output_dir=Path("docs"))
95
+ ```
96
+
97
+ ## Documentation
98
+
99
+ - [Contributing Guide](CONTRIBUTING.md)
100
+ - [Project Structure](PROJECT_STRUCTURE.md)
101
+
102
+ ## License
103
+
104
+ MIT License — see [LICENSE](LICENSE) for details.
@@ -0,0 +1,16 @@
1
+ pydoc2markdown/__init__.py,sha256=VFhL3E7U4NYG0gOjsyIfwsFFcu9CezjpqzdSdfB5cbg,511
2
+ pydoc2markdown/cli.py,sha256=00ssjGZDCX6CRiMRqRPZ8IdavwSSpzTFjUcA-TW0YDs,3826
3
+ pydoc2markdown/core/__init__.py,sha256=2L8gqlhFk5aUEdVweMh6n14qBP3otOvaqY9HFzuItZE,37
4
+ pydoc2markdown/core/config.py,sha256=7a4Xkt9V6wSqlZFXcAxt5UE0KvHPtf-dAtcktrEmE20,939
5
+ pydoc2markdown/core/generator.py,sha256=iSROflUXlqjNnCHTLVVe2yyhfD7ciZORN8fs56bCwnc,7979
6
+ pydoc2markdown/core/parser.py,sha256=gYmFR-iHI69I3fEbo-xne249ftI1aExjMG84dXTeobI,13595
7
+ pydoc2markdown/core/watcher.py,sha256=vYGUi6qzXyO0EpHYpZug8fVbOLsyzdRRR-_d71Qnj6U,2774
8
+ pydoc2markdown/templates/minimal.md,sha256=QywFgdjpYOYgKQC_DGiWYXgAiAa_lsq_yj0FSvkWDdY,516
9
+ pydoc2markdown/templates/module.md,sha256=Hkp-vi02pErjhz4V5iodDmWmqXjfJxU15ruuP6Hhruw,3596
10
+ pydoc2markdown/utils/__init__.py,sha256=MCa1nvJ2iCaeED3zbqu3_2rPiUm19biNiFWxPiACyqU,42
11
+ pydoc2markdown/utils/helpers.py,sha256=HMyAr_pJ0Zz2HlqbyBsMTTCIQmVmvXtDM5Gj4lVHU_c,900
12
+ pydoc2markdown-0.1.0.dist-info/METADATA,sha256=6EgH34a8hNuHvM3OGddTMMeA6SP_Ig6z4WG_8UFibn8,3885
13
+ pydoc2markdown-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
14
+ pydoc2markdown-0.1.0.dist-info/entry_points.txt,sha256=bfIiW3kqPCQExc2kQREAE2ExHDJ1HMA3x1JP6hXDr1w,59
15
+ pydoc2markdown-0.1.0.dist-info/licenses/LICENSE,sha256=aZoyljz1cF1y9zrTZmigukeEoCIdOoWiQ6P4F7FVSgs,1065
16
+ pydoc2markdown-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pydoc2markdown = pydoc2markdown.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 f1sherFM
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.