Skip to content

Commit 1e8811b

Browse files
Key access for Named Attributes on a BlenderObject (#49)
* add string key access for attributes * refactor and remove ColumnAccessor * better setting of new attribute using key * delete comments
1 parent 015f777 commit 1e8811b

6 files changed

Lines changed: 528 additions & 612 deletions

File tree

databpy/array.py

Lines changed: 7 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -3,71 +3,6 @@
33
import bpy
44

55

6-
class ColumnAccessor:
7-
"""
8-
Helper class to handle column operations on AttributeArray.
9-
10-
This provides a clean way to handle operations like pos[:, 2] += 1.0
11-
without the complexity of numpy array views.
12-
"""
13-
14-
def __init__(self, parent, column_idx):
15-
self.parent = parent
16-
self.column_idx = column_idx
17-
self.parent_array = np.asarray(parent).view(np.ndarray)
18-
19-
def _apply_operation(self, operation, value):
20-
"""Common method for all in-place operations."""
21-
operation(self.parent_array[:, self.column_idx], value)
22-
self.parent._sync_to_blender()
23-
return self
24-
25-
def __iadd__(self, value):
26-
return self._apply_operation(lambda arr, val: arr.__iadd__(val), value)
27-
28-
def __isub__(self, value):
29-
return self._apply_operation(lambda arr, val: arr.__isub__(val), value)
30-
31-
def __imul__(self, value):
32-
return self._apply_operation(lambda arr, val: arr.__imul__(val), value)
33-
34-
def __itruediv__(self, value):
35-
return self._apply_operation(lambda arr, val: arr.__itruediv__(val), value)
36-
37-
def __array__(self, dtype=None):
38-
"""Convert to array, handling optional dtype argument."""
39-
column_data = self.parent_array[:, self.column_idx]
40-
return column_data.astype(dtype) if dtype is not None else column_data
41-
42-
def __eq__(self, other):
43-
"""Handle equality comparison."""
44-
column_data = self.parent_array[:, self.column_idx]
45-
46-
if hasattr(other, "__array__"):
47-
return np.array_equal(column_data, np.asarray(other))
48-
return column_data == other
49-
50-
def __array_wrap__(self, out_arr, context=None):
51-
"""Handle the output of NumPy ufuncs and other functions."""
52-
self.parent_array[:, self.column_idx] = out_arr
53-
self.parent._sync_to_blender()
54-
return self
55-
56-
@property
57-
def column_data(self):
58-
"""Get the column data."""
59-
return self.parent_array[:, self.column_idx]
60-
61-
def __getattr__(self, name):
62-
"""Delegate attribute access to the column data."""
63-
column_data = self.parent_array[:, self.column_idx]
64-
if hasattr(column_data, name):
65-
return getattr(column_data, name)
66-
raise AttributeError(
67-
f"'{self.__class__.__name__}' object has no attribute '{name}'"
68-
)
69-
70-
716
class AttributeArray(np.ndarray):
727
"""
738
A numpy array subclass that automatically syncs changes back to the Blender object.
@@ -109,6 +44,8 @@ def __new__(cls, obj: bpy.types.Object, name: str) -> "AttributeArray":
10944
arr._blender_object = obj
11045
arr._attribute = attr
11146
arr._attr_name = name
47+
# Track the root array so that views can sync the full data
48+
arr._root = arr
11249
return arr
11350

11451
def __array_finalize__(self, obj):
@@ -119,72 +56,11 @@ def __array_finalize__(self, obj):
11956
self._blender_object = getattr(obj, "_blender_object", None)
12057
self._attribute = getattr(obj, "_attribute", None)
12158
self._attr_name = getattr(obj, "_attr_name", None)
122-
123-
def __eq__(self, other):
124-
"""Handle equality comparison for array objects."""
125-
self_arr = np.asarray(self).view(np.ndarray)
126-
127-
if isinstance(other, AttributeArray):
128-
other_arr = np.asarray(other).view(np.ndarray)
129-
return np.array_equal(self_arr, other_arr)
130-
131-
if isinstance(other, ColumnAccessor):
132-
return np.array_equal(self_arr, other.parent_array)
133-
134-
if hasattr(other, "__array__"):
135-
other_arr = np.asarray(other)
136-
137-
# Handle shape differences for column comparisons
138-
if (
139-
self_arr.shape != other_arr.shape
140-
and other_arr.ndim == 1
141-
and self_arr.ndim == 2
142-
and other_arr.shape[0] == self_arr.shape[0]
143-
):
144-
return any(
145-
np.array_equal(self_arr[:, i], other_arr)
146-
for i in range(self_arr.shape[1])
147-
)
148-
149-
return np.array_equal(self_arr, other_arr)
150-
151-
return self_arr == other
152-
153-
def __getitem__(self, key):
154-
"""Get item with special handling for column operations."""
155-
# Handle column operations: pos[:, 2]
156-
if (
157-
isinstance(key, tuple)
158-
and len(key) == 2
159-
and isinstance(key[0], slice)
160-
and key[0] == slice(None)
161-
and isinstance(key[1], int)
162-
):
163-
return ColumnAccessor(self, key[1])
164-
165-
return super().__getitem__(key)
59+
# Preserve reference to the root array for syncing
60+
self._root = getattr(obj, "_root", self)
16661

16762
def __setitem__(self, key, value):
16863
"""Set item and sync changes back to Blender."""
169-
# Handle column operations: pos[:, 2] = value
170-
if (
171-
isinstance(key, tuple)
172-
and len(key) == 2
173-
and isinstance(key[0], slice)
174-
and key[0] == slice(None)
175-
and isinstance(key[1], int)
176-
):
177-
arr_view = np.asarray(self).view(np.ndarray)
178-
col_idx = key[1]
179-
180-
if isinstance(value, ColumnAccessor):
181-
arr_view[:, col_idx] = value.column_data
182-
else:
183-
arr_view[:, col_idx] = value
184-
185-
self._sync_to_blender()
186-
return
187-
18864
super().__setitem__(key, value)
18965
self._sync_to_blender()
19066

@@ -224,7 +100,9 @@ def _sync_to_blender(self):
224100
if self._blender_object is None:
225101
return
226102

227-
data_to_sync = np.asarray(self).view(np.ndarray)
103+
# Always sync using the root array to ensure full shape
104+
root = getattr(self, "_root", self)
105+
data_to_sync = np.asarray(root).view(np.ndarray)
228106
data_to_sync = self._ensure_correct_shape(data_to_sync)
229107

230108
# Ensure float32 dtype

databpy/object.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
list_attributes,
1717
_check_obj_attributes,
1818
evaluate_object,
19+
Attribute,
1920
)
2021
from .collection import create_collection
2122

@@ -179,6 +180,23 @@ def __init__(self, obj: Object | str | None = None):
179180
elif obj is None:
180181
self._object_name = ""
181182

183+
def _ipython_key_completions_(self) -> list[str]:
184+
"""Return possible named attirbutes"""
185+
return self.list_attributes()
186+
187+
def __getitem__(self, name: str) -> AttributeArray:
188+
if not isinstance(name, str):
189+
raise ValueError("Attribute name must be a string")
190+
return AttributeArray(self.object, name)
191+
192+
def __setitem__(self, name: str, data: np.ndarray) -> None:
193+
if name in self.list_attributes():
194+
att = Attribute(self.attributes()[name])
195+
self.store_named_attribute(
196+
data=data, name=name, domain=att.domain, atype=att.atype
197+
)
198+
self.store_named_attribute(data=data, name=name)
199+
182200
def _check_obj(self) -> None:
183201
_check_obj_attributes(self.object)
184202

tests/test_array_print_methods.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import pytest
22
import numpy as np
33
from databpy import create_object
4-
from databpy.array import AttributeArray, ColumnAccessor, Attribute
4+
from databpy.array import AttributeArray, Attribute
55

66

77
class TestAttributeArrayPrintMethods:
@@ -116,8 +116,8 @@ def test_str_method_with_large_array(self):
116116
assert "..." in result or len(result.split("\n")) > 1
117117

118118

119-
class TestColumnAccessorPrintMethods:
120-
"""Test print behavior of ColumnAccessor objects."""
119+
class TestColumnSlicePrintMethods:
120+
"""Test print behavior of column slice views."""
121121

122122
@pytest.fixture
123123
def parent_array_and_data(self):
@@ -131,30 +131,30 @@ def parent_array_and_data(self):
131131

132132
return parent_array, parent_data
133133

134-
def test_column_accessor_str_delegation(self, parent_array_and_data):
135-
"""Test that ColumnAccessor properly delegates string operations."""
134+
def test_column_slice_str_delegation(self, parent_array_and_data):
135+
"""Test that a column slice delegates to numpy string formatting."""
136136
parent_array, parent_data = parent_array_and_data
137137

138-
# Create ColumnAccessor
139-
col_accessor = ColumnAccessor(parent_array, 1) # Second column
138+
# Create column view
139+
col_view = parent_array[:, 1] # Second column
140140

141141
# The string representation should come from the column data
142142
expected_column = parent_data[:, 1] # [2.0, 5.0]
143143

144144
# Test that we can convert to string (should use numpy's default)
145-
result = str(np.asarray(col_accessor))
145+
result = str(np.asarray(col_view))
146146
expected = str(expected_column)
147147

148148
assert result == expected
149149

150-
def test_column_accessor_array_conversion(self, parent_array_and_data):
151-
"""Test that ColumnAccessor converts to array properly for printing."""
150+
def test_column_slice_array_conversion(self, parent_array_and_data):
151+
"""Test that a column slice converts to array properly for printing."""
152152
parent_array, parent_data = parent_array_and_data
153153

154-
col_accessor = ColumnAccessor(parent_array, 0) # First column
154+
col_view = parent_array[:, 0] # First column
155155

156156
# Convert to array and check it matches expected column
157-
as_array = np.asarray(col_accessor)
157+
as_array = np.asarray(col_view)
158158
expected_column = parent_data[:, 0] # [1.0, 4.0]
159159

160160
np.testing.assert_array_equal(as_array, expected_column)

0 commit comments

Comments
 (0)