Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions Stoner/core/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,75 @@ def __array_wrap__(self, obj, context=None, return_scalar=None):
ret = ma.MaskedArray.__array_wrap__(self, obj, context=context, return_scalar=return_scalar)
return ret

# ==============================================================================================================
############################ Descriptor Protocol ################################################
# ==============================================================================================================

def __set_name__(self, owner, name):
"""Record the attribute name when the descriptor is assigned as a class attribute.

Args:
owner (type):
The class that owns this descriptor.
name (str):
The attribute name used for this descriptor on the owner class.
"""
self._attr_name = name
self._private_name = f"_{name}"

def __get__(self, obj, objtype=None):
"""Return the DataArray stored on the owner instance (descriptor getter).

Args:
obj: The owner instance, or ``None`` when accessed on the class itself.
objtype: The owner class.

Returns:
DataArray: The stored data array, guaranteed to be at least 2-dimensional,
or this descriptor instance when accessed on the class.
"""
if obj is None:
return self
try:
return np.atleast_2d(object.__getattribute__(obj, self._private_name))
except AttributeError:
return np.atleast_2d(DataArray([]))

def __set__(self, obj, value):
"""Store *value* as a :class:`DataArray` on the owner instance (descriptor setter).

The setter normalises the incoming value so that it is always a 2-D
:class:`DataArray`. Column headers and ``setas`` assignments are
preserved from the existing data when the number of columns matches.

Args:
obj: The owner instance.
value (array-like): New data to store.

Raises:
ValueError: If *value* has more than 2 dimensions.
"""
nv = value
if not nv.shape: # nv is a scalar - make it a 2D array
nv = ma.atleast_2d(nv)
elif nv.ndim == 1: # nv is a vector - make it a 2D array
nv = ma.atleast_2d(nv).T
elif nv.ndim > 2:
raise ValueError(f"DataFile.data should be no more than 2 dimensional not shape {nv.shape}")
try:
old_data = object.__getattribute__(obj, self._private_name)
except AttributeError:
old_data = DataArray([])
if not isinstance(nv, DataArray): # nv isn't a DataArray, so preserve setas
nv = DataArray(nv)
nv._setas = old_data._setas.clone
elif old_data.ndim >= 2 and nv.shape[1] == old_data.shape[1]: # same columns - preserve column_headers and setas
ch = old_data.column_headers
nv._setas = old_data._setas.clone
nv.column_headers = ch
nv._setas.shape = nv.shape
object.__setattr__(obj, self._private_name, nv)

def _prepare_index(self, ix):
"""Mangle an indexing argument into a standard tuple form."""
match ix:
Expand Down
30 changes: 2 additions & 28 deletions Stoner/core/property.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,34 +72,8 @@ def column_headers(self, value):
"""Write the column_headers attribute (delagated to the setas object)."""
self.data._setas.column_headers = value

@property
def data(self):
"""Property Accessors for the main numerical data."""
return np.atleast_2d(self._data)

@data.setter
def data(self, value):
"""Set the data attribute, but force it through numpy.ma.masked_array first."""
nv = value
if not nv.shape: # nv is a scalar - make it a 2D array
nv = ma.atleast_2d(nv)
elif nv.ndim == 1: # nv is a vector - make it a 2D array
nv = ma.atleast_2d(nv).T
elif nv.ndim > 2:
raise ValueError(f"DataFile.data should be no more than 2 dimensional not shape {nv.shape}")
if not isinstance(
nv, DataArray
): # nv isn't a DataArray, so preserve setas (does this preserve column_headers too?)
nv = DataArray(nv)
nv._setas = getattr(self, "_data")._setas.clone
elif (
nv.shape[1] == self.shape[1]
): # nv is a DataArray with the same number of columns - preserve column_headers and setas
ch = getattr(self, "_data").column_headers
nv._setas = getattr(self, "_data")._setas.clone
nv.column_headers = ch
nv._setas.shape = nv.shape
self._data = nv
data = DataArray([])
"""DataArray descriptor that enforces the data attribute is always a :class:`DataArray` instance."""

@property
def dict_records(self):
Expand Down
10 changes: 7 additions & 3 deletions Stoner/tools/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ def isnone(iterator: Optional[IterableType]) -> bool:


def isproperty(obj: Any, name: str) -> bool:
"""Check whether an attribute of an object or class is a property.
"""Check whether an attribute of an object or class is a property or data descriptor.

Args:
obj (instance or class):
Expand All @@ -181,13 +181,17 @@ def isproperty(obj: Any, name: str) -> bool:

Returns:
(bool):
Whether the name is a property or not.
Whether the name is a property or data descriptor or not.
"""
if not isinstance(obj, type):
obj = type(obj)
elif not issubclass(obj, object):
raise TypeError(f"Can only check for property status on attributes of an object or a class not a {type(obj)}")
return hasattr(obj, name) and isinstance(getattr(obj, name), property)
for klass in obj.__mro__:
if name in klass.__dict__:
attr = klass.__dict__[name]
return isinstance(attr, property) or (hasattr(attr, "__get__") and hasattr(attr, "__set__"))
return False


def istuple(obj: Any, *args: type, strict: bool = True) -> bool:
Expand Down
30 changes: 15 additions & 15 deletions tests/Stoner/mixedmetatest.dat
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
TDI Format 1.5 1 2 3 4
Stoner.class{String}=Data 0 1 2 3
t0{Boolean}=True 4 5 6 7
t1{I32}=1 8 9 10 11
t10{List}=[1, (1, 2), 'abc']
t11{List}=[[[1]]]
t12{Void}=None
t2{Double Float}=0.2
t3{Cluster (I32,String)}={'a': 1, 'b': 'abc'}
t4{Cluster (I32,I32)}=(1, 2)
t5{1D Array (Invalid Type)}=array([0, 1, 2])
t6{List}=[1, 2, 3]
t7{String}=abc
t8{String}=\\abc\cde
t9{Double Float}=1e-20
TDI Format 1.5 1 2 3 4
Stoner.class{String}=Data 0 1 2 3
t0{Boolean}=True 4 5 6 7
t1{I32}=1 8 9 10 11
t10{List}=[1, (1, 2), 'abc']
t11{List}=[[[1]]]
t12{Void}=None
t2{Double Float}=0.2
t3{Cluster (I32,String)}={'a': 1, 'b': 'abc'}
t4{Cluster (I32,I32)}=(1, 2)
t5{1D Array (Invalid Type)}=array([0, 1, 2])
t6{List}=[1, 2, 3]
t7{String}=abc
t8{String}=\\abc\cde
t9{Double Float}=1e-20
Loading
Loading