From 390decc82a4f66b717cad4be9468be7a21311d70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afonso=20Janu=C3=A1rio?= Date: Sat, 29 Aug 2026 18:45:23 +0100 Subject: [PATCH] Fix lazyproperty to cache a legitimately None return value lazyproperty.__get__ checked obj.__dict__.get(self._name) is None to decide whether the getter had already run, so any lazyproperty whose getter legitimately returns None was silently recomputed on every access instead of being cached once, contradicting the class's own documented immutability and idempotence guarantee. Switched the check to key presence in obj.__dict__ instead. --- src/pptx/util.py | 8 +++----- tests/test_util.py | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/pptx/util.py b/src/pptx/util.py index fdec79298..0ddceff92 100644 --- a/src/pptx/util.py +++ b/src/pptx/util.py @@ -183,14 +183,12 @@ def __get__(self, obj: Any, type: Any = None) -> _T: # --- when accessed on instance, start by checking instance __dict__ for # --- item with key matching the wrapped function's name - value = obj.__dict__.get(self._name) - if value is None: + if self._name not in obj.__dict__: # --- on first access, the __dict__ item will be absent. Evaluate fget() # --- and store that value in the (otherwise unused) host-object # --- __dict__ value of same name ('fget' nominally) - value = self._fget(obj) - obj.__dict__[self._name] = value - return cast(_T, value) + obj.__dict__[self._name] = self._fget(obj) + return cast(_T, obj.__dict__[self._name]) def __set__(self, obj: Any, value: Any) -> None: """Raises unconditionally, to preserve read-only behavior. diff --git a/tests/test_util.py b/tests/test_util.py index 97e46fa4c..99c6361c7 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -4,7 +4,41 @@ import pytest -from pptx.util import Centipoints, Cm, Emu, Inches, Length, Mm, Pt +from pptx.util import Centipoints, Cm, Emu, Inches, Length, Mm, Pt, lazyproperty + + +class DescribeLazyproperty(object): + def it_only_calls_the_decorated_method_once(self): + class Obj(object): + def __init__(self): + self.call_count = 0 + + @lazyproperty + def fget(self): + self.call_count += 1 + return self.call_count + + obj = Obj() + + assert obj.fget == 1 + assert obj.fget == 1 + assert obj.call_count == 1 + + def it_caches_a_None_return_value_too(self): + class Obj(object): + def __init__(self): + self.call_count = 0 + + @lazyproperty + def fget(self): + self.call_count += 1 + return None + + obj = Obj() + + assert obj.fget is None + assert obj.fget is None + assert obj.call_count == 1 class DescribeLength(object):