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
41 changes: 13 additions & 28 deletions Stoner/core/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from ..compat import int_types
from ..tools import AttributeStore, all_size, all_type, isiterable, isnone
from .exceptions import StonerSetasError
from .setas import Setas as _setas
from .setas import ColumnHeadersDescriptor, Setas as _setas


class DataArray(ma.MaskedArray):
Expand Down Expand Up @@ -52,6 +52,18 @@ class DataArray(ma.MaskedArray):
more attractive.
"""

# ==============================================================================================================
############################ Class-level descriptors ###############################
# ==============================================================================================================

#: setas (:py:class:`Setas`): Descriptor managing column-type assignments (x, y, z, …).
#: The per-instance :py:class:`Setas` object is stored as ``_setas`` on each
#: :py:class:`DataArray` instance and is retrieved / shape-synced by the descriptor.
setas = _setas()

#: column_headers (list): Descriptor that forwards to ``setas.column_headers``.
column_headers = ColumnHeadersDescriptor()

# ==============================================================================================================
############################ Object Construction ###############################
# ==============================================================================================================
Expand Down Expand Up @@ -326,33 +338,6 @@ def i(self, value):
else: # No iterable
self._ibase = np.arange(value, value + r)

@property
def column_headers(self):
"""Pass through to the setas attribute."""
return self._setas.column_headers

@column_headers.setter
def column_headers(self, value):
"""Write the column_headers attribute (delagated to the setas object)."""
self._setas.column_headers = value

@property
def setas(self):
"""Return an object for setting column assignments."""
if self._setas is None:
self._setas = _setas()
if self._setas.shape != self.shape:
self._setas.shape = self.shape
return self._setas

@setas.setter
def setas(self, value):
"""Set the object for setting column assignments."""
if isinstance(value, _setas):
value = value.clone
setas = self.setas
setas(value)

# ==============================================================================================================
############################ Special Methods ####################################################
# ==============================================================================================================
Expand Down
31 changes: 10 additions & 21 deletions Stoner/core/property.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from ..tools.classes import copy_into
from ..tools.file import URL_SCHEMES
from .array import DataArray
from .setas import ColumnHeadersDescriptor, Setas

try:
from tabulate import tabulate
Expand All @@ -30,6 +31,15 @@ class DataFilePropertyMixin:

_subclasses = None

#: setas (:py:class:`Setas`): Descriptor that delegates column-type assignments to the
#: internal :py:class:`DataArray` (``_data``). Getting or setting ``obj.setas`` is
#: equivalent to ``obj._data.setas``.
setas = Setas(source="_data")

#: column_headers (list): Descriptor that forwards to ``setas.column_headers`` via the
#: delegating :py:class:`Setas` descriptor above.
column_headers = ColumnHeadersDescriptor()

@property
def _repr_html_(self):
"""Generate an html representation of the DataFile.
Expand Down Expand Up @@ -62,16 +72,6 @@ def clone(self):
print("Cloning in DataFile")
return copy_into(self, c)

@property
def column_headers(self):
"""Pass through to the setas attribute."""
return self.data._setas.column_headers

@column_headers.setter
def column_headers(self, value):
"""Write the column_headers attribute (delagated to the setas object)."""
self.data._setas.column_headers = value

data = DataArray([])
"""DataArray descriptor that enforces the data attribute is always a :class:`DataArray` instance."""

Expand Down Expand Up @@ -196,17 +196,6 @@ def shape(self):
"""Pass through the numpy shape attribute of the data."""
return self.data.shape

@property
def setas(self):
"""Get the list of column assignments."""
setas = self._data._setas
return setas

@setas.setter
def setas(self, value):
"""Set a new setas assignment by calling the setas object."""
self._data._setas(value)

@property
def T(self):
"""Get the current data transposed."""
Expand Down
134 changes: 132 additions & 2 deletions Stoner/core/setas.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""setas module provides the setas class for DataFile and friends."""
__all__ = ["Setas"]
__all__ = ["Setas", "ColumnHeadersDescriptor"]
import copy
import re
from collections.abc import Iterable, MutableMapping
Expand Down Expand Up @@ -38,21 +38,26 @@ class Setas(MutableMapping):
assignments before setting new ones (default).
"""

