Niklas R
Niklas R

Reputation: 16870

Searching for a better design (inheritance)

I have a class Block which functions as a base class. One of its subclasses is the TemplateBlock.

class Block(object):

    def render(self, dest):
        # ...
        pass

class TemplateBlock(Block):

    def render(self, dest):
        # ...
        pass

The sub-package blocks.ext.django (while blocks is my top-level module) is intended to provide the same classes as the original module, but with improved functionality (eg. additional methods).

# blocks.ext.django

import blocks
import django.http

class Block(blocks.Block):

    def render_to_response(self):
        # ...
        result = self.render(dest)
        return django.http.HtppRequest(result)

But how should I make the render_to_response method available for the blocks.ext.django equivalent of the TemplateBlock class? The following does actually not look like a good design to me:

# blocks.ext.django

# ...

class TemplateBlock(blocks.TemplateBlock, Block):

    pass

Can you think of a better design to achieve this?


Note: I didn't want to abstract the question completely, that is why I kept the original name. It doesn't matter if it has something to with django or not.

Upvotes: 0

Views: 109

Answers (2)

Niklas R
Niklas R

Reputation: 16870

I am very satisfied this time. :) I think the idea with the ExtensionManager isn't bad, so I enhanced the overall design.

Extensions can now be added to an extension manager like this:

class Test(object):

    ext = ExtensionManager()

    def __init__(self, v):
        self.v = v

class Extensions():
    __metaclass__ = ExtensionClassMeta
    managers = [Test.ext]

    @Extension('method')
    def print_v(self):
        print self.v

Test('Value of Test instance').ext.print_v()
print Extensions

Which prints the following:

C:\Users\niklas\Desktop\blog>python -m blocks.utils.ext_manager
Value of Test instance
(<__main__.ExtensionManager object at 0x021725B0>,)

The extension manager set up is fully customizeable. For example, you can create your own LookupManager instance that will be used by the ExtensionManager to lookup and wrap extensions.

class CoolLookupManager(LookupManager):

    extension_types = ('ice',)

    def wrap_ice(self, name, object, instance, owner):
        return "%s is cool as ice." % object(instance)

class Test(object):

    ext = ExtensionManager(lookup_manager=CoolLookupManager())

    def __init__(self, v):
        self.v = v

class Extensions():
    __metaclass__ = ExtensionClassMeta
    managers = [Test.ext]

    @Extension('ice')
    def get_v(self):
        return self.v

print Test('StackOverflow').ext.get_v

Resulting in the following output:

C:\Users\niklas\Desktop\blog>python -m blocks.utils.ext_manager
StackOverflow is cool as ice.

I'm thinking about putting this into a separate module and publish it to PyPi. So far, this is the code:

# coding: UTF-8
# file:   blocks/utils/ext_manager.py
#
# Copyright (C) 2012, Niklas Rosenstein
""" blocks.utils.ext_manager - Class extension-manager. """

import functools

class ExtensionTypeError(Exception):
    """ Raised when an extension type is not supported. """

class Extension(object):
    """ This decorator is used to mark an attribute on an extension class
        as being actually an extension. """

    def __init__(self, type):
        super(Extension, self).__init__()
        self.type = type
        self.object = None

    def __str__(self):
        return '<Extension: %s>' % self.type

    def __call__(self, object):
        self.object = object
        return self

class ExtensionClassMeta(type):
    """ This meta-class processes an extension class and adds the defined
        extensions into the `ExtensionManager` objects defined in the
        extension class. """

    def __new__(self, name, bases, dict):
        # Ensure there is no base.
        if bases:
            raise ValueError('the ExtensionClassMeta meta-class does not accept bases.')

        # Obtain a list of the managers that need to be extended.
        managers = dict.pop('managers', None)
        if not managers:
            raise ValueError('at least one manager must be given in the class.')

        # A single ExtensionManager instance of the `managers` attribute is
        # allowed, so convert it to a list to ensure that the next test
        # will not fail.
        if isinstance(managers, ExtensionManager):
            managers = [managers]

        # Make sure the managers is a list.
        if not isinstance(managers, (list, tuple)):
            raise ValueError('managers names must be list or tuple.')

        # Iterate over all managers to ensure they're all ExtensionManager
        # instances.
        for manager in managers:
            if not isinstance(manager, ExtensionManager):
                raise ValueError('object in managers not instance of ExtensionManager class.')

        # Iterate over all attributes of the class and extend the managers.
        for name, value in dict.iteritems():
            # Only `Extension` instances will be registered to the extension
            # managers. Other values are just ignored.
            if isinstance(value, Extension):
                for manager in managers:
                    manager.register_extension(name, value.object, value.type)

        return tuple(managers)

