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.
@@ -6,3 +6,6 @@ _site/
6
6
  .jekyll-cache/
7
7
  .jekyll-metadata
8
8
  .ruff_cache/
9
+ .vscode/*
10
+ !.vscode/settings.json
11
+ !.vscode/launch.json
@@ -0,0 +1,3 @@
1
+ {
2
+ "files.eol": "\n",
3
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: qdocs
3
- Version: 0.2.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 documented like a
93
- module of constants: no signature and no methods, only the description and the attributes. Enums are documented this
94
- way automatically. Subclass `ApiBuilder` to change which attributes are listed (`attributes` returns a list of
95
- `Attribute`) or how values are shown:
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="default_value"><span class="class">Unit</span>(...)</span>'
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` is used for default values in signatures and for attribute values. Color the parts of a value with
112
- the classes `string`, `bool`, `num`, `class` and `func` inside a `default_value` span.
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
- or a module such as `qspec.analyze`. Unknown or ambiguous names stop the build.
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 base units."""
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 documented like a
75
- module of constants: no signature and no methods, only the description and the attributes. Enums are documented this
76
- way automatically. Subclass `ApiBuilder` to change which attributes are listed (`attributes` returns a list of
77
- `Attribute`) or how values are shown:
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="default_value"><span class="class">Unit</span>(...)</span>'
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` is used for default values in signatures and for attribute values. Color the parts of a value with
94
- the classes `string`, `bool`, `num`, `class` and `func` inside a `default_value` span.
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
- or a module such as `qspec.analyze`. Unknown or ambiguous names stop the build.
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 base units."""
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.
@@ -20,6 +20,7 @@ classifiers = [
20
20
  "Topic :: Software Development :: Documentation",
21
21
  ]
22
22
  dependencies = [
23
+ "catppuccin[pygments]",
23
24
  "jinja2",
24
25
  "pygments>=2.17",
25
26
  ]
@@ -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 module_aliases(module: str) -> set[str]:
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 x = ...`, `x = ...`) and classes defined in the module.
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
- tree = ast.parse(inspect.getsource(importlib.import_module(module)))
23
- names = set()
24
- for node in tree.body:
25
- if isinstance(node, ast.TypeAlias):
26
- names.add(node.name.id) # type: ignore
27
- elif isinstance(node, ast.Assign):
28
- names.update(t.id for t in node.targets if isinstance(t, ast.Name) and not t.id.startswith("_"))
29
- elif isinstance(node, ast.ClassDef):
30
- names.add(node.name)
31
- return names - {"__all__"}
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], aliases: set[str] = frozenset(), aliases_url: str = "") -> None: # type: ignore
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 aliases: The names of type aliases, linked to `aliases_url`.
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.aliases = aliases
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.aliases:
133
- url = self.aliases_url
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 _default(value: Any) -> tuple[str, str]:
66
+ def _ctor_args(value: Any) -> list[tuple[str | None, Any]] | None:
64
67
  """
65
- :returns: (text, color_class) The HTML text of a default value and the CSS class that colors it.
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
- if isinstance(value, bool):
68
- return str(value), "bool"
69
- if isinstance(value, str):
70
- return html.escape(repr(value)), "string" # Quotes and control characters as in the source.
71
- if isinstance(value, (int, float, tuple)):
72
- return html.escape(str(value)), "num"
73
- if callable(value):
74
- return html.escape(value.__name__), ""
75
- return html.escape(str(value)), ""
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
- aliases, aliases_url = set(), ""
103
- if project.type_aliases is not None:
104
- aliases = annotations.module_aliases(f"{project.package}.{project.type_aliases}")
105
- aliases_url = f"/doc/modules/{project.type_aliases}.html"
106
- self.link_types = annotations.Linker(class_urls, aliases, aliases_url)
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. Types and descriptions are the annotations and the docstrings below the attributes in the class body
126
- (or of its bases), and the return annotations and docstrings of the properties.
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
- return sorted(ret, key=lambda a: a.name)
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 or the value of an attribute.
158
- :returns: The HTML of the value. Parts are colored with the classes string, bool, num, class and func
159
- inside a `default_value` span.
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
- f'<span class="default_value"><span class="class">{html.escape(type(value).__name__)}</span>.'
164
- f'<span class="func">{html.escape(value.name)}</span></span>'
165
- )
166
- text, color_class = _default(value)
167
- return f'<span class="default_value{" " + color_class if color_class else ""}">{text}</span>'
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.format_value(a.value),
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
- doc = docstring.parse((func.__doc__ or entry.obj.__doc__) if is_class else inspect.getdoc(func))
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.format_value(p.default)
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 used as a reference target).
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
- modules.append(Module(file, f"/doc/modules/{file}.html", entries))
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
- and the module names (`qspec.analyze`).
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