nodebpy 0.12.0__tar.gz → 0.14.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 (74) hide show
  1. {nodebpy-0.12.0 → nodebpy-0.14.0}/PKG-INFO +1 -1
  2. {nodebpy-0.12.0 → nodebpy-0.14.0}/pyproject.toml +1 -1
  3. nodebpy-0.14.0/src/nodebpy/__init__.py +14 -0
  4. nodebpy-0.14.0/src/nodebpy/builder/__init__.py +86 -0
  5. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/builder/_utils.py +4 -1
  6. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/builder/accessor.py +2 -3
  7. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/builder/mixins.py +55 -27
  8. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/builder/node.py +38 -22
  9. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/builder/socket.py +158 -92
  10. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/builder/tree.py +164 -199
  11. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/compositor/__init__.py +29 -13
  12. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/compositor/color.py +13 -13
  13. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/compositor/converter.py +16 -16
  14. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/compositor/distort.py +14 -14
  15. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/compositor/filter.py +18 -18
  16. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/compositor/group.py +2 -2
  17. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/compositor/input.py +207 -10
  18. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/compositor/interface.py +2 -2
  19. nodebpy-0.14.0/src/nodebpy/nodes/compositor/manual.py +383 -0
  20. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/compositor/matte.py +13 -13
  21. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/compositor/output.py +3 -3
  22. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/compositor/vector.py +2 -2
  23. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/geometry/__init__.py +16 -2
  24. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/geometry/attribute.py +5 -5
  25. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/geometry/color.py +3 -3
  26. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/geometry/converter.py +58 -58
  27. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/geometry/geometry.py +93 -669
  28. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/geometry/grid.py +37 -37
  29. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/geometry/group.py +2 -2
  30. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/geometry/input.py +77 -77
  31. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/geometry/interface.py +8 -8
  32. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/geometry/manual.py +1147 -230
  33. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/geometry/output.py +2 -2
  34. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/geometry/texture.py +11 -11
  35. nodebpy-0.14.0/src/nodebpy/nodes/geometry/utilities.py +69 -0
  36. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/geometry/vector.py +5 -5
  37. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/geometry/zone.py +145 -15
  38. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/shader/__init__.py +18 -14
  39. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/shader/color.py +5 -5
  40. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/shader/converter.py +7 -81
  41. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/shader/grid.py +6 -6
  42. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/shader/group.py +2 -2
  43. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/shader/input.py +20 -20
  44. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/shader/manual.py +76 -14
  45. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/shader/output.py +6 -6
  46. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/shader/script.py +2 -2
  47. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/shader/shader.py +21 -21
  48. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/shader/texture.py +5 -5
  49. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/shader/vector.py +8 -8
  50. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/types.py +22 -4
  51. nodebpy-0.12.0/src/nodebpy/__init__.py +0 -23
  52. nodebpy-0.12.0/src/nodebpy/builder/__init__.py +0 -155
  53. nodebpy-0.12.0/src/nodebpy/builder/interface.py +0 -571
  54. nodebpy-0.12.0/src/nodebpy/nodes/compositor/manual.py +0 -30
  55. nodebpy-0.12.0/src/nodebpy/nodes/shader/interface.py +0 -100
  56. nodebpy-0.12.0/src/nodebpy/sockets.py +0 -48
  57. {nodebpy-0.12.0 → nodebpy-0.14.0}/README.md +0 -0
  58. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/arrange.py +0 -0
  59. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/builder/_registry.py +0 -0
  60. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/diagram.py +0 -0
  61. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/lib/nodearrange/__init__.py +0 -0
  62. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/lib/nodearrange/arrange/graph.py +0 -0
  63. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/lib/nodearrange/arrange/ordering.py +0 -0
  64. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/lib/nodearrange/arrange/ranking.py +0 -0
  65. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/lib/nodearrange/arrange/realize.py +0 -0
  66. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/lib/nodearrange/arrange/stacking.py +0 -0
  67. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/lib/nodearrange/arrange/structs.py +0 -0
  68. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/lib/nodearrange/arrange/sugiyama.py +0 -0
  69. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/lib/nodearrange/arrange/x_coords.py +0 -0
  70. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/lib/nodearrange/arrange/y_coords.py +0 -0
  71. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/lib/nodearrange/config.py +0 -0
  72. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/lib/nodearrange/utils.py +0 -0
  73. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/__init__.py +0 -0
  74. {nodebpy-0.12.0 → nodebpy-0.14.0}/src/nodebpy/nodes/geometry/groups.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nodebpy
