1.1.10 Big Example 2 -- Extending the Framework for Context
Description: Big Example 2 -- Extending the Framework for Context
Keywords: ref

##
1.1.10 Big Example 2 -- Extending the Framework for Context

Now it's time for our second ``big'' example. This time, we're going to add an extension to the `protocols` framework to support ``contextual adaptation''. The tools we've covered so far are probably adequate to support 80-90% of situations requiring adaptation. But, they are essentially global in nature: only one adapter path is allowed between any two points. What if we need to define a different adaptation in a specific context?

For example, let's take the documentation framework we began designing in section 1.1.1. Suppose we'd like, for the duration of a single documentation run, to replace the factory that adapts from `FunctionType` to `IDocumentable`? For example, we might like to do this so that functions used by our ``finite state machine'' objects as ``transitions'' are documented differently than regular functions.

Using only the tools described so far, we can't do this if `IDocumentable` is a single object. The framework that registered the `FunctionAsDocumentable` adapter effectively ensured that we cannot replace that adapter with another, since it is already the shortest adapter path. What can we do?

In section 1.1.7, we discussed how we could create ``subset'' protocols and ``inherit'' adapter declarations from existing protocols. In this way, we could create a new subset protocol of `IDocumentable`, and then register our context-specific adapters with that subset. These subset protocols are just as fast as the original protocols in looking up adapters, so there's no performance penalty.

But who creates the subset protocol? The client or the framework? And how do we get the framework to use our subset instead of its built-in `IDocumentable` protocol?

To answer these questions, we will create an extension to the `protocols` framework that makes it easy for frameworks to manage ``contextual'' or ``local'' protocols. Then, framework creators will have a straightforward way to support context-specific adapter overrides.

As before, we'll start by envisioning our ideal situation. Let's assume that our documentation tools are object-based. That is, we instantiate a ``documentation set'' or ``documentation run'' object in order to generate documentation. How do we want to register adapters? Well, we could have the framework add a bunch of methods to do this, but it seems more straightforward to simply supply the interfaces as attributes of the ``documentation set'' or ``documentation run'' object, e.g.:

    from theDocTool import DocSet
    from myAdapters import specialFunctionAdapter
    from types import FunctionType
    import protocols

    myDocs = DocSet()

    protocols.registerAdapter(
        specialFunctionAdapter,
        provides = [myDocs.IDocumentable],
        forTypes = [FunctionType]
    )

    myDocs.run()

So, instead of importing the interface, we access it as an attribute of some relevant ``context'' object, and declare adapters for it. Anything we don't declare a ``local'' adapter for, will use the adapters declared for the underlying ``global'' protocol.

Naturally, the framework author could implement this by writing code in the `DocSet` class' `__init__` method, to create the new ``local'' protocol and register it as a subset of the ``global'' `IDocumentable` interface. But that would be time-consuming and error prone, and therefore discourage the use of such ``local'' protocols.

Again, let's consider what our ideal situation would be. The author of the `DocSet` class should be able to do something like:

    class DocSet:

        from doctool.interfaces import IDocumentable, ISignature

        IDocumentable = subsetPerInstance(IDocumentable)
        ISignature = subsetPerInstance(ISignature)

        # ... etc.

Our hypothetical `subsetPerInstance` class would be a descriptor that did all the work needed to provide a ``localized'' version of each interface for each instance of `DocSet`. Code in the `DocSet` class would always refer to `self.IDocumentable` or `self.ISignature`, rather than using the ``global'' versions of the interfaces. Thus, we can now register adapters that are unique to a specific `DocSet`, but still use any globally declared adapters as defaults.

