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.
Files changed (82) hide show
  1. {nodebpy-520.7.0 → nodebpy-520.9.0}/PKG-INFO +1 -1
  2. {nodebpy-520.7.0 → nodebpy-520.9.0}/pyproject.toml +1 -1
  3. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/assets/__main__.py +15 -2
  4. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/assets/_codegen.py +131 -16
  5. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/builder/node.py +4 -0
  6. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/compositor/assets.py +586 -71
  7. nodebpy-520.9.0/src/nodebpy/nodes/geometry/assets.py +5572 -0
  8. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/shader/assets.py +108 -16
  9. nodebpy-520.7.0/src/nodebpy/nodes/geometry/assets.py +0 -3296
  10. {nodebpy-520.7.0 → nodebpy-520.9.0}/README.md +0 -0
  11. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/__init__.py +0 -0
  12. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/assets/__init__.py +0 -0
  13. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/builder/__init__.py +0 -0
  14. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/builder/_registry.py +0 -0
  15. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/builder/_utils.py +0 -0
  16. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/builder/accessor.py +0 -0
  17. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/builder/arrange.py +0 -0
  18. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/builder/asset.py +0 -0
  19. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/builder/items.py +0 -0
  20. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/builder/mixins.py +0 -0
  21. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/builder/socket.py +0 -0
  22. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/builder/tree.py +0 -0
  23. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/export/__init__.py +0 -0
  24. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/export/codegen.py +0 -0
  25. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/export/diagram.py +0 -0
  26. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/export/web_render.py +0 -0
  27. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/lib/nodearrange/__init__.py +0 -0
  28. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/lib/nodearrange/arrange/__init__.py +0 -0
  29. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/lib/nodearrange/arrange/graph.py +0 -0
  30. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/lib/nodearrange/arrange/ordering.py +0 -0
  31. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/lib/nodearrange/arrange/ranking.py +0 -0
  32. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/lib/nodearrange/arrange/realize.py +0 -0
  33. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/lib/nodearrange/arrange/stacking.py +0 -0
  34. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/lib/nodearrange/arrange/structs.py +0 -0
  35. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/lib/nodearrange/arrange/sugiyama.py +0 -0
  36. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/lib/nodearrange/arrange/x_coords.py +0 -0
  37. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/lib/nodearrange/arrange/y_coords.py +0 -0
  38. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/lib/nodearrange/config.py +0 -0
  39. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/lib/nodearrange/utils.py +0 -0
  40. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/__init__.py +0 -0
  41. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/_mixins.py +0 -0
  42. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/compositor/__init__.py +0 -0
  43. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/compositor/color.py +0 -0
  44. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/compositor/converter.py +0 -0
  45. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/compositor/distort.py +0 -0
  46. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/compositor/filter.py +0 -0
  47. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/compositor/group.py +0 -0
  48. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/compositor/input.py +0 -0
  49. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/compositor/interface.py +0 -0
  50. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/compositor/manual.py +0 -0
  51. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/compositor/matte.py +0 -0
  52. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/compositor/output.py +0 -0
  53. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/compositor/vector.py +0 -0
  54. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/__init__.py +0 -0
  55. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/attribute.py +0 -0
  56. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/color.py +0 -0
  57. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/converter.py +0 -0
  58. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/geometry.py +0 -0
  59. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/grid.py +0 -0
  60. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/group.py +0 -0
  61. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/groups.py +0 -0
  62. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/input.py +0 -0
  63. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/interface.py +0 -0
  64. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/manual.py +0 -0
  65. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/output.py +0 -0
  66. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/texture.py +0 -0
  67. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/utilities.py +0 -0
  68. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/vector.py +0 -0
  69. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/geometry/zone.py +0 -0
  70. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/shader/__init__.py +0 -0
  71. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/shader/color.py +0 -0
  72. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/shader/converter.py +0 -0
  73. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/shader/grid.py +0 -0
  74. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/shader/group.py +0 -0
  75. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/shader/input.py +0 -0
  76. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/shader/manual.py +0 -0
  77. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/shader/output.py +0 -0
  78. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/shader/script.py +0 -0
  79. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/shader/shader.py +0 -0
  80. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/shader/texture.py +0 -0
  81. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/nodes/shader/vector.py +0 -0
  82. {nodebpy-520.7.0 → nodebpy-520.9.0}/src/nodebpy/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nodebpy
3
- Version: 520.7.0
3
+ Version: 520.9.0
4
4
  Summary: Build nodes trees in Blender more elegantly with code
5
5
  Author: Brady Johnston
6
6
  Author-email: Brady Johnston <brady.johnston@me.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nodebpy"
3
- version = "520.7.0"
3
+ version = "520.9.0"
4
4
  description = "Build nodes trees in Blender more elegantly with code"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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(Path(__file__).parent.parent / "nodes")
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(sockets) -> list[_Socket]:
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 _render_class(cls: _AssetClass) -> str:
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
- inputs_cls = _accessor(cls.inputs, "_Inputs")
228
- outputs_cls = _accessor(cls.outputs, "_Outputs")
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.input_type} = {s.default}" for s in cls.inputs]
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'''class {cls.class_name}({base}):
239
- """{cls.description}"""
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(classes: list[_AssetClass], nodebpy_pkg: str = "nodebpy") -> str:
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 TYPE_CHECKING",
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), encoding="utf-8"
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