Skip to content

Commit

Permalink
v2 (#4)
Browse files Browse the repository at this point in the history
Co-authored-by: Eric Charles <[email protected]>
  • Loading branch information
eacharles and eacharles authored Feb 26, 2021
1 parent ea9481e commit e911b6f
Show file tree
Hide file tree
Showing 16 changed files with 331 additions and 93 deletions.
4 changes: 3 additions & 1 deletion python/cfgmdl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
del get_git_version

from .unit import Unit
from .ref import Ref
from .array import Array
from .property import Property
from .derived import Derived
from .derived import Derived, cached
from .configurable import Configurable
from .choice import Choice
from .parameter import Parameter
from .param_holder import ParamHolder
from .model import Model
from .function import Function
2 changes: 1 addition & 1 deletion python/cfgmdl/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ class Array(Property):
def __init__(self, **kwargs):
super(Array, self).__init__(**kwargs)

def _cast_type(self, value):
def _cast_type(self, value, obj=None):
"""Hook took override type casting"""
return np.array(value).astype(self.dtype)
78 changes: 34 additions & 44 deletions python/cfgmdl/configurable.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
#!/usr/bin/env python
"""
Classes used to describe aspect of Models.
The base class is `Property` which describes any one property of a model,
such as the name, or some other fixed property.
The `Parameter` class describes variable model parameters.
The `Derived` class describes model properies that are derived
from other model properties.
Base class for configurable objects
"""
from collections import OrderedDict as odict
from collections.abc import Mapping

from .property import Property
from .derived import Derived
from .ref import Ref

from .utils import Meta, model_docstring

Expand Down Expand Up @@ -74,9 +66,8 @@ class MyPair(Model):
def __init__(self, **kwargs):
""" C'tor. Build from a set of keyword arguments.
"""
self._properties = self.find_properties()
self._timestamp = 0
self._init_properties(**kwargs)
self._cache()

@classmethod
def find_properties(cls):
Expand All @@ -89,19 +80,36 @@ def find_properties(cls):
props[key] = val
return props

@classmethod
def getp(cls, name):
"""Get a particular property of this model"""
props = cls.find_properties()
return props[name]

@property
def _properties(self):
"""Return the properties of this class"""
return self.find_properties()

def __str__(self, indent=0):
""" Cast model as a formatted string
"""
props = self._properties
try:
ret = '{0:>{2}}{1}'.format('', self.name, indent)
except AttributeError:
ret = "%s" % (type(self))
if not self._properties: #pragma: no cover
if not props: #pragma: no cover
return ret
ret += '\n{0:>{2}}{1}'.format('', 'Parameters:', indent + 2)
width = len(max(self._properties.keys(), key=len))
for name in self._properties.keys():
value = getattr(self, name)

width = len(max(props.keys(), key=len))
for prop in props.values():
name = prop.public_name
if isinstance(prop, Derived):
value = getattr(self, prop.private_name)
else:
value = getattr(self, name)
par = '{0!s:{width}} : {1!r}'.format(name, value, width=width)
ret += '\n{0:>{2}}{1}'.format('', par, indent + 4)
return ret
Expand All @@ -121,13 +129,16 @@ def update(self, *args, **kwargs):
raise ValueError("Argument passed to Model.upate() %s" % args)

kwargs = dict(kwargs)
props = self._properties
for name, value in kwargs.items():
# Raise KeyError if Property not found
try:
prop = self._properties[name]
prop = props[name]
except KeyError as err:
raise KeyError("Warning: %s does not have properties %s" % (type(self), name)) from err

if isinstance(prop, (Ref, Derived)):
continue
attr = getattr(self, '_%s' % name)
if isinstance(attr, Configurable):
# Set attributes
Expand All @@ -146,20 +157,17 @@ def _init_properties(self, **kwargs):
"""
missing = {}
kwcopy = kwargs.copy()
for k, p in self._properties.items():
props = self._properties
for k, p in props.items():
if k not in kwcopy and p.required:
missing[k] = p

pval = kwcopy.pop(k, p.default)

if isinstance(p, Ref):
setattr(self, p.owner_name, p.owner)
setattr(self, p.private_name, None)
continue
p.__set__(self, pval)
if isinstance(p, Derived):
if p.loader is None:
p.loader = self.__getattribute__("_load_%s" % k)
elif isinstance(p.loader, str):
p.loader = self.__getattribute__(p.loader)
if not callable(p.loader):
raise ValueError("Callabe loader not defined for Derived object", type(self), k, p.loader)
if missing:
raise ValueError("One or more required properties are missing ", type(self), missing.keys())
if kwcopy:
Expand All @@ -169,21 +177,3 @@ def todict(self):
""" Return self cast as an '~collections.OrderedDict' object
"""
return odict([(key, val.todict(self)) for key, val in self._properties.items()])


def _cache(self, name=None):
"""
Method called in to cache any computationally
intensive properties after updating the parameters.
Parameters
----------
name : string
The parameter name.
Returns
-------
None
"""
#pylint: disable=unused-argument, no-self-use
return
56 changes: 47 additions & 9 deletions python/cfgmdl/derived.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ class Derived(Property):
that is used to compute the value of the property.
"""
defaults = deepcopy(Property.defaults)
defaults['loader'] = (None, 'Function to load datum')
defaults['loadername'] = (None, 'Name of function to load datum')
defaults['loader'] = (None, 'Function to load datatum')
defaults['uses'] = ([], 'Properties used by this one')

@classmethod
def dummy(cls): #pragma: no cover
Expand All @@ -24,9 +26,27 @@ def dummy(cls): #pragma: no cover

@defaults_decorator(defaults)
def __init__(self, **kwargs):
self.loader = self.dummy
self.loader = None
self.loadername = None
self.uses = []
super(Derived, self).__init__(**kwargs)

def __set_name__(self, owner, name):
"""Set the name of the privately managed value"""
super(Derived, self).__set_name__(owner, name)
if self.loadername is None:
if self.loader is not None:
self.loadername = "_load_%s" % self.loader.__name__
else:
self.loadername = "_load_%s" % name
if self.loader is None:
loader = getattr(owner, self.loadername, None)
self.loader = loader
if not callable(self.loader):
raise ValueError("Callable loader not defined for Derived object", owner, name, self.loadername, self.loader)
return
setattr(owner, self.loadername, self.loader)

def __get__(self, obj, objtype=None):
"""Get the value from the client object
Expand All @@ -50,15 +70,33 @@ def __get__(self, obj, objtype=None):

loader = self.loader

times = [ getattr(obj, "_%s_timestamp" % vn, 0.) for vn in loader.__func__.__code__.co_names ]
if loader is None:
raise ValueError("%s.%s no loader" % (obj, self.public_name))

ts_check = self.check_timestamp(obj)
if ts_check and val is not None:
return val
val = loader(obj)
self.__set__(obj, val)
return getattr(obj, self.private_name)

def check_timestamp(self, obj):
"""Check if object is up to date w.r.t. to the object it uses"""
times = [vn.timestamp(obj) for vn in self.uses]
if times:
ts_max = np.max(times)
else:
ts_max = 0.
my_ts = getattr(obj, self.time_name, -1.)
my_ts = self.timestamp(obj)
return my_ts >= ts_max

if my_ts > ts_max and val is not None:
return val
val = loader()
self.__set__(obj, val)
return getattr(obj, self.private_name)


def cached(**kwargs):
"""Decorator attach a function to a class as a Derived property
"""
def decorator(func):
"""Function that appends default kwargs to a function.
"""
return Derived(loader=func, **kwargs)
return decorator
2 changes: 2 additions & 0 deletions python/cfgmdl/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ def check_inputs(x, inputs):
ret.append(inp(x).astype(np.float))
elif inp is None:
ret.append(np.full(x.shape, np.nan))
elif isinstance(inp, np.ndarray):
ret.append(inp)
elif isinstance(inp, (Iterable, int, float)):
ret.append(np.array(inp).astype(np.float))
else:
Expand Down
7 changes: 5 additions & 2 deletions python/cfgmdl/param_holder.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from .array import Array
from .configurable import Configurable
from .utils import is_none

class ParamHolder(Configurable):
"""Wrapper around a data value
Expand All @@ -24,9 +25,11 @@ def __init__(self, *args, **kwargs):
"""Constructor"""
kwcopy = kwargs.copy()
if args: #pragma: no cover
if 'value' in kwcopy:
raise ValueError("value keyword provided in addition to arguments")
#if 'value' in kwcopy:
# raise ValueError("value keyword provided in addition to arguments")
kwcopy['value'] = args
if is_none(kwargs.get('value', None)):
kwargs.pop('value', None)
super(ParamHolder, self).__init__(**kwargs)
self.check_bounds()

Expand Down
2 changes: 1 addition & 1 deletion python/cfgmdl/parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def tostr(self, obj):
"""Extract values as a string"""
return str(getattr(obj, self.private_name))

def _cast_type(self, value):
def _cast_type(self, value, obj=None):
"""Hook took override type casting"""
return value

Expand Down
21 changes: 15 additions & 6 deletions python/cfgmdl/property.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from collections import OrderedDict as odict


from .utils import cast_type, Meta, Defs, defaults_decorator, defaults_docstring
from .utils import is_none, cast_type, Meta, Defs, defaults_decorator, defaults_docstring

try:
basestring
Expand Down Expand Up @@ -63,6 +63,7 @@ def __set_name__(self, owner, name):
self.private_name = '_' + name
self.time_name = '_' + name + '_timestamp'


def __set__(self, obj, value):
"""Set the value in the client object
Expand All @@ -71,7 +72,7 @@ def __set__(self, obj, value):
obj : ...
The client object
value : ...
The value being seti
The value being set
This will use the `cast_type(self.dtype, value)` method to cast the requested value to the correct type.
Rasies
Expand All @@ -81,14 +82,16 @@ def __set__(self, obj, value):
ValueError : The input value failes validation for a Property sub-class (e.g., not a valid choice, or outside bounds)
"""
try:
cast_value = self._cast_type(value)
cast_value = self._cast_type(value, obj)
self.validate_value(obj, cast_value)
except (TypeError, ValueError) as msg:
setattr(obj, self.private_name, None)
setattr(obj, self.time_name, time.time())
raise TypeError("Failed to set %s %s" % (self.private_name, msg)) from msg
raise TypeError("Failed to set %s %s %s" % (repr(obj), self.private_name, msg)) from msg
setattr(obj, self.private_name, cast_value)
setattr(obj, self.time_name, time.time())
if hasattr(obj, '_timestamp'):
setattr(obj, '_timestamp', time.time())

def __get__(self, obj, objtype=None):
"""Get the value from the client object
Expand All @@ -104,7 +107,7 @@ def __get__(self, obj, objtype=None):
The requested value
"""
attr = getattr(obj, self.private_name)
if self.unit is None:
if self.unit is None or is_none(attr):
return attr
return self.unit(attr) #pylint: disable=not-callable

Expand All @@ -116,6 +119,8 @@ def __delete__(self, obj):
"""
setattr(obj, self.private_name, self.default)
setattr(obj, self.time_name, time.time())
if hasattr(obj, '_timestamp'):
setattr(obj, '_timestamp', time.time())

def _load(self, **kwargs):
"""Load kwargs key,value pairs into __dict__
Expand All @@ -136,10 +141,14 @@ def _load(self, **kwargs):
_ = self._cast_type(self.default)


def _cast_type(self, value):
def _cast_type(self, value, obj=None): #pylint: disable=unused-argument
"""Hook took override type casting"""
return cast_type(self.dtype, value)

def timestamp(self, obj):
"""Get the timestamp when this object was updated"""
return max(getattr(obj, self.time_name), getattr(getattr(obj, self.private_name), '_timestamp', 0))

@classmethod
def defaults_docstring(cls, header=None, indent=None, footer=None):
"""Add the default values to the class docstring"""
Expand Down
Loading

0 comments on commit e911b6f

Please sign in to comment.