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.
Files changed (83) hide show
  1. {nodebpy-520.8.0 → nodebpy-520.10.0}/PKG-INFO +1 -1
  2. {nodebpy-520.8.0 → nodebpy-520.10.0}/pyproject.toml +1 -1
  3. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/assets/__main__.py +15 -2
  4. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/assets/_codegen.py +163 -42
  5. nodebpy-520.10.0/src/nodebpy/nodes/compositor/assets.py +1433 -0
  6. nodebpy-520.10.0/src/nodebpy/nodes/geometry/assets.py +6538 -0
  7. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/shader/assets.py +108 -16
  8. nodebpy-520.8.0/src/nodebpy/nodes/compositor/assets.py +0 -783
  9. nodebpy-520.8.0/src/nodebpy/nodes/geometry/assets.py +0 -3296
  10. {nodebpy-520.8.0 → nodebpy-520.10.0}/README.md +0 -0
  11. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/__init__.py +0 -0
  12. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/assets/__init__.py +0 -0
  13. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/builder/__init__.py +0 -0
  14. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/builder/_registry.py +0 -0
  15. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/builder/_utils.py +0 -0
  16. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/builder/accessor.py +0 -0
  17. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/builder/arrange.py +0 -0
  18. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/builder/asset.py +0 -0
  19. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/builder/items.py +0 -0
  20. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/builder/mixins.py +0 -0
  21. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/builder/node.py +0 -0
  22. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/builder/socket.py +0 -0
  23. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/builder/tree.py +0 -0
  24. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/export/__init__.py +0 -0
  25. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/export/codegen.py +0 -0
  26. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/export/diagram.py +0 -0
  27. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/export/web_render.py +0 -0
  28. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/lib/nodearrange/__init__.py +0 -0
  29. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/lib/nodearrange/arrange/__init__.py +0 -0
  30. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/lib/nodearrange/arrange/graph.py +0 -0
  31. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/lib/nodearrange/arrange/ordering.py +0 -0
  32. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/lib/nodearrange/arrange/ranking.py +0 -0
  33. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/lib/nodearrange/arrange/realize.py +0 -0
  34. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/lib/nodearrange/arrange/stacking.py +0 -0
  35. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/lib/nodearrange/arrange/structs.py +0 -0
  36. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/lib/nodearrange/arrange/sugiyama.py +0 -0
  37. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/lib/nodearrange/arrange/x_coords.py +0 -0
  38. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/lib/nodearrange/arrange/y_coords.py +0 -0
  39. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/lib/nodearrange/config.py +0 -0
  40. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/lib/nodearrange/utils.py +0 -0
  41. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/__init__.py +0 -0
  42. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/_mixins.py +0 -0
  43. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/compositor/__init__.py +0 -0
  44. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/compositor/color.py +0 -0
  45. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/compositor/converter.py +0 -0
  46. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/compositor/distort.py +0 -0
  47. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/compositor/filter.py +0 -0
  48. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/compositor/group.py +0 -0
  49. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/compositor/input.py +0 -0
  50. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/compositor/interface.py +0 -0
  51. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/compositor/manual.py +0 -0
  52. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/compositor/matte.py +0 -0
  53. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/compositor/output.py +0 -0
  54. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/compositor/vector.py +0 -0
  55. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/__init__.py +0 -0
  56. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/attribute.py +0 -0
  57. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/color.py +0 -0
  58. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/converter.py +0 -0
  59. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/geometry.py +0 -0
  60. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/grid.py +0 -0
  61. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/group.py +0 -0
  62. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/groups.py +0 -0
  63. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/input.py +0 -0
  64. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/interface.py +0 -0
  65. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/manual.py +0 -0
  66. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/output.py +0 -0
  67. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/texture.py +0 -0
  68. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/utilities.py +0 -0
  69. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/vector.py +0 -0
  70. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/geometry/zone.py +0 -0
  71. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/shader/__init__.py +0 -0
  72. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/shader/color.py +0 -0
  73. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/shader/converter.py +0 -0
  74. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/shader/grid.py +0 -0
  75. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/shader/group.py +0 -0
  76. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/shader/input.py +0 -0
  77. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/shader/manual.py +0 -0
  78. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/shader/output.py +0 -0
  79. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/shader/script.py +0 -0
  80. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/shader/shader.py +0 -0
  81. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/shader/texture.py +0 -0
  82. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/nodes/shader/vector.py +0 -0
  83. {nodebpy-520.8.0 → nodebpy-520.10.0}/src/nodebpy/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nodebpy
3
- Version: 520.8.0
3
+ Version: 520.10.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.8.0"
3
+ version = "520.10.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,12 +170,21 @@ class _AssetClass:
116
170
  outputs: list[_Socket]
117
171
 
118
172
 
119
- def _collect(sockets) -> list[_Socket]:
120
- raw = [
121
- s
122
- for s in sockets
123
- if s.identifier != "__extend__" and not getattr(s, "is_inactive", False)
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 _render_class(cls: _AssetClass) -> str:
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
- inputs_cls = _accessor(cls.inputs, "_Inputs")
228
- outputs_cls = _accessor(cls.outputs, "_Outputs")
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.input_type} = {s.default}" for s in cls.inputs]
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'''class {cls.class_name}({base}):
239
- """{cls.description}"""
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(classes: list[_AssetClass], nodebpy_pkg: str = "nodebpy") -> str:
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 TYPE_CHECKING",
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), encoding="utf-8"
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