[IMP] fields: rename methods set_class_name and setup, and change their API

The API of those method has been modified to take a recordset instead of a class
as parameter:

    field.set_class_name(cls, name)   ->   field.setup_base(model, name)
    field.setup(env)                  ->   field.setup_full(model)

The recordset gives access to an environment, and will ease code reorganization
in the field's setup methods.
This commit is contained in:
Raphael Collet
2015-08-13 10:44:59 +02:00
parent d46e9f4433
commit c6c1824b7a
3 changed files with 129 additions and 109 deletions
@@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
import unittest2
from openerp import models, fields
class TestFieldToColumn(unittest2.TestCase):
from openerp import fields
from openerp.tests import common
class TestFieldToColumn(common.TransactionCase):
def test_char(self):
# create a field, initialize its attributes, and convert it to a column
field = fields.Char(string="test string", required=True)
field.set_class_name(models.Model, 'test')
field.setup_base(self.env['res.partner'], 'test')
column = field.to_column()
self.assertEqual(column.string, "test string")
+121 -100
View File
@@ -42,16 +42,43 @@ def _check_value(value):
return value.get() if isinstance(value, SpecialValue) else value
def resolve_all_mro(cls, name, reverse=False):
""" Return the (successively overridden) values of attribute ``name`` in ``cls``
in mro order, or inverse mro order if ``reverse`` is true.
def resolve_all_mro(model, name, reverse=False):
""" Return the (successively overridden) values of attribute ``name`` in the
class of ``model`` in mro order (or inverse order if ``reverse``).
"""
klasses = reversed(cls.__mro__) if reverse else cls.__mro__
klasses = reversed(type(model).__mro__) if reverse else type(model).__mro__
for klass in klasses:
if name in klass.__dict__:
yield klass.__dict__[name]
def default_new_to_new(field, value):
""" Convert the new-API default ``value`` to a callable. """
return value if callable(value) else lambda model: value
def default_new_to_old(field, value):
""" Convert the new-API default ``value`` to the old API. """
if callable(value):
from openerp import api
return api.model(lambda model: field.convert_to_write(value(model)))
else:
return value
def default_old_to_new(field, value):
""" Convert the old-API default ``value`` to the new API. """
if callable(value):
return lambda model: field.convert_to_cache(
value(model._model, model._cr, model._uid, model._context),
model, validate=False,
)
else:
return lambda model: field.convert_to_cache(value, model, validate=False)
def default_old_to_old(field, value):
""" Convert the old-API default ``value`` to the old API. """
return value
class MetaField(type):
""" Metaclass for field classes. """
by_type = {}
@@ -258,12 +285,12 @@ class Field(object):
_slots = {
'args': EMPTY_DICT, # the parameters given to __init__()
'_attrs': EMPTY_DICT, # the field's non-slot attributes
'setup_full_done': False, # whether the field has been fully setup
'automatic': False, # whether the field is automatically created ("magic" field)
'inherited': False, # whether the field is inherited (_inherits)
'origin': None, # the column from which the field was created
'column': None, # the column corresponding to the field
'setup_done': False, # whether the field has been set up
'name': None, # name of the field
'model_name': None, # name of the model of this field
@@ -304,6 +331,10 @@ class Field(object):
args = {key: val for key, val in kwargs.iteritems() if val is not None}
self.args = args or EMPTY_DICT
def new(self, **kwargs):
""" Return a field of the same type as ``self``, with its own parameters. """
return type(self)(**kwargs)
def __getattr__(self, name):
""" Access non-slot field attribute. """
try:
@@ -337,23 +368,38 @@ class Field(object):
except KeyError:
raise AttributeError(name)
def new(self, **kwargs):
""" Return a field of the same type as ``self``, with its own parameters. """
return type(self)(**kwargs)
def __str__(self):
return "%s.%s" % (self.model_name, self.name)
def set_class_name(self, cls, name):
""" Assign the model class and field name of ``self``. """
def __repr__(self):
return "%s.%s" % (self.model_name, self.name)
############################################################################
#
# Base field setup: things that do not depend on other models/fields
#
def setup_base(self, model, name):
""" Base setup: things that do not depend on other models/fields. """
self._setup_attrs(model, name)
#
# Setup field parameter attributes
#
def _setup_attrs(self, model, name):
""" Determine field parameter attributes. """
# determine all inherited field attributes
attrs = {}
for field in resolve_all_mro(cls, name, reverse=True):
for field in resolve_all_mro(model, name, reverse=True):
if isinstance(field, type(self)):
attrs.update(field.args)
else:
attrs.clear()
attrs.update(self.args) # necessary in case self is not in cls
attrs.update(self.args) # necessary in case self is not in class
attrs['args'] = self.args
attrs['model_name'] = cls._name
attrs['model_name'] = model._name
attrs['name'] = name
# initialize ``self`` with ``attrs``
@@ -377,18 +423,16 @@ class Field(object):
# related fields get their string from their parent field
self.string = name.replace('_', ' ').capitalize()
# determine self.default and cls._defaults in a consistent way
self._determine_default(cls, name)
self._setup_default(model, name)
def _determine_default(self, cls, name):
""" Retrieve the default value for ``self`` in the hierarchy of ``cls``, and
determine ``self.default`` and ``cls._defaults`` accordingly.
"""
def _setup_default(self, model, name):
""" Determine ``self.default`` and the corresponding ``model._defaults``. """
self.default = None
model._defaults.pop(name, None)
# traverse the class hierarchy upwards, and take the first field
# definition with a default or _defaults for self
for klass in cls.__mro__:
for klass in type(model).__mro__:
if name in klass.__dict__:
field = klass.__dict__[name]
if not isinstance(field, type(self)):
@@ -396,96 +440,75 @@ class Field(object):
return
if 'default' in field.args:
# take the default in field, and adapt it for cls._defaults
# take the value, and adapt it for model._defaults
value = field.args['default']
if callable(value):
from openerp import api
self.default = value
cls._defaults[name] = api.model(
lambda recs: self.convert_to_write(value(recs))
)
else:
self.default = lambda recs: value
cls._defaults[name] = value
self.default = default_new_to_new(self, value)
model._defaults[name] = default_new_to_old(self, value)
return
defaults = klass.__dict__.get('_defaults') or {}
if name in defaults:
# take the value from _defaults, and adapt it for self.default
value = defaults[name]
if callable(value):
func = lambda recs: value(recs._model, recs._cr, recs._uid, recs._context)
else:
func = lambda recs: value
self.default = lambda recs: self.convert_to_cache(
func(recs), recs, validate=False,
)
cls._defaults[name] = value
self.default = default_old_to_new(self, value)
model._defaults[name] = default_old_to_old(self, value)
return
def __str__(self):
return "%s.%s" % (self.model_name, self.name)
def __repr__(self):
return "%s.%s" % (self.model_name, self.name)
############################################################################
#
# Field setup
# Full field setup: everything else, except recomputation triggers
#
def setup(self, env):
""" Make sure that ``self`` is set up, except for recomputation triggers. """
if not self.setup_done:
if self.related:
self._setup_related(env)
def setup_full(self, model):
""" Full setup: everything else, except recomputation triggers. """
if not self.setup_full_done:
if not self.related:
self._setup_regular(model)
else:
self._setup_regular(env)
self.setup_done = True
self._setup_related(model)
self.setup_full_done = True
#
# Setup of non-related fields
#
def _setup_regular(self, env):
def _setup_regular(self, model):
""" Setup the attributes of a non-related field. """
recs = env[self.model_name]
def make_depends(deps):
return tuple(deps(recs) if callable(deps) else deps)
return tuple(deps(model) if callable(deps) else deps)
# convert compute into a callable and determine depends
if isinstance(self.compute, basestring):
# if the compute method has been overridden, concatenate all their _depends
self.depends = ()
for method in resolve_all_mro(type(recs), self.compute, reverse=True):
for method in resolve_all_mro(model, self.compute, reverse=True):
self.depends += make_depends(getattr(method, '_depends', ()))
self.compute = getattr(type(recs), self.compute)
self.compute = getattr(type(model), self.compute)
else:
self.depends = make_depends(getattr(self.compute, '_depends', ()))
# convert inverse and search into callables
if isinstance(self.inverse, basestring):
self.inverse = getattr(type(recs), self.inverse)
self.inverse = getattr(type(model), self.inverse)
if isinstance(self.search, basestring):
self.search = getattr(type(recs), self.search)
self.search = getattr(type(model), self.search)
#
# Setup of related fields
#
def _setup_related(self, env):
def _setup_related(self, model):
""" Setup the attributes of a related field. """
# fix the type of self.related if necessary
if isinstance(self.related, basestring):
self.related = tuple(self.related.split('.'))
# determine the chain of fields, and make sure they are all set up
recs = env[self.model_name]
target = model
for name in self.related:
field = recs._fields[name]
field.setup(env)
recs = recs[name]
field = target._fields[name]
field.setup_full(target)
target = target[name]
self.related_field = field
@@ -1051,13 +1074,13 @@ class Float(Field):
else:
return self._digits
def _setup_digits(self, env):
def _setup_digits(self, model):
""" Setup the digits for ``self`` and its corresponding column """
pass
def _setup_regular(self, env):
super(Float, self)._setup_regular(env)
self._setup_digits(env)
def _setup_regular(self, model):
super(Float, self)._setup_regular(model)
self._setup_digits(model)
_related__digits = property(attrgetter('_digits'))
_related_group_operator = property(attrgetter('group_operator'))
@@ -1098,11 +1121,11 @@ class Monetary(Field):
_related_currency_field = property(attrgetter('currency_field'))
_description_currency_field = property(attrgetter('currency_field'))
def _setup_regular(self, env):
super(Monetary, self)._setup_regular(env)
def _setup_regular(self, model):
super(Monetary, self)._setup_regular(model)
if not self.currency_field:
self.currency_field = 'currency_id'
assert self.currency_field in env[self.model_name]._fields, \
assert self.currency_field in model._fields, \
"Field %s with unknown currency_field %r" % (self, self.currency_field)
def convert_to_write(self, value, target=None, fnames=None):
@@ -1190,8 +1213,8 @@ class Char(_String):
_related_size = property(attrgetter('size'))
_description_size = property(attrgetter('size'))
def _setup_regular(self, env):
super(Char, self)._setup_regular(env)
def _setup_regular(self, model):
super(Char, self)._setup_regular(model)
assert isinstance(self.size, (NoneType, int)), \
"Char field %s with non-integer size %r" % (self, self.size)
@@ -1399,23 +1422,23 @@ class Selection(Field):
selection = api.expected(api.model, selection)
super(Selection, self).__init__(selection=selection, string=string, **kwargs)
def _setup_regular(self, env):
super(Selection, self)._setup_regular(env)
def _setup_regular(self, model):
super(Selection, self)._setup_regular(model)
assert self.selection is not None, "Field %s without selection" % self
def _setup_related(self, env):
super(Selection, self)._setup_related(env)
def _setup_related(self, model):
super(Selection, self)._setup_related(model)
# selection must be computed on related field
field = self.related_field
self.selection = lambda model: field._description_selection(model.env)
def set_class_name(self, cls, name):
super(Selection, self).set_class_name(cls, name)
def _setup_attrs(self, model, name):
super(Selection, self)._setup_attrs(model, name)
# determine selection (applying 'selection_add' extensions)
for field in resolve_all_mro(cls, name, reverse=True):
for field in resolve_all_mro(model, name, reverse=True):
if isinstance(field, type(self)):
# We cannot use field.selection or field.selection_add here
# because those attributes are overridden by ``set_class_name``.
# because those attributes are overridden by ``_setup_attrs``.
if 'selection' in field.args:
self.selection = field.args['selection']
if 'selection_add' in field.args:
@@ -1489,8 +1512,8 @@ class Reference(Selection):
_related_size = property(attrgetter('size'))
_column_size = property(attrgetter('size'))
def _setup_regular(self, env):
super(Reference, self)._setup_regular(env)
def _setup_regular(self, model):
super(Reference, self)._setup_regular(model)
assert isinstance(self.size, (NoneType, int)), \
"Reference field %s with non-integer size %r" % (self, self.size)
@@ -1524,11 +1547,10 @@ class _Relational(Field):
'context': {}, # context for searching values
}
def _setup_regular(self, env):
super(_Relational, self)._setup_regular(env)
if self.comodel_name not in env.registry:
_logger.warning("Field %s with unknown comodel_name %r"
% (self, self.comodel_name))
def _setup_regular(self, model):
super(_Relational, self)._setup_regular(model)
if self.comodel_name not in model.pool:
_logger.warning("Field %s with unknown comodel_name %r", self, self.comodel_name)
self.comodel_name = '_unknown'
@property
@@ -1599,11 +1621,11 @@ class Many2one(_Relational):
def __init__(self, comodel_name=None, string=None, **kwargs):
super(Many2one, self).__init__(comodel_name=comodel_name, string=string, **kwargs)
def set_class_name(self, cls, name):
super(Many2one, self).set_class_name(cls, name)
def _setup_attrs(self, model, name):
super(Many2one, self)._setup_attrs(model, name)
# determine self.delegate
if not self.delegate:
self.delegate = name in cls._inherits.values()
self.delegate = name in model._inherits.values()
_column_ondelete = property(attrgetter('ondelete'))
_column_auto_join = property(attrgetter('auto_join'))
@@ -1808,12 +1830,12 @@ class One2many(_RelationalMulti):
**kwargs
)
def _setup_regular(self, env):
super(One2many, self)._setup_regular(env)
def _setup_regular(self, model):
super(One2many, self)._setup_regular(model)
if self.inverse_name:
# link self to its inverse field and vice-versa
comodel = env[self.comodel_name]
comodel = model.env[self.comodel_name]
invf = comodel._fields[self.inverse_name]
# In some rare cases, a ``One2many`` field can link to ``Int`` field
# (res_model/res_id pattern). Only inverse the field if this is
@@ -1878,18 +1900,17 @@ class Many2many(_RelationalMulti):
**kwargs
)
def _setup_regular(self, env):
super(Many2many, self)._setup_regular(env)
def _setup_regular(self, model):
super(Many2many, self)._setup_regular(model)
if not self.relation and self.store:
# retrieve self.relation from the corresponding column
column = self.to_column()
if isinstance(column, fields.many2many):
self.relation, self.column1, self.column2 = \
column._sql_names(env[self.model_name])
self.relation, self.column1, self.column2 = column._sql_names(model)
if self.relation:
m2m = env.registry._m2m
m2m = model.pool._m2m
# if inverse field has already been setup, it is present in m2m
invf = m2m.get((self.relation, self.column2, self.column1))
if invf:
+3 -5
View File
@@ -446,9 +446,9 @@ class BaseModel(object):
cls._fields[name] = field
# basic setup of field
field.set_class_name(cls, name)
field.setup_base(self, name)
# cls._columns will be updated once fields are set up
# cls._columns will be updated once fields are fully set up
@api.model
def _pop_field(self, name):
@@ -2962,7 +2962,7 @@ class BaseModel(object):
# set up fields, and determine their corresponding column
cls._columns = {}
for name, field in cls._fields.iteritems():
field.setup(self.env)
field.setup_full(self)
column = field.to_column()
if column:
cls._columns[name] = column
@@ -3040,8 +3040,6 @@ class BaseModel(object):
for fname, field in self._fields.iteritems():
if allfields and fname not in allfields:
continue
if not field.setup_done:
continue
if field.groups and not recs.user_has_groups(field.groups):
continue