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.