Aspect oriented programming tools — Pytilities v1.0.0 documentation

### Navigation

- index
- modules |
- previous |
- Pytilities v1.0.0 documentation »
- Programming guide »

# Aspect oriented programming tools

This guide starts with an introduction to give you a general idea of AOP (for a decent introduction to AOP I suggest (e)books, google, ...). It then proceeds with a top down approach running you through: using aspects, making aspects and eventually writing advice for your aspects.

## Introduction

The aim of aspect oriented programming (AOP) is to allow better separation of concerns. It allows you to centralise code spread out over multiple classes into a single class, called an aspect, uncluttering code.

Aspects consist of pieces of advice which are given to methods of a class. Advice are functions/methods that wrap around a method/descriptor, allowing them to change its behaviour.

With this AOP library you can give advice to methods, and any other kind of descriptor (e.g. properties). You can even ‘advise’ unexisting attributes on a class, effectively adding new attributes to the class.

## Using aspects

Advice is given by applying aspects to classes or instances, for example:

    # Making a Vector instance that sends out change events

    from pytilities.geometry import Vector, verbose_vector_aspect

    Vector vector = Vector()  # a Vector is an (x, y) coordinate

    # this adds functionality to just this vector instance so that you can listen for
    # change events on it
    verbose_vector_aspect.apply(vector)

    def on_changed(old_xy_tuple):
        pass

    vector.add_handler('changed', on_changed)  # this now works

The aspect’s apply() method accepts either a class or an instance. If given a class, it is applied to all instances of that class, ‘static’ methods of the class, ... (this is called class advice). If given an instance, it is applied to just that instance (this is refered to as instance advice). Note that built-in/extension types cannot have aspects or any type of advice applied to them.

Multiple aspects can be applied to the same instance/class, but you cannot apply the same advice to the same object more than once. Note that when an aspect is applied to an instance, it takes precedence over aspects applied to the instance’s class. Apart from that non-intuitive bit, aspect advice wraps previously given aspect advice.

Aspects can be unapplied to objects with the unapply() method, this can be done in any order. The unapplied aspect is then removed from the ‘chain’ of aspects applied to the object.

## Writing new aspects

You write new aspects by extending the Aspect class (you don’t have to, but you’d have to use advisor in pytilities.aop directly). Here’s an example of a basic aspect:

    from pytilities import aop
    from pytilities.aop import Aspect

    class SpamAspect(Aspect):

        def __init__(self):
            Aspect.__init__(self)

            # map advice to attribute names of the objects it will be applied to
            self._advise('eat_spam', call=self.spam)  # in this case apply spam advice to calls to some_object.eat_spam

        # some advice that prints spam and then calls the original method
        def spam(self):
            print('Spam')
            yield aop.proceed

    # singleton code (you could parameterise your aspect of course, e.g. like the DelegationAspect)
    verbose_vector_aspect = VerboseVectorAspect()
    del VerboseVectorAspect

The _advise() method inherited from Aspect, is used to map advice onto members/attributes. (the member names refer to those of the objects that to which the aspect may be applied to)

Note that the members should contain either a descriptor (function, property, ...) or contain nothing at all (i.e. the attribute does not exist).

Attributes can have advice applied to them by get, set, del or call access:

- get: whenever you __get__ an attribute: e.g. obj.x
- set: whenever you __set__ an attribute: e.g. obj.x = 1
- del: whenever you __del__ an attribute: e.g. del obj.x
- call: whenever you __call__ an attribute: e.g. obj.x()

The members parameter not only accepts regular attribute names, but also accepts the special ‘*’ attribute. This applies the advice to all attributes on the class (even to missing attributes). Note that ‘*’ advice can only be given to classes/instances that have AOPMeta as its metaclass.

No advice, not even ‘*’ advice, can advice private/protected members. Members are considered private/protected if their name begins with a single ‘_’, but aren’t special. Though you can advise special attributes, you cannot advise the attributes listed in in pytilities.aop.advisor.unadvisables

Note you can also advice non-existing attributes, an example:

    from pytilities import aop
    from pytilities.aop import Aspect

    class MagicAspect(Aspect):

        '''Advise a non-existing attribute to return 3'''

        def __init__(self):
            Aspect.__init__(self)
            self._advise('x', get = self.advice)

        def advice(self):
            yield return_close(3)

    magic_aspect = MagicAspect()

    class SomeClass(object): pass
    someClass = SomeClass()

    # print(someClass.x) would fail
    magic_aspect.apply(someClass)
    print(someClass.x) # now prints 3

