django/tests/utils_tests/test_lazyobject.py

510 lines
15 KiB
Python

import copy
import pickle
import sys
import unittest
import warnings
from django.test import TestCase
from django.utils.functional import LazyObject, SimpleLazyObject, empty
from .models import Category, CategoryInfo
class Foo:
"""
A simple class with just one attribute.
"""
foo = "bar"
def __eq__(self, other):
return self.foo == other.foo
class LazyObjectTestCase(unittest.TestCase):
def lazy_wrap(self, wrapped_object):
"""
Wrap the given object into a LazyObject
"""
class AdHocLazyObject(LazyObject):
def _setup(self):
self._wrapped = wrapped_object
return AdHocLazyObject()
def test_getattribute(self):
"""
Proxy methods don't exist on wrapped objects unless they're set.
"""
attrs = [
"__getitem__",
"__setitem__",
"__delitem__",
"__iter__",
"__len__",
"__contains__",
]
foo = Foo()
obj = self.lazy_wrap(foo)
for attr in attrs:
with self.subTest(attr):
self.assertFalse(hasattr(obj, attr))
setattr(foo, attr, attr)
obj_with_attr = self.lazy_wrap(foo)
self.assertTrue(hasattr(obj_with_attr, attr))
self.assertEqual(getattr(obj_with_attr, attr), attr)
def test_getattr(self):
obj = self.lazy_wrap(Foo())
self.assertEqual(obj.foo, "bar")
def test_getattr_falsey(self):
class Thing:
def __getattr__(self, key):
return []
obj = self.lazy_wrap(Thing())
self.assertEqual(obj.main, [])
def test_setattr(self):
obj = self.lazy_wrap(Foo())
obj.foo = "BAR"
obj.bar = "baz"
self.assertEqual(obj.foo, "BAR")
self.assertEqual(obj.bar, "baz")
def test_setattr2(self):
# Same as test_setattr but in reversed order
obj = self.lazy_wrap(Foo())
obj.bar = "baz"
obj.foo = "BAR"
self.assertEqual(obj.foo, "BAR")
self.assertEqual(obj.bar, "baz")
def test_delattr(self):
obj = self.lazy_wrap(Foo())
obj.bar = "baz"
self.assertEqual(obj.bar, "baz")
del obj.bar
with self.assertRaises(AttributeError):
obj.bar
def test_cmp(self):
obj1 = self.lazy_wrap("foo")
obj2 = self.lazy_wrap("bar")
obj3 = self.lazy_wrap("foo")
self.assertEqual(obj1, "foo")
self.assertEqual(obj1, obj3)
self.assertNotEqual(obj1, obj2)
self.assertNotEqual(obj1, "bar")
def test_lt(self):
obj1 = self.lazy_wrap(1)
obj2 = self.lazy_wrap(2)
self.assertLess(obj1, obj2)
def test_gt(self):
obj1 = self.lazy_wrap(1)
obj2 = self.lazy_wrap(2)
self.assertGreater(obj2, obj1)
def test_bytes(self):
obj = self.lazy_wrap(b"foo")
self.assertEqual(bytes(obj), b"foo")
def test_text(self):
obj = self.lazy_wrap("foo")
self.assertEqual(str(obj), "foo")
def test_bool(self):
# Refs #21840
for f in [False, 0, (), {}, [], None, set()]:
self.assertFalse(self.lazy_wrap(f))
for t in [True, 1, (1,), {1: 2}, [1], object(), {1}]:
self.assertTrue(t)
def test_dir(self):
obj = self.lazy_wrap("foo")
self.assertEqual(dir(obj), dir("foo"))
def test_len(self):
for seq in ["asd", [1, 2, 3], {"a": 1, "b": 2, "c": 3}]:
obj = self.lazy_wrap(seq)
self.assertEqual(len(obj), 3)
def test_class(self):
self.assertIsInstance(self.lazy_wrap(42), int)
class Bar(Foo):
pass
self.assertIsInstance(self.lazy_wrap(Bar()), Foo)
def test_hash(self):
obj = self.lazy_wrap("foo")
d = {obj: "bar"}
self.assertIn("foo", d)
self.assertEqual(d["foo"], "bar")
def test_contains(self):
test_data = [
("c", "abcde"),
(2, [1, 2, 3]),
("a", {"a": 1, "b": 2, "c": 3}),
(2, {1, 2, 3}),
]
for needle, haystack in test_data:
self.assertIn(needle, self.lazy_wrap(haystack))
# __contains__ doesn't work when the haystack is a string and the
# needle a LazyObject.
for needle_haystack in test_data[1:]:
self.assertIn(self.lazy_wrap(needle), haystack)
self.assertIn(self.lazy_wrap(needle), self.lazy_wrap(haystack))
def test_getitem(self):
obj_list = self.lazy_wrap([1, 2, 3])
obj_dict = self.lazy_wrap({"a": 1, "b": 2, "c": 3})
self.assertEqual(obj_list[0], 1)
self.assertEqual(obj_list[-1], 3)
self.assertEqual(obj_list[1:2], [2])
self.assertEqual(obj_dict["b"], 2)
with self.assertRaises(IndexError):
obj_list[3]
with self.assertRaises(KeyError):
obj_dict["f"]
def test_setitem(self):
obj_list = self.lazy_wrap([1, 2, 3])
obj_dict = self.lazy_wrap({"a": 1, "b": 2, "c": 3})
obj_list[0] = 100
self.assertEqual(obj_list, [100, 2, 3])
obj_list[1:2] = [200, 300, 400]
self.assertEqual(obj_list, [100, 200, 300, 400, 3])
obj_dict["a"] = 100
obj_dict["d"] = 400
self.assertEqual(obj_dict, {"a": 100, "b": 2, "c": 3, "d": 400})
def test_delitem(self):
obj_list = self.lazy_wrap([1, 2, 3])
obj_dict = self.lazy_wrap({"a": 1, "b": 2, "c": 3})
del obj_list[-1]
del obj_dict["c"]
self.assertEqual(obj_list, [1, 2])
self.assertEqual(obj_dict, {"a": 1, "b": 2})
with self.assertRaises(IndexError):
del obj_list[3]
with self.assertRaises(KeyError):
del obj_dict["f"]
def test_iter(self):
# Tests whether an object's custom `__iter__` method is being
# used when iterating over it.
class IterObject:
def __init__(self, values):
self.values = values
def __iter__(self):
return iter(self.values)
original_list = ["test", "123"]
self.assertEqual(list(self.lazy_wrap(IterObject(original_list))), original_list)
def test_pickle(self):
# See ticket #16563
obj = self.lazy_wrap(Foo())
obj.bar = "baz"
pickled = pickle.dumps(obj)
unpickled = pickle.loads(pickled)
self.assertIsInstance(unpickled, Foo)
self.assertEqual(unpickled, obj)
self.assertEqual(unpickled.foo, obj.foo)
self.assertEqual(unpickled.bar, obj.bar)
# Test copying lazy objects wrapping both builtin types and user-defined
# classes since a lot of the relevant code does __dict__ manipulation and
# builtin types don't have __dict__.
def test_copy_list(self):
# Copying a list works and returns the correct objects.
lst = [1, 2, 3]
obj = self.lazy_wrap(lst)
len(lst) # forces evaluation
obj2 = copy.copy(obj)
self.assertIsNot(obj, obj2)
self.assertIsInstance(obj2, list)
self.assertEqual(obj2, [1, 2, 3])
def test_copy_list_no_evaluation(self):
# Copying a list doesn't force evaluation.
lst = [1, 2, 3]
obj = self.lazy_wrap(lst)
obj2 = copy.copy(obj)
self.assertIsNot(obj, obj2)
self.assertIs(obj._wrapped, empty)
self.assertIs(obj2._wrapped, empty)
def test_copy_class(self):
# Copying a class works and returns the correct objects.
foo = Foo()
obj = self.lazy_wrap(foo)
str(foo) # forces evaluation
obj2 = copy.copy(obj)
self.assertIsNot(obj, obj2)
self.assertIsInstance(obj2, Foo)
self.assertEqual(obj2, Foo())
def test_copy_class_no_evaluation(self):
# Copying a class doesn't force evaluation.
foo = Foo()
obj = self.lazy_wrap(foo)
obj2 = copy.copy(obj)
self.assertIsNot(obj, obj2)
self.assertIs(obj._wrapped, empty)
self.assertIs(obj2._wrapped, empty)
def test_deepcopy_list(self):
# Deep copying a list works and returns the correct objects.
lst = [1, 2, 3]
obj = self.lazy_wrap(lst)
len(lst) # forces evaluation
obj2 = copy.deepcopy(obj)
self.assertIsNot(obj, obj2)
self.assertIsInstance(obj2, list)
self.assertEqual(obj2, [1, 2, 3])
def test_deepcopy_list_no_evaluation(self):
# Deep copying doesn't force evaluation.
lst = [1, 2, 3]
obj = self.lazy_wrap(lst)
obj2 = copy.deepcopy(obj)
self.assertIsNot(obj, obj2)
self.assertIs(obj._wrapped, empty)
self.assertIs(obj2._wrapped, empty)
def test_deepcopy_class(self):
# Deep copying a class works and returns the correct objects.
foo = Foo()
obj = self.lazy_wrap(foo)
str(foo) # forces evaluation
obj2 = copy.deepcopy(obj)
self.assertIsNot(obj, obj2)
self.assertIsInstance(obj2, Foo)
self.assertEqual(obj2, Foo())
def test_deepcopy_class_no_evaluation(self):
# Deep copying doesn't force evaluation.
foo = Foo()
obj = self.lazy_wrap(foo)
obj2 = copy.deepcopy(obj)
self.assertIsNot(obj, obj2)
self.assertIs(obj._wrapped, empty)
self.assertIs(obj2._wrapped, empty)
class SimpleLazyObjectTestCase(LazyObjectTestCase):
# By inheriting from LazyObjectTestCase and redefining the lazy_wrap()
# method which all testcases use, we get to make sure all behaviors
# tested in the parent testcase also apply to SimpleLazyObject.
def lazy_wrap(self, wrapped_object):
return SimpleLazyObject(lambda: wrapped_object)
def test_repr(self):
# First, for an unevaluated SimpleLazyObject
obj = self.lazy_wrap(42)
# __repr__ contains __repr__ of setup function and does not evaluate
# the SimpleLazyObject
self.assertRegex(repr(obj), "^<SimpleLazyObject:")
self.assertIs(obj._wrapped, empty) # make sure evaluation hasn't been triggered
self.assertEqual(obj, 42) # evaluate the lazy object
self.assertIsInstance(obj._wrapped, int)
self.assertEqual(repr(obj), "<SimpleLazyObject: 42>")
def test_add(self):
obj1 = self.lazy_wrap(1)
self.assertEqual(obj1 + 1, 2)
obj2 = self.lazy_wrap(2)
self.assertEqual(obj2 + obj1, 3)
self.assertEqual(obj1 + obj2, 3)
def test_radd(self):
obj1 = self.lazy_wrap(1)
self.assertEqual(1 + obj1, 2)
def test_trace(self):
# See ticket #19456
old_trace_func = sys.gettrace()
try:
def trace_func(frame, event, arg):
frame.f_locals["self"].__class__
if old_trace_func is not None:
old_trace_func(frame, event, arg)
sys.settrace(trace_func)
self.lazy_wrap(None)
finally:
sys.settrace(old_trace_func)
def test_none(self):
i = [0]
def f():
i[0] += 1
return None
x = SimpleLazyObject(f)
self.assertEqual(str(x), "None")
self.assertEqual(i, [1])
self.assertEqual(str(x), "None")
self.assertEqual(i, [1])
def test_dict(self):
# See ticket #18447
lazydict = SimpleLazyObject(lambda: {"one": 1})
self.assertEqual(lazydict["one"], 1)
lazydict["one"] = -1
self.assertEqual(lazydict["one"], -1)
self.assertIn("one", lazydict)
self.assertNotIn("two", lazydict)
self.assertEqual(len(lazydict), 1)
del lazydict["one"]
with self.assertRaises(KeyError):
lazydict["one"]
def test_list_set(self):
lazy_list = SimpleLazyObject(lambda: [1, 2, 3, 4, 5])
lazy_set = SimpleLazyObject(lambda: {1, 2, 3, 4})
self.assertIn(1, lazy_list)
self.assertIn(1, lazy_set)
self.assertNotIn(6, lazy_list)
self.assertNotIn(6, lazy_set)
self.assertEqual(len(lazy_list), 5)
self.assertEqual(len(lazy_set), 4)
class BaseBaz:
"""
A base class with a funky __reduce__ method, meant to simulate the
__reduce__ method of Model, which sets self._django_version.
"""
def __init__(self):
self.baz = "wrong"
def __reduce__(self):
self.baz = "right"
return super().__reduce__()
def __eq__(self, other):
if self.__class__ != other.__class__:
return False
for attr in ["bar", "baz", "quux"]:
if hasattr(self, attr) != hasattr(other, attr):
return False
elif getattr(self, attr, None) != getattr(other, attr, None):
return False
return True
class Baz(BaseBaz):
"""
A class that inherits from BaseBaz and has its own __reduce_ex__ method.
"""
def __init__(self, bar):
self.bar = bar
super().__init__()
def __reduce_ex__(self, proto):
self.quux = "quux"
return super().__reduce_ex__(proto)
class BazProxy(Baz):
"""
A class that acts as a proxy for Baz. It does some scary mucking about with
dicts, which simulates some crazy things that people might do with
e.g. proxy models.
"""
def __init__(self, baz):
self.__dict__ = baz.__dict__
self._baz = baz
# Grandparent super
super(BaseBaz, self).__init__()
class SimpleLazyObjectPickleTestCase(TestCase):
"""
Regression test for pickling a SimpleLazyObject wrapping a model (#25389).
Also covers other classes with a custom __reduce__ method.
"""
def test_pickle_with_reduce(self):
"""
Test in a fairly synthetic setting.
"""
# Test every pickle protocol available
for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
lazy_objs = [
SimpleLazyObject(lambda: BaseBaz()),
SimpleLazyObject(lambda: Baz(1)),
SimpleLazyObject(lambda: BazProxy(Baz(2))),
]
for obj in lazy_objs:
pickled = pickle.dumps(obj, protocol)
unpickled = pickle.loads(pickled)
self.assertEqual(unpickled, obj)
self.assertEqual(unpickled.baz, "right")
def test_pickle_model(self):
"""
Test on an actual model, based on the report in #25426.
"""
category = Category.objects.create(name="thing1")
CategoryInfo.objects.create(category=category)
# Test every pickle protocol available
for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
lazy_category = SimpleLazyObject(lambda: category)
# Test both if we accessed a field on the model and if we didn't.
lazy_category.categoryinfo
lazy_category_2 = SimpleLazyObject(lambda: category)
with warnings.catch_warnings(record=True) as recorded:
self.assertEqual(
pickle.loads(pickle.dumps(lazy_category, protocol)), category
)
self.assertEqual(
pickle.loads(pickle.dumps(lazy_category_2, protocol)), category
)
# Assert that there were no warnings.
self.assertEqual(len(recorded), 0)