diff --git a/CHANGELOG b/CHANGELOG index 4eab53976e..e3c7775a8c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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 --git a/doc/ref/models/models.rst b/doc/ref/models/models.rst index d03ab49c2c..57ec28ca04 100644 --- a/doc/ref/models/models.rst +++ b/doc/ref/models/models.rst @@ -36,6 +36,17 @@ Class attributes are: 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 --git a/trytond/cache.py b/trytond/cache.py index b80e555575..988fe61400 100644 --- a/trytond/cache.py +++ b/trytond/cache.py @@ -18,7 +18,7 @@ from trytond.config import config 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 @@ class LRUDict(OrderedDict): """ 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 LRUDict(OrderedDict): 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 --git a/trytond/model/fields/field.py b/trytond/model/fields/field.py index 0e4b3f452b..0644e7321b 100644 --- a/trytond/model/fields/field.py +++ b/trytond/model/fields/field.py @@ -142,7 +142,8 @@ def instanciate_values(Target, value, **extra): 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 @@ class Field(object): 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 --git a/trytond/model/model.py b/trytond/model/model.py index 66aedd749e..993c296eb3 100644 --- a/trytond/model/model.py +++ b/trytond/model/model.py @@ -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 @@ class Model(URLMixin, PoolBase, metaclass=ModelMeta): 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 @@ class Model(URLMixin, PoolBase, metaclass=ModelMeta): 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 @@ class Model(URLMixin, PoolBase, metaclass=ModelMeta): # 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 @@ class Model(URLMixin, PoolBase, metaclass=ModelMeta): """ 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 @@ class Model(URLMixin, PoolBase, metaclass=ModelMeta): 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 --git a/trytond/model/modelsql.py b/trytond/model/modelsql.py index 9984dffb21..5c04ca9ea9 100644 --- a/trytond/model/modelsql.py +++ b/trytond/model/modelsql.py @@ -794,10 +794,8 @@ class ModelSQL(ModelStorage): 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 @@ class ModelSQL(ModelStorage): 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 @@ class ModelSQL(ModelStorage): 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 @@ class ModelSQL(ModelStorage): 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 --git a/trytond/model/modelstorage.py b/trytond/model/modelstorage.py index a0c169a45c..40f7d28f75 100644 --- a/trytond/model/modelstorage.py +++ b/trytond/model/modelstorage.py @@ -211,16 +211,14 @@ class ModelStorage(Model): # 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 @@ class ModelStorage(Model): ''' 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 @@ class ModelStorage(Model): 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,31 +1391,36 @@ class ModelStorage(Model): 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] except KeyError: 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 @@ class ModelStorage(Model): 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 @@ class ModelStorage(Model): 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 @@ class ModelStorage(Model): 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 @@ class ModelStorage(Model): 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 @@ class ModelStorage(Model): 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 @@ class ModelStorage(Model): 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 --git a/trytond/model/modelview.py b/trytond/model/modelview.py index adba097d6a..1e8f92b8d5 100644 --- a/trytond/model/modelview.py +++ b/trytond/model/modelview.py @@ -795,10 +795,10 @@ class ModelView(Model): """ 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 @@ class ModelView(Model): 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 @@ class ModelView(Model): 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 --git a/trytond/modules/__init__.py b/trytond/modules/__init__.py index 5ae97e345b..c3bdcf5326 100644 --- a/trytond/modules/__init__.py +++ b/trytond/modules/__init__.py @@ -274,6 +274,8 @@ def load_module_graph(graph, pool, update=None, lang=None): # 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 --git a/trytond/res/user.py b/trytond/res/user.py index 80b7628994..b11fa5464b 100644 --- a/trytond/res/user.py +++ b/trytond/res/user.py @@ -384,8 +384,7 @@ class User(DeactivableMixin, ModelSQL, ModelView): 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 --git a/trytond/tests/modelsql.py b/trytond/tests/modelsql.py index e532407e24..f5f5552215 100644 --- a/trytond/tests/modelsql.py +++ b/trytond/tests/modelsql.py @@ -76,6 +76,12 @@ class ModelSQLOne2ManyTarget(ModelSQL): 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 @@ def register(module): ModelSQLFieldSet, ModelSQLOne2Many, ModelSQLOne2ManyTarget, + ModelSQLSearch, NullOrder, ModelTranslation, ModelCheck, diff --git a/trytond/tests/test_cache.py b/trytond/tests/test_cache.py index 85c017e6e2..741eb17509 100644 --- a/trytond/tests/test_cache.py +++ b/trytond/tests/test_cache.py @@ -5,7 +5,7 @@ import time 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 @@ class MemoryCacheChannelTestCase(MemoryCacheTestCase): 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 --git a/trytond/tests/test_model.py b/trytond/tests/test_model.py index 7407a6f63b..1ecec11be9 100644 --- a/trytond/tests/test_model.py +++ b/trytond/tests/test_model.py @@ -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 @@ class ModelTranslationTestCase(unittest.TestCase): 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 --git a/trytond/tests/test_modelsql.py b/trytond/tests/test_modelsql.py index 8d4f5bd04b..cccc8174e4 100644 --- a/trytond/tests/test_modelsql.py +++ b/trytond/tests/test_modelsql.py @@ -742,6 +742,20 @@ class ModelSQLTranslationTestCase(unittest.TestCase): 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 --git a/trytond/tests/test_modelview.py b/trytond/tests/test_modelview.py index 94e325c853..4dfbd0861d 100644 --- a/trytond/tests/test_modelview.py +++ b/trytond/tests/test_modelview.py @@ -82,7 +82,7 @@ class ModelView(unittest.TestCase): # no initial value record = Model() - record._values = {} + record._values = record._record() target = Target(id=1) record._values['targets'] = [target] target.name = 'foo'