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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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__()
|
||||
|
||||
+12
-3
@@ -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')
|
||||
|
||||
@@ -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):
|
||||
|
||||
+128
-3
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
+1
-2
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
+154
-3
@@ -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_
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user