class ExtensionManager(object):
    """ This class is used as a property to dynamically add methods and
        data-fields (also called extensions in this context) to a class.

        Any attribute  that will be gathered from this object will be wrapped
        according to the type of extension (see `register_extension()`). """

    def __init__(self, lookup_manager=None):
        super(ExtensionManager, self).__init__()
        self._extensions = {}

        if not lookup_manager:
            lookup_manager = StandartLookupManager()
        self.lookup_manager = lookup_manager

    def __get__(self, instance, owner):
        if not instance:
            return self
        else:
            return ExtensionToAttributeConnector(self, instance, owner)

    def __set__(self, instance, value):
        raise AttributeError("can't overwrite ExtensionManager property.")

    def __delete__(self, instance):
        raise AttributeError("can't delete ExtensionManager property.")

    def register_extension(self, name, object, type='method'):
        """ Register an extension to the manager. The type of *object* depends
            on the value of *type*. The extensions name must be passed with
            *name*. It is associated with *object* and used on attribute
            lookup. If the type is not valid, the lookup manager will
            raise an *ExtensionTypeError*.
            """
        self.lookup_manager.validate_type(type)
        self._extensions[name] = [object, type]

    def do_lookup(self, name, instance, owner):
        """ Forward the extension lookup to the lookup manager to obtain the
            value of an extension. """
        return self.lookup_manager.do_lookup(self._extensions, name, instance, owner)

class LookupManager(object):
    """ This is the base-class for lookup managers. A lookup manager is created
        by an `ExtensionManager` instance when watching out for a specific
        attribute on an instance.

        The `ExtensionManager` will ask the `LookupManager` to validate the
        type of an extension. The lookup manager itself will call functions
        depending on the type of an extension.

        If you have a lookup manager which supports the type `'FOO'`,
        and an extension of that type is requested, it will call the
        function `wrap_FOO()`. Such a method has the following signature:

            * `self`: The `LookupManager` instance.
            * `ext_name`: A string defining the name of the extension that
                          is looked up.
            * `instance`: The invoking instance, as passed by `__get__`.
            * `owner`: The invoking class, as passed by `__get__`.

        The `wrap_FOO()` function must wrap and return *object* so it can
        be used by the requestor.

        The types of extensions the lookup manager supports is defined in
        the `extension_types` attribute which *must* be an iterable of string.
        """

    extension_types = ()

    def do_lookup(self, extensions, name, instance, owner):
        """ Perform a lookup on the passed *extensions* and call the
            corresponding `wrap_FOO()` method. *extensions* should be a
            dictionary containing `(object, type)` pairs only where *object*
            is the registered extension and *type* is its type.

            *connector* is an instance of `ExtensionToAttributeConnector`. """

        object = extensions.get(name, None)
        if not object:
            raise AttributeError('no extension named %s.' % name)

        object, type = object
        lookup_name = 'wrap_%s' % type
        processor = getattr(self, lookup_name, None)
        if not processor:
            raise RuntimeError('no processor %s found in lookup manager.' % lookup_name)

        return processor(name, object, instance, owner)

    def validate_type(self, type):
        """ Validate the passed *type* by raising *ExtensionTypeError* if
            it is not supported. The default implementation checks if the
            passed type is defined in the `extension_types` field. """
        if not type in self.extension_types:
            raise ExtensionTypeError('Invalid type %s passed.' % type)

class StandartLookupManager(LookupManager):
    """ This is the standart lookup manager implementing the `'method'`,
        `'property'` and `'attr'` extension types. """

    extension_types = ('method', 'property', 'attr')

    def wrap_method(self, name, object, instance, owner):
        func = lambda *args, **kwargs: object(instance, *args, **kwargs)
        func = functools.wraps(object)(func)
        func.func_name = name
        func.__name__ = name
        return func

    def wrap_property(self, name, object, instance, owner):
        return object(instance)

    def wrap_attr(self, name, object, instance, owner):
        return object

