xoutil.eight.meta - metaclass function using Python 3 syntax

Implements the metaclass() function using the Py3k syntax.

xoutil.eight.meta.metaclass(meta, **kwargs)

Define the metaclass of a class.

New in version 1.7.0.

This function allows to define the metaclass of a class equally in Python 2 and 3.

Usage:

>>> class Meta(type):
...   pass

>>> class Foobar(metaclass(Meta)):
...   pass

>>> class Spam(metaclass(Meta), dict):
...   pass

>>> type(Spam) is Meta
True

>>> Spam.__bases__ == (dict, )
True

New in version 1.5.5: The kwargs keywords arguments with support for __prepare__.

Metaclasses are allowed to have a __prepare__ classmethod to return the namespace into which the body of the class should be evaluated. See PEP 3115.

Warning

The PEP 3115 is not possible to implement in Python 2.7.

Despite our best efforts to have a truly compatible way of creating meta classes in both Python 2.7 and 3.0+, there is an inescapable difference in Python 2.7. The PEP 3115 states that __prepare__ should be called before evaluating the body of the class. This is not possible in Python 2.7, since __new__ already receives the attributes collected in the body of the class. So it’s always too late to call __prepare__ at this point and the Python 2.7 interpreter does not call it.

Our approach for Python 2.7 is calling it inside the __new__ of a “side” metaclass that is used for the base class returned. This means that __prepare__ is called only for classes that use the metaclass() directly. In the following hierarchy:

class Meta(type):
     @classmethod
     def __prepare__(cls, name, bases, **kwargs):
         from xoutil.future.collections import OrderedDict
         return OrderedDict()

class Foo(metaclass(Meta)):
     pass

class Bar(Foo):
     pass

when creating the class Bar the __prepare__() class method is not called in Python 2.7!

Warning

You should always place your metaclass declaration first in the list of bases. Doing otherwise triggers twice the metaclass’ constructors in Python 3.1 or less.

If your metaclass has some non-idempotent side-effect (such as registration of classes), then this would lead to unwanted double registration of the class:

>>> class BaseMeta(type):
...     classes = []
...     def __new__(cls, name, bases, attrs):
...         res = super(BaseMeta, cls).__new__(cls, name, bases, attrs)
...         cls.classes.append(res)   # <-- side effect
...         return res

>>> class Base(metaclass(BaseMeta)):
...     pass

>>> class SubType(BaseMeta):
...     pass

>>> class Egg(metaclass(SubType), Base):   # <-- metaclass first
...     pass

>>> Egg.__base__ is Base   # <-- but the base is Base
True

>>> len(BaseMeta.classes) == 2
True

>>> class Spam(Base, metaclass(SubType)):
...     'Like "Egg" but it will be registered twice in Python 2.x.'

In this case the registration of Spam ocurred twice:

>>> BaseMeta.classes  
[<class Base>, <class Egg>, <class Spam>, <class Spam>]

Bases, however, are just fine:

>>> Spam.__bases__ == (Base, )
True

New in version 1.7.1: Now are accepted atypical meta-classes, for example functions or any callable with the same arguments as those that type accepts (class name, tuple of base classes, attributes mapping).