diff --git a/odoo/addons/test_new_api/tests/test_new_fields.py b/odoo/addons/test_new_api/tests/test_new_fields.py index cf3efe50040..62043b85339 100644 --- a/odoo/addons/test_new_api/tests/test_new_fields.py +++ b/odoo/addons/test_new_api/tests/test_new_fields.py @@ -219,18 +219,18 @@ class TestFields(TransactionCaseWithUserDemo): invalid_depends = fields["x_computed_custom_invalid_depends"] invalid_transitive_depends = fields["x_computed_custom_invalid_transitive_depends"] # `x_computed_custom_valid_depends` in the triggers of the field `value1` - self.assertTrue(valid_depends in get_trigger_tree([value1])[None]) + self.assertTrue(valid_depends in get_trigger_tree([value1]).root) # `x_computed_custom_valid_transitive_depends` in the triggers `x_computed_custom_valid_depends` and `value1` - self.assertTrue(valid_transitive_depends in get_trigger_tree([valid_depends])[None]) - self.assertTrue(valid_transitive_depends in get_trigger_tree([value1])[None]) + self.assertTrue(valid_transitive_depends in get_trigger_tree([valid_depends]).root) + self.assertTrue(valid_transitive_depends in get_trigger_tree([value1]).root) # `x_computed_custom_invalid_depends` not in any triggers, as it was invalid and was skipped self.assertEqual( - sum(invalid_depends in get_trigger_tree([field]).get(None, ()) for field in fields.values()), 0 + sum(invalid_depends in get_trigger_tree([field]).root for field in fields.values()), 0 ) # `x_computed_custom_invalid_transitive_depends` in the triggers of `x_computed_custom_invalid_depends` only - self.assertTrue(invalid_transitive_depends in get_trigger_tree([invalid_depends])[None]) + self.assertTrue(invalid_transitive_depends in get_trigger_tree([invalid_depends]).root) self.assertEqual( - sum(invalid_transitive_depends in get_trigger_tree([field]).get(None, ()) for field in fields.values()), 1 + sum(invalid_transitive_depends in get_trigger_tree([field]).root for field in fields.values()), 1 ) @mute_logger('odoo.fields') diff --git a/odoo/models.py b/odoo/models.py index 17d9890ab67..783a3b09cff 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -6026,14 +6026,11 @@ class BaseModel(metaclass=MetaModel): return # first yield what to compute - for field in tree.get(None, ()): + for field in tree.root: yield field, self, create # then traverse dependencies backwards, and proceed recursively for field, subtree in tree.items(): - if field is None: - continue - if create and field.type in ('many2one', 'many2one_reference'): # upon creation, no other record has a reference to self continue diff --git a/odoo/modules/registry.py b/odoo/modules/registry.py index 19ad8b9f904..be0cc51b3bd 100644 --- a/odoo/modules/registry.py +++ b/odoo/modules/registry.py @@ -332,7 +332,7 @@ class Registry(Mapping): model_name, ", ".join(field.name for field in fields)) return computed - def get_trigger_tree(self, fields: list, select=bool): + def get_trigger_tree(self, fields: list, select=bool) -> "TriggerTree": """ Return the trigger tree to traverse when ``fields`` have been modified. The function ``select`` is called on every field to determine which fields should be kept in the tree nodes. This enables to discard some unnecessary @@ -343,22 +343,18 @@ class Registry(Mapping): for field in fields if field in self._field_triggers ] - if not trees: - return {} - return merge_trigger_trees(trees, select) + return TriggerTree.merge(trees, select) def get_dependent_fields(self, field): """ Return an iterable on the fields that depend on ``field``. """ if field not in self._field_triggers: return () - def traverse(tree): - for key, val in tree.items(): - if key is None: - yield from val - else: - yield from traverse(val) - return traverse(self.get_field_trigger_tree(field)) + return ( + dependent + for tree in self.get_field_trigger_tree(field).depth_first() + for dependent in tree.root + ) def _discard_fields(self, fields: list): """ Discard the given fields from the registry's internal data structures. """ @@ -371,7 +367,7 @@ class Registry(Mapping): # discard fields from field inverses self.field_inverses.discard_keys_and_values(fields) - def get_field_trigger_tree(self, field): + def get_field_trigger_tree(self, field) -> "TriggerTree": """ Return the trigger tree of a field by computing it from the transitive closure of field triggers. """ @@ -383,7 +379,7 @@ class Registry(Mapping): triggers = self._field_triggers if field not in triggers: - return {} + return TriggerTree() def transitive_triggers(field, prefix=(), seen=()): if field in seen or field not in triggers: @@ -406,18 +402,15 @@ class Registry(Mapping): return concat(seq1[:-1], seq2[1:]) return seq1 + seq2 - def Tree(): - return defaultdict(Tree) - - tree = Tree() + tree = TriggerTree() for path, targets in transitive_triggers(field): current = tree for label in path: - current = current[label] - if None in current: - current[None].update(targets) + current = current.increase(label) + if current.root: + current.root.update(targets) else: - current[None] = OrderedSet(targets) + current.root = OrderedSet(targets) self._field_trigger_trees[field] = tree @@ -865,30 +858,68 @@ class DummyRLock(object): self.release() -def merge_trigger_trees(trees: list, select=bool) -> dict: - """ Merge trigger trees list into a final tree. The function ``select`` is - called on every field to determine which fields should be kept in the tree - nodes. This enables to discard some fields from the tree nodes. +class TriggerTree(dict): + """ The triggers of a field F is a tree that contains the fields that + depend on F, together with the fields to inverse to find out which records + to recompute. + + For instance, assume that G depends on F, H depends on X.F, I depends on + W.X.F, and J depends on Y.F. The triggers of F will be the tree: + + [G] + X/ \\Y + [H] [J] + W/ + [I] + + This tree provides perfect support for the trigger mechanism: + when F is # modified on records, + - mark G to recompute on records, + - mark H to recompute on inverse(X, records), + - mark I to recompute on inverse(W, inverse(X, records)), + - mark J to recompute on inverse(Y, records). """ - result_tree = {} # the resulting tree - root_fields = OrderedSet() # the fields in the root node - subtrees_to_merge = defaultdict(list) # the subtrees to merge grouped by key + __slots__ = ['root'] - for tree in trees: - for key, val in tree.items(): - if key is None: - root_fields.update(val) - else: - subtrees_to_merge[key].append(val) + # pylint: disable=keyword-arg-before-vararg + def __init__(self, root=(), *args, **kwargs): + super().__init__(*args, **kwargs) + self.root = root - # the root node contains the collected fields for which select is true - root_node = [field for field in root_fields if select(field)] - if root_node: - result_tree[None] = root_node + def __bool__(self): + return bool(self.root or len(self)) - for key, subtrees in subtrees_to_merge.items(): - subtree = merge_trigger_trees(subtrees, select) - if subtree: - result_tree[key] = subtree + def increase(self, key): + try: + return self[key] + except KeyError: + subtree = self[key] = TriggerTree() + return subtree - return result_tree + def depth_first(self): + yield self + for subtree in self.values(): + yield from subtree.depth_first() + + @classmethod + def merge(cls, trees: list, select=bool) -> "TriggerTree": + """ Merge trigger trees into a single tree. The function ``select`` is + called on every field to determine which fields should be kept in the + tree nodes. This enables to discard some fields from the tree nodes. + """ + root_fields = OrderedSet() # fields in the root node + subtrees_to_merge = defaultdict(list) # subtrees to merge grouped by key + + for tree in trees: + root_fields.update(tree.root) + for label, subtree in tree.items(): + subtrees_to_merge[label].append(subtree) + + # the root node contains the collected fields for which select is true + result = cls([field for field in root_fields if select(field)]) + for label, subtrees in subtrees_to_merge.items(): + subtree = cls.merge(subtrees, select) + if subtree: + result[label] = subtree + + return result