@vx-oss/docs-python 1.0.3

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,57 @@
1
+ import { components_exports } from "./components/index.js";
2
+ import * as JsxRuntime from "react/jsx-runtime";
3
+ import { toJsxRuntime } from "hast-util-to-jsx-runtime";
4
+ import defaultMdxComponents from "@vx-oss/docs-react/mdx";
5
+ //#region src/renderer.ts
6
+ function createRenderer(options) {
7
+ const { tree, structuredData = {
8
+ headings: [],
9
+ contents: []
10
+ }, rehypeToc = [] } = options;
11
+ return {
12
+ structuredData,
13
+ async render(userComponents) {
14
+ const components = {
15
+ ...defaultMdxComponents,
16
+ ...components_exports,
17
+ ...userComponents
18
+ };
19
+ function evaluate(expression) {
20
+ switch (expression.type) {
21
+ case "Literal": return expression.value;
22
+ case "ArrayExpression": return expression.elements.map((item) => item && evaluate(item));
23
+ case "Identifier":
24
+ if (expression.name in components) return components[expression.name];
25
+ throw new Error(`Component "${expression.name}" is missing, pass it to render().`);
26
+ default: throw new Error(`cannot evaluate ${expression.type} in generated content`);
27
+ }
28
+ }
29
+ const evaluater = {
30
+ evaluateExpression: evaluate,
31
+ evaluateProgram() {
32
+ throw new Error("cannot evaluate programs in generated content");
33
+ }
34
+ };
35
+ function render(tree) {
36
+ return toJsxRuntime(tree, {
37
+ components,
38
+ development: false,
39
+ createEvaluater: () => evaluater,
40
+ ...JsxRuntime
41
+ });
42
+ }
43
+ return {
44
+ toc: rehypeToc.map((item) => ({
45
+ ...item,
46
+ title: render({
47
+ type: "root",
48
+ children: item.title.children
49
+ })
50
+ })),
51
+ body: render(tree)
52
+ };
53
+ }
54
+ };
55
+ }
56
+ //#endregion
57
+ export { createRenderer };
@@ -0,0 +1,53 @@
1
+ import { PythonRenderer } from "./renderer.js";
2
+ import { DynamicSource, LoaderPlugin, MetaData, PageData, StaticSource } from "@vx-oss/docs-core/source";
3
+ import { RehypeCodeOptions } from "@vx-oss/docs-core/mdx-plugins/rehype-code";
4
+ import { PluggableList } from "unified";
5
+ import { StructuredData } from "@vx-oss/docs-core/mdx-plugins";
6
+ //#region src/source.d.ts
7
+ interface PythonConfig {
8
+ /** path to the JSON file generated by `fumapy-generate` */
9
+ file: string;
10
+ /**
11
+ * group generated pages in a directory:
12
+ *
13
+ * - `module`: the name of the root module
14
+ * - `none`: place them at the root of the source
15
+ *
16
+ * @defaultValue 'module'
17
+ */
18
+ groupBy?: PythonGroupBy;
19
+ /** additional remark plugins, applied before the structured data is collected */
20
+ remarkPlugins?: PluggableList;
21
+ /** additional rehype plugins, applied before the TOC is collected */
22
+ rehypePlugins?: PluggableList;
23
+ rehypeCodeOptions?: RehypeCodeOptions | false;
24
+ }
25
+ type PythonPageKind = 'module' | 'class';
26
+ type PythonGroupBy = 'module' | 'none';
27
+ interface PythonPage extends PageData {
28
+ title: string;
29
+ /** compile the page, at most once until the source is invalidated */
30
+ load: () => Promise<PythonRenderer>;
31
+ structuredData: () => Promise<StructuredData>;
32
+ _python: {
33
+ kind: PythonPageKind;
34
+ };
35
+ }
36
+ interface PythonSource {
37
+ staticSource: (options?: SourceOptions) => Promise<StaticSource<PythonSourceConfig>>;
38
+ dynamicSource: (options?: SourceOptions) => DynamicSource<PythonSourceConfig>;
39
+ /** decorates generated pages in the page tree with a `module`/`class` badge */
40
+ loaderPlugin: () => LoaderPlugin;
41
+ }
42
+ interface SourceOptions {
43
+ /** base directory for virtual file paths */
44
+ baseDir?: string;
45
+ }
46
+ type PythonSourceConfig = {
47
+ pageData: PythonPage;
48
+ metaData: MetaData;
49
+ };
50
+ /** Create a runtime Fumadocs content source from the JSON generated by `fumapy-generate`. */
51
+ declare function createPython(config: PythonConfig): PythonSource;
52
+ //#endregion
53
+ export { PythonConfig, PythonGroupBy, PythonPage, PythonPageKind, PythonSource, SourceOptions, createPython };
package/dist/source.js ADDED
@@ -0,0 +1,73 @@
1
+ import { buildPages } from "./build.js";
2
+ import { pythonPlugin } from "./plugin.js";
3
+ import { createRenderer } from "./renderer.js";
4
+ import fs from "node:fs/promises";
5
+ import { rehypeCode } from "@vx-oss/docs-core/mdx-plugins/rehype-code";
6
+ import { rehypeToc } from "@vx-oss/docs-core/mdx-plugins/rehype-toc";
7
+ import { remarkHeading } from "@vx-oss/docs-core/mdx-plugins/remark-heading";
8
+ import { remarkStructure } from "@vx-oss/docs-core/mdx-plugins/remark-structure";
9
+ import remarkRehype from "remark-rehype";
10
+ import { unified } from "unified";
11
+ import { VFile } from "vfile";
12
+ //#region src/source.ts
13
+ /** Create a runtime Fumadocs content source from the JSON generated by `fumapy-generate`. */
14
+ function createPython(config) {
15
+ const processor = unified().use(remarkHeading, { generateToc: false }).use(config.remarkPlugins ?? []).use(remarkStructure).use(remarkRehype, { passThrough: ["mdxJsxFlowElement", "mdxJsxTextElement"] }).use(config.rehypeCodeOptions === false ? [] : [[rehypeCode, {
16
+ fallbackLanguage: "plaintext",
17
+ ...config.rehypeCodeOptions
18
+ }]]).use(config.rehypePlugins ?? []).use(rehypeToc, { exportToc: { as: "data" } });
19
+ async function compile(page, href) {
20
+ const file = new VFile({ path: page.path });
21
+ const tree = await processor.run(page.build(href), file);
22
+ return createRenderer({
23
+ tree,
24
+ structuredData: file.data.structuredData,
25
+ rehypeToc: file.data.rehypeToc
26
+ });
27
+ }
28
+ function createSource({ baseDir } = {}) {
29
+ let loader;
30
+ function href(target) {
31
+ const resolved = loader?.getPageByHref(`./${target.path}`, { dir: baseDir });
32
+ if (!resolved) throw new Error(`cannot resolve the URL of ${target.path}`);
33
+ return resolved.page.url;
34
+ }
35
+ async function files() {
36
+ const mod = JSON.parse(await fs.readFile(config.file, "utf-8"));
37
+ return buildPages(mod, config.groupBy).map((page) => {
38
+ let loaded;
39
+ const load = () => loaded ??= compile(page, href);
40
+ return {
41
+ type: "page",
42
+ path: page.path,
43
+ data: {
44
+ title: page.title,
45
+ load,
46
+ structuredData: async () => (await load()).structuredData,
47
+ _python: { kind: page.kind }
48
+ }
49
+ };
50
+ });
51
+ }
52
+ return {
53
+ baseDir,
54
+ files,
55
+ configureStatic(options) {
56
+ loader = options.loader;
57
+ }
58
+ };
59
+ }
60
+ return {
61
+ dynamicSource: (options) => createSource(options),
62
+ async staticSource(options) {
63
+ const { files, ...source } = createSource(options);
64
+ return {
65
+ ...source,
66
+ files: await files()
67
+ };
68
+ },
69
+ loaderPlugin: pythonPlugin
70
+ };
71
+ }
72
+ //#endregion
73
+ export { createPython };
@@ -0,0 +1,65 @@
1
+ import argparse
2
+ import json
3
+ import os
4
+
5
+ import griffe
6
+ from griffe_typingdoc import TypingDocExtension
7
+
8
+ from .mksource import CustomEncoder, parse_module
9
+
10
+ STORE_SOURCE = True
11
+
12
+ def generate() -> None:
13
+ """Generate Python API documentation for a specified module.
14
+
15
+ This function parses command-line arguments, loads the specified module,
16
+ parses its content, and saves the generated API documentation as a JSON file.
17
+
18
+ Args:
19
+ None
20
+
21
+ Returns:
22
+ None
23
+
24
+ Raises:
25
+ argparse.ArgumentTypeError: If invalid arguments are provided.
26
+ FileNotFoundError: If the specified module or output directory doesn't exist.
27
+ PermissionError: If there's no write permission for the output directory.
28
+ """
29
+ parser = argparse.ArgumentParser(description="Generate python API documentation")
30
+ parser.add_argument(
31
+ "module", type=str, help="The module to generate documentation for"
32
+ )
33
+ parser.add_argument(
34
+ "--dir",
35
+ "-d",
36
+ type=str,
37
+ default=".",
38
+ help="The directory to save the documentation in",
39
+ )
40
+ parser.add_argument(
41
+ "--docstring-style",
42
+ "-s",
43
+ choices=["google", "numpy", "sphinx"],
44
+ default="google",
45
+ help="The docstring style to parse sections from",
46
+ )
47
+ args = parser.parse_args()
48
+
49
+ extensions = griffe.load_extensions(TypingDocExtension)
50
+ pkg = parse_module(
51
+ griffe.load(
52
+ args.module,
53
+ docstring_parser=args.docstring_style,
54
+ store_source=STORE_SOURCE,
55
+ extensions=extensions,
56
+ )
57
+ )
58
+ api_filename = f"{args.module}.json"
59
+
60
+ with open(os.path.join(args.dir, api_filename), "w") as file:
61
+ json.dump(pkg, file, cls=CustomEncoder, indent=2, full=True)
62
+
63
+
64
+ if __name__ == "__main__":
65
+ generate()
@@ -0,0 +1,10 @@
1
+ from .document_module import parse_class, parse_function, parse_module
2
+ from .json_encoder import CustomEncoder
3
+
4
+
5
+ __all__ = (
6
+ "parse_class",
7
+ "parse_function",
8
+ "parse_module",
9
+ "CustomEncoder",
10
+ )
@@ -0,0 +1,93 @@
1
+ from __future__ import annotations
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ import griffe
6
+
7
+ from .models import Class, Function, Module
8
+ from .simplify_docstring import simplify_docstring
9
+ from .utils import build_signature
10
+
11
+
12
+ def parse_module(m: griffe.Object) -> Module:
13
+ if not isinstance(m, griffe.Module):
14
+ raise ValueError("Module must be a module")
15
+
16
+ out = simplify_docstring(m.docstring, m)
17
+ res: Module = {
18
+ "name": m.name,
19
+ "path": m.path,
20
+ "filepath": m.filepath,
21
+ "description": out.description,
22
+ "docstring": out.remainder,
23
+ "attributes": out.attributes,
24
+ "modules": {
25
+ name: parse_module(value)
26
+ for name, value in m.modules.items()
27
+ if not value.is_alias
28
+ },
29
+ "classes": {
30
+ name: parse_class(value)
31
+ for name, value in m.classes.items()
32
+ if not value.is_alias
33
+ },
34
+ "functions": {
35
+ name: parse_function(value)
36
+ for name, value in m.functions.items()
37
+ if not value.is_alias
38
+ },
39
+ }
40
+ if m.is_package:
41
+ try:
42
+ res["version"] = version(m.name)
43
+ except PackageNotFoundError:
44
+ pass
45
+
46
+ return res
47
+
48
+
49
+ def parse_class(c: griffe.Class) -> Class:
50
+ out = simplify_docstring(c.docstring, c)
51
+ res: Class = {
52
+ "name": c.name,
53
+ "path": c.path,
54
+ "description": out.description,
55
+ "parameters": out.parameters,
56
+ "attributes": out.attributes,
57
+ "docstring": out.remainder,
58
+ "functions": {
59
+ name: parse_function(value)
60
+ for name, value in c.functions.items()
61
+ if not value.is_alias
62
+ },
63
+ "source": c.source,
64
+ "inherited_members": {},
65
+ }
66
+ # the class docstring documents the constructor's parameters
67
+ init = res["functions"].get("__init__")
68
+ if init is not None:
69
+ documented = {p["name"]: p["description"] for p in res["parameters"]}
70
+ for param in init["parameters"]:
71
+ param["description"] = param["description"] or documented.get(param["name"])
72
+ for member in c.inherited_members.values():
73
+ parent_path = ".".join(member.canonical_path.split(".")[:-1])
74
+ member_info = {"kind": member.kind, "path": member.canonical_path}
75
+ if parent_path not in res["inherited_members"]:
76
+ res["inherited_members"][parent_path] = []
77
+ res["inherited_members"][parent_path].append(member_info)
78
+ return res
79
+
80
+
81
+ def parse_function(f: griffe.Function) -> Function:
82
+ out = simplify_docstring(f.docstring, f)
83
+ res: Function = {
84
+ "name": f.name,
85
+ "path": f.path,
86
+ "signature": build_signature(f),
87
+ "description": out.description,
88
+ "parameters": out.parameters,
89
+ "returns": out.returns,
90
+ "docstring": out.remainder,
91
+ "source": f.source,
92
+ }
93
+ return res
@@ -0,0 +1,29 @@
1
+ import typing as t
2
+ from pathlib import Path, PosixPath, WindowsPath
3
+ import griffe
4
+
5
+ _json_encoder_map: dict[type, t.Callable[[t.Any], t.Any]] = {
6
+ Path: str,
7
+ PosixPath: str,
8
+ WindowsPath: str,
9
+ set: sorted,
10
+ }
11
+
12
+
13
+ class CustomEncoder(griffe.JSONEncoder):
14
+ def default(self, obj: t.Any) -> t.Any:
15
+ """Return a serializable representation of the given object.
16
+
17
+ Parameters:
18
+ obj: The object to serialize.
19
+
20
+ Returns:
21
+ A serializable representation.
22
+ """
23
+
24
+ try:
25
+ if isinstance(obj, griffe.Expr):
26
+ return str(obj)
27
+ return obj.as_dict(full=self.full)
28
+ except AttributeError:
29
+ return _json_encoder_map.get(type(obj), super().default)(obj)
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ import typing as t
4
+
5
+
6
+ class Module(t.TypedDict):
7
+ name: str
8
+ path: str
9
+ filepath: str
10
+ description: str | None
11
+ docstring: Docstring
12
+ attributes: list[Attribute]
13
+ modules: dict[str, Module]
14
+ classes: dict[str, Class]
15
+ functions: dict[str, Function]
16
+ version: str | None
17
+
18
+
19
+ class Class(t.TypedDict):
20
+ name: str
21
+ path: str
22
+ description: str | None
23
+ parameters: list[Parameter]
24
+ attributes: list[Attribute]
25
+ docstring: Docstring
26
+ functions: dict[str, Function]
27
+ source: str
28
+
29
+
30
+ class Function(t.TypedDict):
31
+ name: str
32
+ path: str
33
+ signature: str
34
+ description: str | None
35
+ parameters: list[Parameter]
36
+ returns: dict[str, str | None]
37
+ docstring: Docstring
38
+ source: str
39
+
40
+
41
+ class DocstringSection(t.TypedDict):
42
+ kind: str
43
+ value: str | list[Parameter]
44
+
45
+
46
+ Docstring = list[DocstringSection]
47
+
48
+
49
+ class Parameter(t.TypedDict):
50
+ name: str
51
+ annotation: str
52
+ description: str
53
+ value: str
54
+
55
+
56
+ class Attribute(t.TypedDict):
57
+ name: str
58
+ annotation: str
59
+ description: str
60
+ value: str
@@ -0,0 +1,166 @@
1
+ import typing as t
2
+
3
+ import griffe
4
+
5
+ from .utils import signature_parameters
6
+
7
+
8
+ class SimplifiedDocstring(t.NamedTuple):
9
+ description: str | None
10
+ parameters: list[dict[str, str | None]]
11
+ returns: dict[str, str | None]
12
+ attributes: list[dict[str, str | None]]
13
+ remainder: list[griffe.DocstringSection]
14
+
15
+
16
+ def simplify_docstring(
17
+ doc: griffe.Docstring, parent: griffe.Module | griffe.Class | griffe.Function = None
18
+ ) -> SimplifiedDocstring:
19
+ def get_parameters_from_signature(parent: griffe.Class | griffe.Function):
20
+ return [
21
+ {
22
+ "name": p.name,
23
+ "annotation": p.annotation,
24
+ "description": None,
25
+ "value": p.default,
26
+ }
27
+ for p in signature_parameters(parent)
28
+ ]
29
+
30
+ def get_returns_from_signature(parent: griffe.Function):
31
+ return {
32
+ "name": "",
33
+ "annotation": (
34
+ parent.returns
35
+ if isinstance(parent.returns, (str, type(None)))
36
+ else "".join(
37
+ elem if isinstance(elem, str) else elem.canonical_path
38
+ for elem in parent.returns.iterate(flat=True)
39
+ )
40
+ ),
41
+ "description": None,
42
+ }
43
+
44
+ def get_attributes_from_signature(parent: griffe.Module | griffe.Class):
45
+ return [
46
+ {
47
+ "name": attr.name,
48
+ "annotation": attr.annotation,
49
+ "description": attr.docstring.parsed if attr.docstring else None,
50
+ "value": attr.value,
51
+ }
52
+ for attr in parent.attributes.values()
53
+ if (not attr.is_alias and not attr.is_private)
54
+ ]
55
+
56
+ description = None
57
+ parameters = (
58
+ get_parameters_from_signature(parent)
59
+ if isinstance(parent, (griffe.Class, griffe.Function))
60
+ else None
61
+ )
62
+ attributes = (
63
+ get_attributes_from_signature(parent)
64
+ if isinstance(parent, (griffe.Class, griffe.Module))
65
+ else None
66
+ )
67
+ returns = (
68
+ get_returns_from_signature(parent)
69
+ if isinstance(parent, (griffe.Function))
70
+ else None
71
+ )
72
+ remainder = []
73
+ if not doc:
74
+ return SimplifiedDocstring(
75
+ description, parameters, returns, attributes, remainder
76
+ )
77
+
78
+ for i, sec in enumerate(doc.parsed):
79
+ if sec.kind == "text" and i == 0:
80
+ description = sec.value
81
+ continue
82
+
83
+ # Sort the parameters with the real signature
84
+ if sec.kind == "parameters":
85
+ map = {i.name: i for i in sec.value}
86
+ params_list = []
87
+ for param in signature_parameters(parent):
88
+ docstring = map.get(param.name)
89
+ if docstring is None:
90
+ params_list.append(
91
+ {
92
+ "name": param.name,
93
+ "annotation": param.annotation,
94
+ "description": None,
95
+ "value": param.default,
96
+ }
97
+ )
98
+ continue
99
+
100
+ try:
101
+ documented = griffe.parse_google(griffe.Docstring(docstring.description))
102
+ except AttributeError:
103
+ documented = docstring.description
104
+ params_list.append(
105
+ {
106
+ "name": docstring.name,
107
+ "annotation": docstring.annotation,
108
+ "description": documented,
109
+ "value": docstring.value,
110
+ }
111
+ )
112
+
113
+ parameters = params_list
114
+ continue
115
+
116
+ if sec.kind == "returns":
117
+ returns: griffe.DocstringReturn = sec.value[0]
118
+ returns.annotation = (
119
+ returns.annotation.canonical_path
120
+ if isinstance(returns.annotation, griffe.Expr)
121
+ else returns.annotation
122
+ )
123
+ continue
124
+
125
+ if sec.kind == "attributes":
126
+ map = {i.name: i for i in sec.value}
127
+ attributes_list = []
128
+ for attr in parent.attributes.values():
129
+ # exclude aliased attributes
130
+ if attr.is_alias:
131
+ continue
132
+
133
+ if attr.name in map:
134
+ attr_in_docstring: dict = map[attr.name]
135
+ attr_item: dict = {
136
+ "name": attr_in_docstring.name,
137
+ "description": None,
138
+ "annotation": attr_in_docstring.annotation,
139
+ "value": attr.value,
140
+ }
141
+
142
+ try:
143
+ attr_item["description"] = griffe.parse_google(
144
+ griffe.Docstring(attr_in_docstring.description)
145
+ )
146
+ except AttributeError:
147
+ pass
148
+
149
+ attributes_list.append(attr_item)
150
+ else:
151
+ attributes_list.append(
152
+ {
153
+ "name": attr.name,
154
+ "annotation": attr.annotation,
155
+ "description": (
156
+ attr.docstring.parsed if attr.docstring else None
157
+ ),
158
+ "value": attr.value,
159
+ }
160
+ )
161
+ attributes = attributes_list
162
+ continue
163
+
164
+ remainder.append(sec)
165
+
166
+ return SimplifiedDocstring(description, parameters, returns, attributes, remainder)
@@ -0,0 +1,59 @@
1
+ import griffe
2
+
3
+
4
+ def signature_parameters(obj: griffe.Class | griffe.Function) -> list[griffe.Parameter]:
5
+ """Parameters of the signature, without the implicit `self`/`cls` of methods."""
6
+ parameters = list(obj.parameters)
7
+ if isinstance(obj, griffe.Class) or (
8
+ isinstance(obj.parent, griffe.Class) and "staticmethod" not in obj.labels
9
+ ):
10
+ return parameters[1:]
11
+ return parameters
12
+
13
+
14
+ def build_signature(func: griffe.Function) -> str:
15
+ parameters = signature_parameters(func)
16
+
17
+ s = "("
18
+ positional_only = True
19
+ keyword_only = False
20
+ for i, p in enumerate(parameters):
21
+ if i != 0:
22
+ s += ", "
23
+ if p.kind in (
24
+ griffe.ParameterKind.positional_or_keyword,
25
+ griffe.ParameterKind.keyword_only,
26
+ ):
27
+ if positional_only and i != 0:
28
+ s += "/, "
29
+ positional_only = False
30
+
31
+ if p.kind == griffe.ParameterKind.keyword_only:
32
+ if not keyword_only:
33
+ s += "*, "
34
+ keyword_only = True
35
+
36
+ if p.kind == griffe.ParameterKind.var_keyword:
37
+ s += f"**{p.name}"
38
+ elif p.kind == griffe.ParameterKind.var_positional:
39
+ s += f"*{p.name}"
40
+ else:
41
+ s += p.name
42
+ if p.default is not None:
43
+ s += f"={p.default}"
44
+
45
+ s += ")"
46
+ if func.returns:
47
+ s += f" -> {func.returns}"
48
+
49
+ return s
50
+
51
+
52
+ def filter_non_imported(d: dict[str, griffe.Object]) -> dict:
53
+ return {k: v for k, v in d.items() if not v.is_imported}
54
+
55
+
56
+ def stringify_expr(expr: griffe.Expr | str) -> str:
57
+ if isinstance(expr, griffe.Expr):
58
+ return expr.path
59
+ return expr