nodebpy 520.13.0__tar.gz → 520.15.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 (81) hide show
  1. {nodebpy-520.13.0 → nodebpy-520.15.0}/PKG-INFO +1 -1
  2. {nodebpy-520.13.0 → nodebpy-520.15.0}/pyproject.toml +23 -2
  3. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/assets/_codegen.py +12 -2
  4. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/builder/__init__.py +52 -58
  5. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/builder/_registry.py +4 -4
  6. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/builder/_utils.py +2 -2
  7. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/builder/accessor.py +15 -12
  8. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/builder/arrange.py +5 -3
  9. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/builder/asset.py +6 -5
  10. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/builder/items.py +27 -3
  11. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/builder/mixins.py +66 -52
  12. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/builder/node.py +17 -15
  13. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/builder/socket.py +339 -306
  14. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/builder/tree.py +46 -39
  15. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/export/codegen.py +109 -82
  16. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/export/diagram.py +15 -8
  17. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/export/web_render.py +2 -2
  18. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/lib/nodearrange/arrange/graph.py +7 -6
  19. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/lib/nodearrange/arrange/ordering.py +22 -14
  20. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/lib/nodearrange/arrange/ranking.py +8 -7
  21. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/lib/nodearrange/arrange/realize.py +11 -4
  22. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/lib/nodearrange/arrange/stacking.py +11 -13
  23. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/lib/nodearrange/arrange/x_coords.py +3 -6
  24. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/lib/nodearrange/arrange/y_coords.py +2 -2
  25. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/lib/nodearrange/utils.py +5 -10
  26. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/_mixins.py +13 -9
  27. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/compositor/assets.py +67 -98
  28. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/compositor/color.py +6 -9
  29. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/compositor/converter.py +31 -35
  30. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/compositor/distort.py +8 -10
  31. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/compositor/filter.py +8 -10
  32. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/compositor/input.py +9 -11
  33. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/compositor/interface.py +22 -26
  34. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/compositor/manual.py +12 -11
  35. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/compositor/matte.py +8 -10
  36. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/compositor/output.py +3 -5
  37. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/compositor/vector.py +3 -5
  38. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/__init__.py +208 -209
  39. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/assets.py +452 -542
  40. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/attribute.py +22 -26
  41. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/color.py +4 -6
  42. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/converter.py +158 -162
  43. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/geometry.py +50 -56
  44. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/grid.py +115 -119
  45. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/input.py +18 -24
  46. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/interface.py +29 -33
  47. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/manual.py +281 -130
  48. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/texture.py +7 -9
  49. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/utilities.py +3 -5
  50. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/vector.py +4 -6
  51. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/zone.py +16 -13
  52. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/shader/assets.py +5 -4
  53. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/shader/color.py +4 -6
  54. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/shader/converter.py +21 -25
  55. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/shader/grid.py +6 -8
  56. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/shader/input.py +5 -7
  57. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/shader/manual.py +17 -16
  58. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/shader/output.py +6 -8
  59. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/shader/shader.py +7 -9
  60. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/shader/texture.py +4 -7
  61. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/shader/vector.py +5 -7
  62. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/types.py +44 -49
  63. {nodebpy-520.13.0 → nodebpy-520.15.0}/README.md +0 -0
  64. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/__init__.py +4 -4
  65. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/assets/__init__.py +2 -2
  66. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/assets/__main__.py +0 -0
  67. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/export/__init__.py +1 -1
  68. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/lib/nodearrange/__init__.py +0 -0
  69. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/lib/nodearrange/arrange/__init__.py +0 -0
  70. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/lib/nodearrange/arrange/structs.py +0 -0
  71. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/lib/nodearrange/arrange/sugiyama.py +0 -0
  72. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/lib/nodearrange/config.py +0 -0
  73. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/__init__.py +0 -0
  74. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/compositor/__init__.py +67 -67
  75. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/compositor/group.py +0 -0
  76. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/group.py +0 -0
  77. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/groups.py +0 -0
  78. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/geometry/output.py +0 -0
  79. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/shader/__init__.py +61 -61
  80. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/shader/group.py +0 -0
  81. {nodebpy-520.13.0 → nodebpy-520.15.0}/src/nodebpy/nodes/shader/script.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nodebpy
3
- Version: 520.13.0
3
+ Version: 520.15.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.13.0"
3
+ version = "520.15.0"
4
4
  description = "Build nodes trees in Blender more elegantly with code"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -44,5 +44,26 @@ dev = [
44
44
  "syrupy>=5.0.0",
45
45
  "pytest-xdist>=3.8.0",
46
46
  "griffe<2.0.0",
47
- "ty==0.0.51",
47
+ "ty==0.0.74",
48
+ "types-networkx>=3.6.1.20260728",
48
49
  ]
