import doctest
import unittest


doctests = """

Basic class construction.

    >>> class C:
    ...     def meth(self): print("Hello")
    ...
    >>> C.__class__ is type
    True
    >>> a = C()
    >>> a.__class__ is C
    True
    >>> a.meth()
    Hello
    >>>

Use *args notation for the bases.

    >>> class A: pass
    >>> class B: pass
    >>> bases = (A, B)
    >>> class C(*bases): pass
    >>> C.__bases__ == bases
    True
    >>>

Use a trivial metaclass.

    >>> class M(type):
    ...     pass
    ...
    >>> class C(metaclass=M):
    ...    def meth(self): print("Hello")
    ...
    >>> C.__class__ is M
    True
    >>> a = C()
    >>> a.__class__ is C
    True
    >>> a.meth()
    Hello
    >>>

Use **kwds notation for the metaclass keyword.

    >>> kwds = {'metaclass': M}
    >>> class C(**kwds): pass
    ...
    >>> C.__class__ is M
    True
    >>> a = C()
    >>> a.__class__ is C
    True
    >>>

Use a metaclass with a __prepare__ static method.

    >>> class M(type):
    ...    @staticmethod
    ...    def __prepare__(*args, **kwds):
    ...        print("Prepare called:", args, kwds)
    ...        return dict()
    ...    def __new__(cls, name, bases, namespace, **kwds):
    ...        print("New called:", kwds)
    ...        return type.__new__(cls, name, bases, namespace)
    ...    def __init__(cls, *args, **kwds):
    ...        pass
    ...
    >>> class C(metaclass=M):
    ...     def meth(self): print("Hello")
    ...
    Prepare called: ('C', ()) {}
    New called: {}
    >>>

Also pass another keyword.

    >>> class C(object, metaclass=M, other="haha"):
    ...     pass
    ...
    Prepare called: ('C', (<class 'object'>,)) {'other': 'haha'}
    New called: {'other': 'haha'}
    >>> C.__class__ is M
    True
    >>> C.__bases__ == (object,)
    True
    >>> a = C()
    >>> a.__class__ is C
    True
    >>>

Check that build_class doesn't mutate the kwds dict.

    >>> kwds = {'metaclass': type}
    >>> class C(**kwds): pass
    ...
    >>> kwds == {'metaclass': type}
    True
    >>>

Use various combinations of explicit keywords and **kwds.

    >>> bases = (object,)
    >>> kwds = {'metaclass': M, 'other': 'haha'}
    >>> class C(*bases, **kwds): pass
    ...
    Prepare called: ('C', (<class 'object'>,)) {'other': 'haha'}
    New called: {'other': 'haha'}
    >>> C.__class__ is M
    True
    >>> C.__bases__ == (object,)
    True
    >>> class B: pass
    >>> kwds = {'other': 'haha'}
    >>> class C(B, metaclass=M, *bases, **kwds): pass
    ...
    Prepare called: ('C', (<class 'test.test_metaclass.B'>, <class 'object'>)) {'other': 'haha'}
    New called: {'other': 'haha'}
    >>> C.__class__ is M
    True
    >>> C.__bases__ == (B, object)
    True
    >>>

Check for duplicate keywords.

    >>> class C(metaclass=type, metaclass=type): pass
    ...
    Traceback (most recent call last):
    [...]
    SyntaxError: keyword argument repeated: metaclass
    >>>

Another way.

    >>> kwds = {'metaclass': type}
    >>> class C(metaclass=type, **kwds): pass
    ...
    Traceback (most recent call last):
    [...]
    TypeError: __build_class__() got multiple values for keyword argument 'metaclass'
    >>>

Use a __prepare__ method that returns an instrumented dict.

    >>> class LoggingDict(dict):
    ...     def __setitem__(self, key, value):
    ...         print("d[%r] = %r" % (key, value))
    ...         dict.__setitem__(self, key, value)
    ...
    >>> class Meta(type):
    ...    @staticmethod
    ...    def __prepare__(name, bases):
    ...        return LoggingDict()
    ...
    >>> class C(metaclass=Meta):
    ...     foo = 2+2
    ...     foo = 42
    ...     bar = 123
    ...
    d['__module__'] = 'test.test_metaclass'
    d['__qualname__'] = 'C'
    d['__firstlineno__'] = 1
    d['foo'] = 4
    d['foo'] = 42
    d['bar'] = 123
    d['__static_attributes__'] = ()
    >>>

Use a metaclass that doesn't derive from type.

    >>> def meta(name, bases, namespace, **kwds):
    ...     print("meta:", name, bases)
    ...     print("ns:", sorted(namespace.items()))
    ...     print("kw:", sorted(kwds.items()))
    ...     return namespace
    ...
    >>> class C(metaclass=meta):
    ...     a = 42
    ...     b = 24
    ...
    meta: C ()
    ns: [('__firstlineno__', 1), ('__module__', 'test.test_metaclass'), ('__qualname__', 'C'), ('__static_attributes__', ()), ('a', 42), ('b', 24)]
    kw: []
    >>> type(C) is dict
    True
    >>> print(sorted(C.items()))
    [('__firstlineno__', 1), ('__module__', 'test.test_metaclass'), ('__qualname__', 'C'), ('__static_attributes__', ()), ('a', 42), ('b', 24)]
    >>>

And again, with a __prepare__ attribute.

    >>> def prepare(name, bases, **kwds):
    ...     print("prepare:", name, bases, sorted(kwds.items()))
    ...     return LoggingDict()
    ...
    >>> meta.__prepare__ = prepare
    >>> class C(metaclass=meta, other="booh"):
    ...    a = 1
    ...    a = 2
    ...    b = 3
    ...
    prepare: C () [('other', 'booh')]
    d['__module__'] = 'test.test_metaclass'
    d['__qualname__'] = 'C'
    d['__firstlineno__'] = 1
    d['a'] = 1
    d['a'] = 2
    d['b'] = 3
    d['__static_attributes__'] = ()
    meta: C ()
    ns: [('__firstlineno__', 1), ('__module__', 'test.test_metaclass'), ('__qualname__', 'C'), ('__static_attributes__', ()), ('a', 2), ('b', 3)]
    kw: [('other', 'booh')]
    >>>

The default metaclass must define a __prepare__() method.

    >>> type.__prepare__()
    {}
    >>>

Make sure it works with subclassing.

    >>> class M(type):
    ...     @classmethod
    ...     def __prepare__(cls, *args, **kwds):
    ...         d = super().__prepare__(*args, **kwds)
    ...         d["hello"] = 42
    ...         return d
    ...
    >>> class C(metaclass=M):
    ...     print(hello)
    ...
    42
    >>> print(C.hello)
    42
    >>>

Test failures in looking up the __prepare__ method work.
    >>> class ObscureException(Exception):
    ...     pass
    >>> class FailDescr:
    ...     def __get__(self, instance, owner):
    ...        raise ObscureException
    >>> class Meta(type):
    ...     __prepare__ = FailDescr()
    >>> class X(metaclass=Meta):
    ...     pass
    Traceback (most recent call last):
    [...]
    test.test_metaclass.ObscureException

"""

import sys

# Trace function introduces __locals__ which causes various tests to fail.
if hasattr(sys, 'gettrace') and sys.gettrace():
    __test__ = {}
else:
    __test__ = {'doctests' : doctests}

def load_tests(loader, tests, pattern):
    tests.addTest(doctest.DocTestSuite())
    return tests


if __name__ == "__main__":
    unittest.main()
