diff --git a/django/utils/functional.py b/django/utils/functional.py index e3be6722824..ed344166de1 100644 --- a/django/utils/functional.py +++ b/django/utils/functional.py @@ -89,19 +89,14 @@ def lazy(func, *resultclasses): until one of the methods on the result is called. """ - __prepared = False - def __init__(self, args, kw): - self.__args = args - self.__kw = kw - if not self.__prepared: - self.__prepare_class__() - self.__class__.__prepared = True + self._args = args + self._kw = kw def __reduce__(self): return ( _lazy_proxy_unpickle, - (func, self.__args, self.__kw) + resultclasses, + (func, self._args, self._kw) + resultclasses, ) def __deepcopy__(self, memo): @@ -111,31 +106,8 @@ def lazy(func, *resultclasses): memo[id(self)] = self return self - @classmethod - def __prepare_class__(cls): - for resultclass in resultclasses: - for type_ in resultclass.mro(): - for method_name in type_.__dict__: - # All __promise__ return the same wrapper method, they - # look up the correct implementation when called. - if hasattr(cls, method_name): - continue - meth = cls.__promise__(method_name) - setattr(cls, method_name, meth) - - @classmethod - def __promise__(cls, method_name): - # Builds a wrapper around some magic method - def __wrapper__(self, *args, **kw): - # Automatically triggers the evaluation of a lazy value and - # applies the given magic method of the result type. - res = func(*self.__args, **self.__kw) - return getattr(res, method_name)(*args, **kw) - - return __wrapper__ - def __cast(self): - return func(*self.__args, **self.__kw) + return func(*self._args, **self._kw) # Explicitly wrap methods which are defined on object and hence would # not have been overloaded by the loop over resultclasses below. @@ -144,8 +116,6 @@ def lazy(func, *resultclasses): return repr(self.__cast()) def __str__(self): - # object defines __str__(), so __prepare_class__() won't overload - # a __str__() method from the proxied class. return str(self.__cast()) def __eq__(self, other): @@ -193,6 +163,28 @@ def lazy(func, *resultclasses): def __mod__(self, other): return self.__cast() % other + def __promise__(method_name): + # Builds a wrapper around some method. + def __wrapper__(self, *args, **kw): + # Automatically triggers the evaluation of a lazy value and + # applies the given method of the result type. + res = func(*self._args, **self._kw) + return getattr(res, method_name)(*args, **kw) + + return __wrapper__ + + # Add wrappers for all methods from resultclasses which haven't been + # wrapped explicitly above. + for resultclass in resultclasses: + for type_ in resultclass.mro(): + for method_name in type_.__dict__: + # All __promise__ return the same wrapper method, they look up + # the correct implementation when called. + if hasattr(__proxy__, method_name): + continue + meth = __promise__(method_name) + setattr(__proxy__, method_name, meth) + @wraps(func) def __wrapper__(*args, **kw): # Creates the proxy object, instead of the actual value. diff --git a/tests/utils_tests/test_functional.py b/tests/utils_tests/test_functional.py index d49c2a5b492..0fc6ee97dac 100644 --- a/tests/utils_tests/test_functional.py +++ b/tests/utils_tests/test_functional.py @@ -1,5 +1,3 @@ -from unittest import mock - from django.test import SimpleTestCase from django.utils.functional import cached_property, classproperty, lazy from django.utils.version import PY312 @@ -273,14 +271,10 @@ class FunctionalTests(SimpleTestCase): lazy_obj = lazy(lambda: original_object, bytes) self.assertEqual(repr(original_object), repr(lazy_obj())) - def test_lazy_class_preparation_caching(self): - # lazy() should prepare the proxy class only once i.e. the first time - # it's used. - lazified = lazy(lambda: 0, int) - __proxy__ = lazified().__class__ - with mock.patch.object(__proxy__, "__prepare_class__") as mocked: - lazified() - mocked.assert_not_called() + def test_lazy_regular_method(self): + original_object = 15 + lazy_obj = lazy(lambda: original_object, int) + self.assertEqual(original_object.bit_length(), lazy_obj().bit_length()) def test_lazy_bytes_and_str_result_classes(self): lazy_obj = lazy(lambda: "test", str, bytes)