50
+
51
+ # The bpy stubs mark most collections/pointers as `| None` and cannot type
52
+ # dynamic attributes (e.g. `socket.default_value`), which test code exercises
53
+ # constantly under a real Blender runtime. Relax only those rules for tests;
54
+ # src/ passes `ty check` with no per-directory relaxation.
55
+ [[tool.ty.overrides]]
56
+ include = ["tests/**"]
57
+
58
+ [tool.ty.overrides.rules]
59
+ unresolved-attribute = "ignore"
60
+ not-subscriptable = "ignore"
61
+ invalid-assignment = "ignore"
62
+ invalid-argument-type = "ignore"
63
+
64
+ [tool.ruff.lint]
65
+ ignore = ["SIM117", "RUF059"]
66
+
67
+ [tool.ruff.lint.per-file-ignores]
68
+ "tests/test_codegen.py" = ["S102"]
69
+ "tests/test_assets.py" = ["S102"]
@@ -17,6 +17,7 @@ from collections import Counter
17
17
  from collections.abc import Sequence
18
18
  from dataclasses import dataclass
19
19
  from pathlib import Path
20
+ from typing import cast
20
21
 
21
22
  import bpy
22
23
 
@@ -220,6 +221,7 @@ def _introspect_group(group, name: str, library_source: str) -> _AssetClass:
220
221
  ``node_groups.new`` too narrowly for the runtime attributes used here.
221
222
  """
222
223
  host = bpy.data.node_groups.new("_introspect_host", group.bl_idname)
224
+ assert host is not None
223
225
  try:
224
226
  node_type = {
225
227
  "GeometryNodeTree": "GeometryNodeGroup",
@@ -227,7 +229,12 @@ def _introspect_group(group, name: str, library_source: str) -> _AssetClass:
227
229
  "CompositorNodeTree": "CompositorNodeGroup",
228
230
  }[group.bl_idname]
229
231
  node = host.nodes.new(node_type)
230
- node.node_tree = group # ty: ignore[unresolved-attribute]
232
+ assert node is not None
233
+ node = cast(
234
+ "bpy.types.GeometryNodeGroup | bpy.types.ShaderNodeGroup | bpy.types.CompositorNodeGroup",
235
+ node,
236
+ )
237
+ node.node_tree = group
231
238
  # Tooltips live on the tree *interface* items, not on the node's
232
239
  # sockets — collect them by identifier so the generated docstrings
233
240
  # can use the asset author's own wording.
@@ -442,7 +449,10 @@ def generate_asset_api(
442
449
  editors show documentation alongside the type hints. Defaults to
443
450
  ``True``; pass ``False`` for a terser module.
444
451
 
445
- Returns the list of generated class names.
452
+ Returns
453
+ -------
454
+ list[str]
455
+ The list of generated class names.
446
456
  """
447
457
  if isinstance(libraries, AssetLibrary):
448
458
  libraries = [libraries]
@@ -9,7 +9,7 @@ from .asset import (
9
9
  PackageLibrary,
10
10
  asset_group_base,
11
11
  )
12
- from .items import Item, ItemsMixin
12
+ from .items import Item, ItemsMixin, MenuItem
13
13
  from .mixins import LinkingMixin, OperatorMixin
