281 lines
8.6 KiB
Python
281 lines
8.6 KiB
Python
|
|
from importlib import import_module
|
|
|
|
from .parameter import Parameter
|
|
|
|
from .. import __version__
|
|
from ..errors import ParameterError
|
|
|
|
import logging
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class ParametricObject(object):
|
|
"""
|
|
Parametric objects may be defined like so:
|
|
|
|
.. doctest::
|
|
|
|
>>> from cqparts.params import (
|
|
... ParametricObject,
|
|
... PositiveFloat, IntRange,
|
|
... )
|
|
|
|
>>> class Foo(ParametricObject):
|
|
... x = PositiveFloat(5)
|
|
... i = IntRange(1, 10, 3) # between 1 and 10, defaults to 3
|
|
... blah = 100
|
|
|
|
>>> a = Foo(i=8)
|
|
>>> (a.x, a.i)
|
|
(5.0, 8)
|
|
|
|
>>> a = Foo(i=11) # raises exception # doctest: +SKIP
|
|
ParameterError: value of 11 outside the range {1, 10}
|
|
|
|
>>> a = Foo(z=1) # raises exception # doctest: +SKIP
|
|
ParameterError: <class 'Foo'> does not accept parameter(s): z
|
|
|
|
>>> a = Foo(x='123', i='2')
|
|
>>> (a.x, a.i)
|
|
(123.0, 2)
|
|
|
|
>>> a = Foo(blah=200) # raises exception, parameters must be Parameter types # doctest: +SKIP
|
|
ParameterError: <class 'Foo'> does not accept any of the parameters: blah
|
|
|
|
>>> a = Foo(x=None) # a.x is None, a.i=3
|
|
>>> (a.x, a.i)
|
|
(None, 3)
|
|
|
|
Internally to the object, parameters may be accessed simply with self.x, self.i
|
|
These will always return the type defined
|
|
|
|
"""
|
|
def __init__(self, **kwargs):
|
|
# get all available parameters (recurse through inherited classes)
|
|
params = self.class_params(hidden=True)
|
|
|
|
# parameters explicitly defined during intantiation
|
|
defined_params = set(kwargs.keys())
|
|
|
|
# only accept a subset of params
|
|
invalid_params = defined_params - set(params.keys())
|
|
if invalid_params:
|
|
raise ParameterError("{cls} does not accept parameter(s): {keys}".format(
|
|
cls=repr(type(self)),
|
|
keys=', '.join(sorted(invalid_params)),
|
|
))
|
|
|
|
# Cast parameters into this instance
|
|
for (name, param) in params.items():
|
|
value = param.default
|
|
if name in kwargs:
|
|
value = param.cast(kwargs[name])
|
|
setattr(self, name, value)
|
|
|
|
self.initialize_parameters()
|
|
|
|
@classmethod
|
|
def class_param_names(cls, hidden=True):
|
|
"""
|
|
Return the names of all class parameters.
|
|
|
|
:param hidden: if ``False``, excludes parameters with a ``_`` prefix.
|
|
:type hidden: :class:`bool`
|
|
:return: set of parameter names
|
|
:rtype: :class:`set`
|
|
"""
|
|
param_names = set(
|
|
k for (k, v) in cls.__dict__.items()
|
|
if isinstance(v, Parameter)
|
|
)
|
|
for parent in cls.__bases__:
|
|
if hasattr(parent, 'class_param_names'):
|
|
param_names |= parent.class_param_names(hidden=hidden)
|
|
|
|
if not hidden:
|
|
param_names = set(n for n in param_names if not n.startswith('_'))
|
|
return param_names
|
|
|
|
@classmethod
|
|
def class_params(cls, hidden=True):
|
|
"""
|
|
Gets all class parameters, and their :class:`Parameter` instances.
|
|
|
|
:return: dict of the form: ``{<name>: <Parameter instance>, ... }``
|
|
:rtype: :class:`dict`
|
|
|
|
.. note::
|
|
|
|
The :class:`Parameter` instances returned do not have a value, only
|
|
a default value.
|
|
|
|
To get a list of an **instance's** parameters and values, use
|
|
:meth:`params` instead.
|
|
|
|
"""
|
|
param_names = cls.class_param_names(hidden=hidden)
|
|
return dict(
|
|
(name, getattr(cls, name))
|
|
for name in param_names
|
|
)
|
|
|
|
def params(self, hidden=True):
|
|
"""
|
|
Gets all instance parameters, and their *cast* values.
|
|
|
|
:return: dict of the form: ``{<name>: <value>, ... }``
|
|
:rtype: :class:`dict`
|
|
"""
|
|
param_names = self.class_param_names(hidden=hidden)
|
|
return dict(
|
|
(name, getattr(self, name))
|
|
for name in param_names
|
|
)
|
|
|
|
def __repr__(self):
|
|
# Returns string of the form:
|
|
# <ClassName: diameter=3.0, height=2.0, twist=0.0>
|
|
params = self.params(hidden=False)
|
|
return "<{cls}: {params}>".format(
|
|
cls=type(self).__name__,
|
|
params=", ".join(
|
|
"%s=%r" % (k, v)
|
|
for (k, v) in sorted(params.items(), key=lambda x: x[0]) # sort by name
|
|
),
|
|
)
|
|
|
|
def initialize_parameters(self):
|
|
"""
|
|
A palce to set default parameters more intelegently than just a
|
|
simple default value (does nothing by default)
|
|
|
|
:return: ``None``
|
|
|
|
Executed just prior to exiting the :meth:`__init__` function.
|
|
|
|
When overriding, strongly consider calling :meth:`super`.
|
|
|
|
"""
|
|
pass
|
|
|
|
# Serialize / Deserialize
|
|
def serialize(self):
|
|
"""
|
|
Encode a :class:`ParametricObject` instance to an object that can be
|
|
encoded by the :mod:`json` module.
|
|
|
|
:return: a dict of the format:
|
|
:rtype: :class:`dict`
|
|
|
|
::
|
|
|
|
{
|
|
'lib': { # library information
|
|
'name': 'cqparts',
|
|
'version': '0.1.0',
|
|
},
|
|
'class': { # importable class
|
|
'module': 'yourpartslib.submodule', # module containing class
|
|
'name': 'AwesomeThing', # class being serialized
|
|
},
|
|
'params': { # serialized parameters of AwesomeThing
|
|
'x': 10,
|
|
'y': 20,
|
|
}
|
|
}
|
|
|
|
value of ``params`` key comes from :meth:`serialize_parameters`
|
|
|
|
.. important::
|
|
|
|
Serialize pulls the class name from the classes ``__name__`` parameter.
|
|
|
|
This must be the same name of the object holding the class data, or
|
|
the instance cannot be re-instantiated by :meth:`deserialize`.
|
|
|
|
**Examples (good / bad)**
|
|
|
|
.. doctest::
|
|
|
|
>>> from cqparts.params import ParametricObject, Int
|
|
|
|
>>> # GOOD Example
|
|
>>> class A(ParametricObject):
|
|
... x = Int(10)
|
|
>>> A().serialize()['class']['name']
|
|
'A'
|
|
|
|
>>> # BAD Example
|
|
>>> B = type('Foo', (ParametricObject,), {'x': Int(10)})
|
|
>>> B().serialize()['class']['name'] # doctest: +SKIP
|
|
'Foo'
|
|
|
|
In the second example, the classes import name is expected to be ``B``.
|
|
But instead, the *name* ``Foo`` is recorded. This missmatch will be
|
|
irreconcilable when attempting to :meth:`deserialize`.
|
|
"""
|
|
return {
|
|
# Encode library information (future-proofing)
|
|
'lib': {
|
|
'name': 'cqparts',
|
|
'version': __version__,
|
|
},
|
|
# class & name record, for automated import when decoding
|
|
'class': {
|
|
'module': type(self).__module__,
|
|
'name': type(self).__name__,
|
|
},
|
|
'params': self.serialize_parameters(),
|
|
}
|
|
|
|
def serialize_parameters(self):
|
|
"""
|
|
Get the parameter data in its serialized form.
|
|
|
|
Data is serialized by each parameter's :meth:`Parameter.serialize`
|
|
implementation.
|
|
|
|
:return: serialized parameter data in the form: ``{<name>: <serial data>, ...}``
|
|
:rtype: :class:`dict`
|
|
"""
|
|
|
|
# Get parameter data
|
|
class_params = self.class_params()
|
|
instance_params = self.params()
|
|
|
|
# Serialize each parameter
|
|
serialized = {}
|
|
for name in class_params.keys():
|
|
param = class_params[name]
|
|
value = instance_params[name]
|
|
serialized[name] = param.serialize(value)
|
|
return serialized
|
|
|
|
@staticmethod
|
|
def deserialize(data):
|
|
"""
|
|
Create instance from serial data
|
|
"""
|
|
# Import module & get class
|
|
try:
|
|
module = import_module(data.get('class').get('module'))
|
|
cls = getattr(module, data.get('class').get('name'))
|
|
except ImportError:
|
|
raise ImportError("No module named: %r" % data.get('class').get('module'))
|
|
except AttributeError:
|
|
raise ImportError("module %r does not contain class %r" % (
|
|
data.get('class').get('module'),
|
|
data.get('class').get('name')
|
|
))
|
|
|
|
# Deserialize parameters
|
|
class_params = cls.class_params(hidden=True)
|
|
params = dict(
|
|
(name, class_params[name].deserialize(value))
|
|
for (name, value) in data.get('params').items()
|
|
)
|
|
|
|
# Instantiate new instance
|
|
return cls(**params)
|