[IMP] fields: move computed_fields, inverse_fields and _triggers to models
This removes an obstacle for sharing fields across registries: regular fields no longer contain metadata referring to other models/fields.
This commit is contained in:
+23
-34
@@ -320,10 +320,7 @@ class Field(object):
|
||||
'change_default': False, # whether the field may trigger a "user-onchange"
|
||||
'deprecated': None, # whether the field is deprecated
|
||||
|
||||
'inverse_fields': (), # collection of inverse fields (objects)
|
||||
'computed_fields': (), # fields computed with the same method as self
|
||||
'related_field': None, # corresponding related field
|
||||
'_triggers': (), # invalidation and recomputation triggers
|
||||
}
|
||||
|
||||
def __init__(self, string=None, **kwargs):
|
||||
@@ -385,9 +382,6 @@ class Field(object):
|
||||
if self.setup_full_done and not self.related:
|
||||
# optimization for regular fields: keep the base setup
|
||||
self.setup_full_done = False
|
||||
self.inverse_fields = ()
|
||||
self.computed_fields = ()
|
||||
self._triggers = ()
|
||||
else:
|
||||
# do the base setup from scratch
|
||||
self._setup_attrs(model, name)
|
||||
@@ -608,11 +602,6 @@ class Field(object):
|
||||
# See method ``modified`` below for details.
|
||||
#
|
||||
|
||||
def add_trigger(self, trigger):
|
||||
""" Add a recomputation trigger on ``self``. """
|
||||
if trigger not in self._triggers:
|
||||
self._triggers += (trigger,)
|
||||
|
||||
def setup_triggers(self, env):
|
||||
""" Add the necessary triggers to invalidate/recompute ``self``. """
|
||||
model = env[self.model_name]
|
||||
@@ -640,23 +629,19 @@ class Field(object):
|
||||
continue
|
||||
|
||||
#_logger.debug("Add trigger on %s to recompute %s", field, self)
|
||||
field.add_trigger((self, '.'.join(path0 or ['id'])))
|
||||
model._field_triggers.add(field, (self, '.'.join(path0 or ['id'])))
|
||||
|
||||
# add trigger on inverse fields, too
|
||||
for invf in field.inverse_fields:
|
||||
for invf in model._field_inverses[field]:
|
||||
#_logger.debug("Add trigger on %s to recompute %s", invf, self)
|
||||
invf.add_trigger((self, '.'.join(path0 + [head])))
|
||||
invm = env[invf.model_name]
|
||||
invm._field_triggers.add(invf, (self, '.'.join(path0 + [head])))
|
||||
|
||||
# recursively traverse the dependency
|
||||
if tail:
|
||||
comodel = env[field.comodel_name]
|
||||
self._setup_dependency(path0 + [head], comodel, tail)
|
||||
|
||||
@property
|
||||
def dependents(self):
|
||||
""" Return the computed fields that depend on ``self``. """
|
||||
return (field for field, path in self._triggers)
|
||||
|
||||
############################################################################
|
||||
#
|
||||
# Field description
|
||||
@@ -867,7 +852,7 @@ class Field(object):
|
||||
# set value in cache, inverse field, and mark record as dirty
|
||||
record._cache[self] = value
|
||||
if env.in_onchange:
|
||||
for invf in self.inverse_fields:
|
||||
for invf in record._field_inverses[self]:
|
||||
invf._update(value, record)
|
||||
record._set_dirty(self.name)
|
||||
|
||||
@@ -889,11 +874,12 @@ class Field(object):
|
||||
def _compute_value(self, records):
|
||||
""" Invoke the compute method on ``records``. """
|
||||
# initialize the fields to their corresponding null value in cache
|
||||
for field in self.computed_fields:
|
||||
computed = records._field_computed[self]
|
||||
for field in computed:
|
||||
records._cache[field] = field.null(records.env)
|
||||
records.env.computed[field].update(records._ids)
|
||||
self.compute(records)
|
||||
for field in self.computed_fields:
|
||||
for field in computed:
|
||||
records.env.computed[field].difference_update(records._ids)
|
||||
|
||||
def compute_value(self, records):
|
||||
@@ -923,10 +909,11 @@ class Field(object):
|
||||
self.compute_value(recs)
|
||||
# HACK: if result is in the wrong cache, copy values
|
||||
if recs.env != env:
|
||||
computed = record._field_computed[self]
|
||||
for source, target in zip(recs, recs.with_env(env)):
|
||||
try:
|
||||
values = target._convert_to_cache({
|
||||
f.name: source[f.name] for f in self.computed_fields
|
||||
f.name: source[f.name] for f in computed
|
||||
}, validate=False)
|
||||
except MissingError as e:
|
||||
values = FailedValue(e)
|
||||
@@ -981,7 +968,7 @@ class Field(object):
|
||||
"""
|
||||
# invalidate the fields that depend on self, and prepare recomputation
|
||||
spec = [(self, records._ids)]
|
||||
for field, path in self._triggers:
|
||||
for field, path in records._field_triggers[self]:
|
||||
if path and field.store:
|
||||
# don't move this line to function top, see log
|
||||
env = records.env(user=SUPERUSER_ID, context={'active_test': False})
|
||||
@@ -1007,7 +994,7 @@ class Field(object):
|
||||
# invalidate the fields on the records in cache that depend on
|
||||
# ``records``, except fields currently being computed
|
||||
spec = []
|
||||
for field, path in self._triggers:
|
||||
for field, path in records._field_triggers[self]:
|
||||
target = env[field.model_name]
|
||||
computed = target.browse(env.computed[field])
|
||||
if path == 'id':
|
||||
@@ -1591,11 +1578,11 @@ class _Relational(Field):
|
||||
return env[self.comodel_name]
|
||||
|
||||
def modified(self, records):
|
||||
# Invalidate cache for self.inverse_fields, too. Note that recomputation
|
||||
# of fields that depend on self.inverse_fields is already covered by the
|
||||
# triggers (see above).
|
||||
# Invalidate cache for inverse fields, too. Note that the recomputation
|
||||
# of fields that depend on inverse fields is already covered by the
|
||||
# triggers.
|
||||
spec = super(_Relational, self).modified(records)
|
||||
for invf in self.inverse_fields:
|
||||
for invf in records._field_inverses[self]:
|
||||
spec.append((invf, None))
|
||||
return spec
|
||||
|
||||
@@ -1772,7 +1759,8 @@ class _RelationalMulti(_Relational):
|
||||
if fnames is None:
|
||||
# take all fields in cache, except the inverses of self
|
||||
fnames = set(value._fields) - set(MAGIC_COLUMNS)
|
||||
for invf in self.inverse_fields:
|
||||
model = value.env[self.model_name]
|
||||
for invf in model._field_inverses[self]:
|
||||
fnames.discard(invf.name)
|
||||
|
||||
# add new and existing records
|
||||
@@ -1853,8 +1841,8 @@ class One2many(_RelationalMulti):
|
||||
# (res_model/res_id pattern). Only inverse the field if this is
|
||||
# a ``Many2one`` field.
|
||||
if isinstance(invf, Many2one):
|
||||
self.inverse_fields += (invf,)
|
||||
invf.inverse_fields += (self,)
|
||||
model._field_inverses.add(self, invf)
|
||||
comodel._field_inverses.add(invf, self)
|
||||
|
||||
_description_relation_field = property(attrgetter('inverse_name'))
|
||||
|
||||
@@ -1927,8 +1915,9 @@ class Many2many(_RelationalMulti):
|
||||
# if inverse field has already been setup, it is present in m2m
|
||||
invf = m2m.get((self.relation, self.column2, self.column1))
|
||||
if invf:
|
||||
self.inverse_fields += (invf,)
|
||||
invf.inverse_fields += (self,)
|
||||
comodel = model.env[self.comodel_name]
|
||||
model._field_inverses.add(self, invf)
|
||||
comodel._field_inverses.add(invf, self)
|
||||
else:
|
||||
# add self in m2m, so that its inverse field can find it
|
||||
m2m[(self.relation, self.column1, self.column2)] = self
|
||||
|
||||
+18
-20
@@ -47,7 +47,7 @@ from .api import Environment
|
||||
from .exceptions import AccessError, MissingError, ValidationError, UserError
|
||||
from .osv import fields
|
||||
from .osv.query import Query
|
||||
from .tools import frozendict, lazy_property, ormcache
|
||||
from .tools import frozendict, lazy_property, ormcache, Collector
|
||||
from .tools.config import config
|
||||
from .tools.func import frame_codeinfo
|
||||
from .tools.misc import CountingStream, DEFAULT_SERVER_DATETIME_FORMAT, DEFAULT_SERVER_DATE_FORMAT
|
||||
@@ -2962,6 +2962,11 @@ class BaseModel(object):
|
||||
self.env[parent]._setup_base(partial)
|
||||
self._add_inherited_fields()
|
||||
|
||||
# 4. initialize more field metadata
|
||||
cls._field_computed = {} # fields computed with the same method
|
||||
cls._field_inverses = Collector() # inverse fields for related fields
|
||||
cls._field_triggers = Collector() # list of (field, path) to invalidate
|
||||
|
||||
cls._setup_done = True
|
||||
|
||||
@api.model
|
||||
@@ -2977,15 +2982,12 @@ class BaseModel(object):
|
||||
if column:
|
||||
cls._columns[name] = column
|
||||
|
||||
# determine field.computed_fields
|
||||
computed_fields = defaultdict(list)
|
||||
# map each field to the fields computed with the same method
|
||||
groups = defaultdict(list)
|
||||
for field in cls._fields.itervalues():
|
||||
if field.compute:
|
||||
computed_fields[field.compute].append(field)
|
||||
|
||||
for fields in computed_fields.itervalues():
|
||||
for field in fields:
|
||||
field.computed_fields = fields
|
||||
cls._field_computed[field] = group = groups[field.compute]
|
||||
group.append(field)
|
||||
|
||||
@api.model
|
||||
def _setup_complete(self):
|
||||
@@ -2998,13 +3000,12 @@ class BaseModel(object):
|
||||
|
||||
# add invalidation triggers on model dependencies
|
||||
if cls._depends:
|
||||
triggers = [(field, None) for field in cls._fields.itervalues()]
|
||||
for model_name, field_names in cls._depends.iteritems():
|
||||
model = self.env[model_name]
|
||||
for field_name in field_names:
|
||||
field = model._fields[field_name]
|
||||
for trigger in triggers:
|
||||
field.add_trigger(trigger)
|
||||
for dependent in cls._fields.itervalues():
|
||||
model._field_triggers.add(field, (dependent, None))
|
||||
|
||||
# determine old-api structures about inherited fields
|
||||
cls._inherits_reload()
|
||||
@@ -5427,7 +5428,7 @@ class BaseModel(object):
|
||||
for name in values:
|
||||
field = self._fields.get(name)
|
||||
if field:
|
||||
for invf in field.inverse_fields:
|
||||
for invf in self._field_inverses[field]:
|
||||
invf._update(record[name], record)
|
||||
|
||||
return record
|
||||
@@ -5633,7 +5634,7 @@ class BaseModel(object):
|
||||
|
||||
# invalidate fields and inverse fields, too
|
||||
spec = [(f, ids) for f in fields] + \
|
||||
[(invf, None) for f in fields for invf in f.inverse_fields]
|
||||
[(invf, None) for f in fields for invf in self._field_inverses[f]]
|
||||
self.env.invalidate(spec)
|
||||
|
||||
@api.multi
|
||||
@@ -5683,11 +5684,8 @@ class BaseModel(object):
|
||||
while self.env.has_todo():
|
||||
field, recs = self.env.get_todo()
|
||||
# evaluate the fields to recompute, and save them to database
|
||||
names = [
|
||||
f.name
|
||||
for f in field.computed_fields
|
||||
if f.store and self.env.field_todo(f)
|
||||
]
|
||||
computed = self.env[field.model_name]._field_computed[field]
|
||||
names = [f.name for f in computed if f.store and self.env.field_todo(f)]
|
||||
for rec in recs:
|
||||
try:
|
||||
values = rec._convert_to_write({
|
||||
@@ -5698,7 +5696,7 @@ class BaseModel(object):
|
||||
except MissingError:
|
||||
pass
|
||||
# mark the computed fields as done
|
||||
map(recs._recompute_done, field.computed_fields)
|
||||
map(recs._recompute_done, computed)
|
||||
|
||||
#
|
||||
# Generic onchange method
|
||||
@@ -5711,7 +5709,7 @@ class BaseModel(object):
|
||||
# test whether self has an onchange method for field, or field is a
|
||||
# dependency of any field in other_fields
|
||||
return field.name in self._onchange_methods or \
|
||||
any(dep in other_fields for dep in field.dependents)
|
||||
any(dep in other_fields for dep, _ in self._field_triggers[field])
|
||||
|
||||
@api.model
|
||||
def _onchange_spec(self, view_info=None):
|
||||
|
||||
@@ -1064,6 +1064,24 @@ class frozendict(dict):
|
||||
def update(self, *args, **kwargs):
|
||||
raise NotImplementedError("'update' not supported on frozendict")
|
||||
|
||||
class Collector(Mapping):
|
||||
""" A mapping from keys to lists. This is essentially a space optimization
|
||||
for ``defaultdict(list)``.
|
||||
"""
|
||||
__slots__ = ['_map']
|
||||
def __init__(self):
|
||||
self._map = {}
|
||||
def add(self, key, val):
|
||||
vals = self._map.setdefault(key, [])
|
||||
if val not in vals:
|
||||
vals.append(val)
|
||||
def __getitem__(self, key):
|
||||
return self._map.get(key, ())
|
||||
def __iter__(self):
|
||||
return iter(self._map)
|
||||
def __len__(self):
|
||||
return len(self._map)
|
||||
|
||||
class OrderedSet(OrderedDict):
|
||||
""" A simple collection that remembers the elements insertion order. """
|
||||
def __init__(self, seq=()):
|
||||
|
||||
Reference in New Issue
Block a user