Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-wrapt for openSUSE:Factory checked in at 2026-06-29 17:29:45 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-wrapt (Old) and /work/SRC/openSUSE:Factory/.python-wrapt.new.11887 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-wrapt" Mon Jun 29 17:29:45 2026 rev:27 rq:1362163 version:2.2.2 Changes: -------- --- /work/SRC/openSUSE:Factory/python-wrapt/python-wrapt.changes 2026-05-27 16:12:27.865819785 +0200 +++ /work/SRC/openSUSE:Factory/.python-wrapt.new.11887/python-wrapt.changes 2026-06-29 17:30:15.145805802 +0200 @@ -1,0 +2,39 @@ +Sun Jun 28 11:27:04 UTC 2026 - Dirk Müller <[email protected]> + +- update to 2.2.2: + * When @wrapt.lru_cache was applied to an instance method that + was overridden in a subclass, and the subclass method called + the base class method via super(), a RecursionError was + raised instead of the base class method being invoked. The + per-instance cache for each method was stored as an attribute + on the instance whose name was derived only from the method + __name__, so the base and derived methods shared a single + cache slot. The subclass cache was therefore found again when + the base method was reached through super(), re-entering the + subclass body and recursing without end. The cache attribute + name now incorporates a unique identifier for each decorated + method so that a base method and a method that overrides it + use distinct per-instance caches. With thanks to the reporter + of issue #342. + * When @wrapt.lru_cache was applied to a method of a class + deriving from wrapt.ObjectProxy, the per-instance cache was + stored on the wrapped object rather than on the proxy. This + is because the proxy __setattr__ forwards attribute + assignment to the wrapped object for any name that is not a + recognised proxy attribute, and the cache attribute name was + not one. Storing the cache on the wrapped object had several + consequences: the wrapped object was polluted with cache + attributes it never defined; the cache held a reference back + to the proxy through the bound method it wrapped, so a + wrapped object that outlived the proxy kept the proxy alive + and prevented its collection; wrapping an object that does + not accept arbitrary attributes, such as one using __slots__, + caused the first cached call to fail with an AttributeError; + and two proxies sharing a single wrapped object shared one + cache and could return results computed for the wrong proxy. + The cache attribute is now stored on the proxy itself using + the proxy __self_setattr__ method when the instance is a + wrapt object proxy, falling back to setattr for ordinary + instances. + +------------------------------------------------------------------- Old: ---- 2.2.1.tar.gz New: ---- 2.2.2.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-wrapt.spec ++++++ --- /var/tmp/diff_new_pack.8VeHtV/_old 2026-06-29 17:30:15.729825936 +0200 +++ /var/tmp/diff_new_pack.8VeHtV/_new 2026-06-29 17:30:15.733826075 +0200 @@ -19,7 +19,7 @@ %{?sle15_python_module_pythons} Name: python-wrapt -Version: 2.2.1 +Version: 2.2.2 Release: 0 Summary: A Python module for decorators, wrappers and monkey patching License: BSD-2-Clause ++++++ 2.2.1.tar.gz -> 2.2.2.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/wrapt-2.2.1/docs/bundled.rst new/wrapt-2.2.2/docs/bundled.rst --- old/wrapt-2.2.1/docs/bundled.rst 2026-05-22 16:22:37.000000000 +0200 +++ new/wrapt-2.2.2/docs/bundled.rst 2026-06-21 01:22:53.000000000 +0200 @@ -112,6 +112,35 @@ >>> fibonacci.cache_parameters() {'maxsize': 128, 'typed': False} +Because the per-instance cache is stored as an attribute on the instance, an +instance whose cached method has been called holds a +``functools._lru_cache_wrapper`` object in its ``__dict__``, and that object +is not picklable. If instances of the class need to be pickled, override +``__getstate__`` to drop the cache attributes from the pickled state. The +cache attribute names all begin with ``_lru_cache_``, so filtering on that +prefix excludes every per-instance cache. The caches are recreated lazily on +the next call after unpickling. + +:: + + class MyClass: + + @wrapt.lru_cache + def compute(self, x): + return x * 2 + + def __getstate__(self): + return { + key: value + for key, value in self.__dict__.items() + if not key.startswith("_lru_cache_") + } + +To target the cache for a particular method rather than all of them, filter +on the more specific ``_lru_cache_<name>_`` prefix, where ``<name>`` is the +method name. For example, ``_lru_cache_compute_`` matches only the cache for +the ``compute`` method above. + Thread Synchronization ---------------------- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/wrapt-2.2.1/docs/changes.rst new/wrapt-2.2.2/docs/changes.rst --- old/wrapt-2.2.1/docs/changes.rst 2026-05-22 16:22:37.000000000 +0200 +++ new/wrapt-2.2.2/docs/changes.rst 2026-06-21 01:22:53.000000000 +0200 @@ -1,6 +1,42 @@ Release Notes ============= +Version 2.2.2 +------------- + +**Bugs Fixed** + +* When ``@wrapt.lru_cache`` was applied to an instance method that was + overridden in a subclass, and the subclass method called the base class + method via ``super()``, a ``RecursionError`` was raised instead of the + base class method being invoked. The per-instance cache for each method + was stored as an attribute on the instance whose name was derived only + from the method ``__name__``, so the base and derived methods shared a + single cache slot. The subclass cache was therefore found again when the + base method was reached through ``super()``, re-entering the subclass body + and recursing without end. The cache attribute name now incorporates a + unique identifier for each decorated method so that a base method and a + method that overrides it use distinct per-instance caches. With thanks to + the reporter of `issue #342 + <https://github.com/GrahamDumpleton/wrapt/issues/342>`_. + +* When ``@wrapt.lru_cache`` was applied to a method of a class deriving from + ``wrapt.ObjectProxy``, the per-instance cache was stored on the wrapped + object rather than on the proxy. This is because the proxy ``__setattr__`` + forwards attribute assignment to the wrapped object for any name that is + not a recognised proxy attribute, and the cache attribute name was not one. + Storing the cache on the wrapped object had several consequences: the + wrapped object was polluted with cache attributes it never defined; the + cache held a reference back to the proxy through the bound method it + wrapped, so a wrapped object that outlived the proxy kept the proxy alive + and prevented its collection; wrapping an object that does not accept + arbitrary attributes, such as one using ``__slots__``, caused the first + cached call to fail with an ``AttributeError``; and two proxies sharing a + single wrapped object shared one cache and could return results computed + for the wrong proxy. The cache attribute is now stored on the proxy itself + using the proxy ``__self_setattr__`` method when the instance is a wrapt + object proxy, falling back to ``setattr`` for ordinary instances. + Version 2.2.1 ------------- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/wrapt-2.2.1/docs/issues.rst new/wrapt-2.2.2/docs/issues.rst --- old/wrapt-2.2.1/docs/issues.rst 2026-05-22 16:22:37.000000000 +0200 +++ new/wrapt-2.2.2/docs/issues.rst 2026-06-21 01:22:53.000000000 +0200 @@ -221,6 +221,25 @@ # TypeError: descriptor '__subclasscheck__' for '_wrappers.ObjectProxy' # objects doesn't apply to a 'type' object +Under the pure Python implementation of ``ObjectProxy`` (used when the +C extension is not available, including on PyPy or when +``WRAPT_DISABLE_EXTENSIONS`` is set in the environment), the failure +surfaces earlier, at the ``class Proxy(...)`` statement itself, with a +different message:: + + TypeError: metaclass conflict: the metaclass of a derived class + must be a (non-strict) subclass of the metaclasses of all its bases + +The pure Python ``ObjectProxy`` carries a custom metaclass, used to +make type-level access to ``__module__`` and ``__doc__`` return strings +rather than property objects, whereas the C extension's ``ObjectProxy`` +has plain ``type`` as its metaclass. When a second base class with +``ABCMeta`` as its metaclass is mixed in, the pure Python build cannot +find a common metaclass and aborts class creation. The C build +proceeds and only fails later at the first ``isinstance()`` call. The +underlying problem is the same in both cases; only the point at which +it surfaces differs. + The same failure occurs when the second base class is one of the abstract base classes exported from ``collections.abc`` (for example ``Hashable``, ``Iterable``, ``Container``), since they too use diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/wrapt-2.2.1/src/wrapt/__init__.py new/wrapt-2.2.2/src/wrapt/__init__.py --- old/wrapt-2.2.1/src/wrapt/__init__.py 2026-05-22 16:22:37.000000000 +0200 +++ new/wrapt-2.2.2/src/wrapt/__init__.py 2026-06-21 01:22:53.000000000 +0200 @@ -13,7 +13,7 @@ ) -__version_info__ = ("2", "2", "1") +__version_info__ = ("2", "2", "2") __version__ = _format_version(__version_info__) from .__wrapt__ import ( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/wrapt-2.2.1/src/wrapt/caching.py new/wrapt-2.2.2/src/wrapt/caching.py --- old/wrapt-2.2.1/src/wrapt/caching.py 2026-05-22 16:22:37.000000000 +0200 +++ new/wrapt-2.2.2/src/wrapt/caching.py 2026-06-21 01:22:53.000000000 +0200 @@ -9,7 +9,7 @@ from functools import lru_cache as _functools_lru_cache from functools import partial -from .__wrapt__ import BoundFunctionWrapper, FunctionWrapper +from .__wrapt__ import BaseObjectProxy, BoundFunctionWrapper, FunctionWrapper from .decorators import decorator from .synchronization import synchronized @@ -71,7 +71,19 @@ self.__wrapped__ ) - setattr(instance, cache_attr, cache) + # If the instance the method is bound to is a wrapt + # object proxy, a plain setattr() would fall through and + # store the cache on the wrapped object rather than the + # proxy. Use type() rather than isinstance() so the check + # sees the real proxy type and is not fooled by the proxy + # overriding __class__ to report the wrapped object's + # type. Proxies expose __self_setattr__() which stores the + # attribute on the proxy itself. + + if issubclass(type(instance), BaseObjectProxy): + instance.__self_setattr__(cache_attr, cache) + else: + setattr(instance, cache_attr, cache) return cache(*args, **kwargs) @@ -137,7 +149,17 @@ if name is None: name = wrapped.__func__.__name__ - self._self_cache_attr = "_lru_cache_" + name + # The cache attribute name must be unique per decorated method so + # that a method overridden in a subclass does not share the same + # per-instance cache slot as the method it overrides. If they shared + # a slot, a subclass method calling super() would find the subclass + # cache and re-enter its own body, recursing forever. The owning + # class is not known here, since the decorator runs on the raw + # function before the class exists, but each decorated method has its + # own wrapper instance, so id(self) is a stable discriminator unique + # to this method definition. + + self._self_cache_attr = "_lru_cache_" + name + "_" + str(id(self)) def __call__(self, *args, **kwargs): # Plain function or static method — single cache stored diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/wrapt-2.2.1/tests/core/test_lru_cache.py new/wrapt-2.2.2/tests/core/test_lru_cache.py --- old/wrapt-2.2.1/tests/core/test_lru_cache.py 2026-05-22 16:22:37.000000000 +0200 +++ new/wrapt-2.2.2/tests/core/test_lru_cache.py 2026-06-21 01:22:53.000000000 +0200 @@ -267,6 +267,161 @@ self.assertEqual(info.misses, 1) +class OverrideBase: + def __init__(self): + self.base_calls = 0 + self.derived_calls = 0 + + @wrapt.lru_cache + def compute(self, x): + self.base_calls += 1 + return x * 2 + + +class OverrideDerived(OverrideBase): + @wrapt.lru_cache + def compute(self, x): + self.derived_calls += 1 + return super().compute(x) + 1 + + +class TestOverriddenMethodWithSuper(unittest.TestCase): + def test_super_call_returns_correct_result(self): + obj = OverrideDerived() + self.assertEqual(obj.compute(10), 21) + + def test_super_call_does_not_recurse(self): + # Regression test: a subclass method decorated with lru_cache that + # called the base method via super() used to recurse forever because + # the base and derived methods shared a single per-instance cache + # slot derived from the method name alone. + obj = OverrideDerived() + try: + result = obj.compute(10) + except RecursionError: + self.fail("super() call recursed instead of reaching base method") + self.assertEqual(result, 21) + + def test_both_bodies_execute_once(self): + obj = OverrideDerived() + obj.compute(10) + self.assertEqual(obj.derived_calls, 1) + self.assertEqual(obj.base_calls, 1) + + def test_base_and_derived_cached_independently(self): + obj = OverrideDerived() + obj.compute(10) + obj.compute(10) + # Second call is served from both caches, so neither body re-runs. + self.assertEqual(obj.derived_calls, 1) + self.assertEqual(obj.base_calls, 1) + info = obj.compute.cache_info() + self.assertEqual(info.hits, 1) + self.assertEqual(info.misses, 1) + + def test_base_class_instance_unaffected(self): + obj = OverrideBase() + self.assertEqual(obj.compute(10), 20) + self.assertEqual(obj.base_calls, 1) + self.assertEqual(obj.derived_calls, 0) + + def test_separate_instances_have_separate_caches(self): + obj1 = OverrideDerived() + obj2 = OverrideDerived() + obj1.compute(10) + obj2.compute(10) + self.assertEqual(obj1.derived_calls, 1) + self.assertEqual(obj2.derived_calls, 1) + self.assertEqual(obj1.base_calls, 1) + self.assertEqual(obj2.base_calls, 1) + + +class ProxyWrapped: + def __init__(self, a): + self.a = a + + +class ProxyCached(wrapt.ObjectProxy): + @wrapt.lru_cache + def compute(self, x): + return self.a + x + + +class ProxyCachedWithState(wrapt.ObjectProxy): + def __init__(self, wrapped, factor): + super().__init__(wrapped) + self._self_factor = factor + + @wrapt.lru_cache + def compute(self, x): + return self._self_factor * x + + +class SlottedWrapped: + __slots__ = ("a",) + + def __init__(self, a): + self.a = a + + +class TestObjectProxySubclass(unittest.TestCase): + def test_returns_correct_result(self): + obj = ProxyCached(ProxyWrapped(1)) + self.assertEqual(obj.compute(10), 11) + + def test_caching(self): + obj = ProxyCached(ProxyWrapped(1)) + obj.compute(10) + obj.compute(10) + info = obj.compute.cache_info() + self.assertEqual(info.hits, 1) + self.assertEqual(info.misses, 1) + + def test_cache_not_stored_on_wrapped_object(self): + wrapped = ProxyWrapped(1) + obj = ProxyCached(wrapped) + obj.compute(10) + cache_attrs = [k for k in vars(wrapped) if k.startswith("_lru_cache_")] + self.assertEqual(cache_attrs, []) + + def test_per_proxy_state_not_shared_for_same_wrapped_object(self): + # Two proxies over the same wrapped object must keep independent + # per-instance caches keyed to their own proxy state, not a single + # cache stored on the shared wrapped object. + wrapped = ProxyWrapped(1) + obj1 = ProxyCachedWithState(wrapped, 2) + obj2 = ProxyCachedWithState(wrapped, 10) + self.assertEqual(obj1.compute(5), 10) + self.assertEqual(obj2.compute(5), 50) + + def test_proxy_garbage_collected_with_long_lived_wrapped(self): + # The cache must be stored on the proxy, not the wrapped object, so + # a proxy is not kept alive by a wrapped object that outlives it. + registry = [] + + def make_and_use(): + backing = ProxyWrapped(7) + registry.append(backing) + proxy = ProxyCached(backing) + proxy.compute(4) + return weakref.ref(proxy) + + ref = make_and_use() + gc.collect() + self.assertIsNone(ref()) + + def test_wrapped_object_without_dict(self): + # A wrapped object that does not accept arbitrary attributes (for + # example one using __slots__) must not cause the cache storage to + # fail, since the cache is stored on the proxy. + obj = ProxyCached(SlottedWrapped(1)) + self.assertEqual(obj.compute(10), 11) + self.assertEqual(obj.compute(10), 11) + info = obj.compute.cache_info() + self.assertEqual(info.hits, 1) + self.assertEqual(info.misses, 1) + + class TestIntrospection(unittest.TestCase): def test_function_name(self): self.assertEqual(cached_function.__name__, "cached_function")
