From 32eec483252a57ce4e6732ea624636104cc51545 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 3 Jul 2023 14:52:40 -0700 Subject: [PATCH 1/4] gh-106292: restore checking __dict__ in cached_property.__get__ --- Lib/functools.py | 21 ++++++++++++--------- Lib/test/test_functools.py | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/Lib/functools.py b/Lib/functools.py index 4d5e2709007843..5dc44388e35e2d 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -960,6 +960,7 @@ def __isabstractmethod__(self): ### cached_property() - computed once per instance, cached as attribute ################################################################################ +_NOT_FOUND = object() class cached_property: def __init__(self, func): @@ -990,15 +991,17 @@ def __get__(self, instance, owner=None): f"instance to cache {self.attrname!r} property." ) raise TypeError(msg) from None - val = self.func(instance) - try: - cache[self.attrname] = val - except TypeError: - msg = ( - f"The '__dict__' attribute on {type(instance).__name__!r} instance " - f"does not support item assignment for caching {self.attrname!r} property." - ) - raise TypeError(msg) from None + val = cache.get(self.attrname, _NOT_FOUND) + if val is _NOT_FOUND: + val = self.func(instance) + try: + cache[self.attrname] = val + except TypeError: + msg = ( + f"The '__dict__' attribute on {type(instance).__name__!r} instance " + f"does not support item assignment for caching {self.attrname!r} property." + ) + raise TypeError(msg) from None return val __class_getitem__ = classmethod(GenericAlias) diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index d668fa4c3adf5c..c4eca0f5b79511 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -3037,6 +3037,25 @@ def test_access_from_class(self): def test_doc(self): self.assertEqual(CachedCostItem.cost.__doc__, "The cost of the item.") + def test_subclass_with___set__(self): + """Caching still works for a subclass defining __set__.""" + class readonly_cached_property(py_functools.cached_property): + def __set__(self, obj, value): + raise AttributeError("read only property") + + class Test: + def __init__(self, prop): + self._prop = prop + + @readonly_cached_property + def prop(self): + return self._prop + + t = Test(1) + self.assertEqual(t.prop, 1) + t._prop = 999 + self.assertEqual(t.prop, 1) + if __name__ == '__main__': unittest.main() From 43ed75875243b41eebc258e2c7f342f4bbe7b2af Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 3 Jul 2023 15:09:51 -0700 Subject: [PATCH 2/4] add news entry --- .../Library/2023-07-03-15-09-44.gh-issue-106292.3npldV.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2023-07-03-15-09-44.gh-issue-106292.3npldV.rst diff --git a/Misc/NEWS.d/next/Library/2023-07-03-15-09-44.gh-issue-106292.3npldV.rst b/Misc/NEWS.d/next/Library/2023-07-03-15-09-44.gh-issue-106292.3npldV.rst new file mode 100644 index 00000000000000..2d6043168de2ad --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-07-03-15-09-44.gh-issue-106292.3npldV.rst @@ -0,0 +1,4 @@ +Check for an instance-dict cached value in the ``__get__`` method of +:func:`functools.cached_property`. This better matches the pre-3.12 behavior +and improves compatibility for users subclassing +:func:`functools.cached_property` and adding a ``__set__`` method. From 465dbd35fe6a2e3197b0ac97933944c7bff1afa6 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 3 Jul 2023 18:23:37 -0700 Subject: [PATCH 3/4] update comment to not make promises we can't guarantee --- Lib/functools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/functools.py b/Lib/functools.py index 5dc44388e35e2d..8518450a8d499d 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -957,7 +957,7 @@ def __isabstractmethod__(self): ################################################################################ -### cached_property() - computed once per instance, cached as attribute +### cached_property() - property result cached as instance attribute ################################################################################ _NOT_FOUND = object() From 19b5320e67e3ac87bed01b4100a313c6e4dc0706 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Wed, 5 Jul 2023 11:40:27 -0600 Subject: [PATCH 4/4] Apply suggestions from code review Co-authored-by: Dong-hee Na --- .../Library/2023-07-03-15-09-44.gh-issue-106292.3npldV.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2023-07-03-15-09-44.gh-issue-106292.3npldV.rst b/Misc/NEWS.d/next/Library/2023-07-03-15-09-44.gh-issue-106292.3npldV.rst index 2d6043168de2ad..233509344d509b 100644 --- a/Misc/NEWS.d/next/Library/2023-07-03-15-09-44.gh-issue-106292.3npldV.rst +++ b/Misc/NEWS.d/next/Library/2023-07-03-15-09-44.gh-issue-106292.3npldV.rst @@ -1,4 +1,4 @@ -Check for an instance-dict cached value in the ``__get__`` method of +Check for an instance-dict cached value in the :meth:`__get__` method of :func:`functools.cached_property`. This better matches the pre-3.12 behavior and improves compatibility for users subclassing -:func:`functools.cached_property` and adding a ``__set__`` method. +:func:`functools.cached_property` and adding a :meth:`__set__` method.