File functools-cached_property.patch of Package python3.39454

---
 Lib/functools.py           |  102 +++++++++++++++++++
 Lib/test/test_functools.py |  237 +++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 338 insertions(+), 1 deletion(-)

Index: Python-3.4.10/Lib/functools.py
===================================================================
--- Python-3.4.10.orig/Lib/functools.py	2025-06-25 20:06:10.355085315 +0200
+++ Python-3.4.10/Lib/functools.py	2025-06-25 20:08:00.502921555 +0200
@@ -11,7 +11,7 @@
 
 __all__ = ['update_wrapper', 'wraps', 'WRAPPER_ASSIGNMENTS', 'WRAPPER_UPDATES',
            'total_ordering', 'cmp_to_key', 'lru_cache', 'reduce', 'partial',
-           'partialmethod', 'singledispatch']
+           'partialmethod', 'singledispatch', 'cached_property']
 
 try:
     from _functools import reduce
@@ -733,3 +733,103 @@
     wrapper._clear_cache = dispatch_cache.clear
     update_wrapper(wrapper, func)
     return wrapper
+
+################################################################################
+### for ipaddress
+### copied from https://github.com/penguinolog/backports.cached_property
+################################################################################
+"""Backport of python 3.8 functools.cached_property.
+
+cached_property() - computed once per instance, cached as attribute
+"""
+import functools
+from threading import RLock
+
+_NOT_FOUND = object()
+
+
+class cached_property:
+    """
+    Cached property implementation compatible with Python 3.4 and __slots__.
+
+    A property that caches its result after the first access.
+    The cached value is stored in an attribute named `_<original_func_name>_cached_value`.
+    This avoids conflicts when the property name itself is listed in __slots__.
+    """
+    def __init__(self, func):
+        if not callable(func):
+            raise TypeError("cached_property expected a callable, got %r" % type(func))
+        self.func = func
+        self.__doc__ = func.__doc__
+        # The name where the cached value will actually be stored in the instance's slots/dict
+        # We use a mangled name to avoid collision with the property name itself in __slots__
+        self.cache_attr_name = '_{}_cached_value'.format(func.__name__)
+
+        self.lock = RLock()
+
+    def __get__(self, instance, owner=None):
+        if instance is None:
+            return self
+
+        # Check if the instance has a __dict__ for caching
+        has_dict = hasattr(instance, '__dict__')
+        cache = None
+        if has_dict:
+            # If instance has a dict, it's used for caching
+            cache = instance.__dict__
+            val = cache.get(self.cache_attr_name, _NOT_FOUND)
+        else:
+            # For __slots__ classes, try to get the value directly from the instance's attributes
+            try:
+                val = object.__getattribute__(instance, self.cache_attr_name)
+            except AttributeError:
+                val = _NOT_FOUND
+
+        if val is _NOT_FOUND:
+            with self.lock:
+                # check if another thread filled cache while we awaited lock
+                if has_dict:
+                    val = cache.get(self.cache_attr_name, _NOT_FOUND)
+                else:
+                    try:
+                        val = object.__getattribute__(instance, self.cache_attr_name)
+                    except AttributeError:
+                        pass # Still not found, proceed to compute
+
+                if val is _NOT_FOUND:
+                    val = self.func(instance)
+                    try:
+                        if has_dict:
+                            cache[self.cache_attr_name] = val
+                        else:
+                            # For slotted classes, set the attribute directly using the mangled name
+                            object.__setattr__(instance, self.cache_attr_name, val)
+                    except AttributeError as e:
+                        msg = ("Cannot cache {!r} on {!r} instance. ".format(self.func.__name__, type(instance).__name__) +
+                               "Ensure '{}' is a defined slot or the class has a __dict__.".format(self.cache_attr_name))
+                        raise TypeError(msg) from e
+        return val
+
+
+    def __set__(self, instance, value):
+        """
+        Sets the value of the cached property on the instance.
+        This will override any previously cached value and future accesses
+        will return this set value until deleted.
+        """
+        if hasattr(instance, '__dict__'):
+            instance.__dict__[self.cache_attr_name] = value
+        else:
+            # For slotted classes, set the attribute directly using the mangled name
+            object.__setattr__(instance, self.cache_attr_name, value)
+
+    def __delete__(self, instance):
+        """
+        Deletes the cached value from the instance.
+        The next access will recompute the property.
+        """
+        if hasattr(instance, '__dict__'):
+            del instance.__dict__[self.cache_attr_name]
+        else:
+            # For slotted classes, delete the attribute directly using the mangled name
+            object.__delattr__(instance, self.cache_attr_name)
Index: Python-3.4.10/Lib/test/test_functools.py
===================================================================
--- Python-3.4.10.orig/Lib/test/test_functools.py	2025-06-25 20:06:10.355085315 +0200
+++ Python-3.4.10/Lib/test/test_functools.py	2025-06-25 20:08:00.503607401 +0200
@@ -5,6 +5,8 @@
 from random import choice
 import sys
 from test import support
