decoratorinjection – DSL for decorator injection

Presentation

This is an internal DSL providing fluent interface to inject decoration, i.e. decorate methods at runtime.

Method names can be specified directly or using a method grabber, which introspect the class.

Examples

Given the class C, public methods can be decorated with the “log” decorator by:

decorate(PUBLIC_METHODS).of(C).using(log)

which is equivalent to:

C.foo = log(C.foo)
...
C.baz = log(C.baz)

or more usually:

class C(object):
    ...

    @log
    def foo(self, ...):
        ...
    
    ...

    @log
    def baz(self, ...):
        ...

Methods begining with get_ in classes A and B are decorated with “foo” and “bar”:

decorate(method_named("get_.*")).of(A, B).using(foo, bar)

equivalent to:

A.get_spam = bar(foo(A.get_spam))
...
A.get_egg = bar(foo(A.get_egg))
B.get_spamegg = bar(foo(B.get_spamegg))
...

Finally one can set exceptions in grabbers, such as in:

decorate(method_named("get_.*")).but_not('get_something').of(A).using(foo)

Warning

When using a selector based on the method name, the decorators must use functools.wraps() or a second decoration will fail, the effective name of the method being replaced by the first decorator’s one.

Usage examples

Aspect OP

This module can be used as a simple “aspect oriented” programming framework. Indeed, it is easy to decorate methods, i.e. add aspect to these methods, directly at runtime.

Suppose you want to trace the execution of public methods of DAO classes in some module. You first declare a decorator to do this:

def tracer(meth):
    @functools.wraps(meth)
    def wrapped(*args, **kargs):
        log("enter %r with args %r %r" % (
                meth.__name__,
                args, kargs))
        result = meth(*args, **kargs)
        log("exit %r with result %r" % (
                meth.__name__,
                result))
        return result
    return wrapped

and then decorate the methods with it:

decorate(PUBLIC_METHODS).of(classes_named("DAO.*").within(somemodule)).using(tracer)

The DSL

The DSL is made of some predefined grabbers, factory functions and objects that create and configure the needed framework objects, then execute them (i.e. a facade pattern).

The API of this DSL is a fluent interface

Sentence elements

class decoratorinjection.decorate(grabber)

Start a fluent interface “sentence”. Take a method specification as argument (i.e. a MethodsGrabber, one of the predefined or as returned by method_named(), or a user defined one).

but_not(*exceptions)
Add exceptions to the method specified in decorate, in the form of regexp strings. To be used before of().
of(*classes)
Define the list of classes to search method in. Can be classes (the object, not the name) or a ClassGrabber object.
allowing_multiple_decoration

Force multiple decoration by the same decorator.

Note

When a class and one of it superclass are decorated, inherited methods in the class are inspected twice.

To avoid decorating several times the same method with the same decorator, the decoration process register the decorator in a decorated attribute of the method.

However, multiple decoration can be forced by setting the MethodDecorator.force_decoration attribute to True, or using the decorate.allowing_multiple_decoration DSL property.

using(*decorators)
Set the decorators to be used. The last element of the sentence.

Grabbers

Grabbers (or selectors) are elements that select some objects (method, class) based on their name or others criteria (method type, super class, etc.)

Several grabber are provided to ease the writing of sentences. Moreover, functions are here to create custom grabbers easily and “fluently”. However, it is easy to create your own grabbers.

Grabbers Combination

Grabbers of the same kind can be combined using binary boolean operators & and |. For example:

PRIVATE_METHODS & CLASS_METHODS

selects private class methods and:

method_named("spam.*") | PROTECTED_METHODS

selects protected methods or methods which name begin with “spam”.

Method grabbers

decoratorinjection.method_named(*matches)

Contruct a MethodGrabber based on names or regexp.

The constructed grabber selects a method if the name matches one of the supplied regexp, and can be used as a decorate argument.

Predefined grabbers are also defined to be used as decorate argument.

decoratorinjection.ALL_METHODS
Select all methods of the class
decoratorinjection.PUBLIC_METHODS
Select public methods of the class, i.e. not begining with _
decoratorinjection.PRIVATE_METHODS
Select private methods of the class, i.e. begining with __ and not ending with __
decoratorinjection.PROTECTED_METHODS
Select protected methods of the class, i.e. begining with just one _
decoratorinjection.SPECIAL_METHODS
Select special methods of the class, i.e. begining and ending with __
decoratorinjection.STATIC_METHODS
Select static methods of the class
decoratorinjection.CLASS_METHODS
Select class methods of the class
decoratorinjection.NORMAL_METHODS
Select “normal” methods of the class

Class grabbers

Class grabbers, as method grabbers, can be used to select the classes to decorate methods from, as arguments to of()

Two factory functions returns a ClassGrabberDSL, an iterator that is used to construct the grabber. This class should not be used directly, but through factories or predefined constants, except to create user-defined class grabbers.