Okay, so that's our hypothetical ideal. How do we implement it? I personally like to try writing the ideal thing, to find out what other pieces are needed. So let's start with writing the `subsetPerInstance` descriptor, since that's really the only piece we know we need so far.

    from protocols import Protocol, declareAdapterForProtocol, NO_ADAPTER_NEEDED

    class subsetPerInstance(object):

        def __init__(self,protocol,name=None):

            self.protocol = protocol
            self.name = name or getattr(protocol,'__name__',None)

            if not self.name:
                raise TypeError("Descriptor needs a name for", protocol)

        def __get__(self,ob,typ=None):

            if ob is None:
                return self

            name = self.name
            if getattr(type(ob),name) is not self or name in ob.__dict__:
                raise TypeError(
                    "Descriptor is under more than one name or the wrong name",
                    self, name, type(ob)
                )

            local = Protocol()
            declareAdapterForProtocol(local,NO_ADAPTER_NEEDED,self.protocol)

            # save it in the instance's dictionary so we won't be called again
            ob.__dict__[name] = local
            return local

        def __repr__(self):
            return "subsetPerInstance(%r)" % self.protocol

Whew. Most of the complexity above comes from the need for the descriptor to know its ``name'' in the containing class. As written, it will guess its name to be the name of the wrapped interface, if available. It can also detect some potential aliasing/renaming issues that could occur. The actual work of the descriptor occurs in just two lines, buried deep in the middle of the `__get__` method.

As written, it's a handy enough tool. We could leave things where they are right now and still get the job done. But that would hardly be an example of extending the framework, since we didn't even subclass anything!

So let's add another feature. As it sits, our descriptor should work with both old and new-style classes, automatically generating one subset protocol for each instance of its containing class. But, the subset protocol doesn't know it's a subset protocol, or of what context. If we were to print `DocSet().IDocumentable`, we'd just get something like `<protocols.interfaces.Protocol instance at 0x00ABA220>`.

Here's what we'd like it to do instead. We'd like it to say something like `LocalProtocol(<class 'IDocumentable'>, <DocSet instance at 0x00AD9FB0>)`. That is, we want the local protocol to:

- ``know'' it's a local protocol
- know what protocol it's a local subset of
- know what ``context'' object it's a local protocol for

What does this do for us? Aside from debugging, it gives us a chance to find related interfaces, or access methods or data available from the context.

So, let's create a `LocalProtocol` class:

    class LocalProtocol(Protocol):

        def __init__(self, baseProtocol, context):

            self.baseProtocol = baseProtocol
            self.context = context

            # Note: Protocol is a ``classic'' class, so we don't use super()
            Protocol.__init__(self)

            declareAdapterForProtocol(self,NO_ADAPTER_NEEDED,baseProtocol)

        def __repr__(self):
            return "LocalProtocol(%r,%r)" % (self.baseProtocol, self.context)

And now, we can replace these two lines in our earlier `__get__` method:

            local = Protocol()
            declareAdapterForProtocol(local,NO_ADAPTER_NEEDED,self.protocol)

with this one:

            local = LocalProtocol(self.protocol, ob)

Thus, the new local protocol will know its context is the instance it was retrieved from.

Of course, to make this new extension really robust, we would need to add some more documentation. For example, it might be good to add an `ILocalProtocol` interface that documents what local protocols do. Context-sensitive adapters would then be able to verify whether they are working with a local protocol or a global one. Framework developers would also want to document what local interfaces are provided by their frameworks' objects, and authors of context-sensitive adapters need to document what interface they expect their local protocols' `context` attribute to supply! Also, see below for a web site with some interesting papers on patterns for using localized adaptation of this kind.

Note: In practice, the idea of having local protocols turned out to be useful enough that as of version 0.9.1, our `LocalProtocol` example class was added to the protocols package as `protocols.Variation`. So, if you want to make use of the idea, you don't need to type in the source or write your own any more.

See Also:

Object Teams
  If you find the idea of context-specific interfaces and adapters interesting, you'll find ``Object Teams'' intriguing as well. In effect, the ideas we've presented here map onto a subset of the ``Object Teams'' concept. Our local interfaces correspond to their ``abstract roles'', our local adapters' instances map to their ``role instances'', and our contexts are their ``team instances''. Adapting an object corresponds to their ``lifting'', and so on. The main concept that's not directly supported by our implementation here is ``callin binding''. (Callin binding is a way of (possibly temporarily) injecting hooks into an adapted object so that the adapter can be informed when the adapted object's methods are called directly by other code.)