qdocs 0.2.0__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.
- {qdocs-0.2.0 → qdocs-0.3.0}/.gitignore +3 -0
- qdocs-0.3.0/.vscode/settings.json +3 -0
- {qdocs-0.2.0 → qdocs-0.3.0}/PKG-INFO +37 -13
- {qdocs-0.2.0 → qdocs-0.3.0}/README.md +35 -12
- {qdocs-0.2.0 → qdocs-0.3.0}/pyproject.toml +1 -0
- {qdocs-0.2.0 → qdocs-0.3.0}/qdocs/annotations.py +31 -21
- {qdocs-0.2.0 → qdocs-0.3.0}/qdocs/build.py +153 -34
- {qdocs-0.2.0 → qdocs-0.3.0}/qdocs/collect.py +32 -6
- {qdocs-0.2.0 → qdocs-0.3.0}/qdocs/docstring.py +19 -0
- {qdocs-0.2.0 → qdocs-0.3.0}/qdocs/project.py +0 -2
- qdocs-0.3.0/qdocs/templates/types.html +13 -0
- {qdocs-0.2.0 → qdocs-0.3.0}/.gitattributes +0 -0
- {qdocs-0.2.0 → qdocs-0.3.0}/LICENSE +0 -0
- {qdocs-0.2.0 → qdocs-0.3.0}/qdocs/__init__.py +0 -0
- {qdocs-0.2.0 → qdocs-0.3.0}/qdocs/__main__.py +0 -0
- {qdocs-0.2.0 → qdocs-0.3.0}/qdocs/markup.py +0 -0
- {qdocs-0.2.0 → qdocs-0.3.0}/qdocs/templates/function.html +0 -0
- {qdocs-0.2.0 → qdocs-0.3.0}/qdocs/templates/nav.html +0 -0
- {qdocs-0.2.0 → qdocs-0.3.0}/qdocs/tutorials.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: qdocs
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: The website theme and generator of qspec and qunits.
|
|
5
5
|
Project-URL: source, https://github.com/patmlr/qdocs
|
|
6
6
|
Project-URL: tracker, https://github.com/patmlr/qdocs/issues
|
|
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
12
12
|
Classifier: Topic :: Documentation
|
|
13
13
|
Classifier: Topic :: Software Development :: Documentation
|
|
14
14
|
Requires-Python: >=3.12
|
|
15
|
+
Requires-Dist: catppuccin[pygments]
|
|
15
16
|
Requires-Dist: jinja2
|
|
16
17
|
Requires-Dist: pygments>=2.17
|
|
17
18
|
Description-Content-Type: text/markdown
|
|
@@ -45,6 +46,7 @@ The site provides `_data/navigation.yml` (top navigation), `_data/tutorials.yml`
|
|
|
45
46
|
`_includes/badges.html` (optional) and its logos `assets/img/logo.svg`, `logo_dark.svg` and `logo_small.svg`.
|
|
46
47
|
Any theme file can be overridden by a file at the same path in the site. Extra styles go in `_sass/custom.scss`,
|
|
47
48
|
extra `<head>` tags in `_includes/head-custom.html`.
|
|
49
|
+
The search in the top navigation finds the pages with a `title` in their front matter, by title and text.
|
|
48
50
|
|
|
49
51
|
`docs/requirements.txt`, with the same version as `remote_theme`:
|
|
50
52
|
|
|
@@ -66,7 +68,6 @@ PROJECT = Project(
|
|
|
66
68
|
modules=["algebra", "analyze", "models", "physics"],
|
|
67
69
|
root=ROOT,
|
|
68
70
|
namespaced={"analyze", "models"}, # qspec.models.Model instead of qspec.Model.
|
|
69
|
-
type_aliases="qtypes", # Optional: link these type aliases in annotations to their module page.
|
|
70
71
|
)
|
|
71
72
|
|
|
72
73
|
if __name__ == "__main__":
|
|
@@ -79,20 +80,34 @@ python docs/_gen/build.py [api | tutorials | logo | all]
|
|
|
79
80
|
|
|
80
81
|
| Target | Source | Output |
|
|
81
82
|
|-------------|-------------------------------------------|--------------------------------------------------|
|
|
82
|
-
| `api` | docstrings and signatures of the package | `doc/functions/**`, `_includes/api-nav.html
|
|
83
|
+
| `api` | docstrings and signatures of the package | `doc/functions/**`, `_includes/api-nav.html`, |
|
|
84
|
+
| | | `_includes/types/<module>.html` |
|
|
83
85
|
| `tutorials` | `# docs: <name>` regions in `examples/` | `_includes/code/<name>.html` |
|
|
84
86
|
| `logo` | `assets/img/logo.svg` | `assets/img/logo_dark.svg` |
|
|
85
87
|
|
|
86
88
|
The module pages `doc/modules/<module>.html` and the overview `doc/doc.html` are written by hand with the `api`
|
|
87
89
|
layout, which adds the navigation. See `Project` in [`qdocs/project.py`](qdocs/project.py) for all options.
|
|
88
90
|
|
|
91
|
+
The type aliases of a module (`type scalar = int | float`) are listed in `_includes/types/<module>.html`. Include the
|
|
92
|
+
list in the module page with `{% include types/<module>.html %}`. Type aliases in annotations link to their entry in
|
|
93
|
+
that list, and the build notes module pages that do not include their list.
|
|
94
|
+
|
|
89
95
|
### Customizations
|
|
90
96
|
|
|
91
97
|
Every class page has an Attributes section with the public class attributes, the annotated instance attributes
|
|
92
|
-
(including `__slots__`) and the properties of the class. Classes listed in `Project.namespaces` are
|
|
93
|
-
module of constants: no signature and no methods, only the description and the attributes
|
|
94
|
-
|
|
95
|
-
|
|
98
|
+
(including `__slots__`) and the properties of the class, sorted by name. Classes listed in `Project.namespaces` are
|
|
99
|
+
documented like a module of constants: no signature and no methods, only the description and the attributes in source
|
|
100
|
+
order. Enums are documented this way automatically.
|
|
101
|
+
|
|
102
|
+
Default values in signatures and attribute values are shown as Python source:
|
|
103
|
+
|
|
104
|
+
- Strings, numbers, booleans, enum members, classes and functions as written in code. Documented classes are linked.
|
|
105
|
+
- Tuples, lists, sets and dicts item by item. Sets are sorted, so the pages do not change between builds.
|
|
106
|
+
- Other objects as the call of their class that recreates them, e.g. `Unit(1000.0, frozenset({('km', 1)}), Length,
|
|
107
|
+
Context.I)`, if every parameter of the class is an attribute of the object, otherwise as `str(value)`.
|
|
108
|
+
|
|
109
|
+
Subclass `ApiBuilder` to change which attributes are listed (`attributes` returns a list of `Attribute`) or how
|
|
110
|
+
values are shown:
|
|
96
111
|
|
|
97
112
|
```python
|
|
98
113
|
from qdocs import ApiBuilder, Project, main
|
|
@@ -101,15 +116,15 @@ from qdocs import ApiBuilder, Project, main
|
|
|
101
116
|
class Builder(ApiBuilder):
|
|
102
117
|
def format_value(self, value) -> str:
|
|
103
118
|
if isinstance(value, Unit):
|
|
104
|
-
return f'<span class="
|
|
119
|
+
return f'<span class="class">Unit</span>({self.format_value(value.scale)})'
|
|
105
120
|
return super().format_value(value)
|
|
106
121
|
|
|
107
122
|
|
|
108
123
|
main(PROJECT, Builder)
|
|
109
124
|
```
|
|
110
125
|
|
|
111
|
-
`format_value`
|
|
112
|
-
the classes `string`, `bool`, `num`, `class` and `func
|
|
126
|
+
`format_value` returns the HTML of a value or of a part of a value, and is called for the items of containers and
|
|
127
|
+
the arguments of objects, too. Color the parts with the classes `string`, `bool`, `num`, `class` and `func`.
|
|
113
128
|
|
|
114
129
|
## Docstrings
|
|
115
130
|
|
|
@@ -136,21 +151,30 @@ def york_fit(x, y, sigma_x=None, ...):
|
|
|
136
151
|
|
|
137
152
|
- `` `code` `` and raw HTML (e.g. links to papers) are passed through.
|
|
138
153
|
- `$inline$` and `$$block$$` math is rendered by MathJax. Block math always stands on its own line on the website.
|
|
139
|
-
- `` [`name`] `` links to a documented object: `fit`, `King.fit`, `qspec.models.fit`, `qspec.physics.doppler
|
|
140
|
-
|
|
154
|
+
- `` [`name`] `` links to a documented object: `fit`, `King.fit`, `qspec.models.fit`, `qspec.physics.doppler`,
|
|
155
|
+
a module such as `qspec.analyze` or a type alias such as `scalar`. Unknown or ambiguous names stop the build.
|
|
141
156
|
- Blank lines separate paragraphs, lines starting with `- ` form a list.
|
|
142
157
|
- A leading `(name)` in `:returns:` labels the return value(s), otherwise it is shown as `out`.
|
|
143
158
|
- Types are taken from the annotations as written in the source.
|
|
144
159
|
- Methods without a docstring inherit the docstring of the overridden method.
|
|
145
160
|
- The signature and the parameters of a class are those of `__init__`, or of `__new__` if only that is overridden.
|
|
146
161
|
A class that overrides neither has no signature.
|
|
162
|
+
- The description of a class is its docstring, followed by the description in the docstring of `__init__` or
|
|
163
|
+
`__new__`. The fields can be in either docstring.
|
|
164
|
+
- Type aliases are described by a string on the line right below them (a string after a blank line is not taken):
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
type ExpMap = frozenset[tuple[str, int]]
|
|
168
|
+
"""An exponent map, a frozenset of (symbol, exponent) pairs."""
|
|
169
|
+
```
|
|
170
|
+
|
|
147
171
|
- Attributes are described by a string below their assignment or annotation in the class body, properties by their
|
|
148
172
|
docstring:
|
|
149
173
|
|
|
150
174
|
```python
|
|
151
175
|
class Unit:
|
|
152
176
|
scale: float
|
|
153
|
-
"""The scale factor of the unit relative to the SI
|
|
177
|
+
"""The scale factor of the unit relative to the SI unit of its dimension."""
|
|
154
178
|
```
|
|
155
179
|
|
|
156
180
|
The generator warns about parameters that are missing in the docstring or the signature.
|
|
@@ -27,6 +27,7 @@ The site provides `_data/navigation.yml` (top navigation), `_data/tutorials.yml`
|
|
|
27
27
|
`_includes/badges.html` (optional) and its logos `assets/img/logo.svg`, `logo_dark.svg` and `logo_small.svg`.
|
|
28
28
|
Any theme file can be overridden by a file at the same path in the site. Extra styles go in `_sass/custom.scss`,
|
|
29
29
|
extra `<head>` tags in `_includes/head-custom.html`.
|
|
30
|
+
The search in the top navigation finds the pages with a `title` in their front matter, by title and text.
|
|
30
31
|
|
|
31
32
|
`docs/requirements.txt`, with the same version as `remote_theme`:
|
|
32
33
|
|
|
@@ -48,7 +49,6 @@ PROJECT = Project(
|
|
|
48
49
|
modules=["algebra", "analyze", "models", "physics"],
|
|
49
50
|
root=ROOT,
|
|
50
51
|
namespaced={"analyze", "models"}, # qspec.models.Model instead of qspec.Model.
|
|
51
|
-
type_aliases="qtypes", # Optional: link these type aliases in annotations to their module page.
|
|
52
52
|
)
|
|
53
53
|
|
|
54
54
|
if __name__ == "__main__":
|
|
@@ -61,20 +61,34 @@ python docs/_gen/build.py [api | tutorials | logo | all]
|
|
|
61
61
|
|
|
62
62
|
| Target | Source | Output |
|
|
63
63
|
|-------------|-------------------------------------------|--------------------------------------------------|
|
|
64
|
-
| `api` | docstrings and signatures of the package | `doc/functions/**`, `_includes/api-nav.html
|
|
64
|
+
| `api` | docstrings and signatures of the package | `doc/functions/**`, `_includes/api-nav.html`, |
|
|
65
|
+
| | | `_includes/types/<module>.html` |
|
|
65
66
|
| `tutorials` | `# docs: <name>` regions in `examples/` | `_includes/code/<name>.html` |
|
|
66
67
|
| `logo` | `assets/img/logo.svg` | `assets/img/logo_dark.svg` |
|
|
67
68
|
|
|
68
69
|
The module pages `doc/modules/<module>.html` and the overview `doc/doc.html` are written by hand with the `api`
|
|
69
70
|
layout, which adds the navigation. See `Project` in [`qdocs/project.py`](qdocs/project.py) for all options.
|
|
70
71
|
|
|
72
|
+
The type aliases of a module (`type scalar = int | float`) are listed in `_includes/types/<module>.html`. Include the
|
|
73
|
+
list in the module page with `{% include types/<module>.html %}`. Type aliases in annotations link to their entry in
|
|
74
|
+
that list, and the build notes module pages that do not include their list.
|
|
75
|
+
|
|
71
76
|
### Customizations
|
|
72
77
|
|
|
73
78
|
Every class page has an Attributes section with the public class attributes, the annotated instance attributes
|
|
74
|
-
(including `__slots__`) and the properties of the class. Classes listed in `Project.namespaces` are
|
|
75
|
-
module of constants: no signature and no methods, only the description and the attributes
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
(including `__slots__`) and the properties of the class, sorted by name. Classes listed in `Project.namespaces` are
|
|
80
|
+
documented like a module of constants: no signature and no methods, only the description and the attributes in source
|
|
81
|
+
order. Enums are documented this way automatically.
|
|
82
|
+
|
|
83
|
+
Default values in signatures and attribute values are shown as Python source:
|
|
84
|
+
|
|
85
|
+
- Strings, numbers, booleans, enum members, classes and functions as written in code. Documented classes are linked.
|
|
86
|
+
- Tuples, lists, sets and dicts item by item. Sets are sorted, so the pages do not change between builds.
|
|
87
|
+
- Other objects as the call of their class that recreates them, e.g. `Unit(1000.0, frozenset({('km', 1)}), Length,
|
|
88
|
+
Context.I)`, if every parameter of the class is an attribute of the object, otherwise as `str(value)`.
|
|
89
|
+
|
|
90
|
+
Subclass `ApiBuilder` to change which attributes are listed (`attributes` returns a list of `Attribute`) or how
|
|
91
|
+
values are shown:
|
|
78
92
|
|
|
79
93
|
```python
|
|
80
94
|
from qdocs import ApiBuilder, Project, main
|
|
@@ -83,15 +97,15 @@ from qdocs import ApiBuilder, Project, main
|
|
|
83
97
|
class Builder(ApiBuilder):
|
|
84
98
|
def format_value(self, value) -> str:
|
|
85
99
|
if isinstance(value, Unit):
|
|
86
|
-
return f'<span class="
|
|
100
|
+
return f'<span class="class">Unit</span>({self.format_value(value.scale)})'
|
|
87
101
|
return super().format_value(value)
|
|
88
102
|
|
|
89
103
|
|
|
90
104
|
main(PROJECT, Builder)
|
|
91
105
|
```
|
|
92
106
|
|
|
93
|
-
`format_value`
|
|
94
|
-
the classes `string`, `bool`, `num`, `class` and `func
|
|
107
|
+
`format_value` returns the HTML of a value or of a part of a value, and is called for the items of containers and
|
|
108
|
+
the arguments of objects, too. Color the parts with the classes `string`, `bool`, `num`, `class` and `func`.
|
|
95
109
|
|
|
96
110
|
## Docstrings
|
|
97
111
|
|
|
@@ -118,21 +132,30 @@ def york_fit(x, y, sigma_x=None, ...):
|
|
|
118
132
|
|
|
119
133
|
- `` `code` `` and raw HTML (e.g. links to papers) are passed through.
|
|
120
134
|
- `$inline$` and `$$block$$` math is rendered by MathJax. Block math always stands on its own line on the website.
|
|
121
|
-
- `` [`name`] `` links to a documented object: `fit`, `King.fit`, `qspec.models.fit`, `qspec.physics.doppler
|
|
122
|
-
|
|
135
|
+
- `` [`name`] `` links to a documented object: `fit`, `King.fit`, `qspec.models.fit`, `qspec.physics.doppler`,
|
|
136
|
+
a module such as `qspec.analyze` or a type alias such as `scalar`. Unknown or ambiguous names stop the build.
|
|
123
137
|
- Blank lines separate paragraphs, lines starting with `- ` form a list.
|
|
124
138
|
- A leading `(name)` in `:returns:` labels the return value(s), otherwise it is shown as `out`.
|
|
125
139
|
- Types are taken from the annotations as written in the source.
|
|
126
140
|
- Methods without a docstring inherit the docstring of the overridden method.
|
|
127
141
|
- The signature and the parameters of a class are those of `__init__`, or of `__new__` if only that is overridden.
|
|
128
142
|
A class that overrides neither has no signature.
|
|
143
|
+
- The description of a class is its docstring, followed by the description in the docstring of `__init__` or
|
|
144
|
+
`__new__`. The fields can be in either docstring.
|
|
145
|
+
- Type aliases are described by a string on the line right below them (a string after a blank line is not taken):
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
type ExpMap = frozenset[tuple[str, int]]
|
|
149
|
+
"""An exponent map, a frozenset of (symbol, exponent) pairs."""
|
|
150
|
+
```
|
|
151
|
+
|
|
129
152
|
- Attributes are described by a string below their assignment or annotation in the class body, properties by their
|
|
130
153
|
docstring:
|
|
131
154
|
|
|
132
155
|
```python
|
|
133
156
|
class Unit:
|
|
134
157
|
scale: float
|
|
135
|
-
"""The scale factor of the unit relative to the SI
|
|
158
|
+
"""The scale factor of the unit relative to the SI unit of its dimension."""
|
|
136
159
|
```
|
|
137
160
|
|
|
138
161
|
The generator warns about parameters that are missing in the docstring or the signature.
|
|
@@ -14,21 +14,30 @@ from typing import Any
|
|
|
14
14
|
IDENTIFIER = re.compile(r"[A-Za-z_][A-Za-z0-9_.]*")
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
def
|
|
17
|
+
def type_aliases(module: str) -> list[tuple[str, str, str, str]]:
|
|
18
18
|
"""
|
|
19
|
-
:param module: The fully qualified name of a module.
|
|
20
|
-
:returns: The type aliases (`type
|
|
19
|
+
:param module: The fully qualified name of a module. The aliases of a package are those in its `__init__.py`.
|
|
20
|
+
:returns: (name, params, value, description) The public type aliases (`type name[params] = value`) of the module
|
|
21
|
+
in source order, with their type parameters ("" if there are none) and the docstring on the line right below
|
|
22
|
+
the alias ("" if there is none).
|
|
21
23
|
"""
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
source = inspect.getsource(importlib.import_module(module))
|
|
25
|
+
tree = ast.parse(source)
|
|
26
|
+
ret = []
|
|
27
|
+
for node, after in zip(tree.body, [*tree.body[1:], None]):
|
|
28
|
+
if not isinstance(node, ast.TypeAlias) or node.name.id.startswith("_"):
|
|
29
|
+
continue
|
|
30
|
+
params = ", ".join(ast.get_source_segment(source, p) for p in node.type_params) # type: ignore
|
|
31
|
+
doc = ""
|
|
32
|
+
if (
|
|
33
|
+
isinstance(after, ast.Expr)
|
|
34
|
+
and isinstance(getattr(after.value, "value", None), str)
|
|
35
|
+
and after.lineno == node.end_lineno + 1 # type: ignore
|
|
36
|
+
):
|
|
37
|
+
doc = inspect.cleandoc(after.value.value) # type: ignore
|
|
38
|
+
value = ast.get_source_segment(source, node.value)
|
|
39
|
+
ret.append((node.name.id, f"[{params}]" if params else "", value, doc)) # type: ignore
|
|
40
|
+
return ret
|
|
32
41
|
|
|
33
42
|
|
|
34
43
|
def _strip_quotes(text: str) -> str:
|
|
@@ -109,19 +118,19 @@ def _fallback(func: Callable[..., Any]) -> tuple[dict[str, str], str | None]:
|
|
|
109
118
|
|
|
110
119
|
|
|
111
120
|
class Linker:
|
|
112
|
-
def __init__(self, class_urls: dict[str, str],
|
|
121
|
+
def __init__(self, class_urls: dict[str, str], alias_urls: dict[str, dict[str, str]]) -> None:
|
|
113
122
|
"""
|
|
114
123
|
:param class_urls: A map from class names (short and fully qualified) to their site urls.
|
|
115
|
-
:param
|
|
116
|
-
:param aliases_url: The site url of the module page that describes the type aliases.
|
|
124
|
+
:param alias_urls: A map from type alias names to the site urls of their definitions, by defining module.
|
|
117
125
|
"""
|
|
118
126
|
self.class_urls = class_urls
|
|
119
|
-
self.
|
|
120
|
-
self.aliases_url = aliases_url
|
|
127
|
+
self.alias_urls = alias_urls
|
|
121
128
|
|
|
122
|
-
def __call__(self, annotation: str) -> str:
|
|
129
|
+
def __call__(self, annotation: str, module: str | None = None) -> str:
|
|
123
130
|
"""
|
|
124
131
|
:param annotation: The annotation source text.
|
|
132
|
+
:param module: The module of the annotation. Its own type aliases take precedence over those of the other
|
|
133
|
+
modules. A type alias defined in several other modules is not linked.
|
|
125
134
|
:returns: The HTML of the annotation with documented classes and type aliases linked.
|
|
126
135
|
"""
|
|
127
136
|
out = []
|
|
@@ -129,8 +138,9 @@ class Linker:
|
|
|
129
138
|
for m in IDENTIFIER.finditer(annotation):
|
|
130
139
|
name = m.group(0)
|
|
131
140
|
url = self.class_urls.get(name)
|
|
132
|
-
if url is None and name in self.
|
|
133
|
-
|
|
141
|
+
if url is None and name in self.alias_urls:
|
|
142
|
+
urls = self.alias_urls[name]
|
|
143
|
+
url = urls.get(module) or (next(iter(urls.values())) if len(urls) == 1 else None) # type: ignore
|
|
134
144
|
if url is None:
|
|
135
145
|
continue
|
|
136
146
|
out.append(html.escape(annotation[last : m.start()]))
|
|
@@ -12,6 +12,7 @@ import argparse
|
|
|
12
12
|
import enum
|
|
13
13
|
import html
|
|
14
14
|
import inspect
|
|
15
|
+
import numbers
|
|
15
16
|
import os
|
|
16
17
|
import re
|
|
17
18
|
import shutil
|
|
@@ -40,6 +41,8 @@ env = Environment(
|
|
|
40
41
|
# The colors of the light logo and their replacements in the dark logo.
|
|
41
42
|
DARK_LOGO_COLORS = {"#000000": "#e8e6ed", "#808080": "#a9a6b0"}
|
|
42
43
|
|
|
44
|
+
TYPES_INCLUDE = re.compile(r"{%\s*include\s+types/(\S+?)\.html\s*%}")
|
|
45
|
+
|
|
43
46
|
# The value of an attribute that has none on the class, e.g. an instance attribute or a property.
|
|
44
47
|
EMPTY = inspect.Parameter.empty
|
|
45
48
|
|
|
@@ -60,19 +63,27 @@ def write(path: str, text: str) -> None:
|
|
|
60
63
|
f.write(text)
|
|
61
64
|
|
|
62
65
|
|
|
63
|
-
def
|
|
66
|
+
def _ctor_args(value: Any) -> list[tuple[str | None, Any]] | None:
|
|
64
67
|
"""
|
|
65
|
-
:
|
|
68
|
+
:param value: An object.
|
|
69
|
+
:returns: The arguments that recreate the object with its class, as (keyword, value) pairs. The keyword is `None`
|
|
70
|
+
for positional arguments. `None` if the class takes no arguments, `*args` or `**kwargs`, or if a parameter is not
|
|
71
|
+
an attribute of the object.
|
|
66
72
|
"""
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return
|
|
71
|
-
if
|
|
72
|
-
return
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
try:
|
|
74
|
+
params = list(inspect.signature(type(value)).parameters.values())
|
|
75
|
+
except (TypeError, ValueError):
|
|
76
|
+
return None
|
|
77
|
+
if not params or any(p.kind in (p.VAR_POSITIONAL, p.VAR_KEYWORD) for p in params):
|
|
78
|
+
return None
|
|
79
|
+
args = []
|
|
80
|
+
for p in params:
|
|
81
|
+
try:
|
|
82
|
+
arg = getattr(value, p.name)
|
|
83
|
+
except Exception:
|
|
84
|
+
return None
|
|
85
|
+
args.append((p.name if p.kind == p.KEYWORD_ONLY else None, arg))
|
|
86
|
+
return args
|
|
76
87
|
|
|
77
88
|
|
|
78
89
|
def _is_enum(obj: Any) -> bool:
|
|
@@ -99,12 +110,19 @@ class ApiBuilder:
|
|
|
99
110
|
if entry.kind == "class":
|
|
100
111
|
class_urls[entry.name] = entry.url
|
|
101
112
|
class_urls[entry.qualname] = entry.url
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
self.link_types = annotations.Linker(class_urls,
|
|
113
|
+
alias_urls: dict[str, dict[str, str]] = {}
|
|
114
|
+
for module in self.modules:
|
|
115
|
+
for alias in module.aliases:
|
|
116
|
+
alias_urls.setdefault(alias.name, {})[module.name] = alias.url
|
|
117
|
+
self.link_types = annotations.Linker(class_urls, alias_urls)
|
|
107
118
|
self.convert = markup.Converter(self._resolve)
|
|
119
|
+
self._class_urls = {
|
|
120
|
+
entry.obj: entry.url
|
|
121
|
+
for module in self.modules
|
|
122
|
+
for entry in module.entries
|
|
123
|
+
if entry.kind in ("class", "namespace")
|
|
124
|
+
}
|
|
125
|
+
self._formatting: set[int] = set()
|
|
108
126
|
|
|
109
127
|
def _resolve(self, name: str) -> str:
|
|
110
128
|
entries = self.index.get(name, [])
|
|
@@ -122,8 +140,9 @@ class ApiBuilder:
|
|
|
122
140
|
:param entry: A class, namespace or enum.
|
|
123
141
|
:returns: The attributes listed on the page of the entry. These are the members of an enum, and the public
|
|
124
142
|
class attributes, annotated instance attributes (including `__slots__`) and properties of a class, sorted by
|
|
125
|
-
name.
|
|
126
|
-
(or of its bases), and the return annotations and
|
|
143
|
+
name. Enums and namespaces keep the source order. Types and descriptions are the annotations and the
|
|
144
|
+
docstrings below the attributes in the class body (or of its bases), and the return annotations and
|
|
145
|
+
docstrings of the properties.
|
|
127
146
|
"""
|
|
128
147
|
cls = entry.obj
|
|
129
148
|
source = annotations.class_attributes(cls)
|
|
@@ -150,21 +169,73 @@ class ApiBuilder:
|
|
|
150
169
|
for name, (annotation, description) in source.items():
|
|
151
170
|
if annotation is not None and not name.startswith("_") and not hasattr(cls, name):
|
|
152
171
|
ret.append(Attribute(name, type=annotation, description=description))
|
|
153
|
-
|
|
172
|
+
ret.sort(key=lambda a: a.name)
|
|
173
|
+
if entry.kind == "namespace":
|
|
174
|
+
order: dict[str, int] = {}
|
|
175
|
+
for name in [*source, *(n for c in reversed(cls.__mro__) for n in vars(c))]:
|
|
176
|
+
order.setdefault(name, len(order))
|
|
177
|
+
ret.sort(key=lambda a: order.get(a.name, len(order)))
|
|
178
|
+
return ret
|
|
154
179
|
|
|
155
180
|
def format_value(self, value: Any) -> str:
|
|
156
181
|
"""
|
|
157
|
-
:param value: A default value of a parameter
|
|
158
|
-
:returns: The HTML of the value
|
|
159
|
-
|
|
182
|
+
:param value: A default value of a parameter, the value of an attribute or a part of such a value.
|
|
183
|
+
:returns: The HTML of the value, with its parts colored by the classes string, bool, num, class and func.
|
|
184
|
+
Tuples, lists, sets and dicts are formatted item by item, sets sorted. Other objects are shown as the call
|
|
185
|
+
of their class that recreates them, e.g. `Unit(1000.0, frozenset({('km', 1)}), Length, Context.I)`, if all
|
|
186
|
+
parameters of the class are attributes of the object, otherwise as `str(value)`. Documented classes are
|
|
187
|
+
linked to their pages.
|
|
160
188
|
"""
|
|
161
189
|
if isinstance(value, enum.Enum):
|
|
162
|
-
return (
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
190
|
+
return f'{self._class(type(value))}.<span class="func">{html.escape(value.name)}</span>'
|
|
191
|
+
if isinstance(value, bool):
|
|
192
|
+
return f'<span class="bool">{value}</span>'
|
|
193
|
+
if isinstance(value, str):
|
|
194
|
+
return f'<span class="string">{html.escape(repr(value))}</span>' # Quotes and escapes as in the source.
|
|
195
|
+
if isinstance(value, numbers.Number):
|
|
196
|
+
return f'<span class="num">{html.escape(str(value))}</span>'
|
|
197
|
+
if value is None:
|
|
198
|
+
return "None"
|
|
199
|
+
if inspect.isclass(value):
|
|
200
|
+
return self._class(value)
|
|
201
|
+
if inspect.isroutine(value):
|
|
202
|
+
return f'<span class="func">{html.escape(value.__name__)}</span>'
|
|
203
|
+
if id(value) in self._formatting:
|
|
204
|
+
return "..."
|
|
205
|
+
self._formatting.add(id(value))
|
|
206
|
+
try:
|
|
207
|
+
return self._format_object(value)
|
|
208
|
+
finally:
|
|
209
|
+
self._formatting.discard(id(value))
|
|
210
|
+
|
|
211
|
+
def _format_object(self, value: Any) -> str:
|
|
212
|
+
cls = type(value)
|
|
213
|
+
if cls is tuple:
|
|
214
|
+
items = [self.format_value(v) for v in value]
|
|
215
|
+
return f"({', '.join(items)}{',' if len(items) == 1 else ''})"
|
|
216
|
+
if cls is list:
|
|
217
|
+
return f"[{', '.join(self.format_value(v) for v in value)}]"
|
|
218
|
+
if cls is dict:
|
|
219
|
+
return f"{{{', '.join(f'{self.format_value(k)}: {self.format_value(v)}' for k, v in value.items())}}}"
|
|
220
|
+
if cls in (set, frozenset):
|
|
221
|
+
items = sorted(self.format_value(v) for v in value)
|
|
222
|
+
if not items:
|
|
223
|
+
return f"{self._class(cls)}()"
|
|
224
|
+
braced = f"{{{', '.join(items)}}}"
|
|
225
|
+
return braced if cls is set else f"{self._class(cls)}({braced})"
|
|
226
|
+
args = _ctor_args(value)
|
|
227
|
+
if args is None:
|
|
228
|
+
return html.escape(str(value))
|
|
229
|
+
parts = [(f"{keyword}=" if keyword else "") + self.format_value(arg) for keyword, arg in args]
|
|
230
|
+
return f"{self._class(cls)}({', '.join(parts)})"
|
|
231
|
+
|
|
232
|
+
def _class(self, cls: type) -> str:
|
|
233
|
+
name = f'<span class="class">{html.escape(cls.__name__)}</span>'
|
|
234
|
+
url = self._class_urls.get(cls)
|
|
235
|
+
return name if url is None else f"<a href=\"{{{{ '{url}' | relative_url }}}}\">{name}</a>"
|
|
236
|
+
|
|
237
|
+
def _value(self, value: Any) -> str:
|
|
238
|
+
return f'<span class="default_value">{self.format_value(value)}</span>'
|
|
168
239
|
|
|
169
240
|
def page(self, entry: collect.Entry) -> str:
|
|
170
241
|
self._current = entry.qualname
|
|
@@ -201,8 +272,8 @@ class ApiBuilder:
|
|
|
201
272
|
context["attributes"] = [
|
|
202
273
|
dict(
|
|
203
274
|
name=a.name,
|
|
204
|
-
type=self.link_types(a.type) if a.type else None,
|
|
205
|
-
value=None if a.value is EMPTY else self.
|
|
275
|
+
type=self.link_types(a.type, entry.module) if a.type else None,
|
|
276
|
+
value=None if a.value is EMPTY else self._value(a.value),
|
|
206
277
|
description=self.convert(a.description),
|
|
207
278
|
)
|
|
208
279
|
for a in self.attributes(entry)
|
|
@@ -226,18 +297,21 @@ class ApiBuilder:
|
|
|
226
297
|
params = [(n, p) for n, p in sig.parameters.items() if n != "self"]
|
|
227
298
|
context["has_sig"] = True
|
|
228
299
|
|
|
229
|
-
|
|
300
|
+
if is_class:
|
|
301
|
+
doc = docstring.merge(docstring.parse(entry.obj.__doc__), docstring.parse(func.__doc__))
|
|
302
|
+
else:
|
|
303
|
+
doc = docstring.parse(inspect.getdoc(func))
|
|
230
304
|
param_types, return_type = annotations.source_annotations(func)
|
|
231
305
|
|
|
232
306
|
for name, p in params:
|
|
233
307
|
star = {p.VAR_POSITIONAL: "*", p.VAR_KEYWORD: "**"}.get(p.kind, "")
|
|
234
|
-
default = None if p.default is p.empty else self.
|
|
308
|
+
default = None if p.default is p.empty else self._value(p.default)
|
|
235
309
|
context["sig"].append(dict(star=star, name=name, default=default))
|
|
236
310
|
|
|
237
311
|
context["params"] = [
|
|
238
312
|
dict(
|
|
239
313
|
name=name,
|
|
240
|
-
type=self.link_types(param_types[name]) if name in param_types else None,
|
|
314
|
+
type=self.link_types(param_types[name], entry.module) if name in param_types else None,
|
|
241
315
|
description=self.convert(doc.params.get(name, "")),
|
|
242
316
|
)
|
|
243
317
|
for name, _ in params
|
|
@@ -248,7 +322,7 @@ class ApiBuilder:
|
|
|
248
322
|
label, desc = (doc.returns.label, doc.returns.description) if doc.returns else ("out", "")
|
|
249
323
|
context["returns"] = dict(
|
|
250
324
|
label=label,
|
|
251
|
-
type=self.link_types(return_type) if return_type else None,
|
|
325
|
+
type=self.link_types(return_type, entry.module) if return_type else None,
|
|
252
326
|
description=self.convert(desc),
|
|
253
327
|
)
|
|
254
328
|
# A __new__ returns the instance, so it may document ':returns:'.
|
|
@@ -269,6 +343,7 @@ class ApiBuilder:
|
|
|
269
343
|
n += 1
|
|
270
344
|
write(nav_file, env.get_template("nav.html").render(package=self.project.package, modules=self.modules))
|
|
271
345
|
print(f"Wrote {n} API pages and {os.path.relpath(nav_file, self.project.root)}.")
|
|
346
|
+
self._write_types()
|
|
272
347
|
|
|
273
348
|
if self.warnings:
|
|
274
349
|
print(f"\nDocstring warnings ({sum(len(w) for w in self.warnings.values())}):")
|
|
@@ -277,6 +352,50 @@ class ApiBuilder:
|
|
|
277
352
|
print(f" {name}: {w}")
|
|
278
353
|
return self.errors
|
|
279
354
|
|
|
355
|
+
def types(self, module: collect.Module) -> str:
|
|
356
|
+
"""
|
|
357
|
+
:param module: A module with type aliases.
|
|
358
|
+
:returns: The HTML of the list of the type aliases of the module, included by its module page.
|
|
359
|
+
"""
|
|
360
|
+
self._current = f"{self.project.package}.{module.name}"
|
|
361
|
+
aliases = [
|
|
362
|
+
dict(
|
|
363
|
+
name=a.name,
|
|
364
|
+
params=html.escape(a.params),
|
|
365
|
+
value=self.link_types(a.value, module.name),
|
|
366
|
+
description=self.convert(a.description),
|
|
367
|
+
)
|
|
368
|
+
for a in module.aliases
|
|
369
|
+
]
|
|
370
|
+
return env.get_template("types.html").render(aliases=aliases)
|
|
371
|
+
|
|
372
|
+
def _write_types(self) -> None:
|
|
373
|
+
"""
|
|
374
|
+
Write the type alias lists `_includes/types/<module>.html` and check that the module pages include them.
|
|
375
|
+
"""
|
|
376
|
+
types_dir = os.path.join(self.project.docs, "_includes", "types")
|
|
377
|
+
if os.path.exists(types_dir):
|
|
378
|
+
shutil.rmtree(types_dir)
|
|
379
|
+
with_aliases = {m.name for m in self.modules if m.aliases}
|
|
380
|
+
for module in self.modules:
|
|
381
|
+
if module.aliases:
|
|
382
|
+
write(os.path.join(types_dir, f"{module.name}.html"), self.types(module))
|
|
383
|
+
if with_aliases:
|
|
384
|
+
print(f"Wrote the type alias lists of {', '.join(sorted(with_aliases))} to _includes/types/.")
|
|
385
|
+
|
|
386
|
+
for module in self.modules:
|
|
387
|
+
page = os.path.join(self.project.docs, module.url.lstrip("/"))
|
|
388
|
+
text = ""
|
|
389
|
+
if os.path.exists(page):
|
|
390
|
+
with open(page, encoding="utf-8") as f:
|
|
391
|
+
text = f.read()
|
|
392
|
+
included = set(TYPES_INCLUDE.findall(text))
|
|
393
|
+
path = os.path.relpath(page, self.project.root)
|
|
394
|
+
if module.name in with_aliases and module.name not in included:
|
|
395
|
+
print(f" note: {path} does not include types/{module.name}.html, its type aliases are not listed")
|
|
396
|
+
for name in sorted(included - with_aliases):
|
|
397
|
+
self.errors.append(f"{path}: includes types/{name}.html, but module '{name}' has no type aliases")
|
|
398
|
+
|
|
280
399
|
|
|
281
400
|
def dark_logo(svg: str) -> str:
|
|
282
401
|
"""
|
|
@@ -10,6 +10,7 @@ from collections.abc import Callable, Iterator
|
|
|
10
10
|
from dataclasses import dataclass, field
|
|
11
11
|
from typing import Any
|
|
12
12
|
|
|
13
|
+
from qdocs import annotations
|
|
13
14
|
from qdocs.project import Project
|
|
14
15
|
|
|
15
16
|
|
|
@@ -18,11 +19,11 @@ class Entry:
|
|
|
18
19
|
"""A documented function, class, namespace or method."""
|
|
19
20
|
|
|
20
21
|
name: str
|
|
21
|
-
obj: Any # The function, class or method. None for modules.
|
|
22
|
+
obj: Any # The function, class or method. None for modules and aliases.
|
|
22
23
|
module: str # The module (file) this entry is documented in, e.g. "analyze".
|
|
23
24
|
namespace: str # The public namespace, e.g. "qspec.analyze" or "qspec.models.Model".
|
|
24
25
|
url: str # The site url of the page, e.g. "/doc/functions/analyze/york_fit.html".
|
|
25
|
-
kind: str # "function", "class", "namespace", "method" or "module" (only
|
|
26
|
+
kind: str # "function", "class", "namespace", "method", or "module" and "alias" (only reference targets).
|
|
26
27
|
owner: "Entry | None" = None # The class of a method.
|
|
27
28
|
methods: list["Method"] = field(default_factory=list) # The methods of a class.
|
|
28
29
|
|
|
@@ -34,13 +35,13 @@ class Entry:
|
|
|
34
35
|
def func(self) -> Callable[..., Any] | None:
|
|
35
36
|
"""
|
|
36
37
|
The function whose signature and docstring describe this entry. For a class this is `__init__`, or `__new__`
|
|
37
|
-
if only that is overridden. None for a class that overrides neither.
|
|
38
|
+
if only that is overridden. None for a class that overrides neither, or only by `typing`, e.g. a `Protocol`.
|
|
38
39
|
"""
|
|
39
40
|
if self.kind != "class":
|
|
40
41
|
return self.obj
|
|
41
42
|
for name in ("__init__", "__new__"):
|
|
42
43
|
func = getattr(self.obj, name)
|
|
43
|
-
if func is not getattr(object, name):
|
|
44
|
+
if func is not getattr(object, name) and getattr(func, "__module__", None) != "typing":
|
|
44
45
|
return func
|
|
45
46
|
return None
|
|
46
47
|
|
|
@@ -54,11 +55,27 @@ class Method:
|
|
|
54
55
|
inherited_from: Entry | None = None # The documented class the method is inherited from.
|
|
55
56
|
|
|
56
57
|
|
|
58
|
+
@dataclass
|
|
59
|
+
class Alias:
|
|
60
|
+
"""
|
|
61
|
+
A type alias `type name[params] = value`, listed on the page of its module. `params` is the source text of the
|
|
62
|
+
type parameters, e.g. "[T: generic]", or "", `value` the source text of the aliased type and `url` the anchor on
|
|
63
|
+
the module page, e.g. "/doc/modules/prefix.html#ExpMap".
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
name: str
|
|
67
|
+
params: str
|
|
68
|
+
value: str
|
|
69
|
+
description: str
|
|
70
|
+
url: str
|
|
71
|
+
|
|
72
|
+
|
|
57
73
|
@dataclass
|
|
58
74
|
class Module:
|
|
59
75
|
name: str
|
|
60
76
|
url: str
|
|
61
77
|
entries: list[Entry]
|
|
78
|
+
aliases: list[Alias] = field(default_factory=list)
|
|
62
79
|
|
|
63
80
|
|
|
64
81
|
def _public_methods(cls: type) -> list[str]:
|
|
@@ -96,7 +113,12 @@ def collect(project: Project) -> list[Module]:
|
|
|
96
113
|
else:
|
|
97
114
|
entry = Entry(name, obj, file, namespace, f"/doc/functions/{file}/{name}.html", "function")
|
|
98
115
|
entries.append(entry)
|
|
99
|
-
|
|
116
|
+
url = f"/doc/modules/{file}.html"
|
|
117
|
+
aliases = [
|
|
118
|
+
Alias(name, params, value, description, f"{url}#{name}")
|
|
119
|
+
for name, params, value, description in annotations.type_aliases(f"{project.package}.{file}")
|
|
120
|
+
]
|
|
121
|
+
modules.append(Module(file, url, entries, aliases))
|
|
100
122
|
|
|
101
123
|
# Methods are resolved after all classes are known, so inherited methods can link across modules.
|
|
102
124
|
for cls, entry in classes.items():
|
|
@@ -133,7 +155,7 @@ def build_index(project: Project, modules: list[Module]) -> dict[str, list[Entry
|
|
|
133
155
|
:param modules: The documented modules.
|
|
134
156
|
:returns: A map from every name an entry can be referenced by in docstrings to the matching entries.
|
|
135
157
|
Names are the short name (`fit`), `Class.method`, the fully qualified name (`qspec.models.fit`)
|
|
136
|
-
|
|
158
|
+
the module names (`qspec.analyze`) and the type aliases (`scalar`, `qspec.qtypes.scalar`).
|
|
137
159
|
"""
|
|
138
160
|
index: dict[str, list[Entry]] = {}
|
|
139
161
|
package = project.package
|
|
@@ -152,6 +174,10 @@ def build_index(project: Project, modules: list[Module]) -> dict[str, list[Entry
|
|
|
152
174
|
add(f"{entry.owner.name}.{entry.name}", entry)
|
|
153
175
|
for module in modules:
|
|
154
176
|
add(f"{package}.{module.name}", Entry(module.name, None, module.name, package, module.url, "module"))
|
|
177
|
+
for alias in module.aliases:
|
|
178
|
+
entry = Entry(alias.name, None, module.name, f"{package}.{module.name}", alias.url, "alias")
|
|
179
|
+
add(alias.name, entry)
|
|
180
|
+
add(entry.qualname, entry)
|
|
155
181
|
for entry in module.entries:
|
|
156
182
|
for m in entry.methods: # Inherited methods are also reachable as Subclass.method.
|
|
157
183
|
add(f"{entry.name}.{m.name}", m.entry)
|
|
@@ -89,6 +89,25 @@ def parse(doc: str | None) -> Docstring:
|
|
|
89
89
|
return ret
|
|
90
90
|
|
|
91
91
|
|
|
92
|
+
def merge(first: Docstring, second: Docstring) -> Docstring:
|
|
93
|
+
"""
|
|
94
|
+
:param first: A parsed docstring, e.g. of a class.
|
|
95
|
+
:param second: A parsed docstring that adds to the first, e.g. of its `__init__`.
|
|
96
|
+
:returns: The descriptions of both, separated by a blank line, and their fields. The fields of `second` override
|
|
97
|
+
those of `first`.
|
|
98
|
+
"""
|
|
99
|
+
descriptions = [first.description, second.description]
|
|
100
|
+
if first.description == second.description:
|
|
101
|
+
descriptions = [first.description]
|
|
102
|
+
return Docstring(
|
|
103
|
+
description="\n\n".join(d for d in descriptions if d),
|
|
104
|
+
params={**first.params, **second.params},
|
|
105
|
+
returns=second.returns or first.returns,
|
|
106
|
+
raises=[*first.raises, *second.raises],
|
|
107
|
+
warnings=[*first.warnings, *second.warnings],
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
92
111
|
def validate(doc: Docstring, params: list[str], is_class: bool) -> list[str]:
|
|
93
112
|
"""
|
|
94
113
|
:param doc: The parsed docstring.
|
|
@@ -19,7 +19,6 @@ class Project:
|
|
|
19
19
|
:param include: The modules for which only the listed names are documented.
|
|
20
20
|
:param namespaces: The fully qualified names of classes that are documented as a namespace of attributes
|
|
21
21
|
(no signature, no methods), e.g. a registry class used like a module. Enums are documented this way, too.
|
|
22
|
-
:param type_aliases: The module whose type aliases and classes are linked to its module page in annotations.
|
|
23
22
|
:param branch: The branch of the source links.
|
|
24
23
|
:param docs: The Jekyll site. Defaults to `root/docs`.
|
|
25
24
|
:param examples: The example scripts with the tutorial code regions. Defaults to `root/examples`.
|
|
@@ -32,7 +31,6 @@ class Project:
|
|
|
32
31
|
namespaced: set[str] = field(default_factory=set)
|
|
33
32
|
include: dict[str, set[str]] = field(default_factory=dict)
|
|
34
33
|
namespaces: set[str] = field(default_factory=set)
|
|
35
|
-
type_aliases: str | None = None
|
|
36
34
|
branch: str = "main"
|
|
37
35
|
docs: str = ""
|
|
38
36
|
examples: str = ""
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<dl class="field-list">
|
|
2
|
+
<dt class="field-head">Type aliases<span class="colon">:</span></dt>
|
|
3
|
+
<dd class="field-par">
|
|
4
|
+
<dl>
|
|
5
|
+
[%- for a in aliases %]
|
|
6
|
+
<dt id="[[ a.name ]]">[[ a.name ]][[ a.params ]]<span class="attr-value"><em>[[ a.value ]]</em></span></dt>
|
|
7
|
+
[%- if a.description %]
|
|
8
|
+
<dd class="field-desc">[[ a.description ]]</dd>
|
|
9
|
+
[%- endif %]
|
|
10
|
+
[%- endfor %]
|
|
11
|
+
</dl>
|
|
12
|
+
</dd>
|
|
13
|
+
</dl>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|