[PEAK] DecoratorTools, Aspects, and the Trellis

Phillip J. Eby pje at telecommunity.com
Tue Jul 10 20:57:04 EDT 2007

So at this point I have a rather splendidly-working prototype of the 
new Trellis update algorithm, sitting at around 200 lines of code in 
the svn.eby-sarna.com repository.  However, I don't trust it has 100% 
code coverage, because I wrote it basically from scratch, ran it 
against an older test suite, and then added some tests to exercise 
some of the new functionality.

In the process of doing this, I found that there are lots of ways in 
which you could do things that would foul up the implementation, that 
were not covered by tests.  In other words, I found potential bugs by 
inspection, that needed their own proofs of impossibility.  I've 
added tests for the things I've thought of so far, and fixed the bugs 
as I found them, but I suspect there may be one or two more lurking 
beneath the surface.

So I'm going to do one more draft of the code, but this time using 
TDD, both to be absolutely sure I know how the thing works, and also 
to possibly improve the efficiency a bit, although I expect the 
algorithm in its simplest form to be a straightforward translation to 
C if needed.  I've already done some memory-related optimization, 
such that Trellis' Cell objects consume less memory than their 
PyCells counterparts, and about 1/4th the memory consumed by the 
competing "Cellulose" library.

I expect that even a modestly-sized Trellis-based program, using 
cells to simultaneously manage its GUI, persistence, and network I/O 
will be capable of having thousands or even tens of thousands of 
cells in use at any one time, so both space and speed efficiency are 
important for a production system.

The other big step that remains to be done on the core Trellis API is 
to create the class and function decorators I've sketched here 
previously.  In the course of planning how to implement them, it's 
occurred to me that there are some recurring patterns in how I've 
done these sorts of decorators for Chandler, Contextual, and other 
projects, and that I can and should add code for these patterns to 
the DecoratorTools package, so I don't have to keep recoding them in 
an ad-hoc way.

The PEAK-Rules package contains an "Aspect" implementation that's 
used to dynamically attach additional information to existing 
objects, sort of like you might slap a sticker on the side of an 
object in real life.  The advantage of using an Aspect over poking 
extra attributes onto the object is twofold: 1. you don't have to 
come up with a name that doesn't collide with anything else, and 2. 
you don't have to write code to *add* the aspect to the object.  Just 
invoking the aspect constructor is enough to either return the 
existing aspect, or create one and attach it somewhere.

And it has occurred to me recently that this bit really belongs in 
DecoratorTools rather than PEAK-Rules, because with it, you can write 
method decorators that can effectively add "extra attributes" to the 
class' instances.  Like, oh, say...  a map of attribute names to 
trellis.Cell objects.  :)

The second thing that occurred to me, just today, is that the 
decoration of the *class itself* could be done using aspects -- 
aspects attached to the class, rather than the instance.  For 
example, in the Trellis case, we also need information about the 
rules and default values of the cell attributes.

Unfortunately, regular class decoration as currently implemented in 
DecoratorTools is pretty much limited to sticking attributes on the 
class or doing things to it after it's already created -- which is 
much more limited in what can be done with the class.  For example, 
it's no use trying to change __slots__ after the class is created, 
nor can you change a class' metaclass after creation, unless it was 
created using a metaclass other than 'type'.

Granted, for a lot of uses it's not a terrible tragedy.  In my 
examples, I've been using base classes like trellis.Component to 
indicate that some sort of mixin is required, in order for the rules 
to work.  The trickier bit is ensuring that people don't use 
decorators like trellis.rules(), and expect them to work on a 
non-Component class.

My current solution for things like Contextual's "context.replaces()" 
decorator is to add a class decorator that checks the created class 
for suitability.  This ensures that you'll at least get an error, and 
won't be able to proceed without either getting rid of the decorator, 
or adding the right base class.  It could be nice, however, if you 
could just have the decorator add in the mixin.

On the other hand, I can think of some folks who could complain that 
not including an explicit base or metaclass is likely to make code 
harder to debug or understand, especially if someone's not familiar 
with *all* the decorators in a body of code.

So we can probably live with that for the Trellis.  The 
trellis.Component mixin isn't in fact going to have a lot of stuff 
added to the class anyway; just a default __init__ method to handle 
keyword arguments, and a metaclass __call__ implementation that will 
make sure that any eager cell attributes are activated once __init__ 
has completely finished running.

So, I'm thinking that the idiom I have now of adding class decorators 
to check this, could be simplified a little by adding a class 
decoration function like ``class_requires(message, func, *args)``, 
that raises TypeError(message) unless func(cls, *args) returns 
true.  I could then use that in place of the custom code in 
Contextual, and Trellis needs it too.

The next pattern I find myself repeating is the need to process 
inherited metadata from a class' base classes, either by base order 
or MRO order (or the reverse), in order to figure out the combined 
metadata for the new class.  This usually involves a lot of private 
attributes rudely shoved into the class dictionaries, and a loop that 
peers into the base classes, looking for whether those attributes are there.

It would be a lot nicer to write code like:

     for base_aspect in CellMetadata.by_mro(cls):
         # combine some sort of metadata from the base_aspect object

Of course, there are some non-trivial problems to be solved 
here.  Unlike attributes, aspects can't be inherited (unless I make 
them able to be).

Second, at one of the times when we most want to use these class 
aspects, the class itself (including its bases, mro, etc.) doesn't 
exist yet!  So there has to be a way to access and set up the aspect 
before the class exists, then have it do any needed setup later.

However, the solution to the second problem also points to a way of 
solving the first.  If class aspects can be set up lazily, then we 
don't have to worry what happens with base classes not having an 
aspect, because we can simply create them as we go -- even for 
classes that never had the aspect to start with!

(For example, the 'object' class can't be decorated in any normal 
way, but an aspect can certainly be attached to it afterwards.)

In order to implement this, class aspects will have to be looked up 
in a weakref-keyed dictionary, if they're not already in the class' 
dictionary.  (Because new-style class dictionaries can't be directly 
modified once the class exists.)  But, for pre-creation aspect 
access, one can create the aspect inside the class dictionary before 
the class exists, then add a class decorator to invoke the 
after-creation setup.

So, class aspects could have a method like 'setup_for_class(self, 
cls)', that would get called to allow the aspect to initialize 
itself, using any data it wants to from the class object.  It can 
also request aspects of the class' bases or mro, and those aspects 
can then lazily come into being -- and have *their* setup_for_class() 
methods called, if need be.

So, instead of the pattern I use now, where I have to loop over base 
class info and fill in the metadata gaps for classes without it 
(which can be tricky at times), I can instead just let every base 
have an aspect with appropriate registry attributes, each of which 
knows just how to assemble itself from its nearby bases, or its whole 
mro if need be.

Yep, that sounds about right.  Boy I wish I'd had this idea of 
aspects, and especially class aspects, when I was writing the PEAK 
core a zillion years ago when Python *2.2* was the latest and 
greatest.  I would never have created all those special attributes 
like __objectsToBeAssembled__ and __class_offers__ and so on.  But 
then, if I'd had Contextual and the Trellis back then, I wouldn't 
have needed to write a good chunk of the PEAK core in the first place.  :)

So, once I have aspects moved from PEAK-Rules to DecoratorTools, and 
add these new "class aspect" things, I'll be able to start on the 
Trellis class-level API, so you can declare the rules and default 
values and whatnot.

I'll start on that tomorrow.

More information about the PEAK mailing list