Watchpoints for OpenERP (2/2): a proof of concept

In a previous post, I was describing what I thought would be an interesting development tool : a watch point system for OpenERP.

Since then, I've come to a rough implementation, which I'll describe in a simplifed form for the present note. As expected, it is indeed a basic exercise in metaclasses.

Note

this is an article from my old blog, which I unearthed and reformatted as RST. Since then, OpenERP has been renamed as Odoo, somewhat retroactively.

If that matters, the code on this page is copyright 2011 Anybox and released under the GNU Affero GPL License v3

The metaclass

Metaclasses allow to customize the creation of the class object themselves. The interesting feature here is that they are transversal to inheritance, whereas overriding or monkey-patching the "write" method of "orm.orm" to add a conditional breakpoint would not resist subclassing.

It seems that PEP8 does not say much about metaclasses, but I'll try and use the save conventions as in the examples from the reference documentation:

from inspect import isfunction

class metawatch(type):

    def __new__(cls, name, bases, dct):
        for name, attr in dct.items():
            if isfunction(attr):
                dct[name] = watch_wrap(attr)
        return type.__new__(cls, name, bases, dct)

All it does is to intercept the method definitions (which are just functions at this point) and have a decorator wrap them before the class object creation. The decorator

We don't use the @ notation, but it's still a function that takes a function as a single argument and returns a function.

Information about active watchpoints is expected to be available as a dict on the object, whose keys are method names.

def watch_wrap(fun):
    def w(self, *a, **kw):
        # avoid using getattr
        wps = self.__dict__.get('_watchpoints', dict())
        if not fun.__name__ in wps:
            return fun(self, *a, **kw)

        # support ids only for now
        interesting = wps[fun.__name__]['ids'] # a set
        try:
           ids = a[2] # there's room for improvement, yes
        except IndexError:
           ids = ()

        if not interesting.isdisjoint(ids):
                import pdb; pdb.set_trace()

        return fun(self, *a, **kw)

    return w

I'm not really used in writing decorators, my first and naive attempt used a def statement right inside the metaclass main loop, and that really doesn't work, because the locals are shared between all occurrences of that statement : while python has some functional capabilities, it is not a functional language. Putting things together

We use the same method as in explained there:

class WatchedMixin(object):
    __metaclass__ = metawatch

    def _watch_set(self, meth_name, ids):
        if not hasattr(self, '_watchpoints'):
            self._watchpoints = {}
        self._watchpoints[meth_name] = dict(ids=ids)

    def _watch_clear(self, meth_name):
        if not hasattr(self, '_watchpoints'):
            return
        del self._watchpoints[meth_name]

from osv import osv

class WatchedOsv(osv.osv, WatchedMixin):
    pass

osv.osv = WatchedOsv

Maybe a few explanations : the __metaclass__ attribute marks the class as to be created by the given metaclass, and further is itself inherited by subclasses (before the class creation, obviously). Therefore all subclasses (OpenERP models) imported after our code has run will be created with our metaclass.

The final monkey patching is necessary because the metaclass has to act before the class creation, which happens at import time. Simply changing the __metaclass__ attribute of osv would not work (probably only for methods defined in subclasses or overrides, actually).

Finally, the two methods of the mixin are self-explanatory, they will be inherited by all models. As a side note, we could set __metaclass__ after the class creation to so that they don't get wrapped.

Bootstrap and usage

Put all the above code in a module, import it before the osv in your openerp-server.py, et voilà.

Now one can introduce, e.g.:

account_invoice._watch_set('write', [some_ids])

To set the watchpoint on the write method, and that can also be done on the fly from within a pdb session if one wishes so. Final remarks:

  • The bootstrap described above is disappointing, because it duplicates the whole startup script. It would be much better to make those statements and then import openerp-server. For a reason I haven't had time to investigate, the server hangs indefinitely on login requests if one does so.
  • Obviously, the same can be done for about any frameworks, unless it makes extensive use of metaclasses itself (hello, Zope2).
  • The code presented here is a proof of concept. My own version is a bit more advanced, and I've already succesfully used it for real-life business code.
  • This can be extended much beyond watch points. For instance, the first tests I did of openobject metaclassing listed the models that override the write method.
  • Since each call to a model method is intercepted, there are twice as many corresponding frames. It's a bit of a pain while climbing up them or reading tracebacks.
  • Time will tell how useful this really is, but I can already say that it brings the confidence in the debugging success up.
  • This is tested with OpenERP 5 & 6.

Liens

Réseautage & syndication