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'

Reply via email to