changeset e7526f0686ec in trytond:default
details: https://hg.tryton.org/trytond?cmd=changeset&node=e7526f0686ec
description:
Use immutable datastructures for Dict and MultiSelection fields
In case a Dict field is sent during an on_change, modification of its
value
won't be detected because a shallow copy will keep its reference in the
pseudo-record
issue10105
review347541002
diffstat:
CHANGELOG | 1 +
trytond/model/fields/dict.py | 26 +++++++++++++++++++++++++-
trytond/model/fields/multiselection.py | 7 ++++++-
trytond/tests/modelview.py | 13 ++++++++++++-
trytond/tests/test_field_dict.py | 13 +++++++++++++
trytond/tests/test_field_multiselection.py | 14 +++++++-------
trytond/tests/test_modelstorage.py | 2 +-
trytond/tests/test_modelview.py | 18 +++++++++++++++++-
8 files changed, 82 insertions(+), 12 deletions(-)
diffs (267 lines):
diff -r 7a4a537f0f87 -r e7526f0686ec CHANGELOG
--- a/CHANGELOG Mon Apr 12 17:44:16 2021 +0200
+++ b/CHANGELOG Mon Apr 12 19:39:25 2021 +0200
@@ -1,3 +1,4 @@
+* Use immutable datastructures for Dict and MultiSelection fields
* Skip warnings for non-interactive operations
* Check rule only if _check_access is set
* Add statistics to Cache
diff -r 7a4a537f0f87 -r e7526f0686ec trytond/model/fields/dict.py
--- a/trytond/model/fields/dict.py Mon Apr 12 17:44:16 2021 +0200
+++ b/trytond/model/fields/dict.py Mon Apr 12 19:39:25 2021 +0200
@@ -17,6 +17,25 @@
json.dumps, cls=JSONEncoder, separators=(',', ':'), sort_keys=True)
+class ImmutableDict(dict):
+
+ __slots__ = ()
+
+ def _not_allowed(cls, *args, **kwargs):
+ raise TypeError("Operation not allowed on ImmutableDict")
+
+ __setitem__ = _not_allowed
+ __delitem__ = _not_allowed
+ __ior__ = _not_allowed
+ clear = _not_allowed
+ pop = _not_allowed
+ popitem = _not_allowed
+ setdefault = _not_allowed
+ update = _not_allowed
+
+ del _not_allowed
+
+
class Dict(Field):
'Define dict field.'
_type = 'dict'
@@ -41,7 +60,7 @@
# If stored as JSON conversion is done on backend
if isinstance(data, str):
data = json.loads(data, object_hook=JSONDecoder())
- dicts[value['id']] = data
+ dicts[value['id']] = ImmutableDict(data)
return dicts
def sql_format(self, value):
@@ -57,6 +76,11 @@
value = dumps(d)
return value
+ def __set__(self, inst, value):
+ if value:
+ value = ImmutableDict(value)
+ super().__set__(inst, value)
+
def translated(self, name=None, type_='values'):
"Return a descriptor for the translated value of the field"
if name is None:
diff -r 7a4a537f0f87 -r e7526f0686ec trytond/model/fields/multiselection.py
--- a/trytond/model/fields/multiselection.py Mon Apr 12 17:44:16 2021 +0200
+++ b/trytond/model/fields/multiselection.py Mon Apr 12 19:39:25 2021 +0200
@@ -61,7 +61,7 @@
# If stored as JSON conversion is done on backend
if isinstance(data, str):
data = json.loads(data)
- lists[value['id']] = data
+ lists[value['id']] = tuple(data)
return lists
def sql_format(self, value):
@@ -70,6 +70,11 @@
value = dumps(sorted(set(value)))
return value
+ def __set__(self, inst, value):
+ if value:
+ value = tuple(value)
+ super().__set__(inst, value)
+
def _domain_column(self, operator, column):
database = Transaction().database
return database.json_get(super()._domain_column(operator, column))
diff -r 7a4a537f0f87 -r e7526f0686ec trytond/tests/modelview.py
--- a/trytond/tests/modelview.py Mon Apr 12 17:44:16 2021 +0200
+++ b/trytond/tests/modelview.py Mon Apr 12 19:39:25 2021 +0200
@@ -1,7 +1,7 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
-from trytond.model import ModelView, ModelSQL, fields
+from trytond.model import ModelView, ModelSQL, DictSchemaMixin, fields
from trytond.pool import Pool
from trytond.pyson import If, Eval
@@ -20,6 +20,16 @@
'Targets')
m2m_targets = fields.Many2Many('test.modelview.changed_values.target',
None, None, 'Targets')
+ multiselection = fields.MultiSelection([
+ ('a', 'A'), ('b', 'B'),
+ ], "MultiSelection")
+ dictionary = fields.Dict(
+ 'test.modelview.changed_values.dictionary', "Dictionary")
+
+
+class ModelViewChangedValuesDictSchema(DictSchemaMixin, ModelSQL):
+ 'ModelView Changed Values Dict Schema'
+ __name__ = 'test.modelview.changed_values.dictionary'
class ModelViewChangedValuesTarget(ModelView):
@@ -253,6 +263,7 @@
def register(module):
Pool.register(
ModelViewChangedValues,
+ ModelViewChangedValuesDictSchema,
ModelViewChangedValuesTarget,
ModelViewChangedValuesStoredTarget,
ModelViewStoredChangedValues,
diff -r 7a4a537f0f87 -r e7526f0686ec trytond/tests/test_field_dict.py
--- a/trytond/tests/test_field_dict.py Mon Apr 12 17:44:16 2021 +0200
+++ b/trytond/tests/test_field_dict.py Mon Apr 12 19:39:25 2021 +0200
@@ -886,6 +886,19 @@
self.assertDictEqual(
dict_.dico_string_keys, {'a': 'A', 'type': "Type"})
+ @with_transaction
+ def test_set_key(self):
+ "Test setting a key of dict"
+ Dict = Pool().get('test.dict')
+ self.create_schema()
+
+ dict_, = Dict.create([{
+ 'dico': {'a': 1, 'type': 'arabic'},
+ }])
+
+ with self.assertRaises(TypeError):
+ dict.dico['a'] = 5
+
@unittest.skipUnless(backend.name == 'postgresql',
"unaccent works only on postgresql")
diff -r 7a4a537f0f87 -r e7526f0686ec trytond/tests/test_field_multiselection.py
--- a/trytond/tests/test_field_multiselection.py Mon Apr 12 17:44:16
2021 +0200
+++ b/trytond/tests/test_field_multiselection.py Mon Apr 12 19:39:25
2021 +0200
@@ -28,7 +28,7 @@
'selects': None,
}])
- self.assertEqual(selection.selects, ['bar', 'foo'])
+ self.assertEqual(selection.selects, ('bar', 'foo'))
self.assertEqual(selection_none.selects, None)
@with_transaction()
@@ -38,7 +38,7 @@
with self.assertRaises(SelectionValidationError):
Selection.create([{
- 'selects': ['invalid'],
+ 'selects': ('invalid'),
}])
@with_transaction()
@@ -54,8 +54,8 @@
'dyn_selects': ['baz'],
}])
- self.assertEqual(selection_foo.dyn_selects, ['foo'])
- self.assertEqual(selection_bar.dyn_selects, ['baz'])
+ self.assertEqual(selection_foo.dyn_selects, ('foo',))
+ self.assertEqual(selection_bar.dyn_selects, ('baz',))
@with_transaction()
def test_create_dynamic_none(self):
@@ -89,7 +89,7 @@
'static_selects': ['foo', 'bar'],
}])
- self.assertEqual(selection.static_selects, ['bar', 'foo'])
+ self.assertEqual(selection.static_selects, ('bar', 'foo'))
@with_transaction()
def test_create_static_none(self):
@@ -121,7 +121,7 @@
'selects': ['foo', 'bar'],
}])
- self.assertEqual(selection.selects, ['bar', 'foo'])
+ self.assertEqual(selection.selects, ('bar', 'foo'))
@with_transaction()
def test_create_required_without_value(self):
@@ -163,7 +163,7 @@
'selects': ['foo', 'bar'],
})
- self.assertEqual(selection.selects, ['bar', 'foo'])
+ self.assertEqual(selection.selects, ('bar', 'foo'))
@with_transaction()
def test_string(self):
diff -r 7a4a537f0f87 -r e7526f0686ec trytond/tests/test_modelstorage.py
--- a/trytond/tests/test_modelstorage.py Mon Apr 12 17:44:16 2021 +0200
+++ b/trytond/tests/test_modelstorage.py Mon Apr 12 19:39:25 2021 +0200
@@ -638,7 +638,7 @@
record = Model(multiselection=['value1', 'value2'])
env = EvalEnvironment(record, Model)
- self.assertEqual(env.get('multiselection'), ['value1', 'value2'])
+ self.assertEqual(env.get('multiselection'), ('value1', 'value2'))
@with_transaction()
def test_parent_field(self):
diff -r 7a4a537f0f87 -r e7526f0686ec trytond/tests/test_modelview.py
--- a/trytond/tests/test_modelview.py Mon Apr 12 17:44:16 2021 +0200
+++ b/trytond/tests/test_modelview.py Mon Apr 12 19:39:25 2021 +0200
@@ -34,6 +34,8 @@
record.target = Target(1)
record.ref_target = Target(2)
record.targets = [Target(name='bar')]
+ record.multiselection = ['a']
+ record.dictionary = {'key': 'value'}
self.assertEqual(record._changed_values, {
'name': 'foo',
'target': 1,
@@ -43,12 +45,18 @@
(0, {'id': None, 'name': 'bar'}),
],
},
+ 'multiselection': ('a',),
+ 'dictionary': {'key': 'value'},
})
record = Model(name='test', target=1, targets=[
{'id': 1, 'name': 'foo'},
{'id': 2},
- ], m2m_targets=[5, 6, 7])
+ ],
+ m2m_targets=[5, 6, 7],
+ multiselection=['a'],
+ dictionary={'key': 'value'},
+ )
self.assertEqual(record._changed_values, {})
@@ -56,6 +64,12 @@
target.name = 'bar'
record.targets = [target]
record.m2m_targets = [Target(9), Target(10)]
+ ms = list(record.multiselection)
+ ms.append('b')
+ record.multiselection = ms
+ dico = record.dictionary.copy()
+ dico['key'] = 'another value'
+ record.dictionary = dico
self.assertEqual(record._changed_values, {
'targets': {
'update': [{'id': 1, 'name': 'bar'}],
@@ -65,6 +79,8 @@
'remove': [5, 6, 7],
'add': [(0, {'id': 9}), (1, {'id': 10})],
},
+ 'multiselection': ('a', 'b'),
+ 'dictionary': {'key': 'another value'},
})
# change only one2many record