Skip to content

Commit d91d4f4

Browse files
committed
Raise on direct assignment to NDArray.fields entries
1 parent 3e06322 commit d91d4f4

2 files changed

Lines changed: 60 additions & 6 deletions

File tree

src/blosc2/ndarray.py

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import tempfile
1414
from abc import abstractmethod
1515
from collections import OrderedDict, namedtuple
16+
from collections.abc import Mapping
1617
from functools import reduce
1718
from itertools import product
1819
from typing import TYPE_CHECKING, Any, NamedTuple, Protocol, runtime_checkable
@@ -143,6 +144,41 @@ def __getitem__(self, key: Any) -> Any:
143144
...
144145

145146

147+
class FieldsAccessor(Mapping):
148+
"""Read-only mapping of structured field views."""
149+
150+
def __init__(self, field_views: dict[str, Any]):
151+
self._field_views = field_views
152+
153+
def __getitem__(self, key: str) -> Any:
154+
return self._field_views[key]
155+
156+
def __iter__(self) -> Iterator[str]:
157+
return iter(self._field_views)
158+
159+
def __len__(self) -> int:
160+
return len(self._field_views)
161+
162+
def __setitem__(self, key: str, value: object) -> None:
163+
raise TypeError(f'assign through the field view, e.g. array.fields["{key}"][:] = values')
164+
165+
def copy(self) -> dict[str, Any]:
166+
return dict(self._field_views)
167+
168+
def __or__(self, other: object) -> dict[str, Any]:
169+
if not isinstance(other, Mapping):
170+
return NotImplemented
171+
return self.copy() | dict(other)
172+
173+
def __ror__(self, other: object) -> dict[str, Any]:
174+
if not isinstance(other, Mapping):
175+
return NotImplemented
176+
return dict(other) | self.copy()
177+
178+
def __repr__(self) -> str:
179+
return repr(self._field_views)
180+
181+
146182
def is_documented_by(original):
147183
def wrapper(target):
148184
target.__doc__ = original.__doc__
@@ -3695,10 +3731,11 @@ def __init__(self, **kwargs):
36953731
base = kwargs.pop("_base", None)
36963732
super().__init__(kwargs["_array"], base=base)
36973733
# Accessor to fields
3698-
self._fields = {}
3734+
field_views = {}
36993735
if self.dtype.fields:
37003736
for field in self.dtype.fields:
3701-
self._fields[field] = NDField(self, field)
3737+
field_views[field] = NDField(self, field)
3738+
self._fields = FieldsAccessor(field_views)
37023739

37033740
@property
37043741
def cparams(self) -> blosc2.CParams:
@@ -3747,14 +3784,14 @@ def vlmeta(self) -> dict:
37473784
return self.schunk.vlmeta
37483785

37493786
@property
3750-
def fields(self) -> dict:
3787+
def fields(self) -> Mapping[str, NDField]:
37513788
"""
3752-
Dictionary with the fields of the structured array.
3789+
Read-only mapping with the fields of the structured array.
37533790
37543791
Returns
37553792
-------
3756-
fields: dict
3757-
A dictionary with the fields of the structured array.
3793+
fields: Mapping
3794+
A read-only mapping with the fields of the structured array.
37583795
37593796
See Also
37603797
--------
@@ -3770,6 +3807,8 @@ def fields(self) -> dict:
37703807
>>> sa = blosc2.zeros(shape, dtype=dtype)
37713808
>>> # Check that fields are equal
37723809
>>> assert sa.fields['a'] == sa.fields['b']
3810+
>>> # Assign through the field view
3811+
>>> sa.fields['a'][:] = 1
37733812
"""
37743813
return self._fields
37753814

tests/ndarray/test_ndarray.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,21 @@ def test_ndarray_info_has_human_sizes():
115115
assert "cbytes" in text
116116

117117

118+
def test_fields_assignment_requires_field_view_slice():
119+
dtype = np.dtype([("id", np.float64), ("payload", np.int32)])
120+
array = blosc2.zeros(4, dtype=dtype)
121+
122+
with pytest.raises(
123+
TypeError, match=r'assign through the field view, e\.g\. array\.fields\["id"\]\[:\] = values'
124+
):
125+
array.fields["id"] = np.arange(4, dtype=np.float64)
126+
127+
np.testing.assert_array_equal(array[:], np.zeros(4, dtype=dtype))
128+
129+
array.fields["id"][:] = np.arange(4, dtype=np.float64)
130+
np.testing.assert_array_equal(array.fields["id"][:], np.arange(4, dtype=np.float64))
131+
132+
118133
@pytest.mark.parametrize(
119134
("shape", "newshape", "chunks", "blocks"),
120135
[

0 commit comments

Comments
 (0)