diff options
| author | Thomas Grainger <tagrain@gmail.com> | 2018-06-07 14:03:45 +0100 |
|---|---|---|
| committer | Tim Graham <timograham@gmail.com> | 2018-11-19 13:40:49 -0500 |
| commit | 06076999026091cf007d8ea69146340a361259f8 (patch) | |
| tree | 15849dd10829da13a6c09abf6bed9fe8f54acd42 /tests/utils_tests/test_functional.py | |
| parent | 80ba7a881f9810404ba8a660548f1757f8243562 (diff) | |
Fixed #29478 -- Added support for mangled names to cached_property.
Co-Authored-By: Sergey Fedoseev <fedoseev.sergey@gmail.com>
Diffstat (limited to 'tests/utils_tests/test_functional.py')
| -rw-r--r-- | tests/utils_tests/test_functional.py | 169 |
1 files changed, 148 insertions, 21 deletions
diff --git a/tests/utils_tests/test_functional.py b/tests/utils_tests/test_functional.py index befbcf931c..8d26a906b9 100644 --- a/tests/utils_tests/test_functional.py +++ b/tests/utils_tests/test_functional.py @@ -1,9 +1,11 @@ import unittest +from django.test import SimpleTestCase from django.utils.functional import cached_property, lazy +from django.utils.version import PY36 -class FunctionalTestCase(unittest.TestCase): +class FunctionalTests(SimpleTestCase): def test_lazy(self): t = lazy(lambda: tuple(range(3)), list, tuple) for a, b in zip(t(), range(3)): @@ -47,43 +49,168 @@ class FunctionalTestCase(unittest.TestCase): self.assertEqual(str(t), "Î am ā Ǩlâzz.") self.assertEqual(bytes(t), b"\xc3\x8e am \xc4\x81 binary \xc7\xa8l\xc3\xa2zz.") - def test_cached_property(self): - """ - cached_property caches its value and that it behaves like a property - """ - class A: + def assertCachedPropertyWorks(self, attr, Class): + with self.subTest(attr=attr): + def get(source): + return getattr(source, attr) + + obj = Class() + class SubClass(Class): + pass + + subobj = SubClass() + # Docstring is preserved. + self.assertEqual(get(Class).__doc__, 'Here is the docstring...') + self.assertEqual(get(SubClass).__doc__, 'Here is the docstring...') + # It's cached. + self.assertEqual(get(obj), get(obj)) + self.assertEqual(get(subobj), get(subobj)) + # The correct value is returned. + self.assertEqual(get(obj)[0], 1) + self.assertEqual(get(subobj)[0], 1) + # State isn't shared between instances. + obj2 = Class() + subobj2 = SubClass() + self.assertNotEqual(get(obj), get(obj2)) + self.assertNotEqual(get(subobj), get(subobj2)) + # It behaves like a property when there's no instance. + self.assertIsInstance(get(Class), cached_property) + self.assertIsInstance(get(SubClass), cached_property) + # 'other_value' doesn't become a property. + self.assertTrue(callable(obj.other_value)) + self.assertTrue(callable(subobj.other_value)) + + def test_cached_property(self): + """cached_property caches its value and behaves like a property.""" + class Class: @cached_property def value(self): """Here is the docstring...""" return 1, object() + @cached_property + def __foo__(self): + """Here is the docstring...""" + return 1, object() + def other_value(self): - return 1 + """Here is the docstring...""" + return 1, object() other = cached_property(other_value, name='other') - # docstring should be preserved - self.assertEqual(A.value.__doc__, "Here is the docstring...") + attrs = ['value', 'other', '__foo__'] + for attr in attrs: + self.assertCachedPropertyWorks(attr, Class) + + @unittest.skipUnless(PY36, '__set_name__ is new in Python 3.6') + def test_cached_property_auto_name(self): + """ + cached_property caches its value and behaves like a property + on mangled methods or when the name kwarg isn't set. + """ + class Class: + @cached_property + def __value(self): + """Here is the docstring...""" + return 1, object() + + def other_value(self): + """Here is the docstring...""" + return 1, object() + + other = cached_property(other_value) + other2 = cached_property(other_value, name='different_name') + + attrs = ['_Class__value', 'other'] + for attr in attrs: + self.assertCachedPropertyWorks(attr, Class) + + # An explicit name is ignored. + obj = Class() + obj.other2 + self.assertFalse(hasattr(obj, 'different_name')) + + @unittest.skipUnless(PY36, '__set_name__ is new in Python 3.6') + def test_cached_property_reuse_different_names(self): + """Disallow this case because the decorated function wouldn't be cached.""" + with self.assertRaises(RuntimeError) as ctx: + class ReusedCachedProperty: + @cached_property + def a(self): + pass + + b = a + + self.assertEqual( + str(ctx.exception.__context__), + str(TypeError( + "Cannot assign the same cached_property to two different " + "names ('a' and 'b')." + )) + ) + + @unittest.skipUnless(PY36, '__set_name__ is new in Python 3.6') + def test_cached_property_reuse_same_name(self): + """ + Reusing a cached_property on different classes under the same name is + allowed. + """ + counter = 0 + + @cached_property + def _cp(_self): + nonlocal counter + counter += 1 + return counter + + class A: + cp = _cp + + class B: + cp = _cp a = A() + b = B() + self.assertEqual(a.cp, 1) + self.assertEqual(b.cp, 2) + self.assertEqual(a.cp, 1) - # check that it is cached - self.assertEqual(a.value, a.value) + @unittest.skipUnless(PY36, '__set_name__ is new in Python 3.6') + def test_cached_property_set_name_not_called(self): + cp = cached_property(lambda s: None) - # check that it returns the right thing - self.assertEqual(a.value[0], 1) + class Foo: + pass - # check that state isn't shared between instances - a2 = A() - self.assertNotEqual(a.value, a2.value) + Foo.cp = cp + msg = 'Cannot use cached_property instance without calling __set_name__() on it.' + with self.assertRaisesMessage(TypeError, msg): + Foo().cp - # check that it behaves like a property when there's no instance - self.assertIsInstance(A.value, cached_property) + @unittest.skipIf(PY36, '__set_name__ is new in Python 3.6') + def test_cached_property_mangled_error(self): + msg = ( + 'cached_property does not work with mangled methods on ' + 'Python < 3.6 without the appropriate `name` argument.' + ) + with self.assertRaisesMessage(ValueError, msg): + @cached_property + def __value(self): + pass + with self.assertRaisesMessage(ValueError, msg): + def func(self): + pass + cached_property(func, name='__value') - # check that overriding name works - self.assertEqual(a.other, 1) - self.assertTrue(callable(a.other_value)) + @unittest.skipIf(PY36, '__set_name__ is new in Python 3.6') + def test_cached_property_name_validation(self): + msg = "%s can't be used as the name of a cached_property." + with self.assertRaisesMessage(ValueError, msg % "'<lambda>'"): + cached_property(lambda x: None) + with self.assertRaisesMessage(ValueError, msg % 42): + cached_property(str, name=42) def test_lazy_equality(self): """ |
