nodebpy 520.8.0__tar.gz → 520.10.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.
- {nodebpy-520.8.0 → nodebpy-520.10.0}/PKG-INFO +1 -1
- {nodebpy-520.8.0 → nodebpy-520.10.0}/pyproject.toml +1 -1
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/assets/__main__.py +15 -2
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/assets/_codegen.py +163 -42
- nodebpy-520.10.0/src/nodebpy/nodes/compositor/assets.py +1433 -0
- nodebpy-520.10.0/src/nodebpy/nodes/geometry/assets.py +6538 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/shader/assets.py +108 -16
- nodebpy-520.8.0/src/nodebpy/nodes/compositor/assets.py +0 -783
- nodebpy-520.8.0/src/nodebpy/nodes/geometry/assets.py +0 -3296
- {nodebpy-520.8.0 → nodebpy-520.10.0}/README.md +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/__init__.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/assets/__init__.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/builder/__init__.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/builder/_registry.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/builder/_utils.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/builder/accessor.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/builder/arrange.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/builder/asset.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/builder/items.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/builder/mixins.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/builder/node.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/builder/socket.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/builder/tree.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/export/__init__.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/export/codegen.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/export/diagram.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/export/web_render.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/lib/nodearrange/__init__.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/lib/nodearrange/arrange/__init__.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/lib/nodearrange/arrange/graph.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/lib/nodearrange/arrange/ordering.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/lib/nodearrange/arrange/ranking.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/lib/nodearrange/arrange/realize.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/lib/nodearrange/arrange/stacking.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/lib/nodearrange/arrange/structs.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/lib/nodearrange/arrange/sugiyama.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/lib/nodearrange/arrange/x_coords.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/lib/nodearrange/arrange/y_coords.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/lib/nodearrange/config.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/lib/nodearrange/utils.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/__init__.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/_mixins.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/compositor/__init__.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/compositor/color.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/compositor/converter.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/compositor/distort.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/compositor/filter.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/compositor/group.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/compositor/input.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/compositor/interface.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/compositor/manual.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/compositor/matte.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/compositor/output.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/compositor/vector.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/__init__.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/attribute.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/color.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/converter.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/geometry.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/grid.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/group.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/groups.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/input.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/interface.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/manual.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/output.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/texture.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/utilities.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/vector.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/zone.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/shader/__init__.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/shader/color.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/shader/converter.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/shader/grid.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/shader/group.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/shader/input.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/shader/manual.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/shader/output.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/shader/script.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/shader/shader.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/shader/texture.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/shader/vector.py +0 -0
- {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/types.py +0 -0
|
@@ -31,7 +31,7 @@ _ESSENTIALS: dict[str, tuple[str, ...]] = {
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def generate_essentials(
|
|
34
|
-
nodes_dir: Path, nodebpy_pkg: str = ".."
|
|
34
|
+
nodes_dir: Path, nodebpy_pkg: str = "..", docstrings: bool = True
|
|
35
35
|
) -> dict[str, list[str]]:
|
|
36
36
|
"""Generate the bundled-essentials asset modules into
|
|
37
37
|
``<nodes_dir>/<tree>/assets.py``; returns the class names written per tree
|
|
@@ -50,6 +50,7 @@ def generate_essentials(
|
|
|
50
50
|
libraries,
|
|
51
51
|
Path(nodes_dir) / tree / "assets.py",
|
|
52
52
|
nodebpy_pkg=nodebpy_pkg,
|
|
53
|
+
docstrings=docstrings,
|
|
53
54
|
)
|
|
54
55
|
written[tree] = names
|
|
55
56
|
print(f" nodes/{tree}/assets.py: {len(names)} asset classes")
|
|
@@ -86,6 +87,15 @@ def parse_args() -> argparse.Namespace:
|
|
|
86
87
|
"package — e.g. '..lib.nodebpy'."
|
|
87
88
|
),
|
|
88
89
|
)
|
|
90
|
+
parser.add_argument(
|
|
91
|
+
"--no-docstrings",
|
|
92
|
+
dest="docstrings",
|
|
93
|
+
action="store_false",
|
|
94
|
+
help=(
|
|
95
|
+
"Skip the numpy-style class docstrings (description, Parameters, "
|
|
96
|
+
"Inputs, Outputs) and emit a terser module."
|
|
97
|
+
),
|
|
98
|
+
)
|
|
89
99
|
return parser.parse_args()
|
|
90
100
|
|
|
91
101
|
|
|
@@ -117,10 +127,13 @@ def main() -> None: # pragma: no cover - CLI wrapper
|
|
|
117
127
|
[PackageLibrary(str(output), relative)],
|
|
118
128
|
output,
|
|
119
129
|
nodebpy_pkg=args.nodebpy_pkg,
|
|
130
|
+
docstrings=args.docstrings,
|
|
120
131
|
)
|
|
121
132
|
return
|
|
122
133
|
|
|
123
|
-
generate_essentials(
|
|
134
|
+
generate_essentials(
|
|
135
|
+
Path(__file__).parent.parent / "nodes", docstrings=args.docstrings
|
|
136
|
+
)
|
|
124
137
|
|
|
125
138
|
|
|
126
139
|
if __name__ == "__main__": # pragma: no cover
|
|
@@ -95,6 +95,39 @@ def _format_default(socket: bpy.types.NodeSocket) -> str:
|
|
|
95
95
|
return "None"
|
|
96
96
|
|
|
97
97
|
|
|
98
|
+
def _clean_doc(text: str) -> str:
|
|
99
|
+
"""Make ``text`` safe to drop inside a ``\"\"\"…\"\"\"`` docstring."""
|
|
100
|
+
text = " ".join(text.split())
|
|
101
|
+
text = text.replace('"""', "'''")
|
|
102
|
+
return text.rstrip("\\").rstrip()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _quote(text: str) -> str:
|
|
106
|
+
"""``text`` as a double-quoted Python string literal."""
|
|
107
|
+
return '"' + text.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _menu_items(socket) -> tuple[str, ...]:
|
|
111
|
+
"""The items a menu socket accepts, in order.
|
|
112
|
+
|
|
113
|
+
A menu socket's items come from the Menu Switch node that defines them and
|
|
114
|
+
aren't readable from the socket's RNA enum, but assigning an impossible
|
|
115
|
+
value makes Blender list them in the ``TypeError`` — the same trick as
|
|
116
|
+
``gen.introspect._collect_socket_menu_items`` (duplicated rather than
|
|
117
|
+
imported, since ``gen`` is a build tool and isn't shipped with the package).
|
|
118
|
+
"""
|
|
119
|
+
if getattr(socket, "type", "") != "MENU" or not socket.default_value:
|
|
120
|
+
return ()
|
|
121
|
+
try:
|
|
122
|
+
socket.default_value = "X" * 100
|
|
123
|
+
except TypeError as error:
|
|
124
|
+
_, _, listed = str(error).partition("not found in ")
|
|
125
|
+
items = (item.strip("()'\" ") for item in listed.split(", "))
|
|
126
|
+
return tuple(item for item in items if item)
|
|
127
|
+
# A menu that accepted the impossible value tells us nothing about its items.
|
|
128
|
+
return ()
|
|
129
|
+
|
|
130
|
+
|
|
98
131
|
@dataclass
|
|
99
132
|
class _Socket:
|
|
100
133
|
name: str
|
|
@@ -103,6 +136,27 @@ class _Socket:
|
|
|
103
136
|
input_type: str # e.g. "InputGeometry"
|
|
104
137
|
default: str # source for the default value
|
|
105
138
|
attr: str # normalized accessor/param name
|
|
139
|
+
description: str = "" # interface tooltip, if the asset author set one
|
|
140
|
+
menu_items: tuple[str, ...] = () # menu sockets only: the selectable items
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def doc(self) -> str:
|
|
144
|
+
"""Documentation line for this socket — its tooltip, else its name."""
|
|
145
|
+
return _clean_doc(self.description or self.name)
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def param_type(self) -> str:
|
|
149
|
+
"""Type hint for the ``__init__`` parameter.
|
|
150
|
+
|
|
151
|
+
A menu socket is narrowed to its own items so editors offer them for
|
|
152
|
+
completion, while still accepting a linked ``MenuSocket``.
|
|
153
|
+
"""
|
|
154
|
+
if not self.menu_items:
|
|
155
|
+
return self.input_type
|
|
156
|
+
# Double-quoted to match the formatted source (ruff reformats the code
|
|
157
|
+
# but not the docstring copy of the same annotation).
|
|
158
|
+
literals = ", ".join(_quote(item) for item in self.menu_items)
|
|
159
|
+
return f"{self.input_type} | Literal[{literals}]"
|
|
106
160
|
|
|
107
161
|
|
|
108
162
|
@dataclass
|
|
@@ -116,12 +170,21 @@ class _AssetClass:
|
|
|
116
170
|
outputs: list[_Socket]
|
|
117
171
|
|
|
118
172
|
|
|
119
|
-
def _collect(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
173
|
+
def _collect(
|
|
174
|
+
sockets,
|
|
175
|
+
descriptions: dict[str, str] | None = None,
|
|
176
|
+
menus: bool = False,
|
|
177
|
+
) -> list[_Socket]:
|
|
178
|
+
"""Introspect ``sockets`` into records.
|
|
179
|
+
|
|
180
|
+
``menus`` resolves menu sockets to their items — only worth doing for the
|
|
181
|
+
group's *inputs*, whose parameters are typed from them.
|
|
182
|
+
"""
|
|
183
|
+
descriptions = descriptions or {}
|
|
184
|
+
# Keep inactive sockets: socket-usage inference deactivates inputs that the
|
|
185
|
+
# current node options (e.g. a menu selection) leave unused, but a caller
|
|
186
|
+
# may set those options differently, so the API must expose every input.
|
|
187
|
+
raw = [s for s in sockets if s.identifier != "__extend__"]
|
|
125
188
|
# The accessor resolves attribute names by identifier first, then name, so a
|
|
126
189
|
# group socket's readable name works as the attr/param when it's unambiguous;
|
|
127
190
|
# fall back to the opaque-but-unique identifier only on a name collision.
|
|
@@ -129,6 +192,7 @@ def _collect(sockets) -> list[_Socket]:
|
|
|
129
192
|
out: list[_Socket] = []
|
|
130
193
|
for s in raw:
|
|
131
194
|
socket_class, input_type = _socket_types(type(s).__name__)
|
|
195
|
+
menu_items = _menu_items(s) if menus else ()
|
|
132
196
|
norm_name = normalize_name(s.name)
|
|
133
197
|
attr = (
|
|
134
198
|
norm_name if name_counts[norm_name] == 1 else normalize_name(s.identifier)
|
|
@@ -141,11 +205,50 @@ def _collect(sockets) -> list[_Socket]:
|
|
|
141
205
|
input_type=input_type,
|
|
142
206
|
default=_format_default(s),
|
|
143
207
|
attr=attr,
|
|
208
|
+
description=descriptions.get(s.identifier, ""),
|
|
209
|
+
menu_items=menu_items,
|
|
144
210
|
)
|
|
145
211
|
)
|
|
146
212
|
return out
|
|
147
213
|
|
|
148
214
|
|
|
215
|
+
def _introspect_group(group, name: str, library_source: str) -> _AssetClass:
|
|
216
|
+
"""Introspect the node group ``group`` (a ``bpy.types.NodeTree``) into an
|
|
217
|
+
:class:`_AssetClass` record by instantiating it on a throwaway host node.
|
|
218
|
+
|
|
219
|
+
``group`` is deliberately unannotated: the stubs type interface items and
|
|
220
|
+
``node_groups.new`` too narrowly for the runtime attributes used here.
|
|
221
|
+
"""
|
|
222
|
+
host = bpy.data.node_groups.new("_introspect_host", group.bl_idname)
|
|
223
|
+
try:
|
|
224
|
+
node_type = {
|
|
225
|
+
"GeometryNodeTree": "GeometryNodeGroup",
|
|
226
|
+
"ShaderNodeTree": "ShaderNodeGroup",
|
|
227
|
+
"CompositorNodeTree": "CompositorNodeGroup",
|
|
228
|
+
}[group.bl_idname]
|
|
229
|
+
node = host.nodes.new(node_type)
|
|
230
|
+
node.node_tree = group # ty: ignore[unresolved-attribute]
|
|
231
|
+
# Tooltips live on the tree *interface* items, not on the node's
|
|
232
|
+
# sockets — collect them by identifier so the generated docstrings
|
|
233
|
+
# can use the asset author's own wording.
|
|
234
|
+
descriptions = {
|
|
235
|
+
item.identifier: item.description or ""
|
|
236
|
+
for item in group.interface.items_tree
|
|
237
|
+
if item.item_type == "SOCKET"
|
|
238
|
+
}
|
|
239
|
+
return _AssetClass(
|
|
240
|
+
class_name=_class_name(name),
|
|
241
|
+
asset_name=name,
|
|
242
|
+
description=(group.description or name).strip(),
|
|
243
|
+
library_source=library_source,
|
|
244
|
+
tree_idname=group.bl_idname,
|
|
245
|
+
inputs=_collect(node.inputs, descriptions, menus=True),
|
|
246
|
+
outputs=_collect(node.outputs, descriptions),
|
|
247
|
+
)
|
|
248
|
+
finally:
|
|
249
|
+
bpy.data.node_groups.remove(host)
|
|
250
|
+
|
|
251
|
+
|
|
149
252
|
def _introspect(library: AssetLibrary, names: set[str] | None) -> list[_AssetClass]:
|
|
150
253
|
"""Append each requested group from the library and introspect its
|
|
151
254
|
interface into :class:`_AssetClass` records."""
|
|
@@ -169,29 +272,7 @@ def _introspect(library: AssetLibrary, names: set[str] | None) -> list[_AssetCla
|
|
|
169
272
|
):
|
|
170
273
|
dst.node_groups = [name]
|
|
171
274
|
group = dst.node_groups[0]
|
|
172
|
-
|
|
173
|
-
host = bpy.data.node_groups.new("_introspect_host", group.bl_idname)
|
|
174
|
-
try:
|
|
175
|
-
node_type = {
|
|
176
|
-
"GeometryNodeTree": "GeometryNodeGroup",
|
|
177
|
-
"ShaderNodeTree": "ShaderNodeGroup",
|
|
178
|
-
"CompositorNodeTree": "CompositorNodeGroup",
|
|
179
|
-
}[group.bl_idname]
|
|
180
|
-
node = host.nodes.new(node_type)
|
|
181
|
-
node.node_tree = group # ty: ignore[unresolved-attribute]
|
|
182
|
-
classes.append(
|
|
183
|
-
_AssetClass(
|
|
184
|
-
class_name=_class_name(name),
|
|
185
|
-
asset_name=name,
|
|
186
|
-
description=(group.description or name).strip(),
|
|
187
|
-
library_source=library_source,
|
|
188
|
-
tree_idname=group.bl_idname,
|
|
189
|
-
inputs=_collect(node.inputs),
|
|
190
|
-
outputs=_collect(node.outputs),
|
|
191
|
-
)
|
|
192
|
-
)
|
|
193
|
-
finally:
|
|
194
|
-
bpy.data.node_groups.remove(host)
|
|
275
|
+
classes.append(_introspect_group(group, name, library_source))
|
|
195
276
|
return classes
|
|
196
277
|
|
|
197
278
|
|
|
@@ -210,24 +291,49 @@ def _library_source(library: AssetLibrary) -> str:
|
|
|
210
291
|
raise TypeError(f"Cannot serialise asset library: {library!r}")
|
|
211
292
|
|
|
212
293
|
|
|
213
|
-
def _accessor(sockets: list[_Socket], kind: str) -> str:
|
|
294
|
+
def _accessor(sockets: list[_Socket], kind: str, docstrings: bool) -> str:
|
|
214
295
|
if not sockets:
|
|
215
296
|
return f" class {kind}(SocketAccessor):\n pass"
|
|
216
297
|
lines = [f" class {kind}(SocketAccessor):"]
|
|
217
298
|
for s in sockets:
|
|
218
299
|
lines.append(f" {s.attr}: {s.socket_class}")
|
|
219
|
-
doc = s.name
|
|
300
|
+
doc = s.doc if docstrings else _clean_doc(s.name)
|
|
220
301
|
if doc and doc != s.attr:
|
|
221
302
|
lines.append(f' """{doc}"""')
|
|
222
303
|
return "\n".join(lines)
|
|
223
304
|
|
|
224
305
|
|
|
225
|
-
def
|
|
306
|
+
def _class_docstring(cls: _AssetClass) -> str:
|
|
307
|
+
"""A numpy-style docstring for ``cls``, matching the built-in node classes."""
|
|
308
|
+
lines = [_clean_doc(cls.description), ""]
|
|
309
|
+
if cls.inputs:
|
|
310
|
+
lines += ["Parameters", "----------"]
|
|
311
|
+
for s in cls.inputs:
|
|
312
|
+
lines += [f"{s.attr} : {s.param_type}", f" {s.doc}"]
|
|
313
|
+
lines.append("")
|
|
314
|
+
lines += ["Inputs", "------"]
|
|
315
|
+
for s in cls.inputs:
|
|
316
|
+
lines += [f"i.{s.attr} : {s.socket_class}", f" {s.doc}"]
|
|
317
|
+
lines.append("")
|
|
318
|
+
if cls.outputs:
|
|
319
|
+
lines += ["Outputs", "-------"]
|
|
320
|
+
for s in cls.outputs:
|
|
321
|
+
lines += [f"o.{s.attr} : {s.socket_class}", f" {s.doc}"]
|
|
322
|
+
# Indent to the class body, leaving blank separator lines truly blank so the
|
|
323
|
+
# module needs no formatter pass to be clean.
|
|
324
|
+
body = "\n".join(f" {line}" if line else "" for line in lines).strip("\n")
|
|
325
|
+
return f'"""\n{body}\n """'
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _render_class(cls: _AssetClass, docstrings: bool = False) -> str:
|
|
226
329
|
base = asset_group_base(cls.tree_idname).__name__
|
|
227
|
-
|
|
228
|
-
|
|
330
|
+
docstring = (
|
|
331
|
+
_class_docstring(cls) if docstrings else f'"""{_clean_doc(cls.description)}"""'
|
|
332
|
+
)
|
|
333
|
+
inputs_cls = _accessor(cls.inputs, "_Inputs", docstrings)
|
|
334
|
+
outputs_cls = _accessor(cls.outputs, "_Outputs", docstrings)
|
|
229
335
|
|
|
230
|
-
params = [f"{s.attr}: {s.
|
|
336
|
+
params = [f"{s.attr}: {s.param_type} = {s.default}" for s in cls.inputs]
|
|
231
337
|
signature = (
|
|
232
338
|
"(\n self,\n " + ",\n ".join(params) + ",\n )"
|
|
233
339
|
if params
|
|
@@ -235,8 +341,8 @@ def _render_class(cls: _AssetClass) -> str:
|
|
|
235
341
|
)
|
|
236
342
|
key_args = ", ".join(f'"{s.identifier}": {s.attr}' for s in cls.inputs)
|
|
237
343
|
|
|
238
|
-
return f
|
|
239
|
-
|
|
344
|
+
return f"""class {cls.class_name}({base}):
|
|
345
|
+
{docstring}
|
|
240
346
|
|
|
241
347
|
_name = {cls.asset_name!r}
|
|
242
348
|
_asset_name = {cls.asset_name!r}
|
|
@@ -254,10 +360,14 @@ def _render_class(cls: _AssetClass) -> str:
|
|
|
254
360
|
|
|
255
361
|
def __init__{signature}:
|
|
256
362
|
super().__init__(**{{{key_args}}})
|
|
257
|
-
|
|
363
|
+
"""
|
|
258
364
|
|
|
259
365
|
|
|
260
|
-
def _render_module(
|
|
366
|
+
def _render_module(
|
|
367
|
+
classes: list[_AssetClass],
|
|
368
|
+
nodebpy_pkg: str = "nodebpy",
|
|
369
|
+
docstrings: bool = False,
|
|
370
|
+
) -> str:
|
|
261
371
|
socket_classes = sorted(
|
|
262
372
|
{s.socket_class for c in classes for s in c.inputs + c.outputs}
|
|
263
373
|
)
|
|
@@ -276,9 +386,13 @@ def _render_module(classes: list[_AssetClass], nodebpy_pkg: str = "nodebpy") ->
|
|
|
276
386
|
set(bases) | set(libraries) | {"SocketAccessor"} | set(socket_classes)
|
|
277
387
|
)
|
|
278
388
|
|
|
389
|
+
typing_imports = ["TYPE_CHECKING"]
|
|
390
|
+
if any(s.menu_items for c in classes for s in c.inputs):
|
|
391
|
+
typing_imports.append("Literal")
|
|
392
|
+
|
|
279
393
|
lines = [
|
|
280
394
|
"# Auto-generated by nodebpy.assets.generate_asset_api — do not edit manually.",
|
|
281
|
-
"from typing import
|
|
395
|
+
f"from typing import {', '.join(typing_imports)}",
|
|
282
396
|
"",
|
|
283
397
|
f"from {nodebpy_pkg}.builder import (\n {',\n '.join(builder_imports)},\n)",
|
|
284
398
|
f"from {nodebpy_pkg}.types import (\n {',\n '.join(input_types)},\n)"
|
|
@@ -287,7 +401,7 @@ def _render_module(classes: list[_AssetClass], nodebpy_pkg: str = "nodebpy") ->
|
|
|
287
401
|
]
|
|
288
402
|
header = "\n".join(line for line in lines if line) + "\n\n\n"
|
|
289
403
|
ordered = sorted(classes, key=lambda c: c.class_name)
|
|
290
|
-
body = "\n\n".join(_render_class(c) for c in ordered)
|
|
404
|
+
body = "\n\n".join(_render_class(c, docstrings) for c in ordered)
|
|
291
405
|
all_names = ",\n ".join(f'"{c.class_name}"' for c in ordered)
|
|
292
406
|
footer = (
|
|
293
407
|
f"\n\n__all__ = (\n {all_names},\n)\n" if ordered else "\n__all__ = ()\n"
|
|
@@ -301,6 +415,7 @@ def generate_asset_api(
|
|
|
301
415
|
*,
|
|
302
416
|
names: set[str] | None = None,
|
|
303
417
|
nodebpy_pkg: str = "nodebpy",
|
|
418
|
+
docstrings: bool = True,
|
|
304
419
|
) -> list[str]:
|
|
305
420
|
"""Generate typed asset classes for ``libraries`` into ``output_path``.
|
|
306
421
|
|
|
@@ -321,6 +436,11 @@ def generate_asset_api(
|
|
|
321
436
|
pass the path that reaches it *relative to the generated module's
|
|
322
437
|
package* — e.g. ``"..vendor.nodebpy"`` — so the emitted imports stay
|
|
323
438
|
relative to the install/vendor location.
|
|
439
|
+
docstrings:
|
|
440
|
+
Emit numpy-style class docstrings (description, ``Parameters``,
|
|
441
|
+
``Inputs``, ``Outputs``) using the asset's own socket tooltips, so
|
|
442
|
+
editors show documentation alongside the type hints. Defaults to
|
|
443
|
+
``True``; pass ``False`` for a terser module.
|
|
324
444
|
|
|
325
445
|
Returns the list of generated class names.
|
|
326
446
|
"""
|
|
@@ -334,7 +454,8 @@ def generate_asset_api(
|
|
|
334
454
|
output_path = Path(output_path)
|
|
335
455
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
336
456
|
output_path.write_text(
|
|
337
|
-
_render_module(classes, nodebpy_pkg=nodebpy_pkg
|
|
457
|
+
_render_module(classes, nodebpy_pkg=nodebpy_pkg, docstrings=docstrings),
|
|
458
|
+
encoding="utf-8",
|
|
338
459
|
)
|
|
339
460
|
return [c.class_name for c in classes]
|
|
340
461
|
|