14
14
  from .node import (
15
15
  BaseNode,
@@ -75,82 +75,76 @@ from .tree import (
75
75
  )
76
76
 
77
77
  __all__ = [
78
- # Core
79
- "TreeBuilder",
80
- "MaterialBuilder",
78
+ "AssetCompositorGroup",
79
+ "AssetGeometryGroup",
80
+ "AssetLibrary",
81
+ "AssetShaderGroup",
81
82
  "BaseNode",
82
- "Socket",
83
- "SocketAccessor",
84
- # Mixins
85
- "OperatorMixin",
86
- "LinkingMixin",
87
- "DynamicInputsMixin",
88
- "Item",
89
- "ItemsMixin",
90
- # Node groups
91
- "NodeGroupBuilder",
83
+ "BooleanSocket",
84
+ "BooleanSocketGrid",
85
+ "BooleanSocketList",
86
+ "BundleSocket",
87
+ "BundleSocketList",
88
+ "BundledLibrary",
89
+ "ClosureSocket",
90
+ "ClosureSocketList",
91
+ "CollectionSocket",
92
+ "CollectionSocketList",
93
+ "ColorSocket",
94
+ "ColorSocketList",
92
95
  "CustomCompositorGroup",
93
96
  "CustomGeometryGroup",
94
97
  "CustomShaderGroup",
95
- # Asset-backed node groups
96
- "AssetLibrary",
97
- "BundledLibrary",
98
- "PackageLibrary",
99
- "AssetGeometryGroup",
100
- "AssetShaderGroup",
101
- "AssetCompositorGroup",
102
- "asset_group_base",
103
- # Runtime socket types
98
+ "DynamicInputsMixin",
104
99
  "FloatSocket",
105
100
  "FloatSocketGrid",
106
101
  "FloatSocketList",
107
- "VectorSocket",
108
- "VectorSocketGrid",
109
- "VectorSocketList",
110
- "ColorSocket",
111
- "ColorSocketList",
102
+ "FontSocket",
103
+ "FontSocketList",
104
+ "GeometrySocket",
105
+ "GeometrySocketList",
106
+ "ImageSocket",
107
+ "ImageSocketList",
108
+ "InputInterfaceContext",
112
109
  "IntegerSocket",
113
- "IntegerVectorSocket",
114
110
  "IntegerSocketGrid",
115
111
  "IntegerSocketList",
116
- "BooleanSocket",
117
- "BooleanSocketGrid",
118
- "BooleanSocketList",
119
- "RotationSocket",
120
- "RotationSocketList",
112
+ "IntegerVectorSocket",
113
+ "Item",
114
+ "ItemsMixin",
115
+ "LinkingMixin",
116
+ "MaterialBuilder",
117
+ "MaterialSocket",
118
+ "MaterialSocketList",
121
119
  "MatrixSocket",
122
120
  "MatrixSocketList",
123
- "StringSocket",
124
- "StringSocketList",
121
+ "MenuItem",
125
122
  "MenuSocket",
126
123
  "MenuSocketList",
127
- "GeometrySocket",
128
- "GeometrySocketList",
124
+ "NodeGroupBuilder",
129
125
  "ObjectSocket",
130
126
  "ObjectSocketList",
131
- "FontSocket",
132
- "FontSocketList",
133
- "MaterialSocket",
134
- "MaterialSocketList",
135
- "ImageSocket",
136
- "ImageSocketList",
137
- "CollectionSocket",
138
- "CollectionSocketList",
139
- "BundleSocket",
140
- "BundleSocketList",
141
- "ClosureSocket",
142
- "ClosureSocketList",
127
+ "OperatorMixin",
128
+ "OutputInterfaceContext",
129
+ "PackageLibrary",
130
+ "PanelContext",
131
+ "RotationSocket",
132
+ "RotationSocketList",
143
133
  "ShaderSocket",
144
134
  "ShaderSocketList",
145
- "SoundSocket",
146
- "SoundSocketList",
147
- # Tree context
135
+ "Socket",
136
+ "SocketAccessor",
148
137
  "SocketContext",
149
- "PanelContext",
150
- "InputInterfaceContext",
151
- "OutputInterfaceContext",
152
- # Utilities
153
138
  "SocketError",
154
- "normalize_name",
139
+ "SoundSocket",
140
+ "SoundSocketList",
141
+ "StringSocket",
142
+ "StringSocketList",
143
+ "TreeBuilder",
144
+ "VectorSocket",
145
+ "VectorSocketGrid",
146
+ "VectorSocketList",
147
+ "asset_group_base",
155
148
  "denormalize_name",
149
+ "normalize_name",
156
150
  ]
@@ -7,12 +7,12 @@ from bpy.types import NodeSocket
7
7
  if TYPE_CHECKING:
8
8
  from .socket import Socket
9
9
 
10
- _SOCKET_REGISTRY: dict[str, "type[Socket]"] = {}
11
- _SOCKET_LIST_REGISTRY: dict[str, "type[Socket]"] = {}
12
- _SOCKET_GRID_REGISTRY: dict[str, "type[Socket]"] = {}
10
+ _SOCKET_REGISTRY: dict[str, type[Socket]] = {}
11
+ _SOCKET_LIST_REGISTRY: dict[str, type[Socket]] = {}
12
+ _SOCKET_GRID_REGISTRY: dict[str, type[Socket]] = {}
13
13
 
14
14
 
15
- def _wrap_socket(socket: NodeSocket) -> "Socket":
15
+ def _wrap_socket(socket: NodeSocket) -> Socket:
16
16
  for key, cls in _SOCKET_REGISTRY.items():
17
17
  if key in socket.bl_idname:
18
18
  structure = getattr(socket, "inferred_structure_type", "SINGLE")
@@ -77,7 +77,7 @@ def _allow_innactive_sockets(node: bpy.types.Node) -> bool:
77
77
 
78
78
  def _resolve_promotion(
79
79
  self_socket: NodeSocket, other: Any, reverse: bool
80
- ) -> "tuple[NodeSocket, Any, bool]":
80
+ ) -> tuple[NodeSocket, Any, bool]:
81
81
  """Determine the dominant socket for operator dispatch.
82
82
 
83
83
  When both operands have a socket type, the higher-precedence type wins.
@@ -114,7 +114,7 @@ class _SocketLike(Protocol):
114
114
  socket: NodeSocket
115
115
 
116
116
 
117
- def _output_socket_type(value: Any) -> "SOCKET_TYPES | None":
117
+ def _output_socket_type(value: Any) -> SOCKET_TYPES | None:
118
118
  """The Blender socket ``type`` of *value*'s default output socket.
