Cache Function field getter without context in readonly transactions
issue10936 review367781002
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
* Cache Function field getter without context in readonly transactions
|
||||
* Use expire delay instead of UTC datetime in reset password email
|
||||
* Use RENAME and DROP COLUMN with SQLite backend
|
||||
* Support window functions with SQLite backend
|
||||
|
||||
@@ -961,7 +961,7 @@ Instance methods:
|
||||
Function
|
||||
--------
|
||||
|
||||
.. class:: Function(field, getter[, setter[, searcher]])
|
||||
.. class:: Function(field, getter[, setter[, searcher[, getter_with_context]]])
|
||||
|
||||
A function field can emulate any other given ``field``.
|
||||
|
||||
@@ -1019,6 +1019,14 @@ A function field can emulate any other given ``field``.
|
||||
It must return a list of :ref:`domain <topics-domain>` clauses but the
|
||||
``operand`` can be a SQL query.
|
||||
|
||||
.. attribute:: Function.getter_with_context
|
||||
|
||||
A boolean telling if the getter result depends on the context.
|
||||
If it does not depend, the getter is called without context and the result
|
||||
is stored in the transaction cache when readonly.
|
||||
The default value is ``True``.
|
||||
|
||||
|
||||
Instance methods:
|
||||
|
||||
.. method:: Function.get(ids, model, name[, values])
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import inspect
|
||||
import copy
|
||||
from functools import wraps
|
||||
|
||||
from trytond.i18n import gettext
|
||||
from trytond.model.fields.field import Field
|
||||
@@ -10,13 +11,29 @@ from trytond.tools import is_instance_method
|
||||
from trytond.transaction import Transaction
|
||||
|
||||
|
||||
def getter_context(func):
|
||||
@wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
if not self.getter_with_context:
|
||||
transaction = Transaction()
|
||||
context = {
|
||||
k: v for k, v in transaction.context.items()
|
||||
if k in transaction.cache_keys}
|
||||
with transaction.reset_context(), \
|
||||
transaction.set_context(context):
|
||||
return func(self, *args, **kwargs)
|
||||
else:
|
||||
return func(self, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
class Function(Field):
|
||||
'''
|
||||
Define function field (any).
|
||||
'''
|
||||
|
||||
def __init__(self, field, getter, setter=None, searcher=None,
|
||||
loading='lazy'):
|
||||
getter_with_context=True, loading='lazy'):
|
||||
'''
|
||||
:param field: The field of the function.
|
||||
:param getter: The name of the function for getting values.
|
||||
@@ -29,6 +46,7 @@ class Function(Field):
|
||||
self._field = field
|
||||
self._type = field._type
|
||||
self.getter = getter
|
||||
self.getter_with_context = getter_with_context
|
||||
self.setter = setter
|
||||
if not self.setter:
|
||||
self._field.readonly = True
|
||||
@@ -41,11 +59,15 @@ class Function(Field):
|
||||
|
||||
def __copy__(self):
|
||||
return Function(copy.copy(self._field), self.getter,
|
||||
setter=self.setter, searcher=self.searcher, loading=self.loading)
|
||||
setter=self.setter, searcher=self.searcher,
|
||||
getter_with_context=self.getter_with_context,
|
||||
loading=self.loading)
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
return Function(copy.deepcopy(self._field, memo), self.getter,
|
||||
setter=self.setter, searcher=self.searcher, loading=self.loading)
|
||||
setter=self.setter, searcher=self.searcher,
|
||||
getter_with_context=self.getter_with_context,
|
||||
loading=self.loading)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._field, name)
|
||||
@@ -81,6 +103,7 @@ class Function(Field):
|
||||
'ir.msg_search_function_missing',
|
||||
**Model.__names__(self.name)))
|
||||
|
||||
@getter_context
|
||||
def get(self, ids, Model, name, values=None):
|
||||
'''
|
||||
Call the getter.
|
||||
|
||||
@@ -845,7 +845,9 @@ class ModelSQL(ModelStorage):
|
||||
for fname in getter_fields:
|
||||
field = cls._fields[fname]
|
||||
if isinstance(field, fields.Function):
|
||||
key = (field.getter, getattr(field, 'datetime_field', None))
|
||||
key = (
|
||||
field.getter, field.getter_with_context,
|
||||
getattr(field, 'datetime_field', None))
|
||||
func_fields.setdefault(key, [])
|
||||
func_fields[key].append(fname)
|
||||
elif getattr(field, 'datetime_field', None):
|
||||
@@ -865,7 +867,7 @@ class ModelSQL(ModelStorage):
|
||||
field_list = func_fields[key]
|
||||
fname = field_list[0]
|
||||
field = cls._fields[fname]
|
||||
_, datetime_field = key
|
||||
_, getter_with_context, datetime_field = key
|
||||
if datetime_field:
|
||||
for row in result:
|
||||
with Transaction().set_context(
|
||||
@@ -895,6 +897,9 @@ class ModelSQL(ModelStorage):
|
||||
for row in sub_results:
|
||||
if row['id'] in sub_ids:
|
||||
row[fname] = getter_result[row['id']]
|
||||
if (transaction.readonly
|
||||
and not getter_with_context):
|
||||
cache[row['id']][fname] = row[fname]
|
||||
|
||||
def read_related(field, Target, rows, fields):
|
||||
name = field.name
|
||||
|
||||
@@ -1720,7 +1720,9 @@ class ModelStorage(Model):
|
||||
if (field._type in no_local_cache
|
||||
or field.context
|
||||
or getattr(field, 'datetime_field', None)
|
||||
or isinstance(field, fields.Function)):
|
||||
or (isinstance(field, fields.Function)
|
||||
and (not transaction.readonly
|
||||
or field.getter_with_context))):
|
||||
to_delete.add(fname)
|
||||
self._cache[id_]._update(
|
||||
**{k: v for k, v in data.items() if k not in to_delete})
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
from trytond.model import ModelSQL, fields
|
||||
from trytond.pool import Pool
|
||||
from trytond.transaction import Transaction
|
||||
|
||||
|
||||
class FunctionAccessor(ModelSQL):
|
||||
@@ -25,8 +26,26 @@ class FunctionAccessorTarget(ModelSQL):
|
||||
__name__ = 'test.function.accessor.target'
|
||||
|
||||
|
||||
class FunctionGetterContext(ModelSQL):
|
||||
"Function Getter Context"
|
||||
__name__ = 'test.function.getter_context'
|
||||
|
||||
function_with_context = fields.Function(
|
||||
fields.Char("Function"),
|
||||
'getter', getter_with_context=True)
|
||||
function_without_context = fields.Function(
|
||||
fields.Char("Function"),
|
||||
'getter', getter_with_context=False)
|
||||
|
||||
def getter(self, name):
|
||||
context = Transaction().context
|
||||
return '%s - %s' % (
|
||||
context.get('language', 'empty'), context.get('test', 'empty'))
|
||||
|
||||
|
||||
def register(module):
|
||||
Pool.register(
|
||||
FunctionAccessor,
|
||||
FunctionAccessorTarget,
|
||||
FunctionGetterContext,
|
||||
module=module, type_='model')
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from trytond.pool import Pool
|
||||
from trytond.tests.test_tryton import activate_module, with_transaction
|
||||
from trytond.transaction import Transaction
|
||||
|
||||
|
||||
class FieldFunctionTestCase(unittest.TestCase):
|
||||
@@ -27,6 +29,50 @@ class FieldFunctionTestCase(unittest.TestCase):
|
||||
|
||||
self.assertEqual(record.function, target)
|
||||
|
||||
@with_transaction()
|
||||
def test_getter_context(self):
|
||||
"Test getter context"
|
||||
pool = Pool()
|
||||
Model = pool.get('test.function.getter_context')
|
||||
|
||||
record = Model()
|
||||
record.save()
|
||||
|
||||
with Transaction().set_context(language='en', test='foo'):
|
||||
record = Model(record.id)
|
||||
|
||||
self.assertEqual(record.function_with_context, 'en - foo')
|
||||
self.assertEqual(record.function_without_context, 'en - empty')
|
||||
|
||||
@with_transaction()
|
||||
def test_getter_cached_readonly_transaction(self):
|
||||
"Test getter cached in read only transaction"
|
||||
pool = Pool()
|
||||
Model = pool.get('test.function.getter_context')
|
||||
|
||||
with Transaction().new_transaction(readonly=True), \
|
||||
patch.object(Model, 'getter') as getter:
|
||||
record = Model()
|
||||
record.save()
|
||||
record.function_with_context
|
||||
record.function_without_context
|
||||
self.assertEqual(getter.call_count, 2)
|
||||
|
||||
getter.reset_mock()
|
||||
with Transaction().set_context(test='foo'):
|
||||
record = Model(record.id)
|
||||
record.function_with_context
|
||||
record.function_without_context
|
||||
self.assertEqual(getter.call_count, 1)
|
||||
|
||||
getter.reset_mock()
|
||||
# Change transaction key context
|
||||
with Transaction().set_context(language='en'):
|
||||
record = Model(record.id)
|
||||
record.function_with_context
|
||||
record.function_without_context
|
||||
self.assertEqual(getter.call_count, 2)
|
||||
|
||||
|
||||
def suite():
|
||||
return unittest.TestLoader().loadTestsFromTestCase(FieldFunctionTestCase)
|
||||
|
||||
Reference in New Issue
Block a user