## Writing advice for your aspects

Advice is a generator that yields aop commands (the commands are discussed below). When some form of access is done on a particular attribute of a class (this is defined by your self._advise calls your aspect), the advice is called before that attribute is accessed. The advice function can yield commands to return straight away, manipulate the return value, manipulate the args to pass to the advised member, proceed with accessing the member in the ‘normal’ way.

The following sections introduce each of the commands by example (they are located in pytilities.aop.commands, but can also be imported from pytilities.aop). For brevity, the following examples omit the Aspect class’ declaration.

### The proceed command

Yielding proceed from your advice proceeds with accessing the advised member:

    # the object whose increase() call accesses will be advised
    counter.reset() # reset counter to 0
    counter.increase(1) # increase counter by 1 and return the new value (in this case, 1)

    # this advice proceeds with attribute access,
    # and then prints the return value
    def advice():
        return_value = yield aop.proceed
        print(return_value)

    # with advice2 applied to counter
    counter.reset()
    counter.increase(1)  # prints 1, then returns 1

    # this advice will proceed twice
    def advice2():
        yield aop.proceed
        yield aop.proceed

    # with advice2 applied to counter
    counter.reset()
    counter.increase(1)  # returns 2. increase(1) is called twice by the double proceed, only the last return value is returned

Using yield proceed() you can change the args of the underlying call:

    # using the same counter from the example above
    counter.reset()
    counter.increase(1)  # returns 1
    counter.increase(5)  # returns 6

    def advice():
        yield aop.proceed(2)  # change the argument to 2

    # after applying the advice
    counter.reset()
    counter.increase(1)  # returns 2
    counter.increase(5)  # returns 4

proceed also supports keyword arguments (see the api reference).

### The return_ command

Yielding return_ from your advice returns the last:

    # using the same counter from the example above

    # don't call the original increase() and return 1
    def advice():
        yield aop.return_(1)
        print('this statement is never reached')

    # after applying the advice
    counter.reset()
    counter.increase(1)  # returns 1
    counter.increase(5)  # returns 1

    # proceed, then return 3
    def advice2():
        yield aop.proceed
        yield aop.return_(3)

    # after applying the advice
    counter.reset()
    counter.increase(1)  # returns 3

Upon yielding return_, the generator is closed and the value is returned. When the generator ends naturally (by never yielding return_), an implicit return_ is assumed.

### Various other commands

The rest of the commands available to you:

    # using that same counter from the examples above

    def advice():
        print(yield aop.arguments)  # arguments returns (args, kwargs)
        print(yield aop.name)  # name of the advised member (the same advice can be applied to multiple members)
        print(yield aop.obj)  # the object that carries the advised member
        print(yield aop.advised)  # the value of the advised member

    # after applying the advice
    counter.reset()
    counter.increase(2) # prints: ((2,), {})
                        # then prints: increase
                        # then prints: the counter object
                        # then prints: the increase function descriptor object

## Views

Sometimes you want to apply an aspect only when accesed through a special wrapper; pytilities refers to this as a View and provides pytilities.aop.aspects.create_view() to aid you in making views.

We’ll explain views using an example. You may have a getSize method that returns an x,y coordinate. In your class you store this coordinate in a mutable Vector instance. You don’t want users to be able to manipulate the size using the return of that method. You want to provide an immutable view to your size Vector.

Here’s how you could do this with pytilities:

    from pytilities.aop.aspects import ImmutableAspect, create_view
    from pytilities.geometry import Vector

    # your size vector
    size = new Vector()

    # make an aspect that makes the x and y attributes immutable
    immutable_vector_aspect = ImmutableAspect(('x', 'y'))

    # create a view that only enables the given aspect when
    # you access the object it wraps through the wrapper
    ImmutableVector = create_view(immutable_vector_aspect)

    # make an ImmutableVector view instance of the size vector
    immutableSize = ImmutableVector(size)

    size.x = 1  # this still works
    immutableSize.x = 5  # this will throw an exception

Note that the ImmutableVector of the above example is included in pytilities.geometry.

## More examples

For more examples: See the unit tests in pytilities.test.aop

### Navigation

- index
- modules |
- previous |
- Pytilities v1.0.0 documentation »
- Programming guide »