changeset 22260a3783da in trytond:default
details: https://hg.tryton.org/trytond?cmd=changeset;node=22260a3783da
description:
Use custom class to store record data
By using a class with slots instead of standard dict, we can reduce a
lot the
memory usage to store the record data in the caches and values.
Also we can rely on the data in the cache to exist only for real field
which
can be used to create a fast path in ModelStorage.__getattr__.
As now storing data in the cache has no extra cost, we can store data
from the
transactional cache in the local. This allow also to have a shorter
path in
ModelStorage.__getattr__.
For more memory efficiency, we remove the record from the cache instead
of
clearing them. It was better to clear a dict but a record is less
expensive to
re-instantiate than clearing.
issue9369
issue9225
review297861002
diffstat:
CHANGELOG | 1 +
doc/ref/models/models.rst | 11 ++
trytond/cache.py | 15 +++-
trytond/model/fields/field.py | 5 +-
trytond/model/model.py | 131 ++++++++++++++++++++++++++++++++-
trytond/model/modelsql.py | 9 +-
trytond/model/modelstorage.py | 75 +++++++++---------
trytond/model/modelview.py | 10 +-
trytond/modules/__init__.py | 2 +
trytond/res/user.py | 3 +-
trytond/tests/modelsql.py | 7 +
trytond/tests/test_cache.py | 86 +++++++++++++++++++++-
trytond/tests/test_model.py | 157 +++++++++++++++++++++++++++++++++++++++-
trytond/tests/test_modelsql.py | 14 +++
trytond/tests/test_modelview.py | 2 +-
15 files changed, 465 insertions(+), 63 deletions(-)
diffs (902 lines):
diff -r 183fea18d0fe -r 22260a3783da CHANGELOG
--- a/CHANGELOG Fri Jun 12 12:37:50 2020 +0200
+++ b/CHANGELOG Sat Jun 13 09:48:03 2020 +0200
@@ -1,3 +1,4 @@
+* Use custom class to store record data
* Add configuration check
* Add support for properties to fields.depends
* Allow combining authentication methods together
diff -r 183fea18d0fe -r 22260a3783da doc/ref/models/models.rst
--- a/doc/ref/models/models.rst Fri Jun 12 12:37:50 2020 +0200
+++ b/doc/ref/models/models.rst Sat Jun 13 09:48:03 2020 +0200
@@ -36,6 +36,17 @@
It returns a queue caller for the model. The called method will be pushed
into the queue.
+.. attribute:: Model._fields
+ It contains a dictionary with the field name as key and its
+ :class:`~trytond.model.field` instance as value.
+
+.. attribute:: Model._record
+ It stores the record class to store internaly the values of the instances.
+
+.. attribute:: Model._defaults
+ It contains a dictionary with the field name as key and its default method
+ as value.
+
Class methods:
.. classmethod:: Model.__setup__()
diff -r 183fea18d0fe -r 22260a3783da trytond/cache.py
--- a/trytond/cache.py Fri Jun 12 12:37:50 2020 +0200
+++ b/trytond/cache.py Sat Jun 13 09:48:03 2020 +0200
@@ -18,7 +18,7 @@
from trytond.transaction import Transaction
from trytond.tools import resolve, grouped_slice
-__all__ = ['BaseCache', 'Cache', 'LRUDict']
+__all__ = ['BaseCache', 'Cache', 'LRUDict', 'LRUDictTransaction']
_clear_timeout = config.getint('cache', 'clean_timeout', default=5 * 60)
logger = logging.getLogger(__name__)
@@ -331,19 +331,28 @@
"""
Dictionary with a size limit.
If size limit is reached, it will remove the first added items.
+ The default_factory provides the same behavior as in standard
+ collections.defaultdict.
"""
__slots__ = ('size_limit',)
- def __init__(self, size_limit, *args, **kwargs):
+ def __init__(self, size_limit, default_factory=None, *args, **kwargs):
assert size_limit > 0
self.size_limit = size_limit
super(LRUDict, self).__init__(*args, **kwargs)
+ self.default_factory = default_factory
self._check_size_limit()
def __setitem__(self, key, value):
super(LRUDict, self).__setitem__(key, value)
self._check_size_limit()
+ def __missing__(self, key):
+ if self.default_factory is None:
+ raise KeyError(key)
+ self[key] = value = self.default_factory()
+ return value
+
def update(self, *args, **kwargs):
super(LRUDict, self).update(*args, **kwargs)
self._check_size_limit()
@@ -360,7 +369,7 @@
class LRUDictTransaction(LRUDict):
"""
- Dictionary with a size limit. (see LRUDict)
+ Dictionary with a size limit and default_factory. (see LRUDict)
It is refreshed when transaction counter is changed.
"""
__slots__ = ('transaction', 'counter')
diff -r 183fea18d0fe -r 22260a3783da trytond/model/fields/field.py
--- a/trytond/model/fields/field.py Fri Jun 12 12:37:50 2020 +0200
+++ b/trytond/model/fields/field.py Sat Jun 13 09:48:03 2020 +0200
@@ -142,7 +142,8 @@
kwargs = {}
ids = []
if issubclass(Target, ModelStorage):
- kwargs['_local_cache'] = LRUDictTransaction(cache_size())
+ kwargs['_local_cache'] = LRUDictTransaction(
+ cache_size(), Target._record)
kwargs['_ids'] = ids
def instance(data):
@@ -338,7 +339,7 @@
def __set__(self, inst, value):
assert self.name is not None
if inst._values is None:
- inst._values = {}
+ inst._values = inst._record()
inst._values[self.name] = value
def sql_format(self, value):
diff -r 183fea18d0fe -r 22260a3783da trytond/model/model.py
--- a/trytond/model/model.py Fri Jun 12 12:37:50 2020 +0200
+++ b/trytond/model/model.py Sat Jun 13 09:48:03 2020 +0200
@@ -2,8 +2,11 @@
# this repository contains the full copyright notices and license terms.
import copy
+import collections.abc
+import sys
from collections import defaultdict
from functools import total_ordering
+from itertools import chain
from trytond.i18n import lazy_gettext
from trytond.model import fields
@@ -71,6 +74,7 @@
continue
if isinstance(getattr(cls, attr), fields.Field):
cls._fields[attr] = getattr(cls, attr)
+ cls._record = record(cls.__name__ + '._record', cls._fields.keys())
# Set _defaults
cls._defaults = {}
@@ -248,7 +252,7 @@
self._id = id
self._deleted = self._removed = None
if kwargs:
- self._values = {}
+ self._values = self._record()
parent_values = defaultdict(dict)
has_context = {}
for name, value in kwargs.items():
@@ -268,7 +272,7 @@
# to ensure it was evaluated with all the fields
for name, value in has_context.items():
setattr(self, name, value)
- self._init_values = self._values.copy()
+ self._init_values = self._values._copy()
else:
self._values = None
self._init_values = None
@@ -328,7 +332,7 @@
"""
values = {}
if self._values:
- for fname, value in self._values.items():
+ for fname, value in self._values._items():
field = self._fields[fname]
if field._type in ('many2one', 'one2one', 'reference'):
if value:
@@ -343,3 +347,124 @@
value = [r.id for r in value]
values[fname] = value
return values
+
+
+def record(name, field_names):
+ def __init__(self, **kwargs):
+ for key, value in kwargs.items():
+ setattr(self, key, value)
+
+ def _getitem(self, field):
+ try:
+ return getattr(self, field)
+ except AttributeError:
+ raise KeyError(field)
+
+ def _setitem(self, field, value):
+ try:
+ return setattr(self, field, value)
+ except AttributeError:
+ raise KeyError(field)
+
+ def _contains(self, field):
+ try:
+ getattr(self, field)
+ return True
+ except AttributeError:
+ return False
+
+ def _clear(self):
+ for fname in self.__slots__:
+ try:
+ delattr(self, fname)
+ except AttributeError:
+ pass
+
+ def _copy(self):
+ return copy.copy(self)
+
+ def _get(self, field, default=None):
+ if field not in self.__slots__:
+ raise KeyError(field)
+ return getattr(self, field, default)
+
+ def _keys(self):
+ for fname in self.__slots__:
+ if hasattr(self, fname):
+ yield fname
+
+ def _items(self):
+ for fname in self.__slots__:
+ try:
+ yield fname, getattr(self, fname)
+ except AttributeError:
+ pass
+
+ _undefined = object()
+
+ def _pop(self, field, value=_undefined):
+ if field not in self.__slots__:
+ raise KeyError(field)
+ if value != _undefined:
+ value = getattr(self, field, value)
+ else:
+ try:
+ value = getattr(self, field)
+ except AttributeError:
+ raise KeyError(field)
+ try:
+ delattr(self, field)
+ except AttributeError:
+ pass
+ return value
+
+ def _popitem(self, field, value=_undefined):
+ return (field, self._pop(field, value=value))
+
+ def _setdefault(self, field, default=None):
+ try:
+ return getattr(self, field)
+ except AttributeError:
+ setattr(self, field, default)
+ return default
+
+ def _update(self, _other=None, **kwargs):
+ if isinstance(_other, collections.abc.Mapping):
+ _other = _other.items()
+ elif _other is None:
+ _other = []
+ chained = chain(_other, kwargs.items())
+ for key, value in chained:
+ setattr(self, key, value)
+
+ def _values(self):
+ for fname in self.__slots__:
+ try:
+ yield getattr(self, fname)
+ except AttributeError:
+ pass
+
+ field_names = set(field_names)
+ for fname in field_names:
+ if fname.startswith('_'):
+ raise ValueError(
+ "Field names cannot start with an underscore: %r" % name)
+ field_names = tuple(map(sys.intern, field_names))
+ type_dict = {
+ '__slots__': field_names,
+ '__init__': __init__,
+ '__getitem__': _getitem,
+ '__setitem__': _setitem,
+ '__contains__': _contains,
+ '_clear': _clear,
+ '_copy': _copy,
+ '_get': _get,
+ '_keys': _keys,
+ '_items': _items,
+ '_pop': _pop,
+ '_popitem': _popitem,
+ '_setdefault': _setdefault,
+ '_update': _update,
+ '_values': _values,
+ }
+ return type(name, (), type_dict)
diff -r 183fea18d0fe -r 22260a3783da trytond/model/modelsql.py
--- a/trytond/model/modelsql.py Fri Jun 12 12:37:50 2020 +0200
+++ b/trytond/model/modelsql.py Sat Jun 13 09:48:03 2020 +0200
@@ -794,10 +794,8 @@
if getter_fields and cachable_fields:
cache = transaction.get_cache().setdefault(
- cls.__name__, LRUDict(cache_size()))
+ cls.__name__, LRUDict(cache_size(), cls._record))
for row in result:
- if row['id'] not in cache:
- cache[row['id']] = {}
for fname in cachable_fields:
cache[row['id']][fname] = row[fname]
@@ -1319,7 +1317,7 @@
rows = list(cursor_dict(cursor, transaction.database.IN_MAX))
cache = transaction.get_cache()
if cls.__name__ not in cache:
- cache[cls.__name__] = LRUDict(cache_size())
+ cache[cls.__name__] = LRUDict(cache_size(), cls._record)
delete_records = transaction.delete_records.setdefault(cls.__name__,
set())
@@ -1375,7 +1373,6 @@
keys = list(data.keys())
for k in keys[:]:
if k in ('_timestamp', '_datetime', '__id'):
- keys.remove(k)
continue
field = cls._fields[k]
if not getattr(field, 'datetime_field', None):
@@ -1383,7 +1380,7 @@
continue
for k in keys:
del data[k]
- cache[cls.__name__].setdefault(data['id'], {}).update(data)
+ cache[cls.__name__][data['id']]._update(data)
if len(rows) >= transaction.database.IN_MAX:
if (cls._history
diff -r 183fea18d0fe -r 22260a3783da trytond/model/modelstorage.py
--- a/trytond/model/modelstorage.py Fri Jun 12 12:37:50 2020 +0200
+++ b/trytond/model/modelstorage.py Sat Jun 13 09:48:03 2020 +0200
@@ -211,16 +211,14 @@
# Clean local cache
for record in all_records:
- local_cache = record._local_cache.get(record.id)
- if local_cache:
- local_cache.clear()
+ record._local_cache.pop(record.id, None)
# Clean transaction cache
for cache in Transaction().cache.values():
if cls.__name__ in cache:
+ cache_cls = cache[cls.__name__]
for record in all_records:
- if record.id in cache[cls.__name__]:
- cache[cls.__name__][record.id].clear()
+ cache_cls.pop(record.id, None)
@classmethod
@without_check_access
@@ -597,7 +595,7 @@
'''
transaction = Transaction()
ids = list(map(int, ids))
- local_cache = LRUDictTransaction(cache_size())
+ local_cache = LRUDictTransaction(cache_size(), cls._record)
transaction_cache = transaction.get_cache()
return [cls(x, _ids=ids,
_local_cache=local_cache,
@@ -1385,7 +1383,7 @@
assert isinstance(_local_cache, LRUDictTransaction)
self._local_cache = _local_cache
else:
- self._local_cache = LRUDictTransaction(cache_size())
+ self._local_cache = LRUDictTransaction(cache_size(), self._record)
super(ModelStorage, self).__init__(id, **kwargs)
@@ -1393,18 +1391,23 @@
def _cache(self):
cache = self._transaction_cache
if self.__name__ not in cache:
- cache[self.__name__] = LRUDict(cache_size())
+ cache[self.__name__] = LRUDict(cache_size(), self._record)
return cache[self.__name__]
def __getattr__(self, name):
try:
return super(ModelStorage, self).__getattr__(name)
except AttributeError:
- if self.id is None or self.id < 0:
+ if self.id is None or self.id < 0 or name.startswith('_'):
raise
self._local_cache.refresh()
+ try:
+ return self._local_cache[self.id][name]
+ except KeyError:
+ pass
+
# fetch the definition of the field
try:
field = self._fields[name]
@@ -1412,12 +1415,12 @@
raise AttributeError('"%s" has no attribute "%s"' % (self, name))
try:
- return self._local_cache[self.id][name]
- except KeyError:
- pass
- try:
if field._type not in ('many2one', 'reference'):
- return self._cache[self.id][name]
+ # fill local cache for quicker access later
+ value \
+ = self._local_cache[self.id][name] \
+ = self._cache[self.id][name]
+ return value
else:
skip_eager = name in self._cache[self.id]
except KeyError:
@@ -1437,8 +1440,10 @@
def not_cached(item):
fname, field = item
- return (fname not in self._cache.get(self.id, {})
- and fname not in self._local_cache.get(self.id, {}))
+ return ((self.id not in self._cache
+ or fname not in self._cache[self.id])
+ and (self.id not in self._local_cache
+ or fname not in self._local_cache[self.id]))
def to_load(item):
fname, field = item
@@ -1477,8 +1482,11 @@
def filter_(id_):
return (id_ == self.id # Ensure the value is read
- or (name not in self._cache.get(id_, {})
- and name not in self._local_cache.get(id_, {})))
+ or ((
+ id_ not in self._cache
+ or name not in self._cache[id_])
+ and (id_ not in self._local_cache
+ or name not in self._local_cache[id_])))
def unique(ids):
s = set()
@@ -1527,7 +1535,7 @@
kwargs = {}
key = (Model, freeze(ctx))
kwargs['_local_cache'] = model2cache.setdefault(key,
- LRUDictTransaction(cache_size()))
+ LRUDictTransaction(cache_size(), Model._record))
kwargs['_ids'] = ids = model2ids.setdefault(key, [])
kwargs['_transaction_cache'] = transaction.get_cache()
kwargs['_transaction'] = transaction
@@ -1560,28 +1568,23 @@
read_data = self.read(list(ids), list(ffields.keys()))
# create browse records for 'remote' models
for data in read_data:
+ id_ = data['id']
for fname, field in ffields.items():
fvalue = data[fname]
- if field._type in ('many2one', 'one2one', 'one2many',
- 'many2many', 'reference'):
+ if field._type in {
+ 'many2one', 'one2one', 'one2many', 'many2many',
+ 'reference'}:
fvalue = instantiate(field, data[fname], data)
- if data['id'] == self.id and fname == name:
+ if id_ == self.id and fname == name:
value = fvalue
- if (field._type not in ('many2one', 'one2one', 'one2many',
- 'many2many', 'reference', 'binary')
- and not isinstance(field, fields.Function)):
- continue
- if data['id'] not in self._local_cache:
- self._local_cache[data['id']] = {}
- self._local_cache[data['id']][fname] = fvalue
- if (field._type not in ('many2one', 'reference')
+ self._local_cache[id_][fname] = fvalue
+ if (field._type in {
+ 'one2one', 'one2many', 'many2many', 'binary'}
or field.context
or getattr(field, 'datetime_field', None)
or isinstance(field, fields.Function)):
del data[fname]
- if data['id'] not in self._cache:
- self._cache[data['id']] = {}
- self._cache[data['id']].update(data)
+ self._cache[id_]._update(data)
return value
@property
@@ -1589,7 +1592,7 @@
values = {}
if not self._values:
return values
- for fname, value in self._values.items():
+ for fname, value in self._values._items():
field = self._fields[fname]
if isinstance(field, fields.Function) and not field.setter:
continue
@@ -1619,9 +1622,9 @@
if (field._type == 'one2many'
and field.field
and target._values):
- t_values = target._values.copy()
+ t_values = target._values._copy()
# Don't look at reverse field
- target._values.pop(field.field, None)
+ target._values._pop(field.field, None)
else:
t_values = None
try:
diff -r 183fea18d0fe -r 22260a3783da trytond/model/modelview.py
--- a/trytond/model/modelview.py Fri Jun 12 12:37:50 2020 +0200
+++ b/trytond/model/modelview.py Sat Jun 13 09:48:03 2020 +0200
@@ -795,10 +795,10 @@
"""
from .modelstorage import ModelStorage
changed = {}
- init_values = self._init_values or {}
+ init_values = self._init_values or self._record()
if not self._values:
return changed
- for fname, value in self._values.items():
+ for fname, value in self._values._items():
field = self._fields[fname]
# Always test key presence in case value is None
if (fname in init_values
@@ -820,7 +820,7 @@
value = value.id
elif field._type in ['one2many', 'many2many']:
targets = value
- init_targets = list(init_values.get(
+ init_targets = list(init_values._get(
fname, targets if field._type == 'one2many' else []))
value = collections.defaultdict(list)
previous = [t.id for t in init_targets if t.id]
@@ -828,9 +828,9 @@
if (field._type == 'one2many'
and field.field
and target._values):
- t_values = target._values.copy()
+ t_values = target._values._copy()
# Don't look at reverse field
- target._values.pop(field.field, None)
+ target._values._pop(field.field, None)
else:
t_values = None
try:
diff -r 183fea18d0fe -r 22260a3783da trytond/modules/__init__.py
--- a/trytond/modules/__init__.py Fri Jun 12 12:37:50 2020 +0200
+++ b/trytond/modules/__init__.py Sat Jun 13 09:48:03 2020 +0200
@@ -274,6 +274,8 @@
# Avoid clearing cache to prevent dead lock on ir.cache table
Cache.rollback(transaction)
transaction.commit()
+ # Clear transaction cache to update default_factory
+ transaction.cache.clear()
if not update:
pool.setup()
diff -r 183fea18d0fe -r 22260a3783da trytond/res/user.py
--- a/trytond/res/user.py Fri Jun 12 12:37:50 2020 +0200
+++ b/trytond/res/user.py Sat Jun 13 09:48:03 2020 +0200
@@ -384,8 +384,7 @@
for cache in Transaction().cache.values():
if cls.__name__ in cache:
for user in all_users:
- if user.id in cache[cls.__name__]:
- cache[cls.__name__][user.id].clear()
+ cache[cls.__name__].pop(user.id, None)
# Restart the cache for domain_get method
pool = Pool()
pool.get('ir.rule')._domain_get_cache.clear()
diff -r 183fea18d0fe -r 22260a3783da trytond/tests/modelsql.py
--- a/trytond/tests/modelsql.py Fri Jun 12 12:37:50 2020 +0200
+++ b/trytond/tests/modelsql.py Sat Jun 13 09:48:03 2020 +0200
@@ -76,6 +76,12 @@
origin = fields.Many2One('test.modelsql.one2many', "Origin")
+class ModelSQLSearch(ModelSQL):
+ "ModelSQL Search"
+ __name__ = 'test.modelsql.search'
+ name = fields.Char("Name")
+
+
class NullOrder(ModelSQL):
"Null Order"
__name__ = 'test.modelsql.null_order'
@@ -153,6 +159,7 @@
ModelSQLFieldSet,
ModelSQLOne2Many,
ModelSQLOne2ManyTarget,
+ ModelSQLSearch,
NullOrder,
ModelTranslation,
ModelCheck,
diff -r 183fea18d0fe -r 22260a3783da trytond/tests/test_cache.py
--- a/trytond/tests/test_cache.py Fri Jun 12 12:37:50 2020 +0200
+++ b/trytond/tests/test_cache.py Sat Jun 13 09:48:03 2020 +0200
@@ -5,7 +5,7 @@
import unittest
from trytond import backend, cache as cache_mod
-from trytond.cache import freeze, MemoryCache
+from trytond.cache import freeze, MemoryCache, LRUDict, LRUDictTransaction
from trytond.tests.test_tryton import with_transaction, activate_module
from trytond.tests.test_tryton import DB_NAME, USER
from trytond.transaction import Transaction
@@ -175,12 +175,94 @@
super().test_memory_cache_sync()
+class LRUDictTestCase(unittest.TestCase):
+ "Test LRUDict"
+
+ def test_setitem(self):
+ lru_dict = LRUDict(1)
+
+ lru_dict['foo'] = 'foo'
+ self.assertEqual(len(lru_dict), 1)
+
+ lru_dict['bar'] = 'bar'
+ self.assertEqual(len(lru_dict), 1)
+ self.assertEqual(lru_dict, {'bar': 'bar'})
+
+ def test_update(self):
+ lru_dict = LRUDict(1)
+
+ lru_dict['foo'] = 'foo'
+ self.assertEqual(len(lru_dict), 1)
+
+ lru_dict.update(bar='bar', baz='baz')
+ self.assertEqual(len(lru_dict), 1)
+ self.assertEqual(lru_dict, {'baz': 'baz'})
+
+ def test_setdefault(self):
+ lru_dict = LRUDict(1)
+
+ lru_dict['foo'] = 'foo'
+ self.assertEqual(len(lru_dict), 1)
+
+ lru_dict.setdefault('bar', 'value')
+ self.assertEqual(len(lru_dict), 1)
+ self.assertEqual(lru_dict, {'bar': 'value'})
+
+ def test_default_factory(self):
+ lru_dict = LRUDict(1, default_factory=list)
+
+ self.assertEqual(lru_dict['foo'], [])
+
+ lru_dict['bar'].append('bar')
+ self.assertEqual(lru_dict, {'bar': ['bar']})
+
+
+class LRUDictTransactionTestCase(unittest.TestCase):
+ "Test LRUDictTransaction"
+
+ @with_transaction()
+ def test_init(self):
+ "Test init set to transaction counter"
+ lru_dict = LRUDictTransaction(48)
+
+ self.assertEqual(lru_dict.counter, Transaction().counter)
+
+ @with_transaction()
+ def test_clear(self):
+ "Test clear reset counter"
+ lru_dict = LRUDictTransaction(48)
+
+ Transaction().counter += 1
+ lru_dict.clear()
+
+ self.assertEqual(lru_dict.counter, Transaction().counter)
+
+ @with_transaction()
+ def test_refresh(self):
+ "Test refresh"
+ lru_dict = LRUDictTransaction(48)
+
+ lru_dict['foo'] = 'foo'
+ lru_dict.refresh()
+
+ self.assertEqual(lru_dict, {'foo': 'foo'})
+
+ Transaction().counter += 1
+ lru_dict.refresh()
+
+ self.assertEqual(lru_dict, {})
+ self.assertEqual(lru_dict.counter, Transaction().counter)
+
+
def suite():
func = unittest.TestLoader().loadTestsFromTestCase
suite = unittest.TestSuite()
for testcase in [
CacheTestCase,
MemoryCacheTestCase,
- MemoryCacheChannelTestCase]:
+ MemoryCacheChannelTestCase,
+ LRUDictTestCase,
+ LRUDictTransactionTestCase,
+ ]:
suite.addTests(func(testcase))
return suite
diff -r 183fea18d0fe -r 22260a3783da trytond/tests/test_model.py
--- a/trytond/tests/test_model.py Fri Jun 12 12:37:50 2020 +0200
+++ b/trytond/tests/test_model.py Sat Jun 13 09:48:03 2020 +0200
@@ -3,6 +3,7 @@
import unittest
+from trytond.model.model import record
from trytond.pool import Pool
from trytond.tests.test_tryton import activate_module, with_transaction
from trytond.transaction import Transaction
@@ -219,9 +220,159 @@
self.assertEqual(other['string'], "Nom")
+class RecordTestCase(unittest.TestCase):
+ "Test record"
+
+ def test_creation(self):
+ "Test record creation"
+ Record = record('model', ['foo', 'bar'])
+
+ self.assertEqual(Record.__name__, 'model')
+ self.assertEqual(set(Record.__slots__), {'foo', 'bar'})
+
+ def test_init(self):
+ Record = record('model', ['foo', 'bar'])
+
+ rec = Record(foo='foo')
+
+ self.assertEqual(rec.foo, 'foo')
+
+ def test_getitem(self):
+ Record = record('model', ['foo', 'bar'])
+
+ rec = Record(foo='foo')
+
+ self.assertEqual(rec['foo'], 'foo')
+
+ def test_setitem(self):
+ Record = record('model', ['foo', 'bar'])
+
+ rec = Record(foo='foo')
+
+ rec['foo'] = 'foo'
+ self.assertEqual(rec['foo'], 'foo')
+
+ def test_contains(self):
+ "Test record contains"
+ Record = record('model', ['foo', 'bar'])
+
+ rec = Record(foo='foo')
+
+ self.assertTrue('foo' in rec)
+ self.assertFalse('bar' in rec)
+
+ def test_clear(self):
+ "Test record clear"
+ Record = record('model', ['foo', 'bar'])
+
+ rec = Record(foo='foo', bar='bar')
+
+ rec._clear()
+
+ with self.assertRaises(AttributeError):
+ rec.foo
+ with self.assertRaises(AttributeError):
+ rec.bar
+
+ def test_copy(self):
+ "Test copy record"
+ Record = record('model', ['foo', 'bar'])
+
+ rec = Record(foo='baz')
+ copy = rec._copy()
+
+ self.assertEqual(copy.foo, 'baz')
+ self.assertNotEqual(rec, copy)
+ self.assertIsInstance(copy, Record)
+
+ def test_get(self):
+ "Test get field from record"
+ Record = record('model', ['foo', 'bar'])
+
+ rec = Record(foo='baz')
+
+ self.assertEqual(rec._get('foo'), 'baz')
+ self.assertEqual(rec._get('bar'), None)
+ self.assertEqual(rec._get('bar', 42), 42)
+ with self.assertRaises(KeyError):
+ rec._get('baz')
+
+ def test_keys(self):
+ "Test record keys"
+ Record = record('model', ['foo', 'bar'])
+
+ rec = Record(foo='baz')
+
+ self.assertEqual(list(rec._keys()), ['foo'])
+
+ def test_items(self):
+ "Test record items"
+ Record = record('model', ['foo', 'bar'])
+
+ rec = Record(foo='baz')
+
+ self.assertEqual(list(rec._items()), [('foo', 'baz')])
+
+ def test_pop(self):
+ "Test pop field from record"
+ Record = record('model', ['foo', 'bar'])
+
+ rec = Record(foo='baz')
+
+ value = rec._pop('foo')
+ self.assertEqual(value, 'baz')
+ with self.assertRaises(AttributeError):
+ rec.foo
+
+ with self.assertRaises(KeyError):
+ rec._pop('baz')
+
+ def test_popitem(self):
+ "Test popitem field from record"
+ Record = record('model', ['foo', 'bar'])
+
+ rec = Record(foo='baz')
+
+ value = rec._popitem('foo')
+ self.assertEqual(value, ('foo', 'baz'))
+
+ def test_setdefault(self):
+ "Test set default field on record"
+ Record = record('model', ['foo', 'bar'])
+
+ rec = Record(foo='foo')
+
+ self.assertEqual(rec._setdefault('bar', 'bar'), 'bar')
+ self.assertEqual(rec.bar, 'bar')
+ self.assertEqual(rec._setdefault('foo', 'baz'), 'foo')
+ self.assertEqual(rec.foo, 'foo')
+
+ def test_update(self):
+ "Test update record"
+ Record = record('model', ['foo', 'bar'])
+
+ rec = Record(foo='foo')
+ rec._update(foo='bar', bar='baz')
+
+ self.assertEqual(rec.foo, 'bar')
+ self.assertEqual(rec.bar, 'baz')
+
+ def test_values(self):
+ "Test record values"
+ Record = record('model', ['foo', 'bar'])
+
+ rec = Record(foo='baz')
+
+ self.assertEqual(list(rec._values()), [('baz')])
+
+
def suite():
+ func = unittest.TestLoader().loadTestsFromTestCase
suite_ = unittest.TestSuite()
- suite_.addTests(unittest.TestLoader().loadTestsFromTestCase(ModelTestCase))
- suite_.addTests(unittest.TestLoader().loadTestsFromTestCase(
- ModelTranslationTestCase))
+ for testcase in [
+ ModelTestCase,
+ ModelTranslationTestCase,
+ RecordTestCase,
+ ]:
+ suite_.addTests(func(testcase))
return suite_
diff -r 183fea18d0fe -r 22260a3783da trytond/tests/test_modelsql.py
--- a/trytond/tests/test_modelsql.py Fri Jun 12 12:37:50 2020 +0200
+++ b/trytond/tests/test_modelsql.py Sat Jun 13 09:48:03 2020 +0200
@@ -742,6 +742,20 @@
self.assertEqual(record.name, "Foo")
self.assertEqual(other.name, "Baz")
+ @with_transaction()
+ def test_search_fill_transaction_cache(self):
+ "Test search fill the transaction cache"
+ pool = Pool()
+ Model = pool.get('test.modelsql.search')
+ Model.create([{'name': "Foo"}])
+
+ record, = Model.search([])
+ cache = Transaction().get_cache()[Model.__name__]
+
+ self.assertIn(record.id, cache)
+ self.assertEqual(cache[record.id]['name'], "Foo")
+ self.assertNotIn('_timestamp', cache[record.id])
+
def suite():
suite_ = unittest.TestSuite()
diff -r 183fea18d0fe -r 22260a3783da trytond/tests/test_modelview.py
--- a/trytond/tests/test_modelview.py Fri Jun 12 12:37:50 2020 +0200
+++ b/trytond/tests/test_modelview.py Sat Jun 13 09:48:03 2020 +0200
@@ -82,7 +82,7 @@
# no initial value
record = Model()
- record._values = {}
+ record._values = record._record()
target = Target(id=1)
record._values['targets'] = [target]
target.name = 'foo'