119
119
 
120
120
  Resolves a raw ``NodeSocket`` or any socket/node wrapper to the type string
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING, Iterator, Literal, overload
3
+ from collections.abc import Iterator
4
+ from typing import TYPE_CHECKING, Literal, cast, overload
4
5
 
5
6
  import bpy
6
7
  from bpy.types import NodeSocket
@@ -84,10 +85,10 @@ class SocketAccessor:
84
85
  )
85
86
 
86
87
  @overload
87
- def _get(self, key: slice) -> "list[Socket]": ...
88
+ def _get(self, key: slice) -> list[Socket]: ...
88
89
  @overload
89
- def _get(self, key: str | int) -> "Socket": ...
90
- def _get(self, key: str | int | slice) -> "Socket | list[Socket]":
90
+ def _get(self, key: str | int) -> Socket: ...
91
+ def _get(self, key: str | int | slice) -> Socket | list[Socket]:
91
92
  """Get a Socket for a socket by identifier, name, or index."""
92
93
  if isinstance(key, slice):
93
94
  sockets = [
@@ -104,10 +105,10 @@ class SocketAccessor:
104
105
  return socket
105
106
 
106
107
  @overload
107
- def __getitem__(self, key: slice) -> "list[Socket]": ...
108
+ def __getitem__(self, key: slice) -> list[Socket]: ...
108
109
  @overload
109
- def __getitem__(self, key: str | int) -> "Socket": ...
110
- def __getitem__(self, key: str | int | slice) -> "Socket | list[Socket]":
110
+ def __getitem__(self, key: str | int) -> Socket: ...
111
+ def __getitem__(self, key: str | int | slice) -> Socket | list[Socket]:
111
112
  """Access by identifier, name, or integer index."""
112
113
  return self._get(key)
113
114
 
@@ -122,7 +123,9 @@ class SocketAccessor:
122
123
  for s in self._collection:
123
124
  assert s.node is not None
124
125
  return s.node
125
- return self._collection.data # empty collection fallback
126
+ return cast(
127
+ "bpy.types.Node", self._collection.data
128
+ ) # empty collection fallback
126
129
 
127
130
  @property
128
131
  def _ignore_visibility(self) -> bool:
@@ -191,7 +194,7 @@ class SocketAccessor:
191
194
  f"{socket_type} on {self._node.name}"
192
195
  )
193
196
 
194
- def _values(self) -> "list[Socket]":
197
+ def _values(self) -> list[Socket]:
195
198
  """All visible sockets as Sockets.
196
199
 
197
200
  Uses node-level visibility rules regardless of ``ignore_visibility`` —
@@ -199,7 +202,7 @@ class SocketAccessor:
199
202
  """
200
203
  return [_wrap_socket(s) for s in self._visible_sockets()]
201
204
 
202
- def _items(self) -> "list[tuple[str, Socket]]":
205
+ def _items(self) -> list[tuple[str, Socket]]:
203
206
  """All visible sockets as (name, Socket) pairs.
204
207
 
205
208
  Uses node-level visibility rules regardless of ``ignore_visibility`` —
@@ -214,10 +217,10 @@ class SocketAccessor:
214
217
  def __len__(self) -> int:
215
218
  return len(self._items())
216
219
 
217
- def __iter__(self) -> Iterator["Socket"]:
220
+ def __iter__(self) -> Iterator[Socket]:
218
221
  return iter(self._values())
219
222
 
220
- def __getattr__(self, name: str) -> "Socket":
223
+ def __getattr__(self, name: str) -> Socket:
221
224
  """Dynamic socket access by normalised attribute name.
222
225
 
223
226
  Converts ``node.o.base_color`` to ``self._get("Base Color")``.
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from collections import Counter, deque
4
+ from typing import Any, cast
4
5
 
