Source code for py_meta_utils
from collections import namedtuple
from typing import *
_missing = type('_missing', (), {'__bool__': lambda x: False})()
ABSTRACT_ATTR = '__abstract__'
META_OPTIONS_FACTORY_CLASS_ATTR_NAME = '_meta_options_factory_class'
McsInitArgs = namedtuple('McsInitArgs', ('cls', 'name', 'bases', 'clsdict'))
[docs]class McsArgs:
"""
Data holder for the parameters to ``type.__new__()``::
class Metaclass(type):
def __new__(mcs, name, bases, clsdict):
mcs_args = McsArgs(mcs, name, bases, clsdict)
# do stuff
return super().__new__(*mcs_args)
# or in short-hand:
class Metaclass(type):
def __new__(mcs, *args):
mcs_args = McsArgs(mcs, *args)
# do stuff
return super().__new__(*mcs_args)
"""
def __init__(self,
mcs: type,
name: str,
bases: Tuple[Type[object], ...],
clsdict: Dict[str, Any]):
self.mcs = mcs
self.name = name
self.bases = bases
self.clsdict = clsdict
[docs] def getattr(self, name, default: Any = _missing):
"""
Convenience method equivalent to
``deep_getattr(mcs_args.clsdict, mcs_args.bases, 'attr_name'[, default])``
"""
return deep_getattr(self.clsdict, self.bases, name, default)
@property
def module(self) -> Union[str, None]:
"""
Returns the module of the class-under-construction, or ``None``.
"""
return self.clsdict.get('__module__')
@property
def qualname(self) -> str:
"""
Returns the fully qualified name of the class-under-construction, if possible,
otherwise just the class name.
"""
if self.module:
return self.module + '.' + self.name
return self.name
@property
def Meta(self) -> Type[object]:
"""
Returns the class ``Meta`` from the class-under-construction.
Raises ``KeyError`` if it's not present.
"""
return self.clsdict['Meta']
@property
def is_abstract(self) -> bool:
"""
Whether or not the class-under-construction was declared as abstract (**NOTE:**
this property is usable even *before* the :class:`MetaOptionsFactory` has run)
"""
meta_value = getattr(self.clsdict.get('Meta'), 'abstract', False)
return self.clsdict.get(ABSTRACT_ATTR, meta_value) is True
# we implement __iter__() to allow using the *args unpacking syntax
def __iter__(self):
return iter([self.mcs, self.name, self.bases, self.clsdict])
def __repr__(self):
return '<McsArgs class={qualname!r}>'.format(qualname=self.qualname)
[docs]class MetaOption:
"""
Base class for custom meta options.
"""
def __init__(self, name: str, default: Any = None, inherit: bool = False):
self.name = name
"""
The attribute name of the option on class ``Meta`` objects.
"""
self.default = default
"""
The default value for this meta option.
"""
self.inherit = inherit
"""
Whether or not this option's value should be inherited from the class ``Meta``
of any base classes.
"""
[docs] def get_value(self, Meta: Type[object], base_classes_meta, mcs_args: McsArgs) -> Any:
"""
Returns the value for ``self.name`` given the class-under-construction's class
``Meta``. If it's not found there, and ``self.inherit == True`` and there is a
base class that has a class ``Meta``, use that value, otherwise ``self.default``.
:param Meta: the class ``Meta`` (if any) from the class-under-construction
(**NOTE:** this will be an ``object`` or ``None``, NOT an instance
of :class:`MetaOptionsFactory`)
:param base_classes_meta: the :class:`MetaOptionsFactory` instance (if any) from
the base class of the class-under-construction
:param mcs_args: the :class:`McsArgs` for the class-under-construction
"""
value = self.default
if self.inherit and base_classes_meta is not None:
value = getattr(base_classes_meta, self.name, value)
if Meta is not None:
value = getattr(Meta, self.name, value)
return value
[docs] def check_value(self, value: Any, mcs_args: McsArgs):
"""
Optional callback to verify the user provided a valid value.
Your implementation should assert/raise with an error message if invalid.
"""
pass
[docs] def contribute_to_class(self, mcs_args: McsArgs, value: Any):
"""
Optional callback to modify the :class:`McsArgs` of the class-under-construction.
"""
pass
def __repr__(self):
return '{cls}(name={name!r}, default={default!r}, inherit={inherit})'.format(
cls=self.__class__.__name__,
name=self.name,
default=self.default,
inherit=self.inherit)
[docs]class AbstractMetaOption(MetaOption):
"""
A meta option that allows designating a class as abstract, using either::
class SomeAbstractBase(metaclass=MetaclassWithAnOptionsFactory):
__abstract__ = True
# or
class SomeAbstractBase(metaclass=MetaclassWithAnOptionsFactory):
class Meta:
abstract = True
In the latter case, we make sure to set the ``__abstract__`` class attribute
for backwards compatibility with libraries that do not understand ``Meta`` options.
"""
def __init__(self):
super().__init__(name='abstract', default=False, inherit=False)
def get_value(self, Meta, base_classes_meta, mcs_args: McsArgs):
# class attributes take precedence over the class Meta's value
if mcs_args.clsdict.get(ABSTRACT_ATTR, False) is True:
return True
return super().get_value(Meta, base_classes_meta, mcs_args) is True
def check_value(self, value: Any, mcs_args: McsArgs):
if not isinstance(value, bool):
raise TypeError('The abstract Meta option must be either True or False')
def contribute_to_class(self, mcs_args: McsArgs, value):
mcs_args.clsdict[ABSTRACT_ATTR] = True if value is True else False
[docs]class EnsureProtectedMembers(type):
"""
Metaclass to ensure that all members (attributes and method names) of consumer
classes are protected (ie, prefixed with an ``_``).
Consumer classes may have an `_allowed_properties` class attribute set to
a list of allowed public properties.
Raises ``NameError`` if any public members not in `cls._allowed_properties`
are found.
"""
def __init__(cls, name, bases, clsdict):
allowed_props = set(getattr(cls, '_allowed_properties', ()))
for attr, value in clsdict.items():
if not attr.startswith('_'):
if attr in allowed_props and isinstance(value, property):
continue
raise NameError('{cls}.{attr} must be protected '
'(rename to {cls}._{attr})'.format(cls=name,
attr=attr))
super().__init__(name, bases, clsdict)
[docs]class MetaOptionsFactory(metaclass=EnsureProtectedMembers):
"""
Base class for meta options factory classes. Subclasses should either set
:attr:`_options` to a list of :class:`MetaOption` subclasses (or instances)::
class MyMetaOptionsFactory(MetaOptionsFactory):
_options = [AbstractMetaOption]
Or override :meth:`_get_meta_options` to return a list of :class:`MetaOption`
*instances*::
class MyMetaOptionsFactory(MetaOptionsFactory):
def _get_meta_options(self):
return [AbstractMetaOption()]
**IMPORTANT:** If you add any attributes and/or methods to your factory subclass,
they *must* be protected (ie, prefixed with an ``_`` character).
"""
_allowed_properties = []
"""
A list of public properties to allow on this factory.
"""
_options = []
"""
A list of :class:`MetaOption` subclasses (or instances) that this factory supports.
"""
def __init__(self):
self._mcs_args = None
[docs] def _get_meta_options(self) -> List[MetaOption]:
"""
Returns a list of :class:`MetaOption` instances that this factory supports.
"""
return [option if isinstance(option, MetaOption) else option()
for option in self._options]
[docs] def _contribute_to_class(self, mcs_args: McsArgs):
"""
Where the magic happens. Takes one parameter, the :class:`McsArgs` of the
class-under-construction, and processes the declared ``class Meta`` from
it (if any). We fill ourself with the declared meta options' name/value pairs,
give the declared meta options a chance to also contribute to the class-under-
construction, and finally replace the class-under-construction's ``class Meta``
with this populated factory instance (aka ``self``).
"""
self._mcs_args = mcs_args
Meta = mcs_args.clsdict.pop('Meta', None) # type: Type[object]
base_classes_meta = mcs_args.getattr('Meta', None) # type: MetaOptionsFactory
mcs_args.clsdict['Meta'] = self # must come before _fill_from_meta, because
# some meta options may depend upon having
# access to the values of earlier meta options
self._fill_from_meta(Meta, base_classes_meta, mcs_args)
for option in self._get_meta_options():
option_value = getattr(self, option.name, None)
option.contribute_to_class(mcs_args, option_value)
[docs] def _fill_from_meta(self, Meta: Type[object], base_classes_meta, mcs_args: McsArgs):
"""
Iterate over our supported meta options, and set attributes on the factory
instance (self) for each meta option's name/value. Raises ``TypeError`` if
we discover any unsupported meta options on the class-under-construction's
``class Meta``.
"""
# Exclude private/protected fields from the Meta
meta_attrs = {} if not Meta else {k: v for k, v in vars(Meta).items()
if not k.startswith('_')}
for option in self._get_meta_options():
existing = getattr(self, option.name, None)
if existing and not (existing in self._allowed_properties
and not isinstance(existing, property)):
raise RuntimeError("Can't override field {name}."
"".format(name=option.name))
value = option.get_value(Meta, base_classes_meta, mcs_args)
option.check_value(value, mcs_args)
meta_attrs.pop(option.name, None)
if option.name != '_':
setattr(self, option.name, value)
if meta_attrs:
# Only allow attributes on the Meta that have a respective MetaOption
raise TypeError(
'`class Meta` for {cls} got unknown attribute(s) {attrs}'.format(
cls=mcs_args.name,
attrs=', '.join(sorted(meta_attrs.keys()))))
def _to_clsdict(self):
return dict(**{option.name: getattr(self, option.name)
for option in self._get_meta_options()
if option.name != '_'},
**{'_mcs_args': self._mcs_args,
'__module__': self._mcs_args.qualname,
},
)
def __repr__(self):
return '{cls}(options={attrs!r})'.format(
cls=self.__class__.__name__,
attrs={option.name: getattr(self, option.name, None)
for option in self._get_meta_options()})
[docs]def process_factory_meta_options(
mcs_args: McsArgs,
default_factory_class: Type[MetaOptionsFactory] = MetaOptionsFactory,
factory_attr_name: str = META_OPTIONS_FACTORY_CLASS_ATTR_NAME) \
-> MetaOptionsFactory:
"""
Main entry point for consumer metaclasses. Usage::
from py_meta_utils import (AbstractMetaOption, McsArgs, MetaOptionsFactory,
process_factory_meta_options)
class YourMetaOptionsFactory(MetaOptionsFactory):
_options = [AbstractMetaOption]
class YourMetaclass(type):
def __new__(mcs, name, bases, clsdict):
mcs_args = McsArgs(mcs, name, bases, clsdict)
# process_factory_meta_options must come *before* super().__new__()
process_factory_meta_options(mcs_args, YourMetaOptionsFactory)
return super().__new__(*mcs_args)
class YourClass(metaclass=YourMetaclass):
pass
Subclasses of ``YourClass`` may set their ``_meta_options_factory_class``
attribute to a subclass of ``YourMetaOptionsFactory`` to customize
their own supported meta options::
from py_meta_utils import MetaOption
class FooMetaOption(MetaOption):
def __init__(self):
super().__init__(name='foo', default=None, inherit=True)
class FooMetaOptionsFactory(YourMetaOptionsFactory):
_options = YourMetaOptionsFactory._options + [
FooMetaOption,
]
class FooClass(YourClass):
_meta_options_factory_class = FooMetaOptionsFactory
class Meta:
foo = 'bar'
:param mcs_args: The :class:`McsArgs` for the class-under-construction
:param default_factory_class: The default MetaOptionsFactory class to use, if
the ``factory_attr_name`` attribute is not set on
the class-under-construction
:param factory_attr_name: The attribute name to look for an overridden factory
meta options class on the class-under-construction
:return: The populated instance of the factory class
"""
factory_cls = mcs_args.getattr(
factory_attr_name or META_OPTIONS_FACTORY_CLASS_ATTR_NAME,
default_factory_class)
options_factory = factory_cls()
options_factory._contribute_to_class(mcs_args)
return options_factory
[docs]class Singleton(type):
"""
A metaclass that makes a consumer class a singleton::
from py_meta_utils import Singleton
class Foo(metaclass=Singleton):
pass
foo = Foo()
assert foo == Foo() == Foo() # True
Note that if you subclass a singleton, then you must inform the base class::
Foo.set_singleton_class(YourFooSubclass)
This way, calling ``Foo()`` will still return the same instance of ``YourFooSubclass``
as if calling ``YourFooSubclass()`` itself::
foo = Foo()
sub = YourFooSubclass()
assert foo == sub == Foo() == YourFooSubclass()
"""
_classes = {}
_instances = {}
def set_singleton_class(self, cls):
if self in self._classes:
from warnings import warn
warn('An instance of this singleton has already been created! Please set '
'the class you wish to use earlier.', UserWarning)
return
for base in self.__mro__:
self._classes[base] = cls
self._classes[cls] = cls
def __call__(cls, *args, **kwargs):
if cls not in cls._classes:
cls._classes[cls] = cls
cls = cls._classes[cls]
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
[docs]def deep_getattr(clsdict: Dict[str, Any],
bases: Tuple[Type[object], ...],
name: str,
default: Any = _missing) -> Any:
"""
Acts just like ``getattr`` would on a constructed class object, except this operates
on the pre-construction class dictionary and base classes. In other words, first we
look for the attribute in the class dictionary, and then we search all the base
classes (in method resolution order), finally returning the default value if the
attribute was not found in any of the class dictionary or base classes (or it raises
``AttributeError`` if no default was given).
"""
value = clsdict.get(name, _missing)
if value != _missing:
return value
for base in bases:
value = getattr(base, name, _missing)
if value != _missing:
return value
if default != _missing:
return default
raise AttributeError(name)
[docs]class OptionalMetaclass(type):
"""
Use this as a generic base metaclass if you need to subclass a metaclass from
an optional package::
try:
from optional_dependency import SomeMetaclass
except ImportError:
from py_meta_utils import OptionalMetaclass as SomeMetaclass
class Optional(metaclass=SomeMetaclass):
pass
"""
__optional_class = None
def __new__(mcs, name, bases, clsdict):
if mcs.__optional_class is None or '__classcell__' in clsdict:
mcs.__optional_class = super().__new__(mcs, name, bases, clsdict)
return mcs.__optional_class
def __getattr__(self, item):
return self.__optional_class
def __setattr__(self, key, value):
pass
def __call__(cls, *args, **kwargs):
return cls.__optional_class
def __getitem__(self, item):
return self.__optional_class
def __setitem__(self, key, value):
pass
def __bool__(self):
return False
def __contains__(self, item):
return False
[docs]class OptionalClass(metaclass=OptionalMetaclass):
"""
Use this as a generic base class if you have classes that depend on an
optional package::
try:
from optional_dependency import SomeClass
except ImportError:
from py_meta_utils import OptionalClass as SomeClass
class Optional(SomeClass):
pass
"""
def __init__(self, *args, **kwargs):
pass