nodebpy 0.17.0__tar.gz → 0.18.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 (69) hide show
  1. {nodebpy-0.17.0 → nodebpy-0.18.0}/PKG-INFO +2 -2
  2. {nodebpy-0.17.0 → nodebpy-0.18.0}/README.md +1 -1
  3. {nodebpy-0.17.0 → nodebpy-0.18.0}/pyproject.toml +4 -3
  4. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/arrange.py +5 -1
  5. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/builder/_utils.py +1 -1
  6. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/builder/accessor.py +2 -0
  7. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/builder/mixins.py +74 -44
  8. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/builder/node.py +3 -3
  9. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/builder/socket.py +157 -13
  10. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/builder/tree.py +10 -7
  11. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/diagram.py +4 -2
  12. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/lib/nodearrange/arrange/graph.py +3 -3
  13. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/lib/nodearrange/arrange/ordering.py +5 -5
  14. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/lib/nodearrange/arrange/ranking.py +2 -2
  15. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/lib/nodearrange/arrange/realize.py +2 -2
  16. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/lib/nodearrange/arrange/stacking.py +3 -3
  17. nodebpy-0.18.0/src/nodebpy/lib/nodearrange/arrange/structs.py +179 -0
  18. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/lib/nodearrange/arrange/sugiyama.py +2 -1
  19. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/lib/nodearrange/arrange/x_coords.py +3 -3
  20. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/lib/nodearrange/arrange/y_coords.py +1 -1
  21. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/compositor/color.py +6 -4
  22. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/compositor/converter.py +16 -238
  23. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/compositor/input.py +1 -1
  24. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/compositor/interface.py +9 -3
  25. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/compositor/manual.py +113 -5
  26. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/compositor/output.py +0 -10
  27. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/geometry/attribute.py +2 -3
  28. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/geometry/converter.py +34 -12
  29. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/geometry/geometry.py +25 -7
  30. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/geometry/grid.py +31 -31
  31. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/geometry/groups.py +49 -10
  32. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/geometry/input.py +7 -5
  33. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/geometry/interface.py +21 -5
  34. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/geometry/manual.py +31 -13
  35. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/geometry/utilities.py +1 -1
  36. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/geometry/zone.py +8 -5
  37. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/shader/converter.py +1 -1
  38. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/shader/shader.py +2 -2
  39. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/shader/texture.py +4 -2
  40. nodebpy-0.17.0/src/nodebpy/lib/nodearrange/arrange/structs.py +0 -139
  41. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/__init__.py +0 -0
  42. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/builder/__init__.py +0 -0
  43. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/builder/_registry.py +0 -0
  44. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/lib/nodearrange/__init__.py +0 -0
  45. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/lib/nodearrange/config.py +0 -0
  46. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/lib/nodearrange/utils.py +0 -0
  47. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/__init__.py +0 -0
  48. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/compositor/__init__.py +1 -1
  49. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/compositor/distort.py +0 -0
  50. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/compositor/filter.py +0 -0
  51. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/compositor/group.py +0 -0
  52. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/compositor/matte.py +0 -0
  53. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/compositor/vector.py +0 -0
  54. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/geometry/__init__.py +0 -0
  55. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/geometry/color.py +0 -0
  56. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/geometry/group.py +0 -0
  57. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/geometry/output.py +0 -0
  58. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/geometry/texture.py +0 -0
  59. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/geometry/vector.py +0 -0
  60. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/shader/__init__.py +0 -0
  61. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/shader/color.py +0 -0
  62. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/shader/grid.py +0 -0
  63. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/shader/group.py +0 -0
  64. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/shader/input.py +0 -0
  65. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/shader/manual.py +0 -0
  66. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/shader/output.py +0 -0
  67. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/shader/script.py +0 -0
  68. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/nodes/shader/vector.py +0 -0
  69. {nodebpy-0.17.0 → nodebpy-0.18.0}/src/nodebpy/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nodebpy
3
- Version: 0.17.0
3
+ Version: 0.18.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>
@@ -130,7 +130,7 @@ Some nodes are manually specified in the `src/nodebpy/nodes/geometry/manual.py`
130
130
  Run the build & format script as such:
131
131
 
132
132
  ```bash
133
- uv run generate.py && uvx ruff format && uvx ruff check --fix
133
+ uv run generate.py && uvx ruff format && uvx ruff check --fix && uvx ty check --fix src
134
134
  ```
135
135
 
136
136
  ## Other Projects
