Source

Extends / README.rst

Extends: a collaborative open-classes library

To start using extends, you simply need to import it:

>>> import extends

But before we delve any further into Extends, answering a basic question might be in order:

What is an open class?

Several languages have so-called "open" classes (or variations thereof). Among those languages are Smalltalk or Ruby, and in some ways C#.

An open class is simply a class to which any user can add content (methods, fields, ...) in-place without having to edit the original source code. By in-place we mean that any user of the original class (within the scope of the opening) will see the new methods, fields and behaviors.

Why add open classes?

The main purpose here is to serve as the basics for a flexible plugins framework: while most systems with plugins only provide a specific set of extension points or behavior customization points (for existing objects of the system), using open classes lets any plugin creator alter any behavior of the base package.

Because extends is cooperative, it is not quite that flexible: only the classes "opted into" being open by the base package creators will be modifiable at will.

Still, by avoiding the creation of a complex plugin system (extending classes simply have to be loaded by Python), extends tries to simplify the work of both the original system's creators and plugin writers: both just have to write regular Python classes.

Using extends

Creating an open class

The first task when using extends is to opt a class in extends's open class policy. This is done by simply inheriting from extends's Base class:

>>> class Example1(extends.Base):
...     pass
...
>>> isinstance(Example1(), Example1)
True

The deed is done, from now onwards Example1 and all of its subclasses will be "open" as far as extends is concerned.

Warning

While extends tries to avoid changing the user's habit, and pretty much any normal Python usage (including multiple inheritance) should be possible, __new__ should only be overridden with great care: extends uses it to return arbitrary instances built on-the-fly. You probably want to avoid overriding __new__ if possible, just in case.

Furthermore, extends uses a (pretty simple) metaclass for some of its busywork, unless you're very good at Python metaprogramming, you probably want to avoid combining extends with your own metaclasses.

Re-opening the original class

"Re-opening" is a term often used for "extending a class in-place". Consider a class is a box with stuff in it, originally the class is created, methods and attributes are put in the box and the box is then closed to be used.

Extending the class in-place means re-opening the box and messing with the existing content of the class.

This is what we're going to do here.

Re-opening a class is almost as simple as opting a class in extends, the class writer simply needs to inherit from the class he wants to extend and from extend's Extend class:

>>> class BasicExtender(extends.Extend, Example1):
...     def exists(self):
...         return True

As you probably noted, Example1 didn't originally have a foo method:

>>> o1 = Example1()
>>> isinstance(o1, Example1)
True
>>> o1.exists()
True

!DANGER!

At this point in time, extends.Extend has to precede the extended class or Python will not be able to create consistent resolution orders.

You will notice quite fast if you mix them up as Python will generate a TypeError.

It is also perfectly possible to inherit from third-party classes, this works as a mixin in that the resulting extension will be the combination of the inherited class and the extension one:

>>> class Arbitrary(object):
...     def random_method(self):
...         pass
...
>>> class InherintingExtend(extends.Extend, Example1, Arbitrary):
...     pass
...
>>> o3 = Example1()
>>> o3.random_method()

Note

while the arbitrarily-inherited class can be at any position (technically), putting them after both the extension marker and the extended class is probably a good idea for readability and simplicity's sake.

Warning

While inheriting from arbitrary classes is possible, the warning about __new__ stays valid: it's a bad idea even if it's transitive.

!DANGER!

Extending two different base classes at the same time is not supported, doing so will result in an undefined (but probably very bad) result.

That's basically it for the introductory usage, you are now able to use extends, and it should generally behave as you'd expect.

For more insight as to the precise behaviors of extends and its advanced usages, keep on reading.

Extending a light in the darkness

Hero: Extends Resolution Order

One of the important points of extends is to understand the order in which methods are resolved and executed between the base class and the various extends.

Overall, extends behaves as you would expect: on call, methods execute in reverse order from loading and the latest-loaded extends are executed first.

>>> class HERO(extends.Base):
...     def shout(self):
...         print 'Elder God'
...
>>> class Heracles(extends.Extend, HERO):
...     def shout(self):
...         print 'First hero'
...         super(Heracles, self).shout()
...
>>> class Theseus(extends.Extend, HERO):
...     def shout(self):
...         print 'Second hero'
...         super(Theseus, self).shout()
...
>>> class Asclepius(extends.Extend, HERO):
...     def shout(self):
...         print 'Third hero'
...         super(Asclepius, self).shout()
...
>>> HERO().shout()
Third hero
Second hero
First hero
Elder God

Note

As you can see, extends uses Python's own object system for its management. Therefore the various rules of cooperative inheritance in Python (including calling super) hold.

You may want to not call super if your goal is to completely replace a base class's method, but be aware that this can break third-party extensions, if not the base object itself.

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

