Skip to content

Commit 368f81f

Browse files
Target Blender 5.1+ (#50)
* update for bpy>=5.1 * cleanup PCA logic * update tests * cleanup and improve socket accessor methods * more cleanup of VectorSocket accessor * more socket helpers * cleanup interface SocketValue return * type returns for math operations * typing for >> operator * cleanup more types * cleanup of default value and name normalisation * cleanup tests * Expose Frame node and context for frames * ruff format * more tests * test coverage * test coverage * test coverage * test coverage * test coverage * update changelog
1 parent b730a57 commit 368f81f

40 files changed

Lines changed: 4154 additions & 1460 deletions

.github/workflows/tests.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ jobs:
1313
max-parallel: 4
1414
fail-fast: false
1515
matrix:
16-
version: ["5.0"]
1716
os: [macos-latest, windows-latest, ubuntu-latest]
1817
steps:
1918
- uses: actions/checkout@v4
@@ -26,19 +25,18 @@ jobs:
2625
- name: Install
2726
run: |
2827
uv sync --all-extras --dev
29-
uv pip install bpy~=${{ matrix.version }}
3028
3129
- name: Run tests
3230
run: |
3331
uv run -m pytest -n auto -vv --cov=nodebpy --cov-report=xml
3432
3533
- name: Upload coverage reports to Codecov
3634
uses: codecov/codecov-action@v5
37-
if: matrix.os == 'macos-latest' && matrix.version == '5.0'
35+
if: matrix.os == 'macos-latest'
3836
with:
3937
token: ${{ secrets.CODECOV_TOKEN }}
4038
name: coverage.xml
4139

4240
- name: Upload coverage reports to Codecov
43-
if: matrix.os == 'macos-latest' && matrix.version == '5.0'
41+
if: matrix.os == 'macos-latest'
4442
uses: codecov/codecov-action@v3

.python-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.11
1+
3.13

docs/changelog.qmd

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,17 @@ mat = g.InstanceTransform().o.transform
1616

1717
vec = g.CombineXYZ(*mat[:3])
1818
```
19+
- **Socket iteration and indexing**`VectorSocket`, `ColorSocket`, and `MatrixSocket` now support `__getitem__`, `__iter__`, and `__len__`. Output sockets decompose via `SeparateXYZ`/`SeparateColor`/`SeparateMatrix` (node reuse on repeated access); input sockets auto-wire a `CombineXYZ`/`CombineColor`/`CombineMatrix` and return the component input. Enables patterns like `for axis in vec_socket` and `value >> node.i.position[1]`.
20+
- **`RotationSocket`/`MatrixSocket` helpers** — Added `.invert` and `.transpose` properties on `MatrixSocket`, `.invert` on `RotationSocket`, following the same node-reuse pattern as `.x`/`.y`/`.z`.
21+
- **`SocketAccessor` overloads**`__getitem__` and `_get` are overloaded so slices return `list[Socket]` and str/int keys return `Socket`, eliminating the `Socket | list[Socket]` union that was blocking `enumerate` and unpacking.
22+
- **Blender 5.1 compatibility** — Generator updated for Blender 5.1: `FontSocket` type, `Frame` node moved to `manually_defined`, `SVD` class name normalisation, and classmethod param deduplication fix (`min_x`/`min_y`/`min_z` no longer collapsed to `min`).
23+
24+
### Bug Fixes
25+
- **Generic factory nodes**`AccumulateField`, `EvaluateAtIndex`, `FieldAverage`, `FieldMinAndMax`, `EvaluateOnDomain`, `FieldVariance`, and `Compare` are now `Generic[_T]`. Their `_Inputs`/`_Outputs` inner classes carry the type parameter so `.point.vector(...)` returns `FieldAverage[VectorSocket]` and `.o.mean` resolves to `VectorSocket`.
26+
- **Precise operator return types** — Arithmetic operators on `FloatSocket``Math`, `VectorSocket``VectorMath`, `IntegerSocket``IntegerMath`. Comparison operators (`<`, `>`, `<=`, `>=`, `==`, `!=`) → `Compare`. The `>>` operator is now typed via `TypeVar` so the right-hand operand's exact type is preserved through chains.
1927
- Fix doc building and will only deploy on tagged releases([#49](https://github.com/BradyAJohnston/nodebpy/pull/49))
28+
- **Domain factory pattern** — All `_domain_factory` / local-class patterns replaced with proper `_DomainFactory` inner classes (including `CaptureAttribute`) so the type checker can see their return types.
29+
- **`SocketAccessor` identifier lookup fix** — Added a normalised-identifier pass (`normalize_name(id)`) so attribute access like `.i.value_001` correctly resolves Blender identifiers such as `Value_001` that cannot be round-tripped through `denormalize_name`.
2030

2131
## v0.10.2 - 2026-04-21
2232

generate.py

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from typing import Any, Literal
1818

1919
import bpy
20-
from bpy.types import bpy_prop_array
20+
from bpy.types import VectorFont, bpy_prop_array
2121
from mathutils import Euler, Vector
2222

2323
# ---------------------------------------------------------------------------
@@ -49,6 +49,7 @@
4949
"NodeSocketBundle": "BundleSocket",
5050
"NodeSocketClosure": "ClosureSocket",
5151
"NodeSocketShader": "ShaderSocket",
52+
"NodeSocketFont": "FontSocket",
5253
}
5354

5455

@@ -78,7 +79,6 @@ def output_dir(self) -> Path:
7879
"Closure",
7980
"Simulation",
8081
"For Each",
81-
"Frame",
8282
"GridBoolean",
8383
"Reroute",
8484
"FieldMinAndMax",
@@ -118,6 +118,7 @@ def output_dir(self) -> Path:
118118
"FieldVariance",
119119
"Compare",
120120
"AttributeStatistic",
121+
"Frame",
121122
"tree",
122123
),
123124
class_name_prefix_strips=[
@@ -133,7 +134,6 @@ def output_dir(self) -> Path:
133134
output_dir_name="shader",
134135
nodes_to_skip=[
135136
"Legacy",
136-
"Frame",
137137
"Reroute",
138138
],
139139
manually_defined=(
@@ -142,6 +142,7 @@ def output_dir(self) -> Path:
142142
"RepeatOutput",
143143
"RepeatZone",
144144
"Attribute",
145+
"Frame",
145146
"tree",
146147
"material",
147148
),
@@ -156,13 +157,13 @@ def output_dir(self) -> Path:
156157
output_dir_name="compositor",
157158
nodes_to_skip=[
158159
"Legacy",
159-
"Frame",
160160
"Reroute",
161161
"Cryptomatte",
162162
"Image",
163163
],
164164
manually_defined=(
165165
"MenuSwitch",
166+
"Frame",
166167
"tree",
167168
),
168169
class_name_prefix_strips=[
@@ -231,6 +232,7 @@ def type_mapped(self) -> str:
231232
"NodeSocketClosure": "InputClosure",
232233
# Shader trees use NodeSocketShader for BSDF/closure outputs
233234
"NodeSocketShader": "InputShader",
235+
"NodeSocketFont": "InputFont",
234236
# Virtual sockets adapt to whatever is connected
235237
"NodeSocketVirtual": "InputLinkable",
236238
}
@@ -405,6 +407,7 @@ def class_name_for_config(self, config: TreeTypeConfig) -> str:
405407
"Vdb": "VDB",
406408
"3DLocation": "Location3D",
407409
"Bsdf": "BSDF",
410+
"Svd": "SVD",
408411
}
409412

410413
# Add prefix strips from config
@@ -518,13 +521,12 @@ def generate_enum_class_methods(self, config: TreeTypeConfig | None = None) -> s
518521
for socket in enum.sockets:
519522
# Use label-based parameter naming
520523
socket_name = get_socket_param_name(socket, sockets_use_same_name)
521-
if socket_name.startswith("min"):
522-
param_name = "min"
523-
elif socket_name.startswith("max"):
524-
param_name = "max"
525-
else:
526-
param_name = socket_name
527-
param_name = param_name.replace("_float", "").replace("_vector", "")
524+
suffixes_to_remove = ["_float", "_vector"]
525+
param_name = socket_name
526+
if socket_name.startswith("min") or socket_name.startswith("max"):
527+
suffixes_to_remove += ["_001", "_002"]
528+
for suffix in suffixes_to_remove:
529+
param_name = param_name.replace(suffix, "")
528530

529531
if (
530532
param_name
@@ -591,13 +593,12 @@ def {method_name}(
591593

592594
for socket in enum.sockets:
593595
socket_name = get_socket_param_name(socket, sockets_use_same_name)
594-
if socket_name.startswith("min"):
595-
param_name = "min"
596-
elif socket_name.startswith("max"):
597-
param_name = "max"
598-
else:
599-
param_name = socket_name
600-
param_name = param_name.replace("_float", "").replace("_vector", "")
596+
suffixes_to_remove = ["_float", "_vector"]
597+
param_name = socket_name
598+
if socket_name.startswith("min") or socket_name.startswith("max"):
599+
suffixes_to_remove += ["_001", "_002"]
600+
for suffix in suffixes_to_remove:
601+
param_name = param_name.replace(suffix, "")
601602

602603
if param_name and param_name != "" and param_name != type_param_name:
603604
input_params.append(
@@ -682,6 +683,8 @@ def format_python_value(value: Any) -> str:
682683
return str(value)
683684
elif isinstance(value, int):
684685
return str(value)
686+
elif isinstance(value, VectorFont):
687+
return "None"
685688
elif isinstance(value, float):
686689
return str(round(value, 4))
687690
elif hasattr(value, "__iter__") and not isinstance(value, str):
@@ -1221,6 +1224,7 @@ def _check_socket(socket):
12211224
"InputString",
12221225
"InputFloat",
12231226
"InputVector",
1227+
"InputFont",
12241228
]
12251229
type_imports = [
12261230
t

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ readme = "README.md"
66
authors = [
77
{ name = "Brady Johnston", email = "brady.johnston@me.com" }
88
]
9-
requires-python = ">=3.11"
9+
requires-python = ">=3.13"
1010
license = { text = "GPL-3.0-or-later" }
1111

1212
[project.scripts]
@@ -17,7 +17,7 @@ networkx = [
1717
"networkx>=3.6.1",
1818
]
1919
bpy = [
20-
"bpy>=5.0.1",
20+
"bpy>=5.1.0",
2121
]
2222

2323
[build-system]

src/nodebpy/builder/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
CollectionSocket,
3636
ColorSocket,
3737
FloatSocket,
38+
FontSocket,
3839
GeometrySocket,
3940
ImageSocket,
4041
IntegerSocket,
@@ -93,6 +94,7 @@
9394
"MenuSocket",
9495
"GeometrySocket",
9596
"ObjectSocket",
97+
"FontSocket",
9698
"MaterialSocket",
9799
"ImageSocket",
98100
"CollectionSocket",
@@ -120,6 +122,7 @@
120122
"SocketMenu",
121123
"SocketObject",
122124
"SocketGeometry",
125+
"SocketFont",
123126
"SocketCollection",
124127
"SocketImage",
125128
"SocketMaterial",

src/nodebpy/builder/accessor.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
from bpy.types import NodeSocket
77

88
from ._registry import _get_socket_linker
9-
from ._utils import SocketError, _allow_innactive_sockets, denormalize_name
9+
from ._utils import (
10+
SocketError,
11+
_allow_innactive_sockets,
12+
denormalize_name,
13+
normalize_name,
14+
)
1015

1116
if TYPE_CHECKING:
1217
from .socket import Socket
@@ -43,6 +48,10 @@ def _index(self, key: str | int) -> int:
4348
for candidate in (key, denorm):
4449
if candidate in ids:
4550
return ids.index(candidate)
51+
# Normalized identifier match: 'value_001' matches identifier 'Value_001'
52+
normalized_ids = [normalize_name(id) for id in ids]
53+
if key in normalized_ids:
54+
return normalized_ids.index(key)
4655
names = [s.name for s in self._collection]
4756
for key in (key, denorm):
4857
if key in names:

src/nodebpy/builder/mixins.py

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
from __future__ import annotations
22

33
from types import EllipsisType
4-
from typing import TYPE_CHECKING, Any
4+
from typing import TYPE_CHECKING, Any, TypeVar
55

66
from bpy.types import NodeLink, NodeSocket
77

88
from ._registry import _get_socket_linker
99
from ._utils import SocketError, _resolve_promotion, _SocketLike
1010

11+
_RShiftT = TypeVar("_RShiftT")
12+
1113
if TYPE_CHECKING:
14+
from ..nodes.geometry import Compare, Math
1215
from ..types import InputLinkable
1316
from .node import BaseNode
1417
from .socket import Socket
@@ -26,97 +29,97 @@ class OperatorMixin:
2629

2730
def _apply_math_operation(
2831
self, other: Any, operation: str, reverse: bool = False
29-
) -> "BaseNode":
32+
) -> "Math":
3033
socket, other, reverse = _resolve_promotion(
3134
self._default_output_socket,
3235
other,
3336
reverse, # type: ignore[attr-defined]
3437
)
3538
return _get_socket_linker(socket)._dispatch_math(other, operation, reverse)
3639

37-
def __mul__(self, other: Any) -> "BaseNode":
40+
def __mul__(self, other: Any) -> "Math":
3841
return self._apply_math_operation(other, "multiply")
3942

40-
def __rmul__(self, other: Any) -> "BaseNode":
43+
def __rmul__(self, other: Any) -> "Math":
4144
return self._apply_math_operation(other, "multiply", reverse=True)
4245

43-
def __truediv__(self, other: Any) -> "BaseNode":
46+
def __truediv__(self, other: Any) -> "Math":
4447
return self._apply_math_operation(other, "divide")
4548

46-
def __rtruediv__(self, other: Any) -> "BaseNode":
49+
def __rtruediv__(self, other: Any) -> "Math":
4750
return self._apply_math_operation(other, "divide", reverse=True)
4851

49-
def __add__(self, other: Any) -> "BaseNode":
52+
def __add__(self, other: Any) -> "Math":
5053
return self._apply_math_operation(other, "add")
5154

52-
def __radd__(self, other: Any) -> "BaseNode":
55+
def __radd__(self, other: Any) -> "Math":
5356
return self._apply_math_operation(other, "add", reverse=True)
5457

55-
def __sub__(self, other: Any) -> "BaseNode":
58+
def __sub__(self, other: Any) -> "Math":
5659
return self._apply_math_operation(other, "subtract")
5760

58-
def __rsub__(self, other: Any) -> "BaseNode":
61+
def __rsub__(self, other: Any) -> "Math":
5962
return self._apply_math_operation(other, "subtract", reverse=True)
6063

61-
def __pow__(self, other: Any) -> "BaseNode":
64+
def __pow__(self, other: Any) -> "Math":
6265
return self._apply_math_operation(other, "power")
6366

64-
def __rpow__(self, other: Any) -> "BaseNode":
67+
def __rpow__(self, other: Any) -> "Math":
6568
return self._apply_math_operation(other, "power", reverse=True)
6669

67-
def __mod__(self, other: Any) -> "BaseNode":
70+
def __mod__(self, other: Any) -> "Math":
6871
return self._apply_math_operation(other, "modulo")
6972

70-
def __rmod__(self, other: Any) -> "BaseNode":
73+
def __rmod__(self, other: Any) -> "Math":
7174
return self._apply_math_operation(other, "modulo", reverse=True)
7275

73-
def __floordiv__(self, other: Any) -> "BaseNode":
76+
def __floordiv__(self, other: Any) -> "Math":
7477
socket, other, reverse = _resolve_promotion(
7578
self._default_output_socket,
7679
other,
7780
False, # type: ignore[attr-defined]
7881
)
7982
return _get_socket_linker(socket)._dispatch_floordiv(other, reverse)
8083

81-
def __rfloordiv__(self, other: Any) -> "BaseNode":
84+
def __rfloordiv__(self, other: Any) -> "Math":
8285
socket, other, reverse = _resolve_promotion(
8386
self._default_output_socket,
8487
other,
8588
True, # type: ignore[attr-defined]
8689
)
8790
return _get_socket_linker(socket)._dispatch_floordiv(other, reverse)
8891

89-
def __neg__(self) -> "BaseNode":
92+
def __neg__(self) -> "Math":
9093
return _get_socket_linker(self._default_output_socket)._dispatch_unary( # type: ignore[attr-defined]
9194
"negate"
9295
)
9396

94-
def __abs__(self) -> "BaseNode":
97+
def __abs__(self) -> "Math":
9598
return _get_socket_linker(self._default_output_socket)._dispatch_unary( # type: ignore[attr-defined]
9699
"absolute"
97100
)
98101

99-
def _apply_compare_operation(self, other: Any, operation: str) -> "BaseNode":
102+
def _apply_compare_operation(self, other: Any, operation: str) -> "Math":
100103
return _get_socket_linker(self._default_output_socket)._dispatch_compare( # type: ignore[attr-defined]
101104
other, operation
102105
)
103106

104-
def __lt__(self, other: Any) -> "BaseNode":
107+
def __lt__(self, other: Any) -> "Compare":
105108
return self._apply_compare_operation(other, "less_than")
106109

107-
def __gt__(self, other: Any) -> "BaseNode":
110+
def __gt__(self, other: Any) -> "Compare":
108111
return self._apply_compare_operation(other, "greater_than")
109112

110-
def __le__(self, other: Any) -> "BaseNode":
113+
def __le__(self, other: Any) -> "Compare":
111114
return self._apply_compare_operation(other, "less_equal")
112115

113-
def __ge__(self, other: Any) -> "BaseNode":
116+
def __ge__(self, other: Any) -> "Compare":
114117
return self._apply_compare_operation(other, "greater_equal")
115118

116-
def __eq__(self, other: Any) -> "BaseNode": # type: ignore[override]
119+
def __eq__(self, other: Any) -> "Compare": # type: ignore[override]
117120
return self._apply_compare_operation(other, "equal")
118121

119-
def __ne__(self, other: Any) -> "BaseNode": # type: ignore[override]
122+
def __ne__(self, other: Any) -> "Compare": # type: ignore[override]
120123
return self._apply_compare_operation(other, "not_equal")
121124

122125
def _apply_boolean_operation(self, other: Any, operation: str):

0 commit comments

Comments
 (0)