#!/usr/bin/env python
# coding=utf-8
"""
This module provides the core functionality for describing objects.
Description
-----------
In brainstorm most objects can be converted into a so called description using
the :func:`get_description` function.
A description is a JSON-serializable data structure that contains all `static`
information to re-create that object using the :func:`create_from_description`
function. It does not, however, contain any `dynamic` information.
This means that an object created from a description is in the same state as
if it had been freshly instantiated, without any later modifications of its
internal state.
The descriptions for the basic types ``int``, ``float``, ``bool``, ``str``,
``list`` and ``dict`` are these values themselves. Other objects need to
inherit from :class:`Describable` and their description is always a ``dict``
containing the ``'@type': 'ClassName'`` key possibly along with other
properties.
"""
from __future__ import division, print_function, unicode_literals
from copy import deepcopy
import numpy as np
import six
from brainstorm.utils import get_inheritors
[docs]class Describable(object):
"""
Base class for all objects that can be described and initialized from a
description.
Derived classes can specify the :attr:`__undescribed__` field to
prevent
certain attributes from being described. This field can be either a set of
attribute names, or a dictionary mapping attribute names to their
initialization value (used when a new object is created from the
description).
Derived classes can also specify an :attr:`__default_values__` dict. This
dict allows for omitting certain attributes from the description if their
value is equal to that default value.
"""
__undescribed__ = {}
"""
Set or dict of attributes that should not be part of the description.
If specified as a dict, then these attributes are initialized to the
specified values.
"""
__default_values__ = {}
"""
Dict of attributes with their corresponding default values.
They will only be part of the description if their value differs from their
default value
"""
[docs] def __describe__(self):
"""
Returns a description of this object. That is a dictionary
containing the name of the class as ``@type`` and all members of the
class. This description is json-serializable.
If a sub-class of Describable contains non-describable members, it has
to override this method to specify how it should be described.
Returns:
dict: Description of this object
"""
description = {}
ignorelist = self.__get_all_undescribed__()
defaultlist = self.__get_all_default_values__()
for member, value in self.__dict__.items():
if member in ignorelist:
continue
if member in defaultlist and defaultlist[member] == value:
continue
try:
description[member] = get_description(value)
except TypeError as err:
err.args = (err.args[0] + "[{}.{}]".format(
self.__class__.__name__, member),)
raise
description['@type'] = self.__class__.__name__
return description
@classmethod
[docs] def __new_from_description__(cls, description):
"""
Creates a new object from a given description.
If a sub-class of Describable contains non-describable fields, it has
to override this method to specify how they should be initialized from
their description.
Args:
description (dict):
description of this object
Returns:
A new instance of this class according to the description.
"""
assert cls.__name__ == description['@type'], \
"Description for '{}' has wrong type '{}'".format(
cls.__name__, description['@type'])
instance = cls.__new__(cls)
for member, init_val in cls.__get_all_undescribed__().items():
instance.__dict__[member] = deepcopy(init_val)
for member, default_val in cls.__get_all_default_values__().items():
instance.__dict__[member] = deepcopy(default_val)
for member, descr in description.items():
if member == '@type':
continue
instance.__dict__[member] = create_from_description(descr)
cls.__init_from_description__(instance, description)
return instance
[docs] def __init_from_description__(self, description):
"""
Subclasses can override this to provide additional initialization when
created from a description.
This method will be called AFTER the object has already been created
from the description.
Args:
description (dict):
the description of this object
"""
pass
@classmethod
def __get_all_undescribed__(cls):
ignore = {}
for c_ignore in _traverse_ancestor_attrs(cls, '__undescribed__'):
if isinstance(c_ignore, dict):
ignore.update(c_ignore)
elif isinstance(c_ignore, set):
ignore.update({k: None for k in c_ignore})
return ignore
@classmethod
def __get_all_default_values__(cls):
default = {}
for c_default in _traverse_ancestor_attrs(cls, '__default_values__'):
if isinstance(c_default, dict):
default.update(c_default)
return default
[docs]def get_description(this):
"""
Create a JSON-serializable description of this object.
This description can be used to create a new instance of this object by
calling :func:`create_from_description`.
Args:
this (Describable):
An object to be described. Must either be a basic datatype
or inherit from Describable.
Returns:
A JSON-serializable description of this object.
"""
if isinstance(this, Describable):
return this.__describe__()
elif isinstance(this, (list, tuple)):
result = []
try:
for i, v in enumerate(this):
result.append(get_description(v))
except TypeError as err:
err.args = (err.args[0] + "[{}]".format(i),)
raise
return result
elif isinstance(this, np.ndarray):
return this.tolist()
elif isinstance(this, dict):
result = {}
try:
for k in this:
result[k] = get_description(this[k])
except TypeError as err:
err.args = (err.args[0] + "[{}]".format(k),)
raise
return result
elif (isinstance(this, (bool, float, type(None))) or
isinstance(this, six.integer_types) or
isinstance(this, six.string_types)):
return this
else:
raise TypeError('Type: "{}" is not describable'.format(type(this)))
[docs]def create_from_description(description):
"""
Instantiate a new object from a description.
Args:
description (dict):
A description of the object.
Returns:
A new instance of the described object
"""
if isinstance(description, dict):
if '@type' in description:
name = description['@type']
for describable in get_inheritors(Describable):
if describable.__name__ == name:
return describable.__new_from_description__(description)
raise TypeError('No describable class "{}" found!'.format(name))
else:
return {k: create_from_description(v)
for k, v in description.items()}
elif (isinstance(description, (bool, float, type(None))) or
isinstance(description, six.integer_types) or
isinstance(description, six.string_types)):
return description
elif isinstance(description, list):
return [create_from_description(d) for d in description]
raise TypeError('Invalid description of type {}'.format(type(description)))
def _traverse_ancestor_attrs(cls, attr_name):
for c in reversed(cls.__mro__):
if hasattr(c, attr_name):
yield getattr(c, attr_name)