Commits

masklinn committed f1ef3e8

Handle 'complex' hierarchies where any level can be extended

Comments (0)

Files changed (2)

           this can break third-party extensions, if not the base
           object itself.
 
-Extends and inheritance: extends-based object hierarchies
----------------------------------------------------------
+Extends and regular inheritance
+-------------------------------
+
+One of the patterns in object-oriented languages such as Python is the
+building of object hierarchies (via inheritance) as means of code
+reuse and sharing of interface and implementation.
+
+It would of course be expected that extends deals correctly with this
+case, and that it behaves sensibly.
+
+In an inheritance hierarchy involving extends-based objects,
+everything simply behaves as if the extends-based objects were
+replaced by their extended versions. Nothing more, nothing less, and
+the original MRO should still be valid (except it will have new
+components interspersed).
+
+For instance if we define a basic hierarchy::
+
+    >>> class Root(extends.Base):
+    ...     def is_a(self):
+    ...         return False
+    ...     def is_b(self):
+    ...         return False
+    >>> class A(Root):
+    ...     def is_a(self):
+    ...         return True
+    >>> class B(Root):
+    ...     def is_b(self):
+    ...         return True
+    >>> map(lambda v: v.is_a(), [Root(), A(), B()])
+    [False, True, False]
+    >>> map(lambda v: v.is_b(), [Root(), A(), B()])
+    [False, False, True]
+
+We can extend the leaves as normal::
+
+    >>> class BecomeB(extends.Extend, A):
+    ...     def is_b(self):
+    ...         return True
+    >>> A().is_b()
+    True
+
+But we can also extend classes higher up the stack, and have its
+descendants behave as expected, getting their ancestor's extension::
+
+    >>> class RootIsRootChild(extends.Extend, Root):
+    ...     def is_root_child(self):
+    ...         return True
+    >>> A().is_root_child() and B().is_root_child()
+    True
 
 Advanced topics in extends: Configuring extends behavior
 ========================================================

extends/__init__.py

+__all__ = ['Base', 'Extend']
+
+def get_extends(cls):
+    # __subclasses__ is sorted in loading order and the mro
+    # executes the bases left-to-right. We want the resolution
+    # order of the extended type to be last-loaded first (the
+    # latest-loaded extender sees its methods executed first,
+    # execution goes up the extension sequence and ends on the
+    # base class), therefore we must revert ``extenders``
+    return [
+        sub for sub in cls.__subclasses__()
+        if issubclass(sub, Extend)
+        if sub is not Extend][::-1]
+
 class Extend(object):
     pass
 
 class Base(object):
     def __new__(cls):
-        extenders = [
-            sub for sub in cls.__subclasses__()
-            if issubclass(sub, Extend)]
-        if not extenders:
+        base_extenders = get_extends(cls)
+        ancestor_extenders = sum(map(get_extends, cls.mro()[1:]), [])
+        if not (base_extenders or ancestor_extenders):
             return object.__new__(cls)
-        # __subclasses__ is sorted in loading order and the mro
-        # executes the bases left-to-right. We want the resolution
-        # order of the extended type to be last-loaded first (the
-        # latest-loaded extender sees its methods executed first,
-        # execution goes up the extension sequence and ends on the
-        # base class), therefore we must revert ``extenders``
+
+        extenders = (base_extenders or [cls]) + ancestor_extenders
         return object.__new__(
-            type(cls.__name__, tuple(reversed(extenders)), {}))
+            type(cls.__name__, tuple(extenders), {}))