Reputation: 16870
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
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
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