3
- Version: 0.12.0
3
+ Version: 0.14.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 = "0.12.0"
3
+ version = "0.14.0"
4
4
  description = "Build nodes trees in Blender more elegantly with code"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -0,0 +1,14 @@
1
+ from . import diagram, nodes
2
+ from .builder import (
3
+ TreeBuilder,
4
+ )
5
+ from .nodes import compositor, geometry, shader
6
+
7
+ __all__ = [
8
+ "nodes",
9
+ "compositor",
10
+ "geometry",
11
+ "shader",
12
+ "diagram",
13
+ "TreeBuilder",
14
+ ]
@@ -0,0 +1,86 @@
1
+ from ._utils import SocketError, denormalize_name, normalize_name
2
+ from .accessor import SocketAccessor
3
+ from .mixins import LinkingMixin, OperatorMixin
4
+ from .node import (
5
+ BaseNode,
6
+ CustomCompositorGroup,
7
+ CustomGeometryGroup,
8
+ CustomShaderGroup,
9
+ DynamicInputsMixin,
10
+ NodeGroupBuilder,
11
+ )
12
+ from .socket import (
13
+ BooleanSocket,
14
+ BundleSocket,
15
+ ClosureSocket,
16
+ CollectionSocket,
17
+ ColorSocket,
18
+ FloatSocket,
19
+ FontSocket,
20
+ GeometrySocket,
21
+ ImageSocket,
22
+ IntegerSocket,
23
+ MaterialSocket,
24
+ MatrixSocket,
25
+ MenuSocket,
26
+ ObjectSocket,
27
+ RotationSocket,
28
+ ShaderSocket,
29
+ Socket,
30
+ StringSocket,
31
+ VectorSocket,
32
+ )
33
+ from .tree import (
34
+ InputInterfaceContext,
35
+ MaterialBuilder,
36
+ OutputInterfaceContext,
37
+ PanelContext,
38
+ SocketContext,
39
+ TreeBuilder,
40
+ )
41
+
42
+ __all__ = [
43
+ # Core
44
+ "TreeBuilder",
45
+ "MaterialBuilder",
46
+ "BaseNode",
47
+ "Socket",
48
+ "SocketAccessor",
49
+ # Mixins
50
+ "OperatorMixin",
51
+ "LinkingMixin",
52
+ "DynamicInputsMixin",
53
+ # Node groups
54
+ "NodeGroupBuilder",
55
+ "CustomCompositorGroup",
56
+ "CustomGeometryGroup",
57
+ "CustomShaderGroup",
58
+ # Runtime socket types
59
+ "FloatSocket",
60
+ "VectorSocket",
61
+ "ColorSocket",
62
+ "IntegerSocket",
63
+ "BooleanSocket",
64
+ "RotationSocket",
65
+ "MatrixSocket",
66
+ "StringSocket",
67
+ "MenuSocket",
68
+ "GeometrySocket",
69
+ "ObjectSocket",
70
+ "FontSocket",
71
+ "MaterialSocket",
72
+ "ImageSocket",
73
+ "CollectionSocket",
74
+ "BundleSocket",
75
+ "ClosureSocket",
76
+ "ShaderSocket",
77
+ # Tree context
78
+ "SocketContext",
79
+ "PanelContext",
80
+ "InputInterfaceContext",
81
+ "OutputInterfaceContext",
82
+ # Utilities
83
+ "SocketError",
84
+ "normalize_name",
85
+ "denormalize_name",
86
+ ]
@@ -67,7 +67,10 @@ def _resolve_promotion(
67
67
 
68
68
  if other_prec > self_prec:
69
69
  # Other side is dominant — swap so the linker wraps the vector/higher socket
70
- other_socket = other._default_output_socket
70
+ if isinstance(other, NodeSocket):
71
+ other_socket = other
72
+ else:
73
+ other_socket = other._default_output_socket
71
74
  return other_socket, self_socket, not reverse
72
75
 
73
76
  return self_socket, other, reverse
@@ -28,7 +28,7 @@ class SocketAccessor:
28
28
 
29
29
  def __init__(
30
30
  self,
31
- collection: bpy.types.NodeInputs | bpy.types.NodeOutputs,
31
+ collection: bpy.types.NodeInputs | bpy.types.NodeOutputs | list[NodeSocket],
32
32
  direction: Literal["input", "output"],
33
33
  ):
34
34
  self._direction = direction
@@ -190,8 +190,7 @@ class SocketAccessor:
190
190
  return len(self._items())
191
191
 
192
192
  def __iter__(self):
193
- """Iterate over socket names (enables ``**node.outputs`` unpacking)."""
194
- return iter(self._keys())
193
+ return iter(self._values())
195
194
 
196
195
  def __getattr__(self, name: str) -> "Socket":
197
196
  """Dynamic socket access by normalised attribute name.
@@ -11,10 +11,11 @@ from ._utils import SocketError, _resolve_promotion, _SocketLike
11
11
  _RShiftT = TypeVar("_RShiftT")
12
12
 
13
13
  if TYPE_CHECKING:
14
- from ..nodes.geometry import Compare, Math
14
+ from ..nodes.geometry import Compare, Math, MultiplyMatrices, TransformPoint
15
15
  from ..types import InputLinkable
16
16
  from .node import BaseNode
17
- from .socket import Socket
17
+ from .socket import MatrixSocket, Socket
18
+ from .tree import TreeBuilder
18
19
 
19
20
 
20
21
  class OperatorMixin:
@@ -100,9 +101,12 @@ class OperatorMixin:
100
101
  )
101
102
 
102
103
  def _apply_compare_operation(self, other: Any, operation: str) -> "Math":
103
- return _get_socket_linker(self._default_output_socket)._dispatch_compare( # type: ignore[attr-defined]
104
- other, operation
104
+ socket, other, _ = _resolve_promotion(
105
+ self._default_output_socket, # type: ignore[attr-defined]
106
+ other,
107
+ False,
105
108
  )
109
+ return _get_socket_linker(socket)._dispatch_compare(other, operation)
106
110
 
107
111
  def __lt__(self, other: Any) -> "Compare":
108
112
  return self._apply_compare_operation(other, "less_than")
@@ -157,46 +161,46 @@ class OperatorMixin:
157
161
  return BooleanMath.l_not(self)
158
162
 
159
163
  @staticmethod
160
- def _cast_to_matrix(value):
164
+ def _cast_to_matrix(value) -> MatrixSocket:
161
165
  from ..nodes.geometry.converter import CombineMatrix
162
166
 
163
167
  if hasattr(value, "shape") and value.shape == (4, 4):
164
- return CombineMatrix(*value.ravel())
168
+ return CombineMatrix(*value.ravel()).o.matrix
165
169
  else:
166
170
  return value
167
171
 
168
- def __matmul__(self, other: Any):
172
+ def __matmul__(self, other: Any) -> "MultiplyMatrices | TransformPoint":
169
173
  from ..nodes.geometry.converter import MultiplyMatrices, TransformPoint
170
174
 
171
175
  other = self._cast_to_matrix(other)
172
- socket = self._default_output_socket # type: ignore[attr-defined]
173
- other_type = getattr(other, "type", None)
176
+ socket = self._default_output_socket
174
177
 
175
- if socket.type == "MATRIX" and other_type == "VECTOR":
178
+ if socket.type == "MATRIX" and other.type == "VECTOR":
176
179
  return TransformPoint(other, socket)
177
180
 
178
- return MultiplyMatrices(self, other)
181
+ return MultiplyMatrices(socket, other)
179
182
 
180
- def __rmatmul__(self, other: Any):
183
+ def __rmatmul__(self, other: Any) -> "MultiplyMatrices | TransformPoint":
181
184
  from ..nodes.geometry.converter import MultiplyMatrices, TransformPoint
182
185
 
183
186
  other = self._cast_to_matrix(other)
184
- socket = self._default_output_socket # type: ignore[attr-defined]
185
- other_type = getattr(other, "type", None)
187
+ socket = self._default_output_socket
186
188
 
187
- if socket.type == "VECTOR" and other_type == "MATRIX":
189
+ if socket.type == "VECTOR" and getattr(other, "type", None) == "MATRIX":
188
190
  return TransformPoint(socket, other)
189
191
 
190
- return MultiplyMatrices(other, self)
192
+ return MultiplyMatrices(other, socket)
191
193
 
192
194
 
193
195
  class LinkingMixin:
194
196
  """Node/socket linking logic: ``>>``, ``_link``, best-socket matching.
195
197
 
196
- Requires ``tree``, ``inputs``, ``outputs``, ``_default_output_socket``,
198
+ Requires ``tree``, ``i``, ``o``, ``_default_output_socket``,
197
199
  and ``_default_input_socket`` on the concrete class.
198
200
  """
199
201
 
202
+ tree: "TreeBuilder"
203
+
200
204
  def _source_socket(self, node: "InputLinkable | Socket | NodeSocket") -> NodeSocket:
201
205
  assert node
202
206
  if isinstance(node, NodeSocket):
@@ -221,21 +225,45 @@ class LinkingMixin:
221
225
  target: "BaseNode | Socket | NodeSocket | EllipsisType | LinkingMixin",
222
226
  ) -> tuple[NodeSocket, NodeSocket]:
223
227
  """Find the best compatible pair of sockets between two nodes/sockets."""
224
- from ..types import SOCKET_COMPATIBILITY
228
+ from ..builder.node import BaseNode
229
+ from ..builder.socket import Socket
230
+ from ..types import PREFER_FIRST_SOCKET, SOCKET_COMPATIBILITY
225
231
 
226
232
  possible_combos = []
227
- if hasattr(source, "outputs"):
228
- outputs = source.outputs._available # type: ignore[union-attr]
233
+ if isinstance(source, BaseNode):
234
+ outputs = source.o._available
229
235
  elif isinstance(source, NodeSocket):
230
236
  outputs = [source]
237
+ elif isinstance(source, Socket):
238
+ outputs = [source.socket]
231
239
  else:
232
240
  raise TypeError(f"Cannot get outputs from {type(source)}")
233
241
 
234
- if hasattr(target, "inputs"):
235
- inputs = target.inputs._available # type: ignore[union-attr]
242
+ if isinstance(target, BaseNode):
243
+ inputs = target.i._available
236
244
  else:
237
245
  inputs = [target]
238
246
 
247
+ # NodeReroute adapts its type to whatever is linked — skip type matching
248
+ if getattr(getattr(target, "node", None), "bl_idname", None) == "NodeReroute":
249
+ if outputs and inputs:
250
+ return inputs[0], outputs[0]
251
+
252
+ # Try first available input first — if the output type matches it exactly,
253
+ # or is a "preferred" implicit conversion (e.g. float→color, vector→color),
254
+ # use the first socket rather than searching for a better-typed later one.
255
+ # This keeps float→Image working in the compositor instead of drifting to
256
+ # a float Factor socket that scores higher on raw compatibility.
257
+ # Pairs not in PREFER_FIRST_SOCKET (e.g. VALUE→BOOLEAN, VECTOR→ROTATION)
258
+ # fall through to the ranked search below.
259
+ if inputs:
260
+ first_input = inputs[0]
261
+ for output in outputs:
262
+ if first_input.type == output.type:
263
+ return first_input, output
264
+ if (output.type, first_input.type) in PREFER_FIRST_SOCKET:
265
+ return first_input, output
266
+
239
267
  for output in outputs:
240
268
  compat_sockets = SOCKET_COMPATIBILITY.get(output.type, ())
241
269
  for input in inputs:
@@ -261,7 +289,7 @@ class LinkingMixin:
261
289
  ) -> NodeLink:
262
290
  source_socket = self._source_socket(source)
263
291
  target_socket = self._target_socket(target)
264
- return self.tree.link(source_socket, target_socket) # type: ignore[attr-defined]
292
+ return self.tree.link(source_socket, target_socket)
265
293
 
266
294
  def _link_from(
267
295
  self,
@@ -270,9 +298,9 @@ class LinkingMixin:
270
298
  ):
271
299
  if isinstance(input, str):
272
300
  try:
273
- self._link(source, self.node.inputs[input]) # type: ignore[attr-defined]
301
+ self._link(source, self.node.inputs[input])
274
302
  except KeyError:
275
- self._link(source, self.node.inputs[self.inputs._index(input)]) # type: ignore[attr-defined]
303
+ self._link(source, self.node.inputs[self.i._index(input)])
276
304
  else:
277
305
  self._link(source, input)
278
306
 
@@ -296,8 +324,8 @@ class LinkingMixin:
296
324
  try:
297
325
  target = other.node.inputs[name]
298
326
  except KeyError:
299
- target = other.node.inputs[other.inputs._index(name)]
300
- source = self.outputs._best_match(target.type)
327
+ target = other.node.inputs[other.i._index(name)]
328
+ source = self.o._best_match(target.type) if hasattr(self, "o") else self
301
329
  else:
302
330
  try:
303
331
  source, target = self._find_best_socket_pair(self, other)
@@ -15,10 +15,13 @@ from typing import (
15
15
 
16
16
  import bpy
17
17
  from bpy.types import (
18
+ CompositorNodeGroup,
18
19
  CompositorNodeTree,
20
+ GeometryNodeGroup,
19
21
  GeometryNodeTree,
20
22
  Node,
21
23
  NodeSocket,
24
+ ShaderNodeGroup,
22
25
  ShaderNodeTree,
23
26
  )
24
27
 
@@ -38,7 +41,7 @@ if TYPE_CHECKING:
38
41
  def _add_inputs(self, *args: Any, **kwargs: Any) -> dict[str, NodeSocket]: ...
39
42
 
40
43
  @property
41
- def inputs(self) -> SocketAccessor: ...
44
+ def i(self) -> SocketAccessor: ...
42
45
 
43
46
 
44
47
  class BaseNode(_NodeLike, OperatorMixin, LinkingMixin):
@@ -50,7 +53,7 @@ class BaseNode(_NodeLike, OperatorMixin, LinkingMixin):
50
53
  _default_output_id: str | None = None
51
54
  _placeholder_inputs: list[str]
52
55
 
53
- def __init__(self, node: bpy.types.Node | None = None):
56
+ def __init__(self, node: Node | None = None):
54
57
  tree = (
55
58
  TreeBuilder._tree_contexts[-1] if len(TreeBuilder._tree_contexts) else None
56
59
  )
@@ -81,13 +84,13 @@ class BaseNode(_NodeLike, OperatorMixin, LinkingMixin):
81
84
  @property
82
85
  def _default_input_socket(self) -> NodeSocket:
83
86
  if self._default_input_id is not None:
84
- return self.node.inputs[self.inputs._index(self._default_input_id)]
87
+ return self.node.inputs[self.i._index(self._default_input_id)]
85
88
  return self.node.inputs[0]
86
89
 
87
90
  @property
88
91
  def _default_output_socket(self) -> NodeSocket:
89
92
  if self._default_output_id is not None:
90
- return self.node.outputs[self.outputs._index(self._default_output_id)]
93
+ return self.node.outputs[self.o._index(self._default_output_id)]
91
94
 
92
95
  counter = 0
93
96
  socket = self.node.outputs[counter]
@@ -97,7 +100,7 @@ class BaseNode(_NodeLike, OperatorMixin, LinkingMixin):
97
100
  return socket
98
101
 
99
102
  @classmethod
100
- def _from_node(cls, node: bpy.types.Node) -> Self:
103
+ def _from_node(cls, node: Node) -> Self:
101
104
  builder = cls()
102
105
  builder.tree.nodes.remove(builder.node)
103
106
  builder.node = node
@@ -112,7 +115,7 @@ class BaseNode(_NodeLike, OperatorMixin, LinkingMixin):
112
115
  if link.to_node.bl_idname == cls._bl_idname:
113
116
  return cls._from_node(link.to_node)
114
117
  node = cls()
115
- node.tree.link(socket, node.inputs._best_match(socket.type))
118
+ node.tree.link(socket, node.i._best_match(socket.type))
116
119
  return node
117
120
  else:
118
121
  if socket.links:
@@ -160,9 +163,7 @@ class BaseNode(_NodeLike, OperatorMixin, LinkingMixin):
160
163
  elif isinstance(value, NodeSocket):
161
164
  self._link_from(value, name)
162
165
  elif isinstance(value, _NodeLike):
163
- self._link_from(
164
- value.outputs._best_match(self.inputs._get(name).type), name
165
- )
166
+ self._link_from(value.o._best_match(self.i._get(name).type), name)
166
167
  else:
167
168
  if name in input_ids:
168
169
  input = self.node.inputs[input_ids.index(name)]
@@ -174,14 +175,6 @@ class BaseNode(_NodeLike, OperatorMixin, LinkingMixin):
174
175
  input = self.node.inputs[name.replace("_", " ").title()]
175
176
  self._set_input_default_value(input, value)
176
177
 
177
- @property
178
- def outputs(self) -> SocketAccessor:
179
- return SocketAccessor(self.node.outputs, "output")
180
-
181
- @property
182
- def inputs(self) -> SocketAccessor:
183
- return SocketAccessor(self.node.inputs, "input")
184
-
185
178
  @property
186
179
  def o(self) -> SocketAccessor:
187
180
  """Output socket accessor. Subclasses narrow the return type via TYPE_CHECKING."""
@@ -222,7 +215,7 @@ class DynamicInputsMixin(ABC):
222
215
  except SocketError:
223
216
  dyn = cast("_DynamicTarget", target)
224
217
  target_name, source_socket = list(dyn._add_inputs(source).items())[0]
225
- return (source_socket, dyn.inputs[target_name].socket)
218
+ return (source_socket, dyn.i[target_name].socket)
226
219
 
227
220
  @abstractmethod
228
221
  def _add_socket(self, name: str, *args: Any, **kwargs: Any) -> NodeSocket: ...
@@ -235,7 +228,9 @@ class DynamicInputsMixin(ABC):
235
228
  items[arg._default_output_socket.name] = arg
236
229
  items.update(kwargs)
237
230
  for key, source in items.items():
238
- socket_source, type = self._match_compatible_data(source.outputs._available)
231
+ socket_source, type = self._match_compatible_data(
232
+ source.o._available if hasattr(source, "o") else [source]
233
+ )
239
234
  if type in self._type_map:
240
235
  type = self._type_map[type]
241
236
  socket = self._add_socket(name=key, type=type)
@@ -274,6 +269,12 @@ class NodeGroupBuilder(BaseNode, ABC, Generic[_T]):
274
269
  self.node.show_options = False
275
270
  self._establish_links(**kwargs)
276
271
 
272
+ @property
273
+ @abstractmethod
274
+ def node_tree(self) -> _T:
275
+ """The internal node tree for this group node."""
276
+ ...
277
+
277
278
  @abstractmethod
278
279
  def _setup_node_group(self) -> None:
279
280
  """Set ``self.node.node_tree`` and any node-type-specific properties.
@@ -304,7 +305,12 @@ class CustomGeometryGroup(NodeGroupBuilder[GeometryNodeTree]):
304
305
  """Node group in a Geometry Nodes tree."""
305
306
 
306
307
  _bl_idname = "GeometryNodeGroup"
307
- node: bpy.types.GeometryNodeGroup
308
+ node: GeometryNodeGroup
309
+
310
+ @property
311
+ def node_tree(self) -> GeometryNodeTree:
312
+ assert self.node.node_tree is not None
313
+ return self.node.node_tree
308
314
 
309
315
  def _setup_node_group(self) -> None:
310
316
  self.node.node_tree = self._get_or_create_group()
@@ -324,7 +330,12 @@ class CustomShaderGroup(NodeGroupBuilder[ShaderNodeTree]):
324
330
  """Node group in a Shader (Material) node tree."""
325
331
 
326
332
  _bl_idname = "ShaderNodeGroup"
327
- node: bpy.types.ShaderNodeGroup
333
+ node: ShaderNodeGroup
334
+
335
+ @property
336
+ def node_tree(self) -> ShaderNodeTree:
337
+ assert self.node.node_tree is not None
338
+ return self.node.node_tree
328
339
 
329
340
  def _setup_node_group(self) -> None:
330
341
  self.node.node_tree = self._get_or_create_group()
@@ -343,7 +354,12 @@ class CustomCompositorGroup(NodeGroupBuilder[CompositorNodeTree]):
343
354
  """Node group in a Compositor node tree."""
344
355
 
345
356
  _bl_idname = "CompositorNodeGroup"
346
- node: bpy.types.CompositorNodeGroup
357
+ node: CompositorNodeGroup
358
+
359
+ @property
360
+ def node_tree(self) -> CompositorNodeTree:
361
+ assert self.node.node_tree is not None
362
+ return self.node.node_tree
347
363
 
348
364
  def _setup_node_group(self) -> None:
349
365
  self.node.node_tree = self._get_or_create_group()