Inside OpenERP inheritance

Analysis of how OpenObject models inheritance works and especially relates to Python inheritance.
par Georges Racinet, mis à jour le 09/09/2012

(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.