Source code for mdt.component_templates.base

from copy import deepcopy

__author__ = 'Robbert Harms'
__date__ = '2017-07-20'
__maintainer__ = 'Robbert Harms'
__email__ = 'robbert@xkls.nl'
__licence__ = 'LGPL v3'


# The list of components we are loading at the moment. This allows keeping track of nested components.
_component_loading = []


[docs]class ComponentBuilder: """The base class for component builders. Component builders, together with ComponentTemplate allow you to define components using Templates, special classes where the properties are defined using class attributes. The ComponentTemplate contains class attributes defining the component which can be used by the ComponentBuilder to create a class of the right type from the information in that template. """
[docs] def create_class(self, template): """Create a class of the right type given the information in the template. Args: template (ComponentTemplate): the information as a component config Returns: class: the class of the right type """ from mdt.lib.components import temporary_component_updates, add_template_component cls = self._create_class(template) if template.subcomponents: subcomponents = template.subcomponents class SubComponentConstruct(cls): def __init__(self, *args, **kwargs): with temporary_component_updates(): for component in subcomponents: add_template_component(component) super().__init__(*args, **kwargs) return SubComponentConstruct return cls
def _create_class(self, template): """Create a class of the right type given the information in the template. This is to be used by subclasses. Args: template (ComponentTemplate): the information as a component config Returns: class: the class of the right type """ raise NotImplementedError()
class _MergeDict(dict): def __init__(self, child_dict): """Carrier object for merging dictionaries in the ComponentTemplateMeta.""" super().__init__(**child_dict)
[docs]def merge_dict(dictionary): """Makes sure that the given dictionary updates the dictionary of the parent template instead of overwriting it. This is meant to be used inside component templates. Suppose the following hierarchy of templates:: class Template(ComponentTemplate): property = {'a': 1, 'b': 2} class Item(Template): property = {'a': 3} Here, the property of Item will overwrite those of Template and set the value to {'a': 3}. In some instances this is desired, in other instances it can be desired to have the property of Item be set to {'a': 3, 'b': 2}. To automatically merge the dictionaries with those of the parent you can use:: from mdt.component_templates.base import merge_dicts class Item(Template): property = merge_dict({'a': 3}) """ return _MergeDict(dictionary)
[docs]def bind_function(func): """This decorator is for methods in ComponentTemplates that we would like to bind to the constructed component. Example suppose you want to inherit or overwrite a function in the constructed model, then in your template/config you should define the function and add @bind_function to it as a decorator, like this: .. code-block:: python # the class we want to create class MyGoal: def test(self): print('test') # the template class from which we want to construct a new MyGoal, note the @bind_function class MyConfig(ComponentTemplate): @bind_function def test(self): super().test() print('test2') The component builder takes care to actually bind the new method to the final object. What this will do essentially is that it will add the property bind to the function. This should act as a flag indicating that that function should be bound. Args: func (python function): the function to bind to the build object """ func._bind = True return func
[docs]class ComponentTemplateMeta(type): def __new__(mcs, name, bases, attributes): """A pre-processor for the components. On the moment this meta class does two things, first it adds all functions with the '_bind' property to the ``bound_methods`` dictionary for binding them later to the constructed class. Second, it sets the ``name`` attribute to the template class name if there is no ``name`` attribute defined. """ result = super().__new__(mcs, name, bases, attributes) result.component_type = mcs._resolve_attribute(bases, attributes, '_component_type') result.bound_methods = mcs._get_bound_methods(bases, attributes) result.subcomponents = mcs._get_subcomponents(attributes) result.name = mcs._get_component_name_attribute(name, bases, attributes) mcs._apply_merge_dicts(result, bases, attributes) if len(_component_loading) == 1: try: if result.component_type is not None and result.name: from mdt.lib.components import add_template_component add_template_component(result) except ValueError: pass _component_loading.pop() return result @classmethod def __prepare__(mcs, name, bases, **kwargs): _component_loading.append(name) return dict() @staticmethod def _get_component_name_attribute(name, bases, attributes): if 'name' in attributes: return attributes['name'] return name @staticmethod def _get_bound_methods(bases, attributes): """Get all methods in the template that have the ``_bind`` attribute. This collects all methods that have the ``_bind`` attribute to a dictionary. Returns: dict: all the methods that need to be bound. """ bound_methods = {value.__name__: value for value in attributes.values() if hasattr(value, '_bind')} for base in bases: if hasattr(base, 'bound_methods'): for key, value in base.bound_methods.items(): if key not in bound_methods: bound_methods.update({key: value}) return bound_methods @staticmethod def _get_subcomponents(attributes): """Get all the sub-components defined in this template. Returns: list: the defined sub-components. """ return [value for value in attributes.values() if isinstance(value, ComponentTemplateMeta)] @staticmethod def _resolve_attribute(bases, attributes, attribute_name, base_predicate=None): """Search for the given attribute in the given attributes or in the attributes of the bases. Args: base_predicate (func): if given, a predicate that runs on the attribute of one of the bases to determine if we will return that one. Returns: The value for the attribute Raises: ValueError: if the attribute could not be found in the attribute or any of the bases """ base_predicate = base_predicate or (lambda _: True) if attribute_name in attributes: return attributes[attribute_name] for base in bases: if hasattr(base, attribute_name) and base_predicate(getattr(base, attribute_name)): return getattr(base, attribute_name) raise ValueError('Attribute not found in this component config or its superclasses.') @staticmethod def _apply_merge_dicts(result, bases, attributes): """Apply the :class:`_MergeDict` properties, if present. This loops over all the attributes trying to find an attribute which is an instance of :class:`_MergeDict`. If found, we merge the dictionary of the child with the parent dictionary. """ def find_parent_dict(attribute_name): for base in bases: if hasattr(base, attribute_name): if getattr(base, attribute_name) is not None: return getattr(base, attribute_name) return {} for name in attributes: if isinstance(attributes[name], _MergeDict): parent_value = find_parent_dict(name) merged = dict(parent_value) merged.update(attributes[name]) attributes[name] = merged setattr(result, name, merged)
[docs]class ComponentTemplate(object, metaclass=ComponentTemplateMeta): """The component configuration. By overriding the class attributes you can define complex configurations. The actual class distilled from these configurations are loaded by the builder referenced by ``_builder``. Attributes: _component_type (str): the component type of this template. Set to one of the valid template types. _builder (ComponentBuilder): the builder to use for constructing an object of the given template name (str): the name of the template description (str): a description of the object / template """ _component_type = None _builder = None name = '' description = '' def __new__(cls, *args, **kwargs): """Instead of creating an instance of a Template, this will build the actual component. This allows one to build the model of a template by regular object initialization. For example, these two calls (a and b) are exactly the same:: template = Template a = construct_component(template) b = template() """ return cls._builder.create_class(cls)
[docs] @classmethod def meta_info(cls): return {'name': cls.name, 'description': cls.description, 'template': deepcopy(cls)}