509 lines
15 KiB
Python
509 lines
15 KiB
Python
import copy
|
|
import pickle
|
|
import sys
|
|
import warnings
|
|
from unittest 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(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)
|