class ExtensionToAttributeConnector(object):
    """ This class is the direct communication layer between the extensions
        and the user of the `ExtensionManager`. It is returned when the
        `ExtensionManager` is requested on an instance, so an attribute-lookup
        on an instance of this class will result in an extension-lookup. """

    def __init__(self, manager, instance, caller):
        super(ExtensionToAttributeConnector, self).__init__()
        self.manager = manager
        self.instance = instance
        self.caller = caller

    def __getattr__(self, name):
        return self.manager.do_lookup(name, self.instance, self.caller)

Upvotes: 1

Niklas R
Niklas R

Reputation: 16870

This is what I was able to come up with now. Thanks @JakobBowyer who pointed me into the direction of object composition. I am not completely satisfied with this solution, but it works as expected.

I have created a class implementing Pythons' descriptor interface that is used to manage extensions to a class. Extensions can be either a method, a class-method or a property.

I've put this class into blocks.utils.ext_manager and used it in the main Block class like the following:

from blocks.utils.ext_manager import ExtensionManager

class Block(object):

    ext = ExtensionManager()

    # ...

The extension can now be registered like this in blocks.ext.django:
(I'm still looking for a way to make it look a little more nice...)

def Block_render_response(self):
    # ...

blocks.Block.ext.register_extension('render_response', Block_render_response, 'method')

By implementing the descriptor interface, the ExtensionManager is able to obtain the instance that is referencing the ext attribute from the calling instance and pass it to the method registered to the extension manager. See the following example invocation:

from blocks.ext.django import TemplateView

def index(request):
    block = TemplateView(template_name='foo.html')
    return block.ext.render_response()

Drawback: I have not yet implemented, that a method registered as 'classmethod' can be called from the class, because referencing the ext attribute from the class will return the ExtensionManager itself which does not implement obtaining registered extensions via __getattr__.

You can find the source-code of the ExtensionManager class blow.

# coding: UTF-8
# file:   blocks/utils/ext_manager.py
""" blocks.utils.ext_manager - Class extension-manager. """

import functools

class ExtensionManager(object):
    """ This class is used as a property to dynamically add methods and
        data-fields (also called extensions in this context) to a class.

        Any attribute  that will be gathered from this object will be wrapped
        according to the type of extension (see `register_extension()`). """

    def __init__(self):
        super(ExtensionManager, self).__init__()
        self._extensions = {}

    def __get__(self, instance, owner):
        if not instance:
            return self
        else:
            return ExtensionLookup(self._extensions, instance, owner)

    def __set__(self, instance, value):
        raise AttributeError("can't overwrite ExtensionManager property.")

    def register_extension(self, name, object, type='method'):
        """ Register an extension to the manager. The type of *object* depends
            on the value of *type*. The extensions name must be passed with
            *name*. It is associated with *object* and used on attribute
            lookup.

            * `type == 'method'`:

                *object* is assumed to be callable and is passed the calling
                instance of the host-class plus the arguments passed on
                method invocation.

            * `type == 'classmethod`:

                *object* is assumed to be callable and is passed the host-class
                plus the arguments passed on invocation.

            * `type == 'property'`:

                *object* can be of anytype and is returned as is.
            """

        self._extensions[name] = [object, type]

class ExtensionLookup(object):
    """ This is a private class used by the `ExtensionManager` class to
        wrap the registered together with an instance.

        Attribute lookup will be redirected to the registered extensions. """

    def __init__(self, extensions, instance, owner):
        super(ExtensionLookup, self).__init__()
        self.extensions = extensions
        self.instance = instance
        self.owner = owner

    def __getattr__(self, name):
        object, type = self.extensions[name]
        if type == 'method':
            func = lambda *args, **kwargs: object(self.instance, *args, **kwargs)
        elif type == 'staticmethod':
            func = lambda *args, **kwargs: object(self.owner, *args, **kwargs)
        elif type == 'property':
            return object
        else:
            raise RuntimeError('invalid extension-type found.')

        func = functools.wraps(object)(func)
        return func

Disclaimer: By showing the code above to the public, I permit the duplication and modification of the source, as well as publications of such.

Upvotes: 0

Related Questions