decoratorinjection.classes_named(*matches)
Select classes based on their name.
decoratorinjection.subclasses_of(cls)
Select classes based on their superclass.
decoratorinjection.ALL_CLASSES
Select all classes

ClassGrabberDSL provides several methods to grab classes (fluent interface).

ClassGrabberDSL.within(source)

Define the source to grabb classes from. Can be a module or a scope (dict) such as returned by globals().

Note

To respect the fluency of the interface, the classes from the DSL are usually imported using from ... import, and are therefore in the local scope. However, in most cases, they are not supposed to be decorated themselves.

Classes pertaining to this module are therefore automatically pruned from the scope given to the class grabber (e.g. by globals()) when it is built by the DSL.

If you want to include them anyway, combine the grabber with another one specifying explicitly the module. For example:

classes_named('Test*').within(globals()) | ALL_CLASSES.within(decoratorinjection)
ClassGrabberDSL.recursively
Search recursively in modules and submodules.

The framework

Base framework

Object in the framework can be used directly to decorate object, instead of using the DSL.

Moreover, the MethodGrabber and ClassGrabber object can be used to define new grabbers by instanciating them. Some utility function are defined in the module to ease the creation of custom grabbers.

Classes

class decoratorinjection.MethodDecorator(grabber, classes=[], decorators=[], force_decoration=False)

Object to decorate the class. Usually built by the DSL. It uses the given grabber on every given classe to decorate the obtained method with every given decorator.

force_decoration
Tels if we allow to apply the same decorator several times to the same method
add_class(cls)
Add a class to decorate.
add_decorator(deco)
Add a decorator to decorate methods with.
set_grabber(grabber)
Set the method grabber.
add_exception(ex)
Add a exception to the grabber (delegate to the grabber).
decorate()
Decorate every matching methods of the classes with every decorator.
class decoratorinjection.ClassGrabber(matchers, exceptions=[])

Grabber to collect classes to be decorated from a module

grab_module(module, recurse=False)
Grabb classes from the given module. Search submodules if recurse is True.
grab_scope(elements, recurse=False)
Grabb classes from the given scope (a dict, such as returned by globals(). Also search in modules if recurse is True.
grab(source, recurse=False)
Grabb from the given source, a scope or a module. Return an iterator on class objects.
class decoratorinjection.MethodsGrabber(matchers, exceptions=[])

Grabber to collect methods to be docorated from a class.

grab(cls)
Grabb methods from the given class. Return an iterator on method names.

Selectors

These selectors can be used to define custom grabbers. Selectors take two arguments: the name and the object itself, and return a boolean. Some of the function defined here are actually factory functions returning a selector.

decoratorinjection.isboundedto(obj)
Return a matcher returning True if the given method is bounded to an instance of obj (can be used to filter class methods).
decoratorinjection.isfunction(name, obj)
Return True if obj is a function.
decoratorinjection.issubclassof(supercls)
Return a matcher returning True if the argument is a subclass of supercls.
decoratorinjection.isstrictsubclassof(supercls)
Return a matcher returning True if the argument is a subclass of supercls but not the superclass itself.

Extending

class decoratorinjection.GenericGrabber(matchers, exceptions=[])

Object to collect objects matching some criteria

  • matchers are string, regexps or boolean functions that select the

    object, taking the name and the object (list is OR);

  • exceptions are matchers that exclude some objects from the list.

To evaluate the grabber on a single object (i.e. test if it matches or not), use the match() method or call the grabber (instances are callable)

add_matcher(matcher)
Add a matcher to the grabber. matcher can be a regexp (compiled or string) or any callable taking the object name and the object itself as returned by inspect.getmembers and returning a boolean.
add_exception(matcher)
Add an exception to the grabber. matcher can be a regexp (compiled or string) or any callable taking the object name and the object itself and returning a boolean.
grab(container)
Generator on matching object names. MUST be overloaded.
match
Apply the matcher

Combining Grabbers

When combining grabbers, the matchers and exceptions of the two grabbers are combined using the following functions. They can be used to create complex selectors (boolean function) to pass to the grabber when creating it.

decoratorinjection.negate(selector)
Return a new selector matching the negation of the given selector.
decoratorinjection.and_combined(selectors)
Return a new selector function as the AND reduction of the given selectors.
decoratorinjection.or_combined(selectors)
Return a new selector function as the OR reduction of the given selectors.
decoratorinjection.true(*x)
Always match
decoratorinjection.false(*x)
Never match
decoratorinjection.make_re_matcher(regexp)
Convert a regexp into a selector suitable for a Grabber object.

Install

To install this module, just copy the downloaded file in your PYTHONPATH.

All comments are welcome.

License

This project is licensed under the Apache License, Version 2.0