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 """
[docs] def __init__(self, name): self.name = name
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)