+import threading
+import time
 import unittest
 from weakref import proxy
 
@@ -1590,6 +1592,7 @@
         TestReduce,
         TestLRU,
         TestSingleDispatch,
+        TestCachedProperty,
     )
     support.run_unittest(*test_classes)
 
@@ -1603,5 +1606,239 @@
             counts[i] = sys.gettotalrefcount()
         print(counts)
 
+class TestCachedProperty(unittest.TestCase):
+    """Tests for the backported functools.cached_property."""
+
+    def test_basic_caching(self):
+        class MyClass:
+            def __init__(self):
+                self.counter = 0
+
+            @functools.cached_property
+            def value(self):
+                self.counter += 1
+                return 42
+
+        obj = MyClass()
+        self.assertEqual(obj.counter, 0)
+        self.assertEqual(obj.value, 42)
+        self.assertEqual(obj.counter, 1) # Should have been called once
+        self.assertEqual(obj.value, 42)
+        self.assertEqual(obj.counter, 1) # Should still be 1 (cached)
+
+    def test_docstring_and_name_preserved(self):
+        class MyClass:
+            @functools.cached_property
+            def my_prop(self):
+                """This is a docstring."""
+                return 1
+
+        obj = MyClass()
+        self.assertEqual(obj.my_prop, 1)
+        self.assertEqual(obj.__class__.my_prop.__doc__, "This is a docstring.")
+        self.assertEqual(obj.__class__.my_prop.func.__name__, "my_prop")
+
+    def test_different_instances_cache_independently(self):
+        class MyClass:
+            def __init__(self):
+                self.counter = 0
+
+            @functools.cached_property
+            def value(self):
+                self.counter += 1
+                return self.counter
+
+        obj1 = MyClass()
+        obj2 = MyClass()
+
+        self.assertEqual(obj1.value, 1)
+        self.assertEqual(obj1.value, 1) # Cached
+        self.assertEqual(obj2.value, 1) # New instance, separate counter
+        self.assertEqual(obj2.value, 1) # Cached
+
+        self.assertEqual(obj1.counter, 1)
+        self.assertEqual(obj2.counter, 1)
+
+    def test_slots_compatibility(self):
+        class MySlottedClass:
+            # Must include the mangled name generated by cached_property
+            __slots__ = ('_value_cached_value', 'other_attr', '_compute_count')
+
+            def __init__(self):
+                self.other_attr = "hello"
+                self._compute_count = 0
+
+            @functools.cached_property
+            def value(self):
+                self._compute_count += 1
+                return 100
+
+        obj = MySlottedClass()
+        self.assertEqual(obj._compute_count, 0)
+        self.assertEqual(obj.value, 100)
+        self.assertEqual(obj._compute_count, 1)
+        self.assertEqual(obj.value, 100)
+        self.assertEqual(obj._compute_count, 1) # Still 1, cached via slot
+
+        # Check if the cached value is directly accessible via the mangled name
+        self.assertTrue(hasattr(obj, '_value_cached_value'))
+        self.assertEqual(getattr(obj, '_value_cached_value'), 100)
+
+
+    def test_slots_compatibility_with_no_dict_subclass(self):
+        class BaseClass:
+            # Must declare all instance attributes as slots
+            __slots__ = ('_base_val',)
+            def __init__(self):
+                self._base_val = 5
+
+        class SlottedSubClass(BaseClass):
+            # This class defines its own slots, and potentially no __dict__
+            __slots__ = ('_my_prop_cached_value', '_compute_count',)
+
+            def __init__(self):
+                super(SlottedSubClass, self).__init__() # Python 3.4 super() call
+                self._compute_count = 0
+
+            @functools.cached_property
+            def my_prop(self):
+                self._compute_count += 1
+                return self._base_val * 2
+
+        obj = SlottedSubClass()
+        self.assertEqual(obj.my_prop, 10)
+        self.assertEqual(obj._compute_count, 1)
+        self.assertEqual(obj.my_prop, 10)
+        self.assertEqual(obj._compute_count, 1)
+        self.assertTrue(hasattr(obj, '_my_prop_cached_value'))
+        self.assertEqual(getattr(obj, '_my_prop_cached_value'), 10)
+
+
+    def test_set_and_delete(self):
+        class MyClass:
+            def __init__(self):
+                self.counter = 0
+
+            @functools.cached_property
+            def value(self):
+                self.counter += 1
+                return self.counter
+
+        obj = MyClass()
+        self.assertEqual(obj.value, 1) # Calls func
+        self.assertEqual(obj.counter, 1)
+
+        obj.value = 50 # Manually set the property
+        self.assertEqual(obj.value, 50)
+        self.assertEqual(obj.counter, 1) # Original func not called again
+
+        del obj.value # Delete the cached value
+        self.assertFalse(hasattr(obj, '_value_cached_value')) # Check mangled name
+        self.assertEqual(obj.value, 2) # Calls func again
+        self.assertEqual(obj.counter, 2)
+
+    def test_set_and_delete_with_slots(self):
+        class MySlottedClass:
+            __slots__ = ('_value_cached_value', 'counter') # Add counter if it's a slot too
+
+            def __init__(self):
+                self.counter = 0
+
+            @functools.cached_property
+            def value(self):
+                self.counter += 1
+                return self.counter
+
+        obj = MySlottedClass()
+        self.assertEqual(obj.value, 1)
+        self.assertEqual(obj.counter, 1)
+        self.assertEqual(getattr(obj, '_value_cached_value'), 1)
+
+        obj.value = 75
+        self.assertEqual(obj.value, 75)
+        self.assertEqual(obj.counter, 1)
+        self.assertEqual(getattr(obj, '_value_cached_value'), 75)
+
+        del obj.value
+        self.assertFalse(hasattr(obj, '_value_cached_value')) # Check mangled slot
+        self.assertEqual(obj.value, 2)
+        self.assertEqual(obj.counter, 2)
+        self.assertEqual(getattr(obj, '_value_cached_value'), 2)
+
+
+    def test_thread_safety(self):
+        # This test ensures that the property function is called only once
+        # even with concurrent access.
+        class MyClass:
+            def __init__(self):
+                self.counter = 0
+                self.start_event = threading.Event() # For precise timing control
+
+            @functools.cached_property
+            def expensive_computation(self):
+                self.start_event.wait() # Wait for all threads to be ready
+                time.sleep(0.01) # Simulate expensive computation, shorter sleep for faster test
+                self.counter += 1
+                return self.counter
+
+        obj = MyClass()
+        results = []
+        threads = []
+
+        def get_value():
+            results.append(obj.expensive_computation)
+
+        for _ in range(10): # More threads to stress concurrency
+            t = threading.Thread(target=get_value)
+            threads.append(t)
+            t.start()
+
+        # Give threads a moment to start and reach the .wait()
+        time.sleep(0.001)
+        obj.start_event.set() # Release all threads concurrently
+
+        for t in threads:
+            t.join()
+
+        # All threads should get the same, first computed value
+        self.assertEqual(len(results), 10)
+        self.assertTrue(all(res == 1 for res in results), "All results should be 1")
+        self.assertEqual(obj.counter, 1, "Computation should only run once")
+
+    def test_non_callable_decorator(self):
+        with self.assertRaises(TypeError) as cm:
+            class MyClass:
+                # In Python 3.4, `cached_property(123)` directly calls __init__
+                # of cached_property with 123 as 'func'.
+                # The TypeError should come from cached_property.__init__.
+                @functools.cached_property(123)
+                def invalid_prop(self):
+                    pass
+        # Check for the expected error message parts.
+        self.assertIn("cached_property expected a callable", str(cm.exception))
+        self.assertIn("got <class 'int'>", str(cm.exception))
+
+
+    # Test for the scenario where __slots__ are present but the mangled name is not
+    # This should raise TypeError as per your cached_property implementation
+    def test_slots_no_mangled_name_error(self):
+        class MyBadSlottedClass:
+            __slots__ = ('some_other_slot',) # Missing the mangled name for 'value'
+
+            def __init__(self):
+                pass
+
+            @functools.cached_property
+            def value(self):
+                return 1
+
+        obj = MyBadSlottedClass()
+        with self.assertRaises(TypeError) as cm:
+            # Accessing the property will try to cache it and fail
+            obj.value
+        self.assertIn("Cannot cache 'value' on 'MyBadSlottedClass' instance", str(cm.exception))
+        self.assertIn("'_value_cached_value' is a defined slot", str(cm.exception))
+
+
 if __name__ == '__main__':
     test_main(verbose=True)
openSUSE Build Service is sponsored by