sphinxext-bsb 0.2.1__tar.gz → 0.2.2__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.
Potentially problematic release.
This version of sphinxext-bsb might be problematic. Click here for more details.
- {sphinxext-bsb-0.2.1 → sphinxext-bsb-0.2.2}/LICENSE +21 -21
- {sphinxext-bsb-0.2.1/sphinxext_bsb.egg-info → sphinxext-bsb-0.2.2}/PKG-INFO +6 -5
- {sphinxext-bsb-0.2.1 → sphinxext-bsb-0.2.2}/README.md +2 -2
- {sphinxext-bsb-0.2.1 → sphinxext-bsb-0.2.2}/bsbdocs.py +376 -376
- {sphinxext-bsb-0.2.1 → sphinxext-bsb-0.2.2}/pyproject.toml +11 -11
- {sphinxext-bsb-0.2.1 → sphinxext-bsb-0.2.2}/setup.cfg +4 -4
- {sphinxext-bsb-0.2.1 → sphinxext-bsb-0.2.2/sphinxext_bsb.egg-info}/PKG-INFO +6 -5
- {sphinxext-bsb-0.2.1 → sphinxext-bsb-0.2.2}/sphinxext_bsb.egg-info/SOURCES.txt +0 -0
- {sphinxext-bsb-0.2.1 → sphinxext-bsb-0.2.2}/sphinxext_bsb.egg-info/dependency_links.txt +0 -0
- {sphinxext-bsb-0.2.1 → sphinxext-bsb-0.2.2}/sphinxext_bsb.egg-info/requires.txt +0 -0
- {sphinxext-bsb-0.2.1 → sphinxext-bsb-0.2.2}/sphinxext_bsb.egg-info/top_level.txt +0 -0
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2022 Robin De Schepper
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Robin De Schepper
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: sphinxext-bsb
|
|
3
|
-
Version: 0.2.
|
|
4
|
-
Summary: BSB Sphinx documentation extension
|
|
5
|
-
License-File: LICENSE
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: sphinxext-bsb
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: BSB Sphinx documentation extension
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Dist: sphinx-design
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
# sphinxext-bsb
|
|
2
|
-
Sphinx extension for the BSB documentation
|
|
1
|
+
# sphinxext-bsb
|
|
2
|
+
Sphinx extension for the BSB documentation
|
|
@@ -1,376 +1,376 @@
|
|
|
1
|
-
import itertools
|
|
2
|
-
from inspect import isclass
|
|
3
|
-
from types import FunctionType
|
|
4
|
-
|
|
5
|
-
import docutils.parsers.rst.directives
|
|
6
|
-
from bsb.config import get_config_attributes
|
|
7
|
-
from bsb.config._make import MISSING
|
|
8
|
-
from bsb.config._attrs import (
|
|
9
|
-
ConfigurationListAttribute,
|
|
10
|
-
ConfigurationDictAttribute,
|
|
11
|
-
)
|
|
12
|
-
from bsb.config.parsers import
|
|
13
|
-
from bsb.config.types import class_
|
|
14
|
-
from docutils import nodes
|
|
15
|
-
from docutils.parsers.rst import Directive
|
|
16
|
-
from docutils.statemachine import StringList
|
|
17
|
-
from sphinx.util.docutils import SphinxDirective
|
|
18
|
-
|
|
19
|
-
__version__ = "0.2.1"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def example_function():
|
|
23
|
-
pass
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
_example_values = {
|
|
27
|
-
bool: True,
|
|
28
|
-
list: [],
|
|
29
|
-
str: "example",
|
|
30
|
-
int: 42,
|
|
31
|
-
float: 3.14,
|
|
32
|
-
FunctionType: "my_module.my_function",
|
|
33
|
-
dict: {},
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
class ComponentIntro(Directive):
|
|
38
|
-
has_content = False
|
|
39
|
-
|
|
40
|
-
def run(self):
|
|
41
|
-
# Use the state machine to generate a block quote for us and parse our text :)
|
|
42
|
-
return self.state.block_quote(
|
|
43
|
-
StringList(
|
|
44
|
-
[
|
|
45
|
-
":octicon:`light-bulb;1em;sd-text-info` New to components?"
|
|
46
|
-
+ " Write your first one with :doc:`our guide </guides/components>`"
|
|
47
|
-
]
|
|
48
|
-
),
|
|
49
|
-
self.content_offset,
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
class AutoconfigNode(nodes.General, nodes.Element):
|
|
54
|
-
pass
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def visit_autoconfig_node(self, node):
|
|
58
|
-
pass
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def depart_autoconfig_node(self, node):
|
|
62
|
-
pass
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
class AutoconfigDirective(SphinxDirective):
|
|
66
|
-
required_arguments = 1
|
|
67
|
-
has_content = False
|
|
68
|
-
option_spec = {
|
|
69
|
-
"no-imports": docutils.parsers.rst.directives.flag,
|
|
70
|
-
"max-depth": docutils.parsers.rst.directives.positive_int,
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
def run(self):
|
|
74
|
-
clsref = self.arguments[0]
|
|
75
|
-
cls = class_()(clsref)
|
|
76
|
-
max_depth = self.options.get("max-depth", 100)
|
|
77
|
-
tree = self.guess_example(cls, max_depth)
|
|
78
|
-
elem = AutoconfigNode()
|
|
79
|
-
self.state.nested_parse(
|
|
80
|
-
StringList(
|
|
81
|
-
[
|
|
82
|
-
".. tab-set-code::",
|
|
83
|
-
"",
|
|
84
|
-
" .. code-block:: Python",
|
|
85
|
-
"",
|
|
86
|
-
*self.get_python_tab(
|
|
87
|
-
cls, tree, "no-imports" not in self.options, max_depth
|
|
88
|
-
),
|
|
89
|
-
"",
|
|
90
|
-
*itertools.chain.from_iterable(
|
|
91
|
-
self.get_parser_lines(key, parser(), tree)
|
|
92
|
-
for key, parser in
|
|
93
|
-
),
|
|
94
|
-
]
|
|
95
|
-
),
|
|
96
|
-
self.content_offset,
|
|
97
|
-
elem,
|
|
98
|
-
)
|
|
99
|
-
return [elem]
|
|
100
|
-
|
|
101
|
-
def get_python_tab(self, cls, tree, imports, max_depth):
|
|
102
|
-
lines = [
|
|
103
|
-
*(
|
|
104
|
-
f" {imp}"
|
|
105
|
-
for imp in self.get_import_lines(cls, max_depth if imports else 0)
|
|
106
|
-
),
|
|
107
|
-
"",
|
|
108
|
-
*(f" {line}" for line in self.get_python_lines(cls, tree)),
|
|
109
|
-
]
|
|
110
|
-
return self.collapse_empties(lines)
|
|
111
|
-
|
|
112
|
-
def collapse_empties(self, lines, chars=[("(", ")"), ("[", "]"), ("{", "}")]):
|
|
113
|
-
outlines = []
|
|
114
|
-
skip = False
|
|
115
|
-
for i in range(len(lines)):
|
|
116
|
-
if skip:
|
|
117
|
-
skip = False
|
|
118
|
-
continue
|
|
119
|
-
line1 = lines[i]
|
|
120
|
-
if i == len(lines) - 1:
|
|
121
|
-
outlines.append(line1)
|
|
122
|
-
break
|
|
123
|
-
line2 = lines[i + 1]
|
|
124
|
-
for schar, echar in chars:
|
|
125
|
-
if line1.endswith(schar) and line2.strip().startswith(echar):
|
|
126
|
-
outlines.append(line1 + f"{echar},")
|
|
127
|
-
skip = True
|
|
128
|
-
break
|
|
129
|
-
else:
|
|
130
|
-
outlines.append(line1)
|
|
131
|
-
return outlines
|
|
132
|
-
|
|
133
|
-
def guess_example(self, cls, deeper):
|
|
134
|
-
if deeper == 0:
|
|
135
|
-
return ...
|
|
136
|
-
attrs = get_config_attributes(cls)
|
|
137
|
-
tree = {
|
|
138
|
-
attr.attr_name: self.guess_default(attr, deeper) for attr in attrs.values()
|
|
139
|
-
}
|
|
140
|
-
return tree
|
|
141
|
-
|
|
142
|
-
def guess_default(self, attr, deeper):
|
|
143
|
-
"""
|
|
144
|
-
Guess a default value/structure for the given attribute. Defaults are paired in
|
|
145
|
-
tuples with functions that can unpack the tree value to their Python
|
|
146
|
-
representation. The most basic "argument unpacker" exemplifies this, and turns
|
|
147
|
-
the value ``x`` into ``f"{key}={repr(x)}"``.
|
|
148
|
-
|
|
149
|
-
:param attr:
|
|
150
|
-
:return:
|
|
151
|
-
"""
|
|
152
|
-
type_ = self.get_attr_type(attr)
|
|
153
|
-
|
|
154
|
-
# If the attribute is a node type, we have to guess recursively.
|
|
155
|
-
if attr.is_node_type():
|
|
156
|
-
# Node types that come from private modules shouldn't be promoted,
|
|
157
|
-
# so instead we make use of the dictionary notation. Or, this autoconfig
|
|
158
|
-
# has the "no-imports" option set, which disables the prepended import stmnt
|
|
159
|
-
if "no-imports" in self.options or any(
|
|
160
|
-
m.startswith("_") for m in type_.__module__.split(".")
|
|
161
|
-
):
|
|
162
|
-
untree = self.private_untree(type_)
|
|
163
|
-
else:
|
|
164
|
-
untree = self.public_untree(type_)
|
|
165
|
-
value = self.guess_example(type_, deeper - 1)
|
|
166
|
-
else:
|
|
167
|
-
untree = self.argument_untree
|
|
168
|
-
value = self.guess_example_value(attr)
|
|
169
|
-
# Configuration lists and dicts should be packed into a list/dict
|
|
170
|
-
if isinstance(attr, ConfigurationListAttribute):
|
|
171
|
-
untree = self.list_untree(untree)
|
|
172
|
-
elif isinstance(attr, ConfigurationDictAttribute):
|
|
173
|
-
untree = self.dict_untree(untree)
|
|
174
|
-
return untree, value
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
def guess_example_value(self, attr):
|
|
178
|
-
type_ = self.get_attr_type(attr)
|
|
179
|
-
# The attribute may have a hinted example from the declaration `hint` kwarg,
|
|
180
|
-
# or from the `__hint__` method of its type handler
|
|
181
|
-
hint = attr.get_hint()
|
|
182
|
-
if hint is not MISSING:
|
|
183
|
-
example = hint
|
|
184
|
-
else:
|
|
185
|
-
# No hint, so check if the default is sensible
|
|
186
|
-
default = attr.get_default()
|
|
187
|
-
example = None
|
|
188
|
-
if default is not None:
|
|
189
|
-
example = default
|
|
190
|
-
elif isclass(type_):
|
|
191
|
-
# No default value, and likely a primitive was passed as type handler
|
|
192
|
-
for parent_type, value in _example_values.items():
|
|
193
|
-
# Loop over some basic possible basic primitives
|
|
194
|
-
if issubclass(type_, parent_type):
|
|
195
|
-
example = value
|
|
196
|
-
break
|
|
197
|
-
if example is None:
|
|
198
|
-
# Try to have the type handler cast some primitive examples,
|
|
199
|
-
# if no error is raised we assume it's a good example
|
|
200
|
-
example = self.try_types(type_, *_example_values.values())
|
|
201
|
-
# `str` is a kind of greedy type handler, so correct the casts
|
|
202
|
-
if example == "[]":
|
|
203
|
-
example = []
|
|
204
|
-
elif example == "{}":
|
|
205
|
-
example = {}
|
|
206
|
-
elif example == "true":
|
|
207
|
-
example = True
|
|
208
|
-
# Some values need to be cast back to a tree-form, so we create a shim
|
|
209
|
-
# for the attribute descriptor to use as an instance.
|
|
210
|
-
shim = type("AttrShim", (), {})()
|
|
211
|
-
setattr(shim, f"_{attr.attr_name}", example)
|
|
212
|
-
try:
|
|
213
|
-
example = attr.tree(shim)
|
|
214
|
-
except Exception:
|
|
215
|
-
pass
|
|
216
|
-
# Hope we have a default value.
|
|
217
|
-
return example
|
|
218
|
-
|
|
219
|
-
def get_attr_type(self, attr):
|
|
220
|
-
type_ = attr.get_type()
|
|
221
|
-
type_ = str if type_ is None else type_
|
|
222
|
-
return type_
|
|
223
|
-
|
|
224
|
-
def try_types(self, type_, arg, *args):
|
|
225
|
-
try:
|
|
226
|
-
return type_(arg)
|
|
227
|
-
except Exception:
|
|
228
|
-
if args:
|
|
229
|
-
return self.try_types(type_, *args)
|
|
230
|
-
|
|
231
|
-
def get_python_lines(self, cls, tree):
|
|
232
|
-
return [
|
|
233
|
-
f"{cls.__name__}(",
|
|
234
|
-
*(
|
|
235
|
-
f" {line}"
|
|
236
|
-
for line in itertools.chain.from_iterable(
|
|
237
|
-
self.get_argument_lines(key, *value) for key, value in tree.items()
|
|
238
|
-
)
|
|
239
|
-
),
|
|
240
|
-
")",
|
|
241
|
-
]
|
|
242
|
-
|
|
243
|
-
def get_argument_lines(self, key, untree, value):
|
|
244
|
-
return untree(key, value)
|
|
245
|
-
|
|
246
|
-
def get_parser_lines(self, name, parser, tree):
|
|
247
|
-
raw = self.raw_untree((None, tree))
|
|
248
|
-
language = getattr(parser, "data_syntax", False) or name
|
|
249
|
-
return [
|
|
250
|
-
f" .. code-block:: {language}",
|
|
251
|
-
"",
|
|
252
|
-
*(
|
|
253
|
-
f" {line}"
|
|
254
|
-
for line in parser.generate(raw, pretty=True).split("\n")
|
|
255
|
-
),
|
|
256
|
-
"",
|
|
257
|
-
]
|
|
258
|
-
|
|
259
|
-
def raw_untree(self, tree):
|
|
260
|
-
try:
|
|
261
|
-
node_data = tree[1]
|
|
262
|
-
except TypeError:
|
|
263
|
-
# If the element wasn't packed as a tuple it's just a list/dict attr
|
|
264
|
-
return tree
|
|
265
|
-
list_repack = getattr(tree[0], "list_repack", False)
|
|
266
|
-
dict_repack = getattr(tree[0], "dict_repack", False)
|
|
267
|
-
if node_data is ...:
|
|
268
|
-
if list_repack:
|
|
269
|
-
return []
|
|
270
|
-
else:
|
|
271
|
-
return {}
|
|
272
|
-
if dict_repack:
|
|
273
|
-
node_data = {"name_of_the_thing": (None, node_data)}
|
|
274
|
-
if list_repack:
|
|
275
|
-
node_data = [(None, node_data)]
|
|
276
|
-
if isinstance(node_data, dict):
|
|
277
|
-
return {k: self.raw_untree(v) for k, v in node_data.items()}
|
|
278
|
-
elif isinstance(node_data, list):
|
|
279
|
-
return [self.raw_untree(v) for v in node_data]
|
|
280
|
-
else:
|
|
281
|
-
return node_data
|
|
282
|
-
|
|
283
|
-
def public_untree(self, cls):
|
|
284
|
-
def untree(key, value):
|
|
285
|
-
if value is ...:
|
|
286
|
-
lines = self.get_python_lines(cls, {})
|
|
287
|
-
else:
|
|
288
|
-
lines = self.get_python_lines(cls, value)
|
|
289
|
-
lines[0] = f"{key}={lines[0]}"
|
|
290
|
-
lines[-1] += ","
|
|
291
|
-
return lines
|
|
292
|
-
|
|
293
|
-
return untree
|
|
294
|
-
|
|
295
|
-
def private_untree(self, cls):
|
|
296
|
-
def untree(key, value):
|
|
297
|
-
if value is ...:
|
|
298
|
-
lines = self.get_python_lines(cls, {})
|
|
299
|
-
else:
|
|
300
|
-
lines = self.get_python_lines(cls, value)
|
|
301
|
-
lines[0] = key + "={"
|
|
302
|
-
for i in range(1, len(lines) - 1):
|
|
303
|
-
line = lines[i]
|
|
304
|
-
if "=" in line:
|
|
305
|
-
lp = line.split("=")
|
|
306
|
-
indent = len(lp[0]) - len(lp[0].lstrip())
|
|
307
|
-
lines[i] = f"{' ' * indent}'{lp[0].lstrip()}': " + "=".join(lp[1:])
|
|
308
|
-
lines[-1] = "},"
|
|
309
|
-
return lines
|
|
310
|
-
|
|
311
|
-
return untree
|
|
312
|
-
|
|
313
|
-
def dict_untree(self, inner_untree):
|
|
314
|
-
def untree(key, value):
|
|
315
|
-
lines = inner_untree(key, value)
|
|
316
|
-
return lines
|
|
317
|
-
|
|
318
|
-
untree.dict_repack = True
|
|
319
|
-
return untree
|
|
320
|
-
|
|
321
|
-
def list_untree(self, inner_untree):
|
|
322
|
-
def untree(key, value):
|
|
323
|
-
if value is ...:
|
|
324
|
-
return [f"{key}=[", "],"]
|
|
325
|
-
lines = inner_untree(key, value)
|
|
326
|
-
lines[0] = lines[0].split("=")[0] + "=["
|
|
327
|
-
lines.insert(1, " {")
|
|
328
|
-
lines[2:] = [" " + line for line in lines[2:]]
|
|
329
|
-
lines.append("],")
|
|
330
|
-
return lines
|
|
331
|
-
|
|
332
|
-
untree.list_repack = True
|
|
333
|
-
return untree
|
|
334
|
-
|
|
335
|
-
def argument_untree(self, key, value):
|
|
336
|
-
return [f"{key}={repr(value)},"]
|
|
337
|
-
|
|
338
|
-
def get_imports(self, cls, deeper):
|
|
339
|
-
imports = {}
|
|
340
|
-
if not any(m.startswith("_") for m in cls.__module__.split(".")):
|
|
341
|
-
imports.setdefault(cls.__module__, set()).add(cls.__name__)
|
|
342
|
-
if deeper > 0:
|
|
343
|
-
for attr in get_config_attributes(cls).values():
|
|
344
|
-
if attr.is_node_type():
|
|
345
|
-
for k, v in self.get_imports(attr.get_type(), deeper - 1).items():
|
|
346
|
-
imports.setdefault(k, set()).update(v)
|
|
347
|
-
|
|
348
|
-
return imports
|
|
349
|
-
|
|
350
|
-
def get_import_lines(self, cls, depth):
|
|
351
|
-
return [
|
|
352
|
-
f"from {key} import {', '.join(value)}"
|
|
353
|
-
for key, value in self.get_imports(cls, depth).items()
|
|
354
|
-
]
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
def setup(app):
|
|
358
|
-
if "sphinx_design" not in app.extensions:
|
|
359
|
-
from sphinx_design import setup as sphinx_design_setup
|
|
360
|
-
|
|
361
|
-
sphinx_design_setup(app)
|
|
362
|
-
|
|
363
|
-
app.add_node(
|
|
364
|
-
AutoconfigNode,
|
|
365
|
-
html=(visit_autoconfig_node, depart_autoconfig_node),
|
|
366
|
-
latex=(visit_autoconfig_node, depart_autoconfig_node),
|
|
367
|
-
text=(visit_autoconfig_node, depart_autoconfig_node),
|
|
368
|
-
)
|
|
369
|
-
app.add_directive("bsb_component_intro", ComponentIntro)
|
|
370
|
-
app.add_directive("autoconfig", AutoconfigDirective)
|
|
371
|
-
|
|
372
|
-
return {
|
|
373
|
-
"version": __version__,
|
|
374
|
-
"parallel_read_safe": True,
|
|
375
|
-
"parallel_write_safe": True,
|
|
376
|
-
}
|
|
1
|
+
import itertools
|
|
2
|
+
from inspect import isclass
|
|
3
|
+
from types import FunctionType
|
|
4
|
+
|
|
5
|
+
import docutils.parsers.rst.directives
|
|
6
|
+
from bsb.config import get_config_attributes
|
|
7
|
+
from bsb.config._make import MISSING
|
|
8
|
+
from bsb.config._attrs import (
|
|
9
|
+
ConfigurationListAttribute,
|
|
10
|
+
ConfigurationDictAttribute,
|
|
11
|
+
)
|
|
12
|
+
from bsb.config.parsers import get_configuration_parser_classes
|
|
13
|
+
from bsb.config.types import class_
|
|
14
|
+
from docutils import nodes
|
|
15
|
+
from docutils.parsers.rst import Directive
|
|
16
|
+
from docutils.statemachine import StringList
|
|
17
|
+
from sphinx.util.docutils import SphinxDirective
|
|
18
|
+
|
|
19
|
+
__version__ = "0.2.1"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def example_function():
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_example_values = {
|
|
27
|
+
bool: True,
|
|
28
|
+
list: [],
|
|
29
|
+
str: "example",
|
|
30
|
+
int: 42,
|
|
31
|
+
float: 3.14,
|
|
32
|
+
FunctionType: "my_module.my_function",
|
|
33
|
+
dict: {},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ComponentIntro(Directive):
|
|
38
|
+
has_content = False
|
|
39
|
+
|
|
40
|
+
def run(self):
|
|
41
|
+
# Use the state machine to generate a block quote for us and parse our text :)
|
|
42
|
+
return self.state.block_quote(
|
|
43
|
+
StringList(
|
|
44
|
+
[
|
|
45
|
+
":octicon:`light-bulb;1em;sd-text-info` New to components?"
|
|
46
|
+
+ " Write your first one with :doc:`our guide </guides/components>`"
|
|
47
|
+
]
|
|
48
|
+
),
|
|
49
|
+
self.content_offset,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class AutoconfigNode(nodes.General, nodes.Element):
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def visit_autoconfig_node(self, node):
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def depart_autoconfig_node(self, node):
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class AutoconfigDirective(SphinxDirective):
|
|
66
|
+
required_arguments = 1
|
|
67
|
+
has_content = False
|
|
68
|
+
option_spec = {
|
|
69
|
+
"no-imports": docutils.parsers.rst.directives.flag,
|
|
70
|
+
"max-depth": docutils.parsers.rst.directives.positive_int,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
def run(self):
|
|
74
|
+
clsref = self.arguments[0]
|
|
75
|
+
cls = class_()(clsref)
|
|
76
|
+
max_depth = self.options.get("max-depth", 100)
|
|
77
|
+
tree = self.guess_example(cls, max_depth)
|
|
78
|
+
elem = AutoconfigNode()
|
|
79
|
+
self.state.nested_parse(
|
|
80
|
+
StringList(
|
|
81
|
+
[
|
|
82
|
+
".. tab-set-code::",
|
|
83
|
+
"",
|
|
84
|
+
" .. code-block:: Python",
|
|
85
|
+
"",
|
|
86
|
+
*self.get_python_tab(
|
|
87
|
+
cls, tree, "no-imports" not in self.options, max_depth
|
|
88
|
+
),
|
|
89
|
+
"",
|
|
90
|
+
*itertools.chain.from_iterable(
|
|
91
|
+
self.get_parser_lines(key, parser(), tree)
|
|
92
|
+
for key, parser in get_configuration_parser_classes().items()
|
|
93
|
+
),
|
|
94
|
+
]
|
|
95
|
+
),
|
|
96
|
+
self.content_offset,
|
|
97
|
+
elem,
|
|
98
|
+
)
|
|
99
|
+
return [elem]
|
|
100
|
+
|
|
101
|
+
def get_python_tab(self, cls, tree, imports, max_depth):
|
|
102
|
+
lines = [
|
|
103
|
+
*(
|
|
104
|
+
f" {imp}"
|
|
105
|
+
for imp in self.get_import_lines(cls, max_depth if imports else 0)
|
|
106
|
+
),
|
|
107
|
+
"",
|
|
108
|
+
*(f" {line}" for line in self.get_python_lines(cls, tree)),
|
|
109
|
+
]
|
|
110
|
+
return self.collapse_empties(lines)
|
|
111
|
+
|
|
112
|
+
def collapse_empties(self, lines, chars=[("(", ")"), ("[", "]"), ("{", "}")]):
|
|
113
|
+
outlines = []
|
|
114
|
+
skip = False
|
|
115
|
+
for i in range(len(lines)):
|
|
116
|
+
if skip:
|
|
117
|
+
skip = False
|
|
118
|
+
continue
|
|
119
|
+
line1 = lines[i]
|
|
120
|
+
if i == len(lines) - 1:
|
|
121
|
+
outlines.append(line1)
|
|
122
|
+
break
|
|
123
|
+
line2 = lines[i + 1]
|
|
124
|
+
for schar, echar in chars:
|
|
125
|
+
if line1.endswith(schar) and line2.strip().startswith(echar):
|
|
126
|
+
outlines.append(line1 + f"{echar},")
|
|
127
|
+
skip = True
|
|
128
|
+
break
|
|
129
|
+
else:
|
|
130
|
+
outlines.append(line1)
|
|
131
|
+
return outlines
|
|
132
|
+
|
|
133
|
+
def guess_example(self, cls, deeper):
|
|
134
|
+
if deeper == 0:
|
|
135
|
+
return ...
|
|
136
|
+
attrs = get_config_attributes(cls)
|
|
137
|
+
tree = {
|
|
138
|
+
attr.attr_name: self.guess_default(attr, deeper) for attr in attrs.values()
|
|
139
|
+
}
|
|
140
|
+
return tree
|
|
141
|
+
|
|
142
|
+
def guess_default(self, attr, deeper):
|
|
143
|
+
"""
|
|
144
|
+
Guess a default value/structure for the given attribute. Defaults are paired in
|
|
145
|
+
tuples with functions that can unpack the tree value to their Python
|
|
146
|
+
representation. The most basic "argument unpacker" exemplifies this, and turns
|
|
147
|
+
the value ``x`` into ``f"{key}={repr(x)}"``.
|
|
148
|
+
|
|
149
|
+
:param attr:
|
|
150
|
+
:return:
|
|
151
|
+
"""
|
|
152
|
+
type_ = self.get_attr_type(attr)
|
|
153
|
+
|
|
154
|
+
# If the attribute is a node type, we have to guess recursively.
|
|
155
|
+
if attr.is_node_type():
|
|
156
|
+
# Node types that come from private modules shouldn't be promoted,
|
|
157
|
+
# so instead we make use of the dictionary notation. Or, this autoconfig
|
|
158
|
+
# has the "no-imports" option set, which disables the prepended import stmnt
|
|
159
|
+
if "no-imports" in self.options or any(
|
|
160
|
+
m.startswith("_") for m in type_.__module__.split(".")
|
|
161
|
+
):
|
|
162
|
+
untree = self.private_untree(type_)
|
|
163
|
+
else:
|
|
164
|
+
untree = self.public_untree(type_)
|
|
165
|
+
value = self.guess_example(type_, deeper - 1)
|
|
166
|
+
else:
|
|
167
|
+
untree = self.argument_untree
|
|
168
|
+
value = self.guess_example_value(attr)
|
|
169
|
+
# Configuration lists and dicts should be packed into a list/dict
|
|
170
|
+
if isinstance(attr, ConfigurationListAttribute):
|
|
171
|
+
untree = self.list_untree(untree)
|
|
172
|
+
elif isinstance(attr, ConfigurationDictAttribute):
|
|
173
|
+
untree = self.dict_untree(untree)
|
|
174
|
+
return untree, value
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def guess_example_value(self, attr):
|
|
178
|
+
type_ = self.get_attr_type(attr)
|
|
179
|
+
# The attribute may have a hinted example from the declaration `hint` kwarg,
|
|
180
|
+
# or from the `__hint__` method of its type handler
|
|
181
|
+
hint = attr.get_hint()
|
|
182
|
+
if hint is not MISSING:
|
|
183
|
+
example = hint
|
|
184
|
+
else:
|
|
185
|
+
# No hint, so check if the default is sensible
|
|
186
|
+
default = attr.get_default()
|
|
187
|
+
example = None
|
|
188
|
+
if default is not None:
|
|
189
|
+
example = default
|
|
190
|
+
elif isclass(type_):
|
|
191
|
+
# No default value, and likely a primitive was passed as type handler
|
|
192
|
+
for parent_type, value in _example_values.items():
|
|
193
|
+
# Loop over some basic possible basic primitives
|
|
194
|
+
if issubclass(type_, parent_type):
|
|
195
|
+
example = value
|
|
196
|
+
break
|
|
197
|
+
if example is None:
|
|
198
|
+
# Try to have the type handler cast some primitive examples,
|
|
199
|
+
# if no error is raised we assume it's a good example
|
|
200
|
+
example = self.try_types(type_, *_example_values.values())
|
|
201
|
+
# `str` is a kind of greedy type handler, so correct the casts
|
|
202
|
+
if example == "[]":
|
|
203
|
+
example = []
|
|
204
|
+
elif example == "{}":
|
|
205
|
+
example = {}
|
|
206
|
+
elif example == "true":
|
|
207
|
+
example = True
|
|
208
|
+
# Some values need to be cast back to a tree-form, so we create a shim
|
|
209
|
+
# for the attribute descriptor to use as an instance.
|
|
210
|
+
shim = type("AttrShim", (), {})()
|
|
211
|
+
setattr(shim, f"_{attr.attr_name}", example)
|
|
212
|
+
try:
|
|
213
|
+
example = attr.tree(shim)
|
|
214
|
+
except Exception:
|
|
215
|
+
pass
|
|
216
|
+
# Hope we have a default value.
|
|
217
|
+
return example
|
|
218
|
+
|
|
219
|
+
def get_attr_type(self, attr):
|
|
220
|
+
type_ = attr.get_type()
|
|
221
|
+
type_ = str if type_ is None else type_
|
|
222
|
+
return type_
|
|
223
|
+
|
|
224
|
+
def try_types(self, type_, arg, *args):
|
|
225
|
+
try:
|
|
226
|
+
return type_(arg)
|
|
227
|
+
except Exception:
|
|
228
|
+
if args:
|
|
229
|
+
return self.try_types(type_, *args)
|
|
230
|
+
|
|
231
|
+
def get_python_lines(self, cls, tree):
|
|
232
|
+
return [
|
|
233
|
+
f"{cls.__name__}(",
|
|
234
|
+
*(
|
|
235
|
+
f" {line}"
|
|
236
|
+
for line in itertools.chain.from_iterable(
|
|
237
|
+
self.get_argument_lines(key, *value) for key, value in tree.items()
|
|
238
|
+
)
|
|
239
|
+
),
|
|
240
|
+
")",
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
def get_argument_lines(self, key, untree, value):
|
|
244
|
+
return untree(key, value)
|
|
245
|
+
|
|
246
|
+
def get_parser_lines(self, name, parser, tree):
|
|
247
|
+
raw = self.raw_untree((None, tree))
|
|
248
|
+
language = getattr(parser, "data_syntax", False) or name
|
|
249
|
+
return [
|
|
250
|
+
f" .. code-block:: {language}",
|
|
251
|
+
"",
|
|
252
|
+
*(
|
|
253
|
+
f" {line}"
|
|
254
|
+
for line in parser.generate(raw, pretty=True).split("\n")
|
|
255
|
+
),
|
|
256
|
+
"",
|
|
257
|
+
]
|
|
258
|
+
|
|
259
|
+
def raw_untree(self, tree):
|
|
260
|
+
try:
|
|
261
|
+
node_data = tree[1]
|
|
262
|
+
except TypeError:
|
|
263
|
+
# If the element wasn't packed as a tuple it's just a list/dict attr
|
|
264
|
+
return tree
|
|
265
|
+
list_repack = getattr(tree[0], "list_repack", False)
|
|
266
|
+
dict_repack = getattr(tree[0], "dict_repack", False)
|
|
267
|
+
if node_data is ...:
|
|
268
|
+
if list_repack:
|
|
269
|
+
return []
|
|
270
|
+
else:
|
|
271
|
+
return {}
|
|
272
|
+
if dict_repack:
|
|
273
|
+
node_data = {"name_of_the_thing": (None, node_data)}
|
|
274
|
+
if list_repack:
|
|
275
|
+
node_data = [(None, node_data)]
|
|
276
|
+
if isinstance(node_data, dict):
|
|
277
|
+
return {k: self.raw_untree(v) for k, v in node_data.items()}
|
|
278
|
+
elif isinstance(node_data, list):
|
|
279
|
+
return [self.raw_untree(v) for v in node_data]
|
|
280
|
+
else:
|
|
281
|
+
return node_data
|
|
282
|
+
|
|
283
|
+
def public_untree(self, cls):
|
|
284
|
+
def untree(key, value):
|
|
285
|
+
if value is ...:
|
|
286
|
+
lines = self.get_python_lines(cls, {})
|
|
287
|
+
else:
|
|
288
|
+
lines = self.get_python_lines(cls, value)
|
|
289
|
+
lines[0] = f"{key}={lines[0]}"
|
|
290
|
+
lines[-1] += ","
|
|
291
|
+
return lines
|
|
292
|
+
|
|
293
|
+
return untree
|
|
294
|
+
|
|
295
|
+
def private_untree(self, cls):
|
|
296
|
+
def untree(key, value):
|
|
297
|
+
if value is ...:
|
|
298
|
+
lines = self.get_python_lines(cls, {})
|
|
299
|
+
else:
|
|
300
|
+
lines = self.get_python_lines(cls, value)
|
|
301
|
+
lines[0] = key + "={"
|
|
302
|
+
for i in range(1, len(lines) - 1):
|
|
303
|
+
line = lines[i]
|
|
304
|
+
if "=" in line:
|
|
305
|
+
lp = line.split("=")
|
|
306
|
+
indent = len(lp[0]) - len(lp[0].lstrip())
|
|
307
|
+
lines[i] = f"{' ' * indent}'{lp[0].lstrip()}': " + "=".join(lp[1:])
|
|
308
|
+
lines[-1] = "},"
|
|
309
|
+
return lines
|
|
310
|
+
|
|
311
|
+
return untree
|
|
312
|
+
|
|
313
|
+
def dict_untree(self, inner_untree):
|
|
314
|
+
def untree(key, value):
|
|
315
|
+
lines = inner_untree(key, value)
|
|
316
|
+
return lines
|
|
317
|
+
|
|
318
|
+
untree.dict_repack = True
|
|
319
|
+
return untree
|
|
320
|
+
|
|
321
|
+
def list_untree(self, inner_untree):
|
|
322
|
+
def untree(key, value):
|
|
323
|
+
if value is ...:
|
|
324
|
+
return [f"{key}=[", "],"]
|
|
325
|
+
lines = inner_untree(key, value)
|
|
326
|
+
lines[0] = lines[0].split("=")[0] + "=["
|
|
327
|
+
lines.insert(1, " {")
|
|
328
|
+
lines[2:] = [" " + line for line in lines[2:]]
|
|
329
|
+
lines.append("],")
|
|
330
|
+
return lines
|
|
331
|
+
|
|
332
|
+
untree.list_repack = True
|
|
333
|
+
return untree
|
|
334
|
+
|
|
335
|
+
def argument_untree(self, key, value):
|
|
336
|
+
return [f"{key}={repr(value)},"]
|
|
337
|
+
|
|
338
|
+
def get_imports(self, cls, deeper):
|
|
339
|
+
imports = {}
|
|
340
|
+
if not any(m.startswith("_") for m in cls.__module__.split(".")):
|
|
341
|
+
imports.setdefault(cls.__module__, set()).add(cls.__name__)
|
|
342
|
+
if deeper > 0:
|
|
343
|
+
for attr in get_config_attributes(cls).values():
|
|
344
|
+
if attr.is_node_type():
|
|
345
|
+
for k, v in self.get_imports(attr.get_type(), deeper - 1).items():
|
|
346
|
+
imports.setdefault(k, set()).update(v)
|
|
347
|
+
|
|
348
|
+
return imports
|
|
349
|
+
|
|
350
|
+
def get_import_lines(self, cls, depth):
|
|
351
|
+
return [
|
|
352
|
+
f"from {key} import {', '.join(value)}"
|
|
353
|
+
for key, value in self.get_imports(cls, depth).items()
|
|
354
|
+
]
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def setup(app):
|
|
358
|
+
if "sphinx_design" not in app.extensions:
|
|
359
|
+
from sphinx_design import setup as sphinx_design_setup
|
|
360
|
+
|
|
361
|
+
sphinx_design_setup(app)
|
|
362
|
+
|
|
363
|
+
app.add_node(
|
|
364
|
+
AutoconfigNode,
|
|
365
|
+
html=(visit_autoconfig_node, depart_autoconfig_node),
|
|
366
|
+
latex=(visit_autoconfig_node, depart_autoconfig_node),
|
|
367
|
+
text=(visit_autoconfig_node, depart_autoconfig_node),
|
|
368
|
+
)
|
|
369
|
+
app.add_directive("bsb_component_intro", ComponentIntro)
|
|
370
|
+
app.add_directive("autoconfig", AutoconfigDirective)
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
"version": __version__,
|
|
374
|
+
"parallel_read_safe": True,
|
|
375
|
+
"parallel_write_safe": True,
|
|
376
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["setuptools"]
|
|
3
|
-
build-backend = "setuptools.build_meta"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "sphinxext-bsb"
|
|
7
|
-
description = "BSB Sphinx documentation extension"
|
|
8
|
-
version = "0.2.
|
|
9
|
-
dependencies = [
|
|
10
|
-
"sphinx-design"
|
|
11
|
-
]
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sphinxext-bsb"
|
|
7
|
+
description = "BSB Sphinx documentation extension"
|
|
8
|
+
version = "0.2.2"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"sphinx-design"
|
|
11
|
+
]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
[egg_info]
|
|
2
|
-
tag_build =
|
|
3
|
-
tag_date = 0
|
|
4
|
-
|
|
1
|
+
[egg_info]
|
|
2
|
+
tag_build =
|
|
3
|
+
tag_date = 0
|
|
4
|
+
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: sphinxext-bsb
|
|
3
|
-
Version: 0.2.
|
|
4
|
-
Summary: BSB Sphinx documentation extension
|
|
5
|
-
License-File: LICENSE
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: sphinxext-bsb
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: BSB Sphinx documentation extension
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Dist: sphinx-design
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|