nodebpy 520.7.0__tar.gz → 520.9.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.7.0 → nodebpy-520.9.0}/PKG-INFO +1 -1
- {nodebpy-520.7.0 → nodebpy-520.9.0}/pyproject.toml +1 -1
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/assets/__main__.py +15 -2
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/assets/_codegen.py +131 -16
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/builder/node.py +4 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/compositor/assets.py +586 -71
- nodebpy-520.9.0/src/nodebpy/nodes/geometry/assets.py +5572 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/shader/assets.py +108 -16
- nodebpy-520.7.0/src/nodebpy/nodes/geometry/assets.py +0 -3296
- {nodebpy-520.7.0 → nodebpy-520.9.0}/README.md +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/__init__.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/assets/__init__.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/builder/__init__.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/builder/_registry.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/builder/_utils.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/builder/accessor.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/builder/arrange.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/builder/asset.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/builder/items.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/builder/mixins.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/builder/socket.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/builder/tree.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/export/__init__.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/export/codegen.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/export/diagram.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/export/web_render.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/lib/nodearrange/__init__.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/lib/nodearrange/arrange/__init__.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/lib/nodearrange/arrange/graph.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/lib/nodearrange/arrange/ordering.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/lib/nodearrange/arrange/ranking.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/lib/nodearrange/arrange/realize.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/lib/nodearrange/arrange/stacking.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/lib/nodearrange/arrange/structs.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/lib/nodearrange/arrange/sugiyama.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/lib/nodearrange/arrange/x_coords.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/lib/nodearrange/arrange/y_coords.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/lib/nodearrange/config.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/lib/nodearrange/utils.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/__init__.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/_mixins.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/compositor/__init__.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/compositor/color.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/compositor/converter.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/compositor/distort.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/compositor/filter.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/compositor/group.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/compositor/input.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/compositor/interface.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/compositor/manual.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/compositor/matte.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/compositor/output.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/compositor/vector.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/__init__.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/attribute.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/color.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/converter.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/geometry.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/grid.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/group.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/groups.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/input.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/interface.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/manual.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/output.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/texture.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/utilities.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/vector.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/zone.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/shader/__init__.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/shader/color.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/shader/converter.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/shader/grid.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/shader/group.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/shader/input.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/shader/manual.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/shader/output.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/shader/script.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/shader/shader.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/shader/texture.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/shader/vector.py +0 -0
- {nodebpy-520.7.0 → nodebpy-520.9.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,7 +170,17 @@ class _AssetClass:
|
|
|
116
170
|
outputs: list[_Socket]
|
|
117
171
|
|
|
118
172
|
|
|
119
|
-
def _collect(
|
|
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 {}
|
|
120
184
|
raw = [
|
|
121
185
|
s
|
|
122
186
|
for s in sockets
|
|
@@ -129,6 +193,7 @@ def _collect(sockets) -> list[_Socket]:
|
|
|
129
193
|
out: list[_Socket] = []
|
|
130
194
|
for s in raw:
|
|
131
195
|
socket_class, input_type = _socket_types(type(s).__name__)
|
|
196
|
+
menu_items = _menu_items(s) if menus else ()
|
|
132
197
|
norm_name = normalize_name(s.name)
|
|
133
198
|
attr = (
|
|
134
199
|
norm_name if name_counts[norm_name] == 1 else normalize_name(s.identifier)
|
|
@@ -141,6 +206,8 @@ def _collect(sockets) -> list[_Socket]:
|
|
|
141
206
|
input_type=input_type,
|
|
142
207
|
default=_format_default(s),
|
|
143
208
|
attr=attr,
|
|
209
|
+
description=descriptions.get(s.identifier, ""),
|
|
210
|
+
menu_items=menu_items,
|
|
144
211
|
)
|
|
145
212
|
)
|
|
146
213
|
return out
|
|
@@ -179,6 +246,14 @@ def _introspect(library: AssetLibrary, names: set[str] | None) -> list[_AssetCla
|
|
|
179
246
|
}[group.bl_idname]
|
|
180
247
|
node = host.nodes.new(node_type)
|
|
181
248
|
node.node_tree = group # ty: ignore[unresolved-attribute]
|
|
249
|
+
# Tooltips live on the tree *interface* items, not on the node's
|
|
250
|
+
# sockets — collect them by identifier so the generated docstrings
|
|
251
|
+
# can use the asset author's own wording.
|
|
252
|
+
descriptions = {
|
|
253
|
+
item.identifier: item.description or ""
|
|
254
|
+
for item in group.interface.items_tree
|
|
255
|
+
if item.item_type == "SOCKET"
|
|
256
|
+
}
|
|
182
257
|
classes.append(
|
|
183
258
|
_AssetClass(
|
|
184
259
|
class_name=_class_name(name),
|
|
@@ -186,8 +261,8 @@ def _introspect(library: AssetLibrary, names: set[str] | None) -> list[_AssetCla
|
|
|
186
261
|
description=(group.description or name).strip(),
|
|
187
262
|
library_source=library_source,
|
|
188
263
|
tree_idname=group.bl_idname,
|
|
189
|
-
inputs=_collect(node.inputs),
|
|
190
|
-
outputs=_collect(node.outputs),
|
|
264
|
+
inputs=_collect(node.inputs, descriptions, menus=True),
|
|
265
|
+
outputs=_collect(node.outputs, descriptions),
|
|
191
266
|
)
|
|
192
267
|
)
|
|
193
268
|
finally:
|
|
@@ -210,24 +285,49 @@ def _library_source(library: AssetLibrary) -> str:
|
|
|
210
285
|
raise TypeError(f"Cannot serialise asset library: {library!r}")
|
|
211
286
|
|
|
212
287
|
|
|
213
|
-
def _accessor(sockets: list[_Socket], kind: str) -> str:
|
|
288
|
+
def _accessor(sockets: list[_Socket], kind: str, docstrings: bool) -> str:
|
|
214
289
|
if not sockets:
|
|
215
290
|
return f" class {kind}(SocketAccessor):\n pass"
|
|
216
291
|
lines = [f" class {kind}(SocketAccessor):"]
|
|
217
292
|
for s in sockets:
|
|
218
293
|
lines.append(f" {s.attr}: {s.socket_class}")
|
|
219
|
-
doc = s.name
|
|
294
|
+
doc = s.doc if docstrings else _clean_doc(s.name)
|
|
220
295
|
if doc and doc != s.attr:
|
|
221
296
|
lines.append(f' """{doc}"""')
|
|
222
297
|
return "\n".join(lines)
|
|
223
298
|
|
|
224
299
|
|
|
225
|
-
def
|
|
300
|
+
def _class_docstring(cls: _AssetClass) -> str:
|
|
301
|
+
"""A numpy-style docstring for ``cls``, matching the built-in node classes."""
|
|
302
|
+
lines = [_clean_doc(cls.description), ""]
|
|
303
|
+
if cls.inputs:
|
|
304
|
+
lines += ["Parameters", "----------"]
|
|
305
|
+
for s in cls.inputs:
|
|
306
|
+
lines += [f"{s.attr} : {s.param_type}", f" {s.doc}"]
|
|
307
|
+
lines.append("")
|
|
308
|
+
lines += ["Inputs", "------"]
|
|
309
|
+
for s in cls.inputs:
|
|
310
|
+
lines += [f"i.{s.attr} : {s.socket_class}", f" {s.doc}"]
|
|
311
|
+
lines.append("")
|
|
312
|
+
if cls.outputs:
|
|
313
|
+
lines += ["Outputs", "-------"]
|
|
314
|
+
for s in cls.outputs:
|
|
315
|
+
lines += [f"o.{s.attr} : {s.socket_class}", f" {s.doc}"]
|
|
316
|
+
# Indent to the class body, leaving blank separator lines truly blank so the
|
|
317
|
+
# module needs no formatter pass to be clean.
|
|
318
|
+
body = "\n".join(f" {line}" if line else "" for line in lines).strip("\n")
|
|
319
|
+
return f'"""\n{body}\n """'
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _render_class(cls: _AssetClass, docstrings: bool = False) -> str:
|
|
226
323
|
base = asset_group_base(cls.tree_idname).__name__
|
|
227
|
-
|
|
228
|
-
|
|
324
|
+
docstring = (
|
|
325
|
+
_class_docstring(cls) if docstrings else f'"""{_clean_doc(cls.description)}"""'
|
|
326
|
+
)
|
|
327
|
+
inputs_cls = _accessor(cls.inputs, "_Inputs", docstrings)
|
|
328
|
+
outputs_cls = _accessor(cls.outputs, "_Outputs", docstrings)
|
|
229
329
|
|
|
230
|
-
params = [f"{s.attr}: {s.
|
|
330
|
+
params = [f"{s.attr}: {s.param_type} = {s.default}" for s in cls.inputs]
|
|
231
331
|
signature = (
|
|
232
332
|
"(\n self,\n " + ",\n ".join(params) + ",\n )"
|
|
233
333
|
if params
|
|
@@ -235,8 +335,8 @@ def _render_class(cls: _AssetClass) -> str:
|
|
|
235
335
|
)
|
|
236
336
|
key_args = ", ".join(f'"{s.identifier}": {s.attr}' for s in cls.inputs)
|
|
237
337
|
|
|
238
|
-
return f
|
|
239
|
-
|
|
338
|
+
return f"""class {cls.class_name}({base}):
|
|
339
|
+
{docstring}
|
|
240
340
|
|
|
241
341
|
_name = {cls.asset_name!r}
|
|
242
342
|
_asset_name = {cls.asset_name!r}
|
|
@@ -254,10 +354,14 @@ def _render_class(cls: _AssetClass) -> str:
|
|
|
254
354
|
|
|
255
355
|
def __init__{signature}:
|
|
256
356
|
super().__init__(**{{{key_args}}})
|
|
257
|
-
|
|
357
|
+
"""
|
|
258
358
|
|
|
259
359
|
|
|
260
|
-
def _render_module(
|
|
360
|
+
def _render_module(
|
|
361
|
+
classes: list[_AssetClass],
|
|
362
|
+
nodebpy_pkg: str = "nodebpy",
|
|
363
|
+
docstrings: bool = False,
|
|
364
|
+
) -> str:
|
|
261
365
|
socket_classes = sorted(
|
|
262
366
|
{s.socket_class for c in classes for s in c.inputs + c.outputs}
|
|
263
367
|
)
|
|
@@ -276,9 +380,13 @@ def _render_module(classes: list[_AssetClass], nodebpy_pkg: str = "nodebpy") ->
|
|
|
276
380
|
set(bases) | set(libraries) | {"SocketAccessor"} | set(socket_classes)
|
|
277
381
|
)
|
|
278
382
|
|
|
383
|
+
typing_imports = ["TYPE_CHECKING"]
|
|
384
|
+
if any(s.menu_items for c in classes for s in c.inputs):
|
|
385
|
+
typing_imports.append("Literal")
|
|
386
|
+
|
|
279
387
|
lines = [
|
|
280
388
|
"# Auto-generated by nodebpy.assets.generate_asset_api — do not edit manually.",
|
|
281
|
-
"from typing import
|
|
389
|
+
f"from typing import {', '.join(typing_imports)}",
|
|
282
390
|
"",
|
|
283
391
|
f"from {nodebpy_pkg}.builder import (\n {',\n '.join(builder_imports)},\n)",
|
|
284
392
|
f"from {nodebpy_pkg}.types import (\n {',\n '.join(input_types)},\n)"
|
|
@@ -287,7 +395,7 @@ def _render_module(classes: list[_AssetClass], nodebpy_pkg: str = "nodebpy") ->
|
|
|
287
395
|
]
|
|
288
396
|
header = "\n".join(line for line in lines if line) + "\n\n\n"
|
|
289
397
|
ordered = sorted(classes, key=lambda c: c.class_name)
|
|
290
|
-
body = "\n\n".join(_render_class(c) for c in ordered)
|
|
398
|
+
body = "\n\n".join(_render_class(c, docstrings) for c in ordered)
|
|
291
399
|
all_names = ",\n ".join(f'"{c.class_name}"' for c in ordered)
|
|
292
400
|
footer = (
|
|
293
401
|
f"\n\n__all__ = (\n {all_names},\n)\n" if ordered else "\n__all__ = ()\n"
|
|
@@ -301,6 +409,7 @@ def generate_asset_api(
|
|
|
301
409
|
*,
|
|
302
410
|
names: set[str] | None = None,
|
|
303
411
|
nodebpy_pkg: str = "nodebpy",
|
|
412
|
+
docstrings: bool = True,
|
|
304
413
|
) -> list[str]:
|
|
305
414
|
"""Generate typed asset classes for ``libraries`` into ``output_path``.
|
|
306
415
|
|
|
@@ -321,6 +430,11 @@ def generate_asset_api(
|
|
|
321
430
|
pass the path that reaches it *relative to the generated module's
|
|
322
431
|
package* — e.g. ``"..vendor.nodebpy"`` — so the emitted imports stay
|
|
323
432
|
relative to the install/vendor location.
|
|
433
|
+
docstrings:
|
|
434
|
+
Emit numpy-style class docstrings (description, ``Parameters``,
|
|
435
|
+
``Inputs``, ``Outputs``) using the asset's own socket tooltips, so
|
|
436
|
+
editors show documentation alongside the type hints. Defaults to
|
|
437
|
+
``True``; pass ``False`` for a terser module.
|
|
324
438
|
|
|
325
439
|
Returns the list of generated class names.
|
|
326
440
|
"""
|
|
@@ -334,7 +448,8 @@ def generate_asset_api(
|
|
|
334
448
|
output_path = Path(output_path)
|
|
335
449
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
336
450
|
output_path.write_text(
|
|
337
|
-
_render_module(classes, nodebpy_pkg=nodebpy_pkg
|
|
451
|
+
_render_module(classes, nodebpy_pkg=nodebpy_pkg, docstrings=docstrings),
|
|
452
|
+
encoding="utf-8",
|
|
338
453
|
)
|
|
339
454
|
return [c.class_name for c in classes]
|
|
340
455
|
|
|
@@ -387,6 +387,10 @@ class NodeGroupBuilder(BaseNode, ABC, Generic[_T]):
|
|
|
387
387
|
self._establish_links(**kwargs)
|
|
388
388
|
if named_links:
|
|
389
389
|
self._establish_named_links(named_links)
|
|
390
|
+
# Name the node after its tree (Blender deduplicates with a
|
|
391
|
+
# ``.001``-style suffix), matching how group assets added from the
|
|
392
|
+
# Add menu are named, instead of Blender's default ``Group``.
|
|
393
|
+
self.node.name = self.node_tree.name
|
|
390
394
|
|
|
391
395
|
@property
|
|
392
396
|
@abstractmethod
|