def __init__(self, row=False, bless=None):
def __init__(self, row=False, bless=None, source=None):
"""Construct the setas instance and sets an initial value.

Args:
ref (DataFile): Contains a reference to the owning DataFile instance

Keyword Arguments:
initial_val (string or list or dict): Initial values to set
source (str, optional): When used as a descriptor, if set the descriptor will delegate to
``getattr(obj, source).setas`` rather than looking up the private attribute directly.
This is used to make :py:class:`DataFilePropertyMixin` delegate to its internal
:py:class:`DataArray`.
"""
self._row = row
self._cols = AttributeStore()
self._shape = tuple()
self._setas = []
self._column_headers = TypedList(string_types)
self._object = bless
self._source = source
self._col_defaults = {
2: {
"axes": 2,
Expand Down Expand Up @@ -116,6 +121,78 @@ def __init__(self, row=False, bless=None):
},
} # xyzuvw

# ==========================================================================
# Descriptor protocol methods
# ==========================================================================

def __set_name__(self, owner, name):
"""Record the public and private attribute names when assigned to a class.

Args:
owner (type): The class that owns this descriptor.
name (str): The attribute name under which this descriptor is stored.

Note:
``public_name`` is stored for introspection and debugging; the
actual dispatch logic uses ``private_name`` (and ``_source`` when
set).
"""
self.public_name = name
self.private_name = f"_{name}"

def __get__(self, obj, objtype=None):
"""Retrieve the per-instance :py:class:`Setas` object from the owner.

When used as a class-level descriptor this returns the :py:class:`Setas` instance
stored on *obj* under the private attribute name (e.g. ``_setas``). If
*source* was supplied to the constructor the descriptor instead delegates to
``getattr(obj, source).setas``, which is the pattern used by
:py:class:`DataFilePropertyMixin` to forward to the underlying
:py:class:`DataArray`.

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

Returns:
:py:class:`Setas`: The per-instance setas object, or *self* when
accessed on the class.
"""
if obj is None:
return self
if self._source is not None:
target = getattr(obj, self._source)
return target.setas
setas = getattr(obj, self.private_name, None)
if setas is None:
# _source is None here (the delegating path returned above), so a
# plain Setas() with no source is exactly what we want.
setas = type(self)()
setattr(obj, self.private_name, setas)
if hasattr(obj, "shape") and setas.shape != obj.shape:
setas.shape = obj.shape
return setas

def __set__(self, obj, value):
"""Set the column assignments on the per-instance :py:class:`Setas` object.

Delegates the actual assignment to ``setas(value)``, preserving all of the
calling-convention logic already implemented in :py:meth:`Setas.__call__`.

Args:
obj: The owner instance.
value: Anything accepted by :py:meth:`Setas.__call__` (string, list,
dict, or another :py:class:`Setas`).
"""
if self._source is not None:
target = getattr(obj, self._source)
target.setas = value
return
setas = self.__get__(obj, type(obj))
if isinstance(value, type(self)):
value = value.clone
setas(value)

def _prepare_call(self, args, kwargs):
"""Extract a value to be used to evaluate the setas attribute during a call."""
reset = kwargs.pop("reset", True)
Expand Down Expand Up @@ -862,3 +939,56 @@ def _get_cols(self, what=None, startx=0, no_guess=False):
elif what in ("ycols", "zcols", "ucols", "vcols", "wcols", "yerrs", "zerrs"):
ret = ret[what[0:-1]]
return ret


class ColumnHeadersDescriptor:
"""Descriptor that exposes the column headers managed by the :py:class:`Setas` object.

Both :py:class:`DataArray` and :py:class:`DataFilePropertyMixin` use this descriptor
for their ``column_headers`` attribute. Because both classes expose a ``setas``
attribute (either as a :py:class:`Setas` descriptor or as a delegating property),
this descriptor can obtain the column headers uniformly via ``obj.setas.column_headers``.

Examples::

class DataArray(ma.MaskedArray):
setas = Setas()
column_headers = ColumnHeadersDescriptor()

class DataFilePropertyMixin:
setas = Setas(source="_data")
column_headers = ColumnHeadersDescriptor()
"""

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

Args:
owner (type): The class that owns this descriptor.
name (str): The attribute name under which this descriptor is stored.
"""
self.name = name

def __get__(self, obj, objtype=None):
"""Return the column headers from the owning object's setas.

Args:
obj: The owner instance, or ``None`` when accessed on the class.
objtype (type, optional): The owner class.

Returns:
list: The current column header strings.
"""
if obj is None:
return self
return obj.setas.column_headers

def __set__(self, obj, value):
"""Set the column headers on the owning object's setas.

Args:
obj: The owner instance.
value (list or array-like): New column header strings.
"""
obj.setas.column_headers = value

Loading