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)