@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.
- package/LICENSE +21 -0
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/badge.js +11 -0
- package/dist/build.js +179 -0
- package/dist/components/collapsible.js +16 -0
- package/dist/components/index.d.ts +33 -0
- package/dist/components/index.js +150 -0
- package/dist/convert.d.ts +29 -0
- package/dist/convert.js +31 -0
- package/dist/generated.d.ts +83 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/dist/plugin.js +28 -0
- package/dist/renderer.d.ts +24 -0
- package/dist/renderer.js +57 -0
- package/dist/source.d.ts +53 -0
- package/dist/source.js +73 -0
- package/fumapy/__init__.py +65 -0
- package/fumapy/mksource/__init__.py +10 -0
- package/fumapy/mksource/document_module.py +93 -0
- package/fumapy/mksource/json_encoder.py +29 -0
- package/fumapy/mksource/models.py +60 -0
- package/fumapy/mksource/simplify_docstring.py +166 -0
- package/fumapy/mksource/utils.py +59 -0
- package/package.json +95 -0
- package/pyproject.toml +15 -0
- package/styles/preset.css +8 -0
package/dist/renderer.js
ADDED
|
@@ -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 };
|
package/dist/source.d.ts
ADDED
|
@@ -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,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
|