from copy import copy
from .util import (
add_args_to_init_call,
add_init_call_hook,
)
[docs]def declarative(member_class=None, parameter='members', add_init_kwargs=True, sort_key=None, is_member=None):
"""
Class decorator to enable classes to be defined in the style of django models.
That is, @declarative classes will get an additional argument to constructor,
containing a dict with all class members matching the specified type.
:param class member_class: Class(es) to collect
:param is_member: Function to determine if an object should be collected
:param str parameter: Name of constructor parameter to inject
:param bool add_init_kwargs: If constructor parameter should be injected (Default: True)
:param sort_key: Function to invoke on members to obtain ordering (Default is to use ordering from `creation_ordered`)
:type is_member: (object) -> bool
:type sort_key: (object) -> object
"""
if member_class is None and is_member is None:
raise TypeError("The @declarative decorator needs either a member_class parameter or an is_member check function (or both)")
def decorator(class_to_decorate):
class DeclarativeMeta(class_to_decorate.__class__):
# noinspection PyTypeChecker,PyMethodParameters
def __init__(cls, name, bases, dict_):
members = get_members(cls, member_class=member_class, is_member=is_member, sort_key=sort_key, _parameter=parameter)
set_declared(cls, members, parameter)
super(DeclarativeMeta, cls).__init__(name, bases, dict_)
new_class = DeclarativeMeta(class_to_decorate.__name__,
class_to_decorate.__bases__,
{k: v for k, v in class_to_decorate.__dict__.items() if k not in ['__dict__', '__weakref__']})
def get_extra_args_function(self):
declared = get_declared(self, parameter)
copied_members = {k: copy(v) for k, v in declared.items()}
self.__dict__.update(copied_members)
return {parameter: copied_members}
if add_init_kwargs:
add_args_to_init_call(new_class, get_extra_args_function)
else:
add_init_call_hook(new_class, get_extra_args_function)
setattr(new_class, 'get_declared', classmethod(get_declared))
setattr(new_class, 'set_declared', classmethod(set_declared))
return new_class
return decorator
def set_declared(cls, value, parameter='members'):
"""
:type cls: class
:type value: dict
:type parameter: str
"""
setattr(cls, '_declarative_' + parameter, value)
def get_declared(cls, parameter='members'):
"""
Get the :code:`dict` value of the parameter collected by the :code:`@declarative` class decorator.
This is the same value that would be submitted to the :code:`__init__` invocation in the :code:`members`
argument (or another name if overridden by the :code:`parameter` specification)
:type cls: class
:type parameter: str
:rtype: dict
"""
return getattr(cls, '_declarative_' + parameter, {})
# noinspection PyIncorrectDocstring
[docs]def get_members(cls, member_class=None, is_member=None, sort_key=None, _parameter=None):
"""
Collect all class level attributes matching the given criteria.
:param cls: Class to traverse
:param class member_class: Class(es) to collect
:param is_member: Function to determine if an object should be collected
:param sort_key: Function to invoke on members to obtain ordering (Default is to use ordering from `creation_ordered`)
:type is_member: (object) -> bool
:type sort_key: (object) -> object
"""
if member_class is None and is_member is None:
raise TypeError("get_members either needs a member_class parameter or an is_member check function (or both)")
members = {}
for base in cls.__bases__:
if _parameter is None:
inherited_members = get_members(base, member_class=member_class, is_member=is_member, sort_key=sort_key)
else:
# When user by @declarative, only traverse up the class inheritance to the decorated class.
inherited_members = get_declared(base, _parameter)
members.update(inherited_members)
def generate_member_bindings():
def is_a_member(maybe_member):
return (
(member_class is not None and isinstance(maybe_member, member_class))
or (is_member is not None and is_member(maybe_member))
)
for name in cls.__dict__:
if name.startswith('__'):
continue
obj = getattr(cls, name)
if is_a_member(obj):
yield name, obj
elif type(obj) is tuple and len(obj) == 1 and is_a_member(obj[0]):
raise TypeError(f"'{name}' is a one-tuple containing what we are looking for. Trailing comma much? Don't... just don't.") # pragma: no mutate
bindings = generate_member_bindings()
if sort_key is not None:
sorted_bindings = sorted(bindings, key=lambda x: sort_key(x[1]))
members.update(sorted_bindings)
else:
members.update(bindings)
return members