5
6
  import bpy
6
7
 
@@ -114,10 +115,12 @@ def calculate_node_dimensions(
114
115
  enabled_outputs = sum(1 for s in node.outputs if s.enabled)
115
116
 
116
117
  # count properties specific to this node type (not inherited)
118
+ # ``bl_rna`` exists on bpy classes via their metaclass, invisible to type
119
+ # checkers looking at plain ``type``.
117
120
  inherited_ids = {
118
121
  prop.identifier
119
122
  for base in type(node).__bases__
120
- for prop in getattr(base, "bl_rna").properties
123
+ for prop in cast(Any, base).bl_rna.properties
121
124
  }
122
125
  node_property_count = sum(
123
126
  1 for prop in node.bl_rna.properties if prop.identifier not in inherited_ids
@@ -275,8 +278,7 @@ def position_nodes_in_columns(
275
278
 
276
279
  width, height = calculate_node_dimensions(node, connection_counts, 1.0)
277
280
 
278
- if width > col_width:
279
- col_width = width
281
+ col_width = max(col_width, width)
280
282
 
281
283
  node.location = (x, y)
282
284
 
@@ -12,6 +12,7 @@ from __future__ import annotations
12
12
  import os
13
13
  from abc import ABC, abstractmethod
14
14
  from pathlib import Path
15
+ from typing import Any
15
16
 
16
17
  import bpy
17
18
 
@@ -122,19 +123,19 @@ _ASSET_GROUP_BASE = {
122
123
  }
123
124
 
124
125
 
125
- def asset_group_base(tree_idname: str) -> type:
126
+ def asset_group_base(tree_idname: str) -> type[Any]:
126
127
  """The ``Asset*Group`` base class for a tree-type bl_idname."""
127
128
  return _ASSET_GROUP_BASE[tree_idname]
128
129
 
129
130
 
130
131
  __all__ = [
132
+ "AssetCompositorGroup",
133
+ "AssetGeometryGroup",
131
134
  "AssetLibrary",
132
- "BundledLibrary",
133
- "PackageLibrary",
134
135
  "AssetNodeGroup",
135
- "AssetGeometryGroup",
136
136
  "AssetShaderGroup",
137
- "AssetCompositorGroup",
137
+ "BundledLibrary",
138
+ "PackageLibrary",
138
139
  "asset_group_base",
139
140
  ]
140
141
 
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING, Any, Generic, Mapping, TypeVar, cast
3
+ from collections.abc import Mapping
4
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast
4
5
 
5
6
  from bpy.types import ID, Node, NodeSocket
6
7
  from mathutils import Euler
@@ -9,7 +10,7 @@ from ..types import _is_default_value
9
10
  from ._registry import _wrap_socket
10
11
  from ._utils import _SocketLike
11
12
  from .node import DynamicInputsMixin
12
- from .socket import Socket
13
+ from .socket import BaseSocket, Socket
13
14
 
14
15
  if TYPE_CHECKING:
15
16
  from ..types import (
@@ -95,7 +96,7 @@ def _infer_value_type(value: Any) -> str | None:
95
96
  return None
96
97
 
97
98
 
98
- _SocketT = TypeVar("_SocketT", bound=Socket, default=Socket)
99
+ _SocketT = TypeVar("_SocketT", bound=BaseSocket, default=Socket)
99
100
 
100
101
 
101
102
  class Item(Generic[_SocketT]):
@@ -151,6 +152,29 @@ class Item(Generic[_SocketT]):
151
152
  )
152
153
 
153
154
 
155
+ class MenuItem(Item[_SocketT]):
156
+ """Handle for one Menu Switch enum item.
157
+
158
+ ``input`` is the item's input socket (typed to the switch's data
159
+ type), ``is_selected`` the item's boolean output socket, and
160
+ ``description`` the tooltip Blender shows for the item in the menu.
161
+ """
162
+
163
+ @property
164
+ def is_selected(self) -> BooleanSocket:
165
+ """Boolean output socket — True when this item is selected."""
166
+ return cast("BooleanSocket", self.output)
167
+
168
+ @property
169
+ def description(self) -> str:
170
+ """Tooltip shown for this item in the drop-down menu."""
171
+ return self._item.description
172
+
173
+ @description.setter
174
+ def description(self, value: str) -> None:
175
+ self._item.description = value
176
+
177
+
154
178
  class ItemsMixin(DynamicInputsMixin):
155
179
  """Socket machinery for nodes whose sockets are driven by a bpy item
156
180
  collection (``capture_items``, ``bake_items``, ``format_items``, ...).