The original classes are not modified at any point, so we can check their MRO:

>>> A.mro() == [A, Root, extends.Base, object]
True

and that its orderings hold:

>>> mro = type(A()).mro()
>>> mro.index(A) < mro.index(Root)
True
>>> mro.index(Root) < mro.index(extends.Base)
True

Advanced topics in extends: Configuring extends behavior

By default, extends considers all the currently loaded classes for inclusions into the extension scheme. But whether you're the system's original writer or one of its user, it might not be what you want, and you could need to accept or reject extensions to original types based on entirely arbitrary criteria.

To that end, extends provides a powerful but simple filtering API: you can provide a callable to any subclass of extends.Base (including extends.Base itself), and for each extender candidate found that callable will be called with the class being extended and the potential extender. If it returns False then the candidate will be ignored [1].

>>> class Rejector(extends.Base):
...     def accepted_extension(self):
...         return False
>>> Rejector.add_filter(
...     lambda *args: False)
>>> class Rejectee(extends.Extend, Rejector):
...     def accepted_extension(self):
...         return True
>>> Rejector().accepted_extension()
False

Naturally, the filter works for any subclass of the one it was added on:

>>> class RejectSon(Rejector):
...     pass
>>> class SonRejectee(extends.Extend, RejectSon):
...     def accepted_extension(self):
...         return True
>>> RejectSon().accepted_extension()
False

And it is possible to create stacks of rejection:

>>> import re
>>> class RejectUnCap(extends.Base):
...    def accepted_extensions(self):
...        return []
>>> RejectUnCap.add_filter(
...     lambda cls, candidate: \
...         re.match(r'^[A-Z]', candidate.__name__) is not None)
>>> class RejectQ(RejectUnCap):
...     pass
>>> RejectQ.add_filter(
...     lambda cls, candidate: \
...         not candidate.__name__.lower().startswith('q'))
>>> class rejectedCaps(extends.Extend, RejectQ):
...     def accepted_extensions(self):
...         return [rejectedCaps] \
...                + super(rejectedCaps, self).accepted_extensions()
>>> class QRejected(extends.Extend, RejectQ):
...     def accepted_extensions(self):
...         return [QRejected] \
...                + super(QRejected, self).accepted_extensions()
>>> class ActuallyAccepted(extends.Extend, RejectQ):
...     def accepted_extensions(self):
...         return [ActuallyAccepted] \
...                + super(ActuallyAccepted, self).accepted_extensions()
>>> RejectQ().accepted_extensions()
[<class 'ActuallyAccepted'>]

And the original class is naturally left as is:

>>> class rejectedByUncap(extends.Extend, RejectUnCap):
...     def accepted_extensions(self):
...         return [rejectedByUncap] \
...                + super(rejectedByUncap, self).accepted_extensions()
>>> class QAccepted(extends.Extend, RejectUnCap):
...     def accepted_extensions(self):
...         return [QAccepted] \
...                + super(QAccepted, self).accepted_extensions()
>>> RejectUnCap().accepted_extensions()
[<class 'QAccepted'>]

And of course, the filters stacks have no issue with multiple inheritance:

>>> class RejectUnCap(extends.Base):
...     pass
>>> RejectUnCap.add_filter(
...     lambda cls, candidate: \
...         re.match(r'^[A-Z]', candidate.__name__) is not None)
>>> class RejectQ(extends.Base):
...     pass
>>> RejectQ.add_filter(
...     lambda cls, candidate: \
...         not candidate.__name__.lower().startswith('q'))
>>> class RejectionMachine(RejectUnCap, RejectQ):
...    def accepted_extensions(self):
...        return []
>>> class rejectedCaps(extends.Extend, RejectionMachine):
...     def accepted_extensions(self):
...         return [rejectedCaps] \
...                + super(rejectedCaps, self).accepted_extensions()
>>> class QRejected(extends.Extend, RejectionMachine):
...     def accepted_extensions(self):
...         return [QRejected] \
...                + super(QRejected, self).accepted_extensions()
>>> class ActuallyAccepted(extends.Extend, RejectionMachine):
...     def accepted_extensions(self):
...         return [ActuallyAccepted] \
...                + super(ActuallyAccepted, self).accepted_extensions()
>>> RejectionMachine().accepted_extensions()
[<class 'ActuallyAccepted'>]

Limitations of Extends

Extending Base

Extends does not allow for the extension of the extends.Base class: not only would that generate some trouble performance-wise, but more importantly it has too high a chance of wreaking havoc on two different libraries using Extends in the same project.

[1]In most situations in Python a falsy value (empty list, None, 0, ...) suffices, but this API specifically requires False to ignore an extender.