@@ -116,7 +116,7 @@ Some nodes are manually specified in the `src/nodebpy/nodes/geometry/manual.py`
116
116
  Run the build & format script as such:
117
117
 
118
118
  ```bash
119
- uv run generate.py && uvx ruff format && uvx ruff check --fix
119
+ uv run generate.py && uvx ruff format && uvx ruff check --fix && uvx ty check --fix src
120
120
  ```
121
121
 
122
122
  ## Other Projects
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nodebpy"
3
- version = "0.17.0"
3
+ version = "0.18.0"
4
4
  description = "Build nodes trees in Blender more elegantly with code"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -28,7 +28,7 @@ build-backend = "uv_build"
28
28
  dev = [
29
29
  "ipython>=8.0.0",
30
30
  "networkx>=3.6.1",
31
- "fake-bpy-module>=20260113",
31
+ "fake-bpy-module>=20260501",
32
32
  "jsondiff>=2.2.1",
33
33
  "pytest>=9.0.2",
34
34
  "pytest-cov>=7.0.0",
@@ -38,5 +38,6 @@ dev = [
38
38
  "syrupy>=5.0.0",
39
39
  "tree-clipper>=0.1.1",
40
40
  "pytest-xdist>=3.8.0",
41
- "griffe<2.0.0"
41
+ "griffe<2.0.0",
42
+ "ty>=0.0.35",
42
43
  ]
@@ -117,7 +117,7 @@ def calculate_node_dimensions(
117
117
  inherited_ids = {
118
118
  prop.identifier
119
119
  for base in type(node).__bases__
120
- for prop in base.bl_rna.properties
120
+ for prop in getattr(base, "bl_rna").properties
121
121
  }
122
122
  node_property_count = sum(
123
123
  1 for prop in node.bl_rna.properties if prop.identifier not in inherited_ids
@@ -143,6 +143,7 @@ def calculate_node_dimensions(
143
143
 
144
144
  def _socket_index(socket: bpy.types.NodeSocket) -> int:
145
145
  """Return the index of a socket among its node's enabled sockets."""
146
+ assert socket.node is not None
146
147
  collection = socket.node.inputs if not socket.is_output else socket.node.outputs
147
148
  idx = 0
148
149
  for s in collection:
@@ -208,6 +209,7 @@ def _reduce_crossings(
208
209
  # on the target node (input side).
209
210
  out_count = max(1, sum(1 for s in src.outputs if s.enabled))
210
211
  in_count = max(1, sum(1 for s in dst.inputs if s.enabled))
212
+ assert link.from_socket is not None and link.to_socket is not None
211
213
  src_frac = _socket_index(link.from_socket) / out_count
212
214
  dst_frac = _socket_index(link.to_socket) / in_count
213
215
 
@@ -299,8 +301,10 @@ def position_reroutes(tree: bpy.types.NodeTree) -> None:
299
301
  targets: list[bpy.types.Node] = []
300
302
  for link in tree.links:
301
303
  if link.to_node == node:
304
+ assert link.from_node is not None
302
305
  sources.append(link.from_node)
303
306
  if link.from_node == node:
307
+ assert link.to_node is not None
304
308
  targets.append(link.to_node)
305
309
 
306
310
  neighbours = sources + targets
@@ -63,7 +63,7 @@ def _resolve_promotion(
63
63
  """
64
64
  other_type = getattr(other, "type", None)
65
65
  self_prec = _TYPE_PRECEDENCE.get(self_socket.type, 1)
66
- other_prec = _TYPE_PRECEDENCE.get(other_type, -1) # type: ignore[arg-type]
66
+ other_prec = _TYPE_PRECEDENCE.get(other_type, -1) if other_type is not None else -1
67
67
 
68
68
  if other_prec > self_prec:
69
69
  # Other side is dominant — swap so the linker wraps the vector/higher socket
@@ -103,10 +103,12 @@ class SocketAccessor:
103
103
  def _node(self) -> bpy.types.Node:
104
104
  """The node this accessor is associated with."""
105
105
  if isinstance(self._collection, list):
106
+ assert self._collection[0].node is not None
106
107
  return self._collection[0].node
107
108
  # bpy NodeInputs/NodeOutputs.id_data returns the NodeTree (top-level ID),
108
109
  # not the Node. Retrieve the node via the first socket instead.
109
110
  for s in self._collection:
111
+ assert s.node is not None
110
112
  return s.node
111
113
  return self._collection.data # empty collection fallback
112
114
 
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from types import EllipsisType
4
- from typing import TYPE_CHECKING, Any, TypeVar, overload
4
+ from typing import TYPE_CHECKING, Any, TypeVar, cast, overload
5
5
 
6
6
  from bpy.types import NodeLink, NodeSocket
7
7
 
@@ -11,8 +11,8 @@ 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
15
14
  from ..types import InputLinkable
15
+ from .accessor import SocketAccessor
16
16
  from .node import BaseNode
17
17
  from .socket import (
18
18
  BooleanSocket,
@@ -35,75 +35,78 @@ class OperatorMixin:
35
35
 
36
36
  __array_ufunc__ = None
37
37
 
38
+ if TYPE_CHECKING:
39
+
40
+ @property
41
+ def _default_output_socket(self) -> "NodeSocket": ...
42
+
38
43
  def _apply_math_operation(
39
44
  self, other: Any, operation: str, reverse: bool = False
40
45
  ) -> "FloatSocket | VectorSocket | IntegerSocket":
41
46
  socket, other, reverse = _resolve_promotion(
42
47
  self._default_output_socket,
43
48
  other,
44
- reverse, # type: ignore[attr-defined]
49
+ reverse,
45
50
  )
46
51
  return _get_socket_linker(socket)._dispatch_math(other, operation, reverse)
47
52
 
48
- def __mul__(self, other: Any) -> "FloatSocket":
53
+ def __mul__(self, other: Any) -> "FloatSocket | VectorSocket | IntegerSocket":
49
54
  return self._apply_math_operation(other, "multiply")
50
55
 
51
- def __rmul__(self, other: Any) -> "FloatSocket":
56
+ def __rmul__(self, other: Any) -> "FloatSocket | VectorSocket | IntegerSocket":
52
57
  return self._apply_math_operation(other, "multiply", reverse=True)
53
58
 
54
- def __truediv__(self, other: Any) -> "FloatSocket":
59
+ def __truediv__(self, other: Any) -> "FloatSocket | VectorSocket | IntegerSocket":
55
60
  return self._apply_math_operation(other, "divide")
56
61
 
57
- def __rtruediv__(self, other: Any) -> "FloatSocket":
62
+ def __rtruediv__(self, other: Any) -> "FloatSocket | VectorSocket | IntegerSocket":
58
63
  return self._apply_math_operation(other, "divide", reverse=True)
59
64
 
60
- def __add__(self, other: Any) -> "FloatSocket":
65
+ def __add__(self, other: Any) -> "FloatSocket | VectorSocket | IntegerSocket":
61
66
  return self._apply_math_operation(other, "add")
62
67
 
63
- def __radd__(self, other: Any) -> "FloatSocket":
68
+ def __radd__(self, other: Any) -> "FloatSocket | VectorSocket | IntegerSocket":
64
69
  return self._apply_math_operation(other, "add", reverse=True)
65
70
 
66
- def __sub__(self, other: Any) -> "FloatSocket":
71
+ def __sub__(self, other: Any) -> "FloatSocket | VectorSocket | IntegerSocket":
67
72
  return self._apply_math_operation(other, "subtract")
68
73
 
69
- def __rsub__(self, other: Any) -> "FloatSocket":
74
+ def __rsub__(self, other: Any) -> "FloatSocket | VectorSocket | IntegerSocket":
70
75
  return self._apply_math_operation(other, "subtract", reverse=True)
71
76
 
72
- def __pow__(self, other: Any) -> "FloatSocket":
77
+ def __pow__(self, other: Any) -> "FloatSocket | VectorSocket | IntegerSocket":
73
78
  return self._apply_math_operation(other, "power")
74
79
 
75
- def __rpow__(self, other: Any) -> "FloatSocket":
80
+ def __rpow__(self, other: Any) -> "FloatSocket | VectorSocket | IntegerSocket":
76
81
  return self._apply_math_operation(other, "power", reverse=True)
77
82
 
78
- def __mod__(self, other: Any) -> "FloatSocket":
83
+ def __mod__(self, other: Any) -> "FloatSocket | VectorSocket | IntegerSocket":
79
84
  return self._apply_math_operation(other, "modulo")
80
85
 
81
- def __rmod__(self, other: Any) -> "FloatSocket":
86
+ def __rmod__(self, other: Any) -> "FloatSocket | VectorSocket | IntegerSocket":
82
87
  return self._apply_math_operation(other, "modulo", reverse=True)
83
88
 
84
- def __floordiv__(self, other: Any) -> "FloatSocket":
89
+ def __floordiv__(self, other: Any) -> "FloatSocket | VectorSocket | IntegerSocket":
85
90
  socket, other, reverse = _resolve_promotion(
86
91
  self._default_output_socket,
87
92
  other,
88
- False, # type: ignore[attr-defined]
93
+ False,
89
94
  )
90
95
  return _get_socket_linker(socket)._dispatch_floordiv(other, reverse)
91
96
 
92
- def __rfloordiv__(self, other: Any) -> "FloatSocket":
97
+ def __rfloordiv__(self, other: Any) -> "FloatSocket | VectorSocket | IntegerSocket":
93
98
  socket, other, reverse = _resolve_promotion(
94
99
  self._default_output_socket,
95
100
  other,
96
- True, # type: ignore[attr-defined]
101
+ True,
97
102
  )
98
103
  return _get_socket_linker(socket)._dispatch_floordiv(other, reverse)
99
104
 
100
- def __neg__(self) -> "FloatSocket":
101
- return _get_socket_linker(self._default_output_socket)._dispatch_unary( # type: ignore[attr-defined]
102
- "negate"
103
- )
105
+ def __neg__(self) -> "FloatSocket | VectorSocket | IntegerSocket":
106
+ return _get_socket_linker(self._default_output_socket)._dispatch_unary("negate")
104
107
 
105
- def __abs__(self) -> "FloatSocket":
106
- return _get_socket_linker(self._default_output_socket)._dispatch_unary( # type: ignore[attr-defined]
108
+ def __abs__(self) -> "FloatSocket | VectorSocket | IntegerSocket":
109
+ return _get_socket_linker(self._default_output_socket)._dispatch_unary(
107
110
  "absolute"
108
111
  )
109
112
 
@@ -111,28 +114,28 @@ class OperatorMixin:
111
114
  self, other: Any, operation: str
112
115
  ) -> "FloatSocket | BooleanSocket":
113
116
  socket, other, _ = _resolve_promotion(
114
- self._default_output_socket, # type: ignore[attr-defined]
117
+ self._default_output_socket,
115
118
  other,
116
119
  False,
117
120
  )
118
121
  return _get_socket_linker(socket)._dispatch_compare(other, operation)
119
122
 
120
- def __lt__(self, other: Any) -> "BooleanSocket":
123
+ def __lt__(self, other: Any) -> "FloatSocket | BooleanSocket":
121
124
  return self._apply_compare_operation(other, "less_than")
122
125
 
123
- def __gt__(self, other: Any) -> "Compare":
126
+ def __gt__(self, other: Any) -> "FloatSocket | BooleanSocket":
124
127
  return self._apply_compare_operation(other, "greater_than")
125
128
 
126
- def __le__(self, other: Any) -> "Compare":
129
+ def __le__(self, other: Any) -> "FloatSocket | BooleanSocket":
127
130
  return self._apply_compare_operation(other, "less_equal")
128
131
 
129
- def __ge__(self, other: Any) -> "Compare":
132
+ def __ge__(self, other: Any) -> "FloatSocket | BooleanSocket":
130
133
  return self._apply_compare_operation(other, "greater_equal")
131
134
 
132
- def __eq__(self, other: Any) -> "Compare": # type: ignore[override]
135
+ def __eq__(self, other: Any) -> "FloatSocket | BooleanSocket": # type: ignore
133
136
  return self._apply_compare_operation(other, "equal")
134
137
 
135
- def __ne__(self, other: Any) -> "Compare": # type: ignore[override]
138
+ def __ne__(self, other: Any) -> "FloatSocket | BooleanSocket": # type: ignore
136
139
  return self._apply_compare_operation(other, "not_equal")
137
140
 
138
141
  def _apply_boolean_operation(self, other: Any, operation: str):
@@ -146,7 +149,7 @@ class OperatorMixin:
146
149
  def __rand__(self, other: Any):
147
150
  from ..nodes.geometry.converter import BooleanMath
148
151
 
149
- return BooleanMath.l_and(other, self)
152
+ return BooleanMath.l_and(other, cast(Any, self))
150
153
 
151
154
  def __or__(self, other: Any):
152
155
  return self._apply_boolean_operation(other, "l_or")
@@ -154,7 +157,7 @@ class OperatorMixin:
154
157
  def __ror__(self, other: Any):
155
158
  from ..nodes.geometry.converter import BooleanMath
156
159
 
157
- return BooleanMath.l_or(other, self)
160
+ return BooleanMath.l_or(other, cast(Any, self))
158
161
 
159
162
  def __xor__(self, other: Any):
160
163
  return self._apply_boolean_operation(other, "not_equal")
@@ -162,12 +165,12 @@ class OperatorMixin:
162
165
  def __rxor__(self, other: Any):
163
166
  from ..nodes.geometry.converter import BooleanMath
164
167
 
165
- return BooleanMath.not_equal(other, self)
168
+ return BooleanMath.not_equal(other, cast(Any, self))
166
169
 
167
170
  def __invert__(self):
168
171
  from ..nodes.geometry.converter import BooleanMath
169
172
 
170
- return BooleanMath.l_not(self)
173
+ return BooleanMath.l_not(cast(Any, self))
171
174
 
172
175
  @staticmethod
173
176
  def _cast_to_matrix(value) -> MatrixSocket:
@@ -214,6 +217,20 @@ class LinkingMixin:
214
217
 
215
218
  tree: "TreeBuilder"
216
219
 
220
+ if TYPE_CHECKING:
221
+ import bpy
222
+
223
+ node: bpy.types.Node
224
+
225
+ @property
226
+ def i(self) -> "SocketAccessor": ...
227
+
228
+ @property
229
+ def o(self) -> "SocketAccessor": ...
230
+
231
+ @property
232
+ def _default_output_socket(self) -> "NodeSocket": ...
233
+
217
234
  def _source_socket(self, node: "InputLinkable | Socket | NodeSocket") -> NodeSocket:
218
235
  assert node is not None
219
236
  if isinstance(node, NodeSocket):
@@ -254,8 +271,12 @@ class LinkingMixin:
254
271
 
255
272
  if isinstance(target, BaseNode):
256
273
  inputs = target.i._available
257
- else:
274
+ elif isinstance(target, Socket):
275
+ inputs = [target.socket]
276
+ elif isinstance(target, NodeSocket):
258
277
  inputs = [target]
278
+ else:
279
+ raise TypeError(f"Cannot get inputs from {type(target)}")
259
280
 
260
281
  # NodeReroute adapts its type to whatever is linked — skip type matching
261
282
  if getattr(getattr(target, "node", None), "bl_idname", None) == "NodeReroute":
@@ -291,8 +312,10 @@ class LinkingMixin:
291
312
  if possible_combos:
292
313
  return sorted(possible_combos, key=lambda x: x[0])[0][1]
293
314
 
315
+ src_name = getattr(getattr(source, "node", None), "name", repr(source))
316
+ tgt_name = getattr(getattr(target, "node", None), "name", repr(target))
294
317
  raise SocketError(
295
- f"Cannot link any output from {source.node.name} to any input of {target.node.name}. " # type: ignore[union-attr]
318
+ f"Cannot link any output from {src_name} to any input of {tgt_name}. "
296
319
  f"Available output types: {[f'{o.name}:{o.type}' for o in outputs]}, "
297
320
  f"Available input types: {[f'{i.name}:{i.type}' for i in inputs]}"
298
321
  )
@@ -333,17 +356,24 @@ class LinkingMixin:
333
356
  source = self._default_output_socket
334
357
  target = other.socket
335
358
  elif getattr(other, "_placeholder_inputs", None):
336
- name = other._placeholder_inputs.pop(0)
359
+ node_other = cast("BaseNode", other)
360
+ name = node_other._placeholder_inputs.pop(0)
337
361
  try:
338
- target = other.node.inputs[name]
362
+ target = node_other.node.inputs[name]
339
363
  except KeyError:
340
- target = other.node.inputs[other.i._index(name)]
341
- source = self.o._best_match(target.type) if hasattr(self, "o") else self
364
+ target = node_other.node.inputs[node_other.i._index(name)]
365
+ source = (
366
+ self.o._best_match(target.type)
367
+ if hasattr(self, "o")
368
+ else self._default_output_socket
369
+ )
342
370
  else:
343
371
  try:
344
- source, target = self._find_best_socket_pair(self, other)
372
+ source, target = self._find_best_socket_pair(self, cast(Any, other))
345
373
  except SocketError:
346
- source, target = other._find_best_socket_pair(self, other)
374
+ source, target = cast("LinkingMixin", other)._find_best_socket_pair(
375
+ self, cast(Any, other)
376
+ )
347
377
 
348
378
  self.tree.link(source, target)
349
379
  return other
@@ -138,9 +138,9 @@ class BaseNode(_NodeLike, OperatorMixin, LinkingMixin):
138
138
  and input.type == "VECTOR"
139
139
  and isinstance(value, (int, float))
140
140
  ):
141
- input.default_value = [value] * len(input.default_value)
141
+ input.default_value = [value] * len(input.default_value) # type: ignore
142
142
  else:
143
- input.default_value = value
143
+ input.default_value = value # type: ignore
144
144
 
145
145
  def _establish_links(self, **kwargs: InputAny):
146
146
  input_ids = [input.identifier for input in self.node.inputs]
@@ -165,7 +165,7 @@ class BaseNode(_NodeLike, OperatorMixin, LinkingMixin):
165
165
  elif isinstance(value, NodeSocket):
166
166
  self._link_from(value, name)
167
167
  elif isinstance(value, _NodeLike):
168
- self._link_from(value.o._best_match(self.i._get(name).type), name)
168
+ self._link_from(value.o._best_match(self.i._get(name).type), name) # type: ignore
169
169
  else:
170
170
  if name in input_ids:
171
171
  input = self.node.inputs[input_ids.index(name)]
@@ -68,11 +68,7 @@ from .mixins import LinkingMixin, OperatorMixin
68
68
 
69
69
  if TYPE_CHECKING:
70
70
  from ..nodes import compositor, geometry, shader
71
- from ..nodes.geometry import (
72
- IntegerMath,
73
- MatchString,
74
- Math,
75
- )
71
+ from ..nodes.geometry import IntegerMath, MatchString, Math, ObjectInfo
76
72
  from ..nodes.geometry.manual import Compare
77
73
  from ..nodes.geometry.vector import VectorMath
78
74
  from .node import BaseNode
@@ -269,8 +265,8 @@ class Socket(BaseSocket, _SocketLike, OperatorMixin, LinkingMixin):
269
265
  def __gt__(self, other: Any) -> "BooleanSocket": ...
270
266
  def __le__(self, other: Any) -> "BooleanSocket": ...
271
267
  def __ge__(self, other: Any) -> "BooleanSocket": ...
272
- def __eq__(self, other: Any) -> "Compare": ...
273
- def __ne__(self, other: Any) -> "Compare": ...
268
+ def __eq__(self, other: Any) -> "BooleanSocket": ...
269
+ def __ne__(self, other: Any) -> "BooleanSocket": ...
274
270
 
275
271
 
276
272
  # ---------------------------------------------------------------------------
@@ -1553,13 +1549,10 @@ class _MatrixMixin(BaseSocket):
1553
1549
  if TYPE_CHECKING:
1554
1550
 
1555
1551
  @overload
1556
- def __matmul__(
1557
- self, other: "VectorSocket | NodeSocketVector"
1558
- ) -> "VectorSocket": ...
1552
+ def __matmul__(self, other: "VectorSocket") -> "VectorSocket": ...
1559
1553
  @overload
1560
- def __matmul__(self, other: Any) -> "MatrixSocket": ...
1561
-
1562
- def __rmatmul__(self, other: Any) -> "MatrixSocket": ...
1554
+ def __matmul__(self, other: "MatrixSocket") -> "MatrixSocket": ...
1555
+ def __rmatmul__(self, other: "MatrixSocket") -> "MatrixSocket": ...
1563
1556
 
1564
1557
 
1565
1558
  # ---------------------------------------------------------------------------
@@ -1621,6 +1614,21 @@ class GeometrySocket(Socket):
1621
1614
 
1622
1615
  socket: NodeSocketGeometry
1623
1616
 
1617
+ def realize_instances(
1618
+ self,
1619
+ selection: InputBoolean = True,
1620
+ realize_all: InputBoolean = False,
1621
+ depth: InputInteger = 0,
1622
+ ) -> "GeometrySocket":
1623
+ from ..nodes.geometry import RealizeInstances
1624
+
1625
+ return RealizeInstances(
1626
+ self.socket,
1627
+ selection=selection,
1628
+ realize_all=realize_all,
1629
+ depth=depth,
1630
+ ).o.geometry
1631
+
1624
1632
 
1625
1633
  class ObjectSocket(Socket):
1626
1634
  """Runtime object socket wrapper."""
@@ -1635,6 +1643,110 @@ class ObjectSocket(Socket):
1635
1643
  def default_value(self, value: bpy.types.Object) -> None:
1636
1644
  self.socket.default_value = value
1637
1645
 
1646
+ @property
1647
+ def _info(self) -> "type[ObjectInfo]":
1648
+ from ..nodes.geometry import ObjectInfo
1649
+
1650
+ return ObjectInfo
1651
+
1652
+ def transform(
1653
+ self, transform_space: Literal["ORIGINAL", "RELATIVE"] = "ORIGINAL"
1654
+ ) -> "MatrixSocket":
1655
+ """The Object's transform matrix, optionally in relative space.
1656
+
1657
+ Adds [`ObjectInfo`](~nodebpy.nodes.geometry.ObjectInfo) to the node tree and returns.
1658
+
1659
+ Parameters
1660
+ ----------
1661
+ transform_space : Literal["ORIGINAL", "RELATIVE"]
1662
+ The space in which to return the transform matrix.
1663
+
1664
+ Returns
1665
+ -------
1666
+ MatrixSocket
1667
+ The output 'Transform' `MatrixSocket`.
1668
+ """
1669
+ return self._info(self.socket, transform_space=transform_space).o.transform
1670
+
1671
+ def location(
1672
+ self, transform_space: Literal["ORIGINAL", "RELATIVE"] = "ORIGINAL"
1673
+ ) -> "VectorSocket":
1674
+ """
1675
+ The object's location, optionally in relative space, via [`ObjectInfo`](~nodebpy.nodes.geometry.ObjectInfo).
1676
+
1677
+ Parameters
1678
+ ----------
1679
+ transform_space : Literal["ORIGINAL", "RELATIVE"]
1680
+ The space in which to return the location.
1681
+
1682
+ Returns
1683
+ -------
1684
+ VectorSocket
1685
+ The output 'Location' `VectorSocket`.
1686
+
1687
+ """
1688
+ return self._info(self.socket, transform_space=transform_space).o.location
1689
+
1690
+ def rotation(
1691
+ self, transform_space: Literal["ORIGINAL", "RELATIVE"] = "ORIGINAL"
1692
+ ) -> "RotationSocket":
1693
+ """
1694
+ The object's rotation, optionally in relative space, via [`ObjectInfo`](~nodebpy.nodes.geometry.ObjectInfo).
1695
+
1696
+ Parameters
1697
+ ----------
1698
+ transform_space : Literal["ORIGINAL", "RELATIVE"]
1699
+ The space in which to return the rotation.
1700
+
1701
+ Returns
1702
+ -------
1703
+ RotationSocket
1704
+ The output 'Rotation' `RotationSocket`.
1705
+ """
1706
+ return self._info(self.socket, transform_space=transform_space).o.rotation
1707
+
1708
+ def scale(
1709
+ self, transform_space: Literal["ORIGINAL", "RELATIVE"] = "ORIGINAL"
1710
+ ) -> "VectorSocket":
1711
+ """
1712
+ The object's scale, optionally in relative space, via [`ObjectInfo`](~nodebpy.nodes.geometry.ObjectInfo).
1713
+
1714
+ Parameters
1715
+ ----------
1716
+ transform_space : Literal["ORIGINAL", "RELATIVE"]
1717
+ The space in which to return the scale.
1718
+
1719
+ Returns
1720
+ -------
1721
+ VectorSocket
1722
+ The output 'Scale' `VectorSocket`.
1723
+ """
1724
+ return self._info(self.socket, transform_space=transform_space).o.scale
1725
+
1726
+ def geometry(
1727
+ self,
1728
+ transform_space: Literal["ORIGINAL", "RELATIVE"] = "ORIGINAL",
1729
+ as_instance: InputBoolean = False,
1730
+ ) -> "GeometrySocket":
1731
+ """
1732
+ The object's geometry, optionally in relative space, via [`ObjectInfo`](~nodebpy.nodes.geometry.ObjectInfo).
1733
+
1734
+ Parameters
1735
+ ----------
1736
+ transform_space : Literal["ORIGINAL", "RELATIVE"]
1737
+ The space in which to return the geometry.
1738
+ as_instance : InputBoolean
1739
+ Whether to return the geometry as an instance.
1740
+
1741
+ Returns
1742
+ -------
1743
+ GeometrySocket
1744
+ The output 'Geometry' `GeometrySocket`.
1745
+ """
1746
+ return self._info(
1747
+ self.socket, as_instance=as_instance, transform_space=transform_space
1748
+ ).o.geometry
1749
+
1638
1750
 
1639
1751
  class MaterialSocket(Socket):
1640
1752
  """Runtime material socket wrapper."""
@@ -1677,6 +1789,38 @@ class CollectionSocket(Socket):
1677
1789
  def default_value(self, value: bpy.types.Collection) -> None:
1678
1790
  self.socket.default_value = value
1679
1791
 
1792
+ def instances(
1793
+ self,
1794
+ transform_space: Literal["ORIGINAL", "RELATIVE"] = "ORIGINAL",
1795
+ separate_children: InputBoolean = False,
1796
+ reset_children: InputBoolean = False,
1797
+ ) -> "GeometrySocket":
1798
+ """Import objects from the collection as instances.
1799
+
1800
+ Parameters
1801
+ ----------
1802
+ transform_space : Literal["ORIGINAL", "RELATIVE"]
1803
+ The transform space to use for the instances.
1804
+ separate_children : bool
1805
+ Whether to separate objects as their own instances.
1806
+ reset_children : bool
1807
+ Whether to reset children of the collection to world origin.
1808
+
1809
+ Returns
1810
+ -------
1811
+ GeometrySocket
1812
+ The output 'Instances' `GeometrySocket`. Will be a single instance or multiple instances if `separate_children` is `True`.
1813
+
1814
+ """
1815
+ from ..nodes.geometry import CollectionInfo
1816
+
1817
+ return CollectionInfo(
1818
+ self.socket,
1819
+ separate_children,
1820
+ reset_children,
1821
+ transform_space=transform_space,
1822
+ ).o.instances
1823
+
1680
1824
 
1681
1825
  class BundleSocket(Socket):
1682
1826
  """Runtime bundle socket wrapper."""
@@ -559,11 +559,13 @@ class SocketContext:
559
559
  return self._wrap(ShaderSocket, iface)
560
560
 
561
561
  def __len__(self) -> int:
562
+ assert self.tree.interface is not None
562
563
  return len(
563
564
  list(
564
565
  item
565
566
  for item in self.tree.interface.items_tree
566
- if item.in_out == self._direction
567
+ if isinstance(item, bpy.types.NodeTreeInterfaceSocket)
568
+ and item.in_out == self._direction
567
569
  )
568
570
  )
569
571
 
@@ -610,7 +612,7 @@ class TreeBuilder(Generic[_TreeT]):
610
612
  if isinstance(tree, str):
611
613
  self.tree = bpy.data.node_groups.new(tree, tree_type) # type: ignore[assignment]
612
614
  else:
613
- self.tree = tree # type: ignore[assignment]
615
+ self.tree = tree # type: ignore
614
616
 
615
617
  self._menu_defaults: dict[str, str] = {}
616
618
  self.inputs = InputInterfaceContext(self)
@@ -720,7 +722,7 @@ class TreeBuilder(Generic[_TreeT]):
720
722
  continue
721
723
  if item.identifier == key:
722
724
  if hasattr(item, "default_value"):
723
- item.default_value = value
725
+ item.default_value = value # type: ignore
724
726
 
725
727
  def __len__(self) -> int:
726
728
  return len(self.nodes)
@@ -728,10 +730,10 @@ class TreeBuilder(Generic[_TreeT]):
728
730
  def arrange(self):
729
731
  if self._arrange == "sugiyama":
730
732
  try:
731
- from ..lib.nodearrange import arrange as nodearrange
733
+ from ..lib.nodearrange.arrange import sugiyama
732
734
 
733
- nodearrange.sugiyama.sugiyama_layout(self.tree)
734
- nodearrange.sugiyama.config.reset()
735
+ sugiyama.sugiyama_layout(self.tree)
736
+ sugiyama.config.reset()
735
737
  except ImportError as e:
736
738
  if "networkx" not in str(e):
737
739
  raise
@@ -805,8 +807,9 @@ class TreeBuilder(Generic[_TreeT]):
805
807
  assert socket1.node
806
808
  assert socket2.node
807
809
  for socket in [socket1, socket2]:
810
+ assert socket.node is not None
808
811
  if socket.is_inactive and not _allow_innactive_sockets(socket.node):
809
- message = f"Socket {socket.name} from node {socket.node.name} is inactive." # type: ignore
812
+ message = f"Socket {socket.name} from node {socket.node.name} is inactive."
810
813
  message += f" It is linked to socket {socket2.name} from node {socket2.node.name}."
811
814
  message += " This link will be created by Blender but ignored when evaluated."
812
815
  message += f"Socket type: {socket.bl_idname}"