Source code for fictive.patterns.types
"""
Common patterns for working with :term:`type`\\s
"""
import functools
import wrapt
from fictive.patterns.decorators import optional_arguments
from fictive.patterns.sequences import append_distinct
__all__ = (
'synthesized_type',
'auto_merge',
'metaclass',
'class_property',
)
[docs]def synthesized_type(name, instances, dict_):
"""
Synthesize a :term:`type` from one or more instance :term:`object`\\s
The :term:`method resolution order` of the new type will be based on the
left-to-right order of `instances`. E.g.:
.. testcode::
import pprint
from fictive.patterns.types import synthesized_type
class FirstClass(object):
def report(self):
return ('FirstClass', )
class SecondClass(object):
def report(self):
return ('SecondClass', ) + super(SecondClass, self).report()
first = FirstClass()
second = SecondClass()
synth = synthesized_type(
'synth', (second, first), {'ATTRIBUTE': 'foo'})
.. doctest::
>>> synth().report()
('SecondClass', 'FirstClass')
.. doctest::
>>> pprint.pprint(synth.mro())
[<class 'fictive.patterns.types.synth'>,
<class 'SecondClass'>,
<class 'FirstClass'>,
<class 'object'>]
.. doctest::
>>> synth.ATTRIBUTE
'foo'
.. doctest::
>>> issubclass(synth, type(first))
True
>>> issubclass(synth, type(second))
True
:param name:
the `__name__` for the new type
:param instances:
any number of :term:`object`\\s that should be instances of the new
type
:param dict_:
any additional keyword arguments that will be used as
:term:`attribute`\\s for the new type
"""
instance_types = tuple(type(instance) for instance in instances)
instance_mros = reversed(
tuple(reversed(type_.__mro__) for type_ in instance_types)
)
bases = reversed(functools.reduce(append_distinct, instance_mros, ()))
return type(name, tuple(bases), dict_)
[docs]def auto_merge(*bases, metaclass=None, **dict_):
# pylint: disable=redefined-outer-name
"""
Create a subclass of `bases` with a single merged :term:`metaclass`
This function can be used to avoid `TypeError: metaclass conflict` when
subclassing from ancestors that do not share a related metaclass. E.g.:
.. testcode::
import pprint
from fictive.patterns.types import auto_merge
class FirstMeta(type):
pass
class FirstClass(object, metaclass=FirstMeta):
pass
class SecondMeta(type):
pass
class SecondClass(object, metaclass=SecondMeta):
pass
class ThirdMeta(type):
pass
class Combined(auto_merge(SecondClass, FirstClass, metaclass=ThirdMeta)):
pass
.. doctest::
>>> pprint.pprint(Combined.mro())
[<class 'Combined'>,
<class 'fictive.patterns.types.AutoMergedSubclass'>,
<class 'SecondClass'>,
<class 'FirstClass'>,
<class 'object'>]
.. doctest::
>>> pprint.pprint(type(Combined).__mro__)
(<class 'fictive.patterns.types.AutoMergedMetaclass'>,
<class 'ThirdMeta'>,
<class 'SecondMeta'>,
<class 'FirstMeta'>,
<class 'type'>,
<class 'object'>)
:param bases:
any number of bases classes that will be ancestors of the new class
:param type metaclass:
an optional additional metaclass to be incorporated into the new class
:param dict_:
any additional keyword arguments that should be used as class
attributes for the new class
"""
if metaclass:
auto_class = type.__new__(metaclass, 'AutoClass', (object,), {})
meta_bases = (auto_class,) + bases
else:
meta_bases = bases
merged_metaclass = synthesized_type('AutoMergedMetaclass', meta_bases, {})
return type.__new__(merged_metaclass, 'AutoMergedSubclass', bases, dict_)
[docs]class metaclass(object): # pylint: disable=invalid-name
"""
:term:`decorator`\\s for controlling :term:`metaclass` :term:`attribute`\\s
Python's :term:`type` system allows modification of :term:`class` behaviors
by modifying the :term:`class`'s :term:`metaclass`. While this system is
powerful and flexible, explicitly defining and assigning a custom metaclass
for small modifications to the :term:`class`\'s behavior can be overly
verbose and error prone.
As an alternative to explicitly defining and invoking a custom
:term:`metaclass`, this group of :term:`decorator`\\s can be used to create
an "ad hoc" :term:`metaclass` based on the decorated :term:`method`\\s and
:term:`attribute`\\s of the :term:`class`. E.g.:
.. testcode::
from fictive.patterns.types import metaclass
@metaclass.auto
class MyClass(object):
@metaclass.mark
def __getattribute__(cls, name):
'''
obfuscate class attribute access
'''
_cls_ = super(type(MyClass), cls).__getattribute__
reversed_name = name[::-1]
if name == 'hidden' or reversed_name == 'hidden':
return _cls_(reversed_name)
return _cls_(name)
@classmethod
def public(cls):
print(f"running {cls.__name__}.public()")
@classmethod
def hidden(cls):
print(f"running {cls.__name__}.hidden()")
return 'The Magic Words are Squeamish Ossifrage'
def __init__(self, value):
self.hidden = value
.. doctest::
>>> MyClass.public()
running MyClass_auto.public()
>>> hasattr(MyClass, 'hidden')
False
>>> hasattr(MyClass, 'neddih')
True
>>> MyClass.neddih()
running MyClass_auto.hidden()
'The Magic Words are Squeamish Ossifrage'
Note, the class's own `__getattribute__` method (used for instance
attribute access) is unaffected:
.. doctest::
>>> instance = MyClass('open sesame')
>>> instance.hidden
'open sesame'
>>> hasattr(instance, 'neddih')
False
>>> del instance.hidden
>>> hasattr(instance, 'neddih')
False
>>> hasattr(instance, 'hidden')
True
>>> instance.hidden()
running MyClass_auto.hidden()
'The Magic Words are Squeamish Ossifrage'
"""
[docs] class MARK_WRAPPER(wrapt.ObjectProxy):
# pylint: disable=invalid-name
# pylint: disable=abstract-method
# pylint: disable=too-few-public-methods
"""
wrapper used to mark an :term:`attribute` for the :term:`metaclass`
"""
[docs] def move_to_metaclass(self, name, class_attrs, meta_attrs):
"""
moves the wrapped object to the :term:`metaclass`
This method should define the behavior for moving an
:term:`attribute` from the :term:`class`\'s `__dict__` to the
`dict` that will be used to create a new :term:`metaclass`
:param class_attrs: the `class`\'s `__dict__`
:param meta_attrs:
the `dictionary` that will be passed to the
new:term:`metaclass`\'s constructor
"""
meta_attrs[name] = self.__wrapped__
del class_attrs[name]
[docs] @classmethod
def mark(cls, *args, **kwargs):
"""
mark an :term:`attribute` as belonging to the :term:`metaclass`
Can be used as a :term:`method` :term:`decorator` or an :term:`object`
wrapper (e.g., for :term:`class variable`\\s). Can be used without
paramaters, or in a parameterized form specfiying a custom wrapper
object. E.g.:
.. testcode::
from fictive.patterns.types import metaclass
class MyWrapper(metaclass.MARK_WRAPPER):
def move_to_metaclass(self, name, class_attrs, meta_attrs):
super(MyWrapper, self).move_to_metaclass(
name, class_attrs, meta_attrs)
reversed_name = name[::-1]
meta_attrs[reversed_name] = meta_attrs.pop(name)
class MyClass(object):
DEFAULT = metaclass.mark('default')
CUSTOM = metaclass.mark(mark_wrapper=MyWrapper)('custom')
@metaclass.mark
def default(cls):
return 'default'
@metaclass.mark(mark_wrapper=MyWrapper)
def custom(cls):
return 'custom'
.. doctest::
>>> type(MyClass.DEFAULT)
<class 'fictive.patterns.types.metaclass.MARK_WRAPPER'>
>>> type(MyClass.CUSTOM)
<class 'MyWrapper'>
>>> type(MyClass.default)
<class 'fictive.patterns.types.metaclass.MARK_WRAPPER'>
>>> type(MyClass.CUSTOM)
<class 'MyWrapper'>
When the class is decorated with `metaclass.auto`, the
marked attributes will be moved to the :term:`metaclass` using the
`mark_wrapper`\\'s :py:meth:`move_to_metaclass`:
.. doctest::
>>> MyClass = metaclass.auto(MyClass)
>>> MyClass.DEFAULT
'default'
>>> MyClass.MOTSUC
'custom'
>>> MyClass.default()
'default'
>>> MyClass.motsuc()
'custom'
"""
def parameterized_marker(mark_wrapper=None):
"""
:param mark_wrapper:
the wrapper to use when marking an :term:`attribute` for
relocation to the :term:`metaclass`
:type mark_wrapper: `metaclass.MARK_WRAPPER`
"""
def wrap_object(object_):
wrapper = mark_wrapper or cls.MARK_WRAPPER
return wrapper(object_)
return wrap_object
if kwargs:
return parameterized_marker(**kwargs)
return parameterized_marker()(*args)
[docs] @classmethod
def auto(cls, *args, **kwargs):
"""
synthesize a :term:`metaclass` for the decorated :term:`class`
Can be used without paramaters, or in a parameterized form specfiying
which attributes of the new :term:`class` that should not be copied
from the original :term:`class`. The default set of attributes to not
copy is (`__dict__` and `__weakref__`).
"""
@optional_arguments
def multimodal_decorator(special=('__dict__', '__weakref__')):
def auto_metaclass(klass):
class_attrs = klass.__dict__.copy()
meta_attrs = dict()
for key, value in klass.__dict__.items():
if isinstance(value, cls.MARK_WRAPPER):
value.move_to_metaclass(key, class_attrs, meta_attrs)
if not meta_attrs:
return klass
metaklass = type(klass)
new_metaclass = type(
f"{metaklass.__name__}_auto", (metaklass, ), meta_attrs)
class_attrs = {
key: value for (key, value) in class_attrs.items()
if key not in special
}
return new_metaclass(
f"{klass.__name__}_auto", klass.__bases__, class_attrs)
return auto_metaclass
return multimodal_decorator(*args, **kwargs)
[docs]class class_property(property): # pylint: disable=invalid-name
"""
Like the `property` :term:`decorator`, but for :term:`class variable`\\s
.. testcode::
from fictive.patterns.types import metaclass, class_property
@metaclass.auto
class MyClass(object):
@class_property
def class_attribute(cls):
return cls._ATTRIBUTE
@class_attribute.setter
def class_attribute(cls, value):
cls._ATTRIBUTE = value[::-1]
@class_attribute.deleter
def class_attribute(cls):
del cls._ATTRIBUTE
.. doctest::
>>> hasattr(MyClass, 'class_attribute')
False
>>> MyClass.class_attribute = 'class attribute'
>>> MyClass.class_attribute
'etubirtta ssalc'
As with other :term:`class variable`\\s, instance variables exist in a
separate :term:`namespace` to the `class_property`:
.. doctest::
>>> instance = MyClass()
>>> instance.class_attribute = 'instance attribute'
>>> instance.class_attribute
'instance attribute'
>>> MyClass.class_attribute
'etubirtta ssalc'
If the instance has no matching :term:`attribute` in its own
:term:`namespace`, it will read from the :term:`class`\'s
:term:`namespace`:
.. doctest::
>>> del instance.class_attribute
>>> instance.class_attribute
'etubirtta ssalc'
NB: because "`data descriptors`_" take precedence over instance variables
(and `property` objects are data descriptors), a `class_property` will take
precedence over class variables defined in subclasses. If a subclass needs
to override a `class_property`, it must declare that override in its own
metaclass.
.. _`data descriptors`: https://docs.python.org/3/howto/descriptor.html#descriptor-protocol
"""
[docs] class MetaclassDescriptorReference(object):
# pylint: disable=too-few-public-methods
"""
a "trampoline" to read the class attribute from an instance
"""
def __get__(self, instance, owner=None):
return getattr(type(owner), self.name).__get__(owner, type(owner))
[docs] class MARK_WRAPPER(metaclass.MARK_WRAPPER):
# pylint: disable=invalid-name
# pylint: disable=abstract-method
# pylint: disable=too-few-public-methods
"""
wrapper used to mark a `property` for the :term:`metaclass`
Leaves a "trampoline" in the :term:`class`\'s `__dict__`
"""
[docs] def move_to_metaclass(self, name, class_attrs, meta_attrs):
super(class_property.MARK_WRAPPER, self).move_to_metaclass(
name,
class_attrs,
meta_attrs
)
class_attrs[name] = self.MetaclassDescriptorReference(name)
def __new__(cls, *args, **kwargs):
instance = super(class_property, cls).__new__(cls, *args, **kwargs)
instance.__init__(*args, **kwargs)
return metaclass.mark(mark_wrapper=cls.MARK_WRAPPER)(instance)