(originally published on 2012-04-07)

The OpenObject framework provides a clear and intuitive way of overriding models through inheritance. This is well documented, and possibly the first thing an OpenERP developer has to learn.

The general Python developer may wonder though how this relates to  Python class inheritance. Since I was somewhat uncomfortable not no  know, I'm sharing this in hope that it might help some other people  feeling better :-) Well, that and a way to make a simple reference for  the colleagues at Anybox

This is work in progress, though, probably lacks references and could  be improved for clarity and illustrations. Don't hesitate to comment or  email me.

Thanks to Sébastien Beau at Akretion France for raising a question which eventually forced the lazy me to try and find out what happens.

The wheat illustration is (C) Wikimedia Commons, their licence applies. OpenERP code extracts are licenced under Affero GPLv3.

The ubiquitous use of super() to relay from an override to the original method obviously suggests  that this inheritance system is indeed based on Python subclassing, but  how exactly ? Let's find out.

Preliminary: dynamic class definition in Python

Let me recall first this stunning fact: in python, a class is itself an object, and it can be created with a constructor syntax. Namely, this declarative syntax:

class A(object):
	x = 0
    
    def f(self):
        return self.x

is equivalent to the following constructor syntax:

def af(self):
    return self.x
    
A = type("A", ("object",), {"x": 0, "f": af})

In effect, our class is itself an instance of a metaclass called "type", which is roughly to metaclasses what "object" is to  ordinary classes. This material is extensively covered in the python documentation and we won't repeat it here. For our purposes, let's simply make it explicit that the constructor  second argument lists the classes to inherit from and the third is the  dictionnary of class attributes.

Obviously, the constructor syntax is much more dynamic, since we can play beforehand with the attributes dict programmatically. Let's see now how the OpenObject framework plays with that.

OpenObject class instantiation: a wheat ear

Wheat ear

In OpenERP client code, one never instantiates the model  classes. Instead, instances are to be picked up from the pool.  Typically, from a model class, one'd write this:  prod = self.pool.get('product.product')

Therefore, the instantiation always goes through the createInstance  classmethod defined in osv.orm, of which all model classes inherit from  through osv.osv (it used to be in osv.osv before the 6.1 version).

Now, roughly, what this create_instance does in the following simple inheritance case:

class p2(osv.osv):
    _inherit = "product.product"
    
p2()

amounts to

  • find the current class for the model specified in the _inherit attribute (here, product.product), and call it the parent class.
  • merge and clean up technical attributes from the parent class in a dict, called nattr.
  • define a new class (let's call it p2_actual) by a call to the type()  constructor (cls in the right-hand-side is p2 in our example):
cls = type(name, (cls, parent_class), dict(nattr, _register=False))
  • instantiate p2_actual (more on this later).

Now, if some p3 later inherits the same model:

class p3(osv.osv):
    _inherit = 'product.product' 
    
p3()

p2_actual will be the parent class in p3's instantiation process. And the pool will store instances of p3_actual.

What we end up with is an ear of wheat inheritance graph of python classes (TODO make an actual image):

osv.osv

osv.osv                  |

       \         product_product

          p2            |
              \         |
                   p2_actual
osv.osv                  |
        \               |          

          p3            |
              \         |
                   p3_actual

The merged technical attributes are in p2_actual and p3_actual only, while the methods (new or overridden) come from p2 and p3.

Complement: super() and the Method Resolution Order (MRO)

It's customary in an override to call the base class through super(). Say we have this in class p2:

def price_get(self, cr, uid, ids, **kw):
    return super(p2, self).price_get(cr, uid, ids, **kw) * 2

It will be actually executed for an instance of p2_actual. Specifying p2 here avoids the infinite loop one'd get with the naive

return super(self.__class__, self).price_get(cr, uid, ids, **kw) * 2

Indeed, the latter calls super() on p2_actual, and the result is then  our instance, viewed as an instance of p2 again, instead of the wished  product_product base class.

Now, if we start from p2, why don't we climb up straight to the  osv.osv it inherits from ? In short, because all these are new-style  classes (they inherit from object), and python's resolution of that  inheritance hierarchy (the MRO) boils down to:

p3_actual -- p3 -- p2_actual -- p2 -- p_actual -- p -- osv.osv

See the reference documentation of the MRO for more details

This can be instrospected by the __mro__ technical attribute that all  new-style classes have. To be fair, the use of super() is reserved to  new-style classes only.

Here's what we get with a pdb session (trace set in a method defined in p3)

(Pdb) self.__class__
<class 'openerp.osv.orm.product.product'>
(Pdb) self.__class__.__mro__
(<class 'openerp.osv.orm.product.product'>, <class 'openerp.addons.inher.inher3.p3'>,
<class 'openerp.osv.orm.product.product'>, <class 'openerp.addons.inher.inher2.p2'>,
<class 'openerp.osv.orm.product.product'>, <class 'openerp.addons.stock.product.product_product'>,
<class 'openerp.addons.product.product.product_product'>, <class 'openerp.osv.orm.Model'>,
<class 'openerp.osv.orm.BaseModel'>, <type 'object'>)

As you can see, there was actually an inheritance of product_product  in the stock addon before my sample addon (inher) kicked in.

We can also verify the ear of wheat shape of successive subclassings:

(Pdb) self.__class__.__bases__
(<class 'openerp.addons.inher.inher3.p3'>, <class 'openerp.osv.orm.product.product'>)
(Pdb) self.__class__.__bases__[1].__bases__
(<class 'openerp.addons.inher.inher2.p2'>, <class 'openerp.osv.orm.product.product'>)

Class call and instantiation process

Let's go through this example again:

class p2(osv.osv):    
    _inherit = 'product.product'
    
p2()

Normally,  a class call such as this p2() should return an instance of p2 and  that'd be it. In OpenERP, what it does is instead some registrations  (extract from osv.osv in 6.0.3):

def __new__(cls):
    module = str(cls)[6:]
    module = module[:len(module)-1]
    module = module.split('.')[0][2:]
    if not hasattr(cls, '_module'):
        cls._module = module
    module_class_list.setdefault(cls._module, []).append(cls)
    class_pool[cls._name] = cls
    if module not in module_list:
        module_list.append(cls._module)
    return None

In  turn, the module_class_list dict is used to call createInstance on all  the relevant classes while loading modules in the proper dependency  order.

Now, of course, at the end of its process, createInstance can't call the  class it created (our p2_actual) either to get the instance, since  __new__() returns None (here I learned that __init__() isn't called at  all in that case).

Instead, it gets back to the standard __new__ provided by the root object class (here cls is p2_actual):

obj = object.__new__(cls)
       obj.__init__(pool, cr)

Metaclasses in OpenERP 6.1

In the earlier post I made about how to make a watchpoint system using metaclasses, I wrote  that OpenERP didn't use metaclasses itself. Actually I already had to  go through the createInstance classmethod to check that, but didn't  analyse what it does back then.

Anyway, in 6.1, OpenERP has a  metaclass : MetaModel, also defined in osv.orm. Its purpose is to store  the python module to model class correspondence, a work that used to  belong to the model class call (see above). I've heard that the class  call is no longer necessary (didn't check carefully) and obviously  MetaModel serves that purpose.

This means of course that the metaclass for watchpoints must be adapted to subclass this MetaModel.

Conclusion

First, a warning: everything described here is subject to change, and  has already evolved between OpenERP 6.0 and 6.1. If you rely on it, you  have to be prepared to adapt your code for further versions.

I  don't know what the plans are, but all that merging and subclassing work  could probably be done in a more natural way by the newly introduced  metaclass (assuming the order of imports is well under control at this  point), so my first hand guess will be that this is the road the  framework has taken (just a guess, really).

EDIT (2012-04-12) : a quick look at the trunk two days ago revealed no difference.

This  kind of trick could also be done with class decorators (requires Python  2.6), and that'd probably even be cleaner, since one does expect a  decorator to modify the decorated class.

For now, some practical conclusions:

  • Overriding  __new__() in a model class is touchy in 6.0 and has to call  osv.osv.__new__() back ; it should be safer in 6.1 but will not be  executed by instance creation in either case.
  • Overriding __init__() appears to be safe.