From be1aac4fac0fa2035ee53a6d45637acdf557f05a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Fri, 5 Jan 2024 01:36:50 +0100 Subject: [PATCH] Support cached_property on object with __slots__ Closes #12894 --- modules/edocument_uncefact/edocument.py | 2 +- trytond/CHANGELOG | 1 + trytond/trytond/model/model.py | 4 +- trytond/trytond/tests/test_tools.py | 56 ++++++++++++++++++++++++- trytond/trytond/tools/__init__.py | 37 +++++++++++++--- 5 files changed, 91 insertions(+), 9 deletions(-) diff --git a/modules/edocument_uncefact/edocument.py b/modules/edocument_uncefact/edocument.py index e73a344346..3b37c94e43 100644 --- a/modules/edocument_uncefact/edocument.py +++ b/modules/edocument_uncefact/edocument.py @@ -53,7 +53,7 @@ def remove_comment(stream): class Invoice(Model): "EDocument UN/CEFACT Invoice" __name__ = 'edocument.uncefact.invoice' - __no_slots__ = True # to work with cached_property + __slots__ = ('invoice',) @classmethod def __setup__(cls): diff --git a/trytond/CHANGELOG b/trytond/CHANGELOG index 5e05dcafeb..017a54caba 100644 --- a/trytond/CHANGELOG +++ b/trytond/CHANGELOG @@ -1,3 +1,4 @@ +* Support cached_property on object with __slots__ * Allow creating indexes concurrently * Allow skipping index creation * Add assert methods to test tools diff --git a/trytond/trytond/model/model.py b/trytond/trytond/model/model.py index 48f7fe5b97..c4fcd66520 100644 --- a/trytond/trytond/model/model.py +++ b/trytond/trytond/model/model.py @@ -32,7 +32,9 @@ class Model(URLMixin, PoolBase, metaclass=ModelMeta): """ Define a model in Tryton. """ - __slots__ = ('_id', '_values', '_init_values', '_removed', '_deleted') + __slots__ = ( + '_id', '_values', '_init_values', '_removed', '_deleted', + '__weakref__') _rec_name = 'name' id = fields.Integer(lazy_gettext('ir.msg_ID'), readonly=True) diff --git a/trytond/trytond/tests/test_tools.py b/trytond/trytond/tests/test_tools.py index 4f2d8e5055..e26145ed7f 100644 --- a/trytond/trytond/tests/test_tools.py +++ b/trytond/trytond/tests/test_tools.py @@ -12,8 +12,8 @@ import sql import sql.operators from trytond.tools import ( - decimal_, escape_wildcard, file_open, firstline, grouped_slice, - is_full_text, is_instance_method, likify, lstrip_wildcard, + cached_property, decimal_, escape_wildcard, file_open, firstline, + grouped_slice, is_full_text, is_instance_method, likify, lstrip_wildcard, pairwise_longest, reduce_domain, reduce_ids, remove_forbidden_chars, rstrip_wildcard, slugify, sortable_values, strip_wildcard, timezone, unescape_wildcard) @@ -1214,6 +1214,58 @@ class QRCodeTestCase(unittest.TestCase): self.assertIsNotNone(image.getvalue()) +class CachedProperty: + _count = 0 + + def get_count(self): + self._count += 1 + return self._count + + cached_count = cached_property(get_count) + + +class CachedPropertySlots: + __slots__ = ('_count', '__weakref__') + + def __init__(self): + self._count = 0 + + def get_count(self): + self._count += 1 + return self._count + + cached_count = cached_property(get_count) + + +class CachedPropertyTestCase(unittest.TestCase): + "Test cached_property" + + def test_cached_property_clear(self): + "Test clear cached property" + item = CachedProperty() + + self.assertEqual(item.cached_count, 1) + del item.cached_count + self.assertEqual(item.cached_count, 2) + + def test_cached_property_with_slots(self): + "Test cached property with slots" + item = CachedPropertySlots() + + self.assertEqual(item.cached_count, 1) + self.assertEqual(item.cached_count, 1) + self.assertEqual(item.get_count(), 2) + self.assertEqual(item.cached_count, 1) + + def test_cached_property_with_slots_clear(self): + "Test clear cached property with slots" + item = CachedPropertySlots() + + self.assertEqual(item.cached_count, 1) + del item.cached_count + self.assertEqual(item.cached_count, 2) + + def load_tests(loader, tests, pattern): tests.addTest(doctest.DocTestSuite(decimal_)) return tests diff --git a/trytond/trytond/tools/__init__.py b/trytond/trytond/tools/__init__.py index 21c78cedc9..58de092665 100644 --- a/trytond/trytond/tools/__init__.py +++ b/trytond/trytond/tools/__init__.py @@ -1,11 +1,8 @@ # 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 posixpath - -try: - from functools import cached_property -except ImportError: - from werkzeug.utils import cached_property +from functools import cached_property +from weakref import WeakKeyDictionary try: from werkzeug.security import safe_join @@ -21,6 +18,36 @@ from .misc import ( rstrip_wildcard, slugify, sortable_values, sql_pairing, strip_wildcard, unescape_wildcard) +_NOT_FOUND = object() + + +class cached_property(cached_property): + _cache = None + + def __get__(self, instance, owner=None): + if instance is None: + return self + cache = getattr(instance, '__dict__', None) + if cache is None: # __slots__ + if self._cache is None: + self._cache = WeakKeyDictionary() + val = self._cache.get(instance, _NOT_FOUND) + if val is _NOT_FOUND: + self._cache[instance] = val = self.func(instance) + return val + else: + return super().__get__(instance, owner) + + def __delete__(self, instance): + cache = getattr(instance, '__dict__', None) + try: + if cache: + del cache[self.attrname] + elif self._cache: + del self._cache[instance] + except KeyError: + pass + class ClassProperty(property): def __get__(self, cls, owner):