Odoo New API Implementation Using Metaclasses and Decorators
API:
An API(Application Programming Interface) is a set of defined functions and methods for interfacing with the underlying system or program or service running on the computer.
Metaclass:
A metaclass is defined as "the class of a class". Any class whose instances are themselves classes, is a metaclass.
Things You Could Do With Metaclasses:
There are lots of things you could do with metaclasses. Here's a partial list:
- Enforce different inheritance semantics, e.g. automatically call base class methods when a derived class overrides.
- Implement class methods (e.g. if the first argument is not named 'self').
- Implement that each instance is initialized with copies of all class variables.
- Implement a different way to store instance variables (e.g. in a list kept outside the the instance but indexed by the instance's id()).
- Automatically wrap or trap all or certain methods:
- for tracing
- for precondition and post-condition checking
- for synchronized methods
- for automatic value caching
Metaclass's __new__ and __init__:
To control the creation and initialization of the class in the metaclass, you can implement the metaclass's __new__ method and/or __init__ constructor. Most real-life metaclasses will probably override just one of them. __new__ should be implemented when you want to control the creation of a new object (class in our case), and __init__ should be implemented when you want to control the initialization of the new object after it has been created.
New API Implementation Details:
This module provides the elements for managing two different API styles, namely the "traditional" and "record" styles.
In the "traditional" style, parameters like the database cursor, user id, context dictionary and record ids (usually denoted as 'cr', 'uid', 'context', 'ids') are passed explicitly to all methods. In the "record" style, those parameters are hidden into model instances, which gives it a more object-oriented feel.
For instance, the statements:
model = self.pool.get(MODEL)
ids = model.search(cr, uid, DOMAIN, context=context)
for rec in model.browse(cr, uid, ids, context=context):
print rec.name
model.write(cr, uid, ids, VALUES, context=context)
may also be written as:
env = Env(cr, uid, context) # cr, uid, context wrapped in env
recs = env[MODEL] # retrieve an instance of MODEL
recs = recs.search(DOMAIN) # search returns a recordset
for rec in recs: # iterate over the records
print rec.name
recs.write(VALUES) # update all records in recs
Methods written in the "traditional" style are automatically decorated, following some heuristics based on parameter names.
1. class Meta(type):
It is used to automatically decorates traditional-style methods by guessing their API. It also implements the inheritance of the :func:'returns' decorators.
2. Metaclass's __new__:
It is used to automatically decorates traditional-style methods by guessing their API.
The guess() decorator is used by the metaclass's __new__ to decorates traditional-style methods by guessing their API.
Decorator's:
A decorator is just a callable that takes a function as an argument and returns a replacement function.
The Decorator's which is used to guess the API:
1. guess():
Decorate 'method' to make it callable in both traditional and record styles. This decorator is applied automatically by the model's metaclass, and has no effect on already-decorated methods.
The API style is determined by heuristics on the parameter names: 'cr' or 'cursor' for the cursor, 'uid' or 'user' for the user id, 'id' or 'ids' for a list of record ids, and 'context' for the context dictionary. If a traditional API is recognized, one of the decorators :func:'cr', :func:'cr_context', :func:'cr_uid', :func:'cr_uid_context', :func:'cr_uid_id', :func:'cr_uid_id_context', :func:'cr_uid_ids', :func:'cr_uid_ids_context' is applied on the method.
Method calls are considered traditional style when their first parameter is a database cursor.
2. noguess():
Decorate a method to prevent any effect from :func:'guess'.
The Decorator's which is used to decorate the traditional-style method:
1. @api.cr:
Decorate a traditional-style method that takes 'cr' as a parameter. Such a method may be called in both record and traditional styles, like:
# recs = model.browse(cr, uid, ids, context)
recs.method(args)
model.method(cr, args)
2. @api.cr_context:
Decorate a traditional-style method that takes 'cr', 'context' as parameters.
3. @api.cr_uid:
Decorate a traditional-style method that takes 'cr', 'uid' as parameters.
4. @api.cr_uid_context:
Decorate a traditional-style method that takes 'cr', 'uid', 'context' as parameters. Such a method may be called in both record and traditional
styles, like:
# recs = model.browse(cr, uid, ids, context)
recs.method(args)
model.method(cr, uid, args, context=context)
5. @api.cr_uid_id:
Decorate a traditional-style method that takes 'cr', 'uid', 'id' as parameters. Such a method may be called in both record and traditional styles. In the record style, the method automatically loops on records.
6. @api.cr_uid_id_context:
Decorate a traditional-style method that takes 'cr', 'uid', 'id', 'context' as parameters. Such a method:
@api.cr_uid_id
def method(self, cr, uid, id, args, context=None):
...
may be called in both record and traditional styles, like:
# rec = model.browse(cr, uid, id, context)
rec.method(args)
model.method(cr, uid, id, args, context=context)
7. @api.cr_uid_ids:
Decorate a traditional-style method that takes 'cr', 'uid', 'ids' as parameters. Such a method may be called in both record and traditional styles.
8. @api.cr_uid_ids_context:
Decorate a traditional-style method that takes 'cr', 'uid', 'ids', 'context' as parameters. Such a method:
@api.cr_uid_ids_context
def method(self, cr, uid, ids, args, context=None):
...
may be called in both record and traditional styles, like:
# recs = model.browse(cr, uid, ids, context)
recs.method(args)
model.method(cr, uid, ids, args, context=context)
It is generally not necessary, see :func:'guess'.
The Decorator's which is used to decorate the record-style method:
1. @api.model:
Decorate a record-style method where 'self' is a recordset, but its contents is not relevant, only the model is. Such a method:
@api.model
def method(self, args):
...
may be called in both record and traditional styles, like:
# recs = model.browse(cr, uid, ids, context)
recs.method(args)
model.method(cr, uid, args, context=context)
Notice that no 'ids' are passed to the method in the traditional style.
2. @api.one:
Decorate a record-style method where 'self' is expected to be a singleton instance. The decorated method automatically loops on records, and makes a list with the results. In case the method is decorated with @returns, it concatenates the resulting instances. Such a method:
@api.one
def method(self, args):
return self.name
may be called in both record and traditional styles, like::
# recs = model.browse(cr, uid, ids, context)
names = recs.method(args)
names = model.method(cr, uid, ids, args, context=context)
Each time 'self' is redefined as current record.
3. @api.multi:
Decorate a record-style method where 'self' is a recordset. The method typically defines an operation on records. Such a method:
@api.multi
def method(self, args):
...
may be called in both record and traditional styles, like::
# recs = model.browse(cr, uid, ids, context)
recs.method(args)
model.method(cr, uid, ids, args, context=context)
4. @api.constrains:
Decorates a constraint checker. Each argument must be a field name used in the check:
@api.one
@api.constrains('name', 'description')
def _check_description(self):
if self.name == self.description:
raise ValidationError("Fields name and description must be different")
Invoked on the records on which one of the named fields has been modified.
Should raise :class:'~openerp.exceptions.ValidationError' if the validation failed.
5. @api.onchange:
Return a decorator to decorate an onchange method for given fields. Each argument must be a field name:
@api.onchange('partner_id')
def _onchange_partner(self):
self.message = "Dear %s" % (self.partner_id.name or "")
In the form views where the field appears, the method will be called when one of the given fields is modified. The method is invoked on a pseudo-record that contains the values present in the form. Field assignments on that record are automatically sent back to the client.
6. @api.depends:
Return a decorator that specifies the field dependencies of a "compute" method (for new-style function fields). Each argument must be a string that consists in a dot-separated sequence of field names:
pname = fields.Char(compute='_compute_pname')
@api.one
@api.depends('partner_id.name', 'partner_id.is_company')
def _compute_pname(self):
if self.partner_id.is_company:
self.pname = (self.partner_id.name or "").upper()
else:
self.pname = self.partner_id.name
One may also pass a single function as argument. In that case, the dependencies are given by calling the function with the field's model.
7.@api.returns:
Return a decorator for methods that return instances of 'model'.
:param model: a model name, or 'self' for the current model
:param downgrade: a function 'downgrade(value)' to convert the
record-style 'value' to a traditional-style output
The decorator adapts the method output to the api style: 'id', 'ids' or 'False' for the traditional style, and recordset for the record style:
@model
@returns('res.partner')
def find_partner(self, arg):
... # return some record
# output depends on call style: traditional vs record style
partner_id = model.find_partner(cr, uid, arg, context=context)
# recs = model.browse(cr, uid, ids, context)
partner_record = recs.find_partner(arg)
Note that the decorated method must satisfy that convention.
Those decorators are automatically *inherited*: a method that overrides a decorated existing method will be decorated with the same
'@returns(model)'.
The Decorator's which is used to decorate a method that supports the old-style API only:
1. @api.v7:
Decorate a method that supports the old-style api only. A new-style api may be provided by redefining a method with the same name and decorated with :func:'~.v8':
@api.v7
def foo(self, cr, uid, ids, context=None):
...
@api.v8
def foo(self):
...
Note that the wrapper method uses the docstring of the first method.
The Decorator's which is used to decorate a method that supports the new-style API only:
1. @api.v8:
Decorate a method that supports the new-style api only. An old-style api may be provided by redefining a method with the same name and decorated with :func:'~.v7':
@api.v8
def foo(self):
...
@api.v7
def foo(self, cr, uid, ids, context=None):
...
Note that the wrapper method uses the docstring of the first method.