diff --git a/tests/utils/test_metautils.py b/tests/utils/test_metautils.py new file mode 100644 index 00000000..ad864d9e --- /dev/null +++ b/tests/utils/test_metautils.py @@ -0,0 +1,91 @@ +from zipline.testing.fixtures import ZiplineTestCase +from zipline.testing.predicates import ( + assert_equal, + assert_is, + assert_is_instance, + assert_is_subclass, + assert_true, +) +from zipline.utils.metautils import compose_types, with_metaclasses + + +class C(object): + @staticmethod + def f(): + return 'C.f' + + def delegate(self): + return 'C.delegate', super(C, self).delegate() + + +class D(object): + @staticmethod + def f(): + return 'D.f' + + @staticmethod + def g(): + return 'D.g' + + def delegate(self): + return 'D.delegate' + + +class ComposeTypesTestCase(ZiplineTestCase): + + def test_identity(self): + assert_is( + compose_types(C), + C, + msg='compose_types of a single class should be identity', + ) + + def test_compose(self): + composed = compose_types(C, D) + + assert_is_subclass(composed, C) + assert_is_subclass(composed, D) + + def test_compose_mro(self): + composed = compose_types(C, D) + + assert_equal(composed.f(), C.f()) + assert_equal(composed.g(), D.g()) + + assert_equal(composed().delegate(), ('C.delegate', 'D.delegate')) + + +class M(type): + def __new__(mcls, name, bases, dict_): + dict_['M'] = True + return super(M, mcls).__new__(mcls, name, bases, dict_) + + +class N(type): + def __new__(mcls, name, bases, dict_): + dict_['N'] = True + return super(N, mcls).__new__(mcls, name, bases, dict_) + + +class WithMetaclassesTestCase(ZiplineTestCase): + def test_with_metaclasses_no_subclasses(self): + class E(with_metaclasses((M, N))): + pass + + assert_true(E.M) + assert_true(E.N) + + assert_is_instance(E, M) + assert_is_instance(E, N) + + def test_with_metaclasses_with_subclasses(self): + class E(with_metaclasses((M, N), C, D)): + pass + + assert_true(E.M) + assert_true(E.N) + + assert_is_instance(E, M) + assert_is_instance(E, N) + assert_is_subclass(E, C) + assert_is_subclass(E, D) diff --git a/zipline/testing/predicates.py b/zipline/testing/predicates.py index 93f3610e..b83bdbf8 100644 --- a/zipline/testing/predicates.py +++ b/zipline/testing/predicates.py @@ -141,6 +141,34 @@ def _fmt_msg(msg): return msg + '\n' +def _safe_cls_name(cls): + try: + return cls.__name__ + except AttributeError: + return repr(cls) + + +def assert_is_subclass(subcls, cls, msg=''): + """Assert that ``subcls`` is a subclass of ``cls``. + + Parameters + ---------- + subcls : type + The type to check. + cls : type + The type to check ``subcls`` against. + msg : str, optional + An extra assertion message to print if this fails. + """ + assert issubclass(subcls, cls), ( + '%s is not a subclass of %s\n%s' % ( + _safe_cls_name(subcls), + _safe_cls_name(cls), + msg, + ) + ) + + @dispatch(object, object) def assert_equal(result, expected, path=(), msg='', **kwargs): """Assert that two objects are equal using the ``==`` operator. diff --git a/zipline/utils/metautils.py b/zipline/utils/metautils.py index fa0dd0ed..a2f86080 100644 --- a/zipline/utils/metautils.py +++ b/zipline/utils/metautils.py @@ -1,7 +1,9 @@ from operator import attrgetter +import six -def compose_types(a, b, *cs): + +def compose_types(a, *cs): """Compose multiple classes together. Parameters @@ -66,9 +68,40 @@ def compose_types(a, b, *cs): Always using ``super()`` to dispatch to your superclass is best practices anyways so most classes should compose without much special considerations. """ - mcls = (a, b) + cs + if not cs: + # if there are no types to compose then just return the single type + return a + + mcls = (a,) + cs return type( 'compose_types(%s)' % ', '.join(map(attrgetter('__name__'), mcls)), mcls, {}, ) + + +def with_metaclasses(metaclasses, *bases): + """Make a class inheriting from ``bases`` whose metaclass inherits from + all of ``metaclasses``. + + Like :func:`six.with_metaclass`, but allows multiple metaclasses. + + Parameters + ---------- + metaclasses : iterable[type] + A tuple of types to use as metaclasses. + *bases : tuple[type] + A tuple of types to use as bases. + + Returns + ------- + base : type + A subtype of ``bases`` whose metaclass is a subtype of ``metaclasses``. + + Notes + ----- + The metaclasses must be written to support cooperative multiple + inheritance. This means that they must delegate all calls to ``super()`` + instead of inlining their super class by name. + """ + return six.with_metaclass(compose_types(*metaclasses), *bases)