(originally published on 2011-06-19)

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

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.

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):                    
            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.