mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 10:44:36 +08:00
Use the landed PyRef target model for a transparent dict/frozendict header view, retaining objects explicitly with PyRef<PyAnyDict>. Keep targets sealed and preserve mutable-only entry points. Borrow frame globals from the object header rather than movable reference wrappers. Unify exact-TypeError key diagnostics with final CPython semantics, including callback failure propagation and no explicit cause. Keep the Python regressions in builtin_dict and test reference layout/ownership in Rust. Preserve existing canonical library updates. Assisted-by: OpenAI Codex:model identifier unavailable
2231 lines
78 KiB
Python
2231 lines
78 KiB
Python
from testutils import assert_raises
|
|
|
|
assert len(dict()) == 0
|
|
|
|
assert len({}) == 0
|
|
assert len({"a": "b"}) == 1
|
|
assert len({"a": "b", "b": 1}) == 2
|
|
assert len({"a": "b", "b": 1, "a" + "b": 2 * 2}) == 3
|
|
|
|
d = {}
|
|
d["a"] = d
|
|
assert repr(d) == "{'a': {...}}"
|
|
|
|
assert {"a": 123}.get("a") == 123
|
|
assert {"a": 123}.get("b") == None
|
|
assert {"a": 123}.get("b", 456) == 456
|
|
|
|
d = {"a": 123, "b": 456}
|
|
assert list(reversed(d)) == ["b", "a"]
|
|
assert list(reversed(d.keys())) == ["b", "a"]
|
|
assert list(reversed(d.values())) == [456, 123]
|
|
assert list(reversed(d.items())) == [("b", 456), ("a", 123)]
|
|
with assert_raises(StopIteration):
|
|
dict_reversed = reversed(d)
|
|
for _ in range(len(d) + 1):
|
|
next(dict_reversed)
|
|
assert "dict" in dict().__doc__
|
|
|
|
|
|
d = {"a": 123, "b": 456}
|
|
assert 1 not in d.items()
|
|
assert "a" not in d.items()
|
|
assert "a", 123 not in d.items()
|
|
assert () not in d.items()
|
|
assert (1) not in d.items()
|
|
assert ("a") not in d.items()
|
|
assert ("a", 123) in d.items()
|
|
assert ("b", 456) in d.items()
|
|
assert ("a", 123, 3) not in d.items()
|
|
assert ("a", 123, "b", 456) not in d.items()
|
|
|
|
d = {1: 10, "a": "ABC", (3, 4): 5}
|
|
assert 1 in d.keys()
|
|
assert (1) in d.keys()
|
|
assert "a" in d.keys()
|
|
assert (3, 4) in d.keys()
|
|
assert () not in d.keys()
|
|
assert 10 not in d.keys()
|
|
assert (1, 10) not in d.keys()
|
|
assert "abc" not in d.keys()
|
|
assert ((3, 4), 5) not in d.keys()
|
|
|
|
d1 = {"a": 1, "b": 2}
|
|
d2 = {"c": 3, "d": 4}
|
|
assert d1.items().isdisjoint(d2.items())
|
|
assert d1.keys().isdisjoint(d2.keys())
|
|
d2 = {"b": 3, "d": 4}
|
|
assert d1.items().isdisjoint(d2.items())
|
|
assert not d1.keys().isdisjoint(d2.keys())
|
|
d2 = {"c": 2, "d": 4}
|
|
assert d1.items().isdisjoint(d2.items())
|
|
assert d1.keys().isdisjoint(d2.keys())
|
|
d2 = {"b": 2, "d": 4}
|
|
assert not d1.items().isdisjoint(d2.items())
|
|
assert not d1.keys().isdisjoint(d2.keys())
|
|
|
|
|
|
assert dict(a=2, b=3) == {"a": 2, "b": 3}
|
|
assert dict({"a": 2, "b": 3}, b=4) == {"a": 2, "b": 4}
|
|
assert dict([("a", 2), ("b", 3)]) == {"a": 2, "b": 3}
|
|
|
|
assert {} == {}
|
|
assert not {"a": 2} == {}
|
|
assert not {} == {"a": 2}
|
|
assert not {"b": 2} == {"a": 2}
|
|
assert not {"a": 4} == {"a": 2}
|
|
assert {"a": 2} == {"a": 2}
|
|
|
|
nan = float("nan")
|
|
assert {"a": nan} == {"a": nan}
|
|
|
|
a = {"g": 5}
|
|
b = {"a": a, "d": 9}
|
|
c = dict(b)
|
|
c["d"] = 3
|
|
c["a"]["g"] = 2
|
|
assert a == {"g": 2}
|
|
assert b == {"a": a, "d": 9}
|
|
|
|
a.clear()
|
|
assert len(a) == 0
|
|
|
|
a = {"a": 5, "b": 6}
|
|
res = set()
|
|
for value in a.values():
|
|
res.add(value)
|
|
assert res == set([5, 6])
|
|
|
|
count = 0
|
|
for key, value in a.items():
|
|
assert a[key] == value
|
|
count += 1
|
|
assert count == len(a)
|
|
|
|
res = set()
|
|
for key in a.keys():
|
|
res.add(key)
|
|
assert res == set(["a", "b"])
|
|
|
|
# Deleted values are correctly skipped over:
|
|
x = {"a": 1, "b": 2, "c": 3, "d": 3}
|
|
del x["c"]
|
|
it = iter(x.items())
|
|
assert ("a", 1) == next(it)
|
|
assert ("b", 2) == next(it)
|
|
assert ("d", 3) == next(it)
|
|
with assert_raises(StopIteration):
|
|
next(it)
|
|
|
|
with assert_raises(KeyError) as cm:
|
|
del x[10]
|
|
assert cm.exception.args[0] == 10
|
|
|
|
# Iterating a dictionary is just its keys:
|
|
assert ["a", "b", "d"] == list(x)
|
|
|
|
# Iterating view captures dictionary when iterated.
|
|
data = {1: 2, 3: 4}
|
|
items = data.items()
|
|
assert list(items) == [(1, 2), (3, 4)]
|
|
data[5] = 6
|
|
assert list(items) == [(1, 2), (3, 4), (5, 6)]
|
|
|
|
# Values can be changed during iteration.
|
|
data = {1: 2, 3: 4}
|
|
items = iter(data.items())
|
|
assert (1, 2) == next(items)
|
|
data[3] = "changed"
|
|
assert (3, "changed") == next(items)
|
|
|
|
# But we can't add or delete items during iteration.
|
|
d = {}
|
|
a = iter(d.items())
|
|
d["a"] = 2
|
|
b = iter(d.items())
|
|
assert ("a", 2) == next(b)
|
|
with assert_raises(RuntimeError):
|
|
next(a)
|
|
del d["a"]
|
|
with assert_raises(RuntimeError):
|
|
next(b)
|
|
|
|
# View isn't itself an iterator.
|
|
with assert_raises(TypeError):
|
|
next(data.keys())
|
|
|
|
assert len(data.keys()) == 2
|
|
|
|
x = {}
|
|
x[1] = 1
|
|
assert x[1] == 1
|
|
|
|
x[7] = 7
|
|
x[2] = 2
|
|
x[(5, 6)] = 5
|
|
|
|
with assert_raises(TypeError):
|
|
x[[]] # Unhashable type.
|
|
|
|
x["here"] = "here"
|
|
assert x.get("not here", "default") == "default"
|
|
assert x.get("here", "default") == "here"
|
|
assert x.get("not here") == None
|
|
|
|
|
|
class LengthDict(dict):
|
|
def __getitem__(self, k):
|
|
return len(k)
|
|
|
|
|
|
x = LengthDict()
|
|
assert type(x) == LengthDict
|
|
assert x["word"] == 4
|
|
assert x.get("word") is None
|
|
|
|
assert 5 == eval("a + word", LengthDict())
|
|
|
|
|
|
class Squares(dict):
|
|
def __missing__(self, k):
|
|
v = k * k
|
|
self[k] = v
|
|
return v
|
|
|
|
|
|
x = Squares()
|
|
assert x[-5] == 25
|
|
|
|
|
|
# An object that hashes to the same value always, and compares equal if any its values match.
|
|
class Hashable(object):
|
|
def __init__(self, *args):
|
|
self.values = args
|
|
|
|
def __hash__(self):
|
|
return 1
|
|
|
|
def __eq__(self, other):
|
|
for x in self.values:
|
|
for y in other.values:
|
|
if x == y:
|
|
return True
|
|
return False
|
|
|
|
|
|
x = {}
|
|
x[Hashable(1, 2)] = 8
|
|
|
|
assert x[Hashable(1, 2)] == 8
|
|
assert x[Hashable(3, 1)] == 8
|
|
|
|
x[Hashable(8)] = 19
|
|
x[Hashable(19, 8)] = 1
|
|
assert x[Hashable(8)] == 1
|
|
assert len(x) == 2
|
|
|
|
assert list({"a": 2, "b": 10}) == ["a", "b"]
|
|
x = {}
|
|
x["a"] = 2
|
|
x["b"] = 10
|
|
assert list(x) == ["a", "b"]
|
|
|
|
y = x.copy()
|
|
x["c"] = 12
|
|
assert y == {"a": 2, "b": 10}
|
|
|
|
y.update({"c": 19, "d": -1, "b": 12})
|
|
assert y == {"a": 2, "b": 12, "c": 19, "d": -1}
|
|
|
|
y.update(y)
|
|
assert y == {"a": 2, "b": 12, "c": 19, "d": -1} # hasn't changed
|
|
|
|
# KeyError has object that used as key as an .args[0]
|
|
with assert_raises(KeyError) as cm:
|
|
x["not here"]
|
|
assert cm.exception.args[0] == "not here"
|
|
with assert_raises(KeyError) as cm:
|
|
x.pop("not here")
|
|
assert cm.exception.args[0] == "not here"
|
|
|
|
with assert_raises(KeyError) as cm:
|
|
x[10]
|
|
assert cm.exception.args[0] == 10
|
|
with assert_raises(KeyError) as cm:
|
|
x.pop(10)
|
|
assert cm.exception.args[0] == 10
|
|
|
|
|
|
class MyClass:
|
|
pass
|
|
|
|
|
|
obj = MyClass()
|
|
|
|
with assert_raises(KeyError) as cm:
|
|
x[obj]
|
|
assert cm.exception.args[0] == obj
|
|
with assert_raises(KeyError) as cm:
|
|
x.pop(obj)
|
|
assert cm.exception.args[0] == obj
|
|
|
|
x = {1: "a", "1": None}
|
|
assert x.pop(1) == "a"
|
|
assert x.pop("1") is None
|
|
assert x == {}
|
|
|
|
x = {1: "a"}
|
|
assert (1, "a") == x.popitem()
|
|
assert x == {}
|
|
with assert_raises(KeyError) as cm:
|
|
x.popitem()
|
|
assert cm.exception.args == ("popitem(): dictionary is empty",)
|
|
|
|
x = {"a": 4}
|
|
assert 4 == x.setdefault("a", 0)
|
|
assert x["a"] == 4
|
|
assert 0 == x.setdefault("b", 0)
|
|
assert x["b"] == 0
|
|
assert None == x.setdefault("c")
|
|
assert x["c"] is None
|
|
|
|
assert {1: None, "b": None} == dict.fromkeys([1, "b"])
|
|
assert {1: 0, "b": 0} == dict.fromkeys([1, "b"], 0)
|
|
|
|
for source in ({i: -1 for i in range(128)}, set(range(128)), frozenset(range(128))):
|
|
shared_value = []
|
|
result = dict.fromkeys(source, shared_value)
|
|
assert list(result) == list(source)
|
|
assert all(value is shared_value for value in result.values())
|
|
|
|
|
|
class UnsizedFromKeys:
|
|
def __iter__(self):
|
|
return iter((1, 1, 2))
|
|
|
|
def __len__(self):
|
|
raise AssertionError("fromkeys must not request a length hint")
|
|
|
|
__length_hint__ = __len__
|
|
|
|
|
|
assert dict.fromkeys(UnsizedFromKeys()) == {1: None, 2: None}
|
|
|
|
x = {"a": 1, "b": 1, "c": 1}
|
|
y = {"b": 2, "c": 2, "d": 2}
|
|
z = {"c": 3, "d": 3, "e": 3}
|
|
|
|
w = {1: 1, **x, 2: 2, **y, 3: 3, **z, 4: 4}
|
|
assert w == {
|
|
1: 1,
|
|
"a": 1,
|
|
"b": 2,
|
|
"c": 3,
|
|
2: 2,
|
|
"d": 3,
|
|
3: 3,
|
|
"e": 3,
|
|
4: 4,
|
|
} # not in cpython test suite
|
|
|
|
assert str({True: True, 1.0: 1.0}) == str({True: 1.0})
|
|
|
|
|
|
class A:
|
|
def __hash__(self):
|
|
return 1
|
|
|
|
def __eq__(self, other):
|
|
return isinstance(other, A)
|
|
|
|
|
|
class B:
|
|
def __hash__(self):
|
|
return 1
|
|
|
|
def __eq__(self, other):
|
|
return isinstance(other, B)
|
|
|
|
|
|
s = {1: 0, A(): 1, B(): 2}
|
|
assert len(s) == 3
|
|
assert s[1] == 0
|
|
assert s[A()] == 1
|
|
assert s[B()] == 2
|
|
|
|
# Test dict usage in set with star expressions!
|
|
a = {"bla": 2}
|
|
b = {"c": 44, "bla": 332, "d": 6}
|
|
x = ["bla", "c", "d", "f"]
|
|
c = {*a, *b, *x}
|
|
# print(c, type(c))
|
|
assert isinstance(c, set)
|
|
assert c == {"bla", "c", "d", "f"}
|
|
|
|
assert not {}.__ne__({})
|
|
assert {}.__ne__({"a": "b"})
|
|
assert {}.__ne__(1) == NotImplemented
|
|
|
|
it = iter({0: 1, 2: 3, 4: 5, 6: 7})
|
|
assert it.__length_hint__() == 4
|
|
next(it)
|
|
assert it.__length_hint__() == 3
|
|
next(it)
|
|
assert it.__length_hint__() == 2
|
|
next(it)
|
|
assert it.__length_hint__() == 1
|
|
next(it)
|
|
assert it.__length_hint__() == 0
|
|
assert_raises(StopIteration, next, it)
|
|
assert it.__length_hint__() == 0
|
|
|
|
# Reverse hints count live entries, including when storage has deleted slots.
|
|
for reverse_iter in (
|
|
reversed,
|
|
lambda d: reversed(d.keys()),
|
|
lambda d: reversed(d.values()),
|
|
lambda d: reversed(d.items()),
|
|
):
|
|
d = {0: 0, 1: 1, 2: 2, 3: 3}
|
|
del d[1]
|
|
it = reverse_iter(d)
|
|
for remaining in (3, 2, 1):
|
|
assert it.__length_hint__() == remaining
|
|
next(it)
|
|
assert it.__length_hint__() == 0
|
|
# Exhaustion is observed by next(), rather than the final successful yield.
|
|
d[4] = 4
|
|
assert_raises(RuntimeError, next, it)
|
|
assert it.__length_hint__() == 0
|
|
|
|
# Test dictionary unpacking with non-mapping objects
|
|
# This should raise TypeError for non-mapping objects
|
|
with assert_raises(TypeError) as cm:
|
|
{**[1, 2]}
|
|
assert "'list' object is not a mapping" in str(cm.exception)
|
|
|
|
with assert_raises(TypeError) as cm:
|
|
{**[[1, 2], [3, 4]]}
|
|
assert "'list' object is not a mapping" in str(cm.exception)
|
|
|
|
with assert_raises(TypeError) as cm:
|
|
{**"string"}
|
|
assert "'str' object is not a mapping" in str(cm.exception)
|
|
|
|
with assert_raises(TypeError) as cm:
|
|
{**(1, 2, 3)}
|
|
assert "'tuple' object is not a mapping" in str(cm.exception)
|
|
|
|
# Test that valid mappings still work
|
|
assert {**{"a": 1}, **{"b": 2}} == {"a": 1, "b": 2}
|
|
|
|
# Test OrderedDict unpacking preserves order
|
|
import collections
|
|
|
|
od = collections.OrderedDict([("a", 1), ("b", 2)])
|
|
od.move_to_end("a") # Move 'a' to end: ['b', 'a']
|
|
expected_order = list(od.items()) # [('b', 2), ('a', 1)]
|
|
|
|
|
|
def test_func(**kwargs):
|
|
return kwargs
|
|
|
|
|
|
result = test_func(**od)
|
|
assert list(result.items()) == expected_order, (
|
|
f"Expected {expected_order}, got {list(result.items())}"
|
|
)
|
|
|
|
# Test multiple OrderedDict unpacking
|
|
od1 = collections.OrderedDict([("x", 10), ("y", 20)])
|
|
od2 = collections.OrderedDict([("z", 30), ("w", 40)])
|
|
od2.move_to_end("z") # Move 'z' to end: ['w', 'z']
|
|
|
|
result = test_func(**od1, **od2)
|
|
# Should preserve order: x, y, w, z
|
|
expected_keys = ["x", "y", "w", "z"]
|
|
assert list(result.keys()) == expected_keys, (
|
|
f"Expected {expected_keys}, got {list(result.keys())}"
|
|
)
|
|
|
|
|
|
def check_view_comparisons(view, other, expected):
|
|
assert (
|
|
view == other,
|
|
view != other,
|
|
view < other,
|
|
view <= other,
|
|
view > other,
|
|
view >= other,
|
|
) == expected
|
|
assert (
|
|
other == view,
|
|
other != view,
|
|
other > view,
|
|
other >= view,
|
|
other < view,
|
|
other <= view,
|
|
) == expected
|
|
|
|
|
|
# Dictionary views support the same comparisons with mutable and frozen sets.
|
|
config = {"host": "localhost", "port": 8080}
|
|
for view in (config.keys(), config.items()):
|
|
for set_type in (set, frozenset):
|
|
schema = set_type(view)
|
|
check_view_comparisons(view, schema, (True, False, False, True, False, True))
|
|
smaller_schema = schema - {next(iter(schema))}
|
|
check_view_comparisons(
|
|
view, smaller_schema, (False, True, False, False, True, True)
|
|
)
|
|
larger_schema = schema | {("extra",)}
|
|
check_view_comparisons(
|
|
view, larger_schema, (False, True, True, True, False, False)
|
|
)
|
|
check_view_comparisons(
|
|
view, set_type({("other",)}), (False, True, False, False, False, False)
|
|
)
|
|
|
|
# Mixed keys/items views compare their members regardless of insertion order.
|
|
indexed_settings = {("port", 8080): None, ("host", "localhost"): None}
|
|
check_view_comparisons(
|
|
config.items(), indexed_settings.keys(), (True, False, False, True, False, True)
|
|
)
|
|
del indexed_settings[("port", 8080)]
|
|
check_view_comparisons(
|
|
config.items(), indexed_settings.keys(), (False, True, False, False, True, True)
|
|
)
|
|
|
|
# Testing for common settings does not require hashable values.
|
|
left_settings = {"ports": [80, 443], "hosts": ["localhost"]}
|
|
right_settings = {"ports": [80, 443], "timeout": 30}
|
|
assert not left_settings.items().isdisjoint(right_settings.items())
|
|
assert not right_settings.items().isdisjoint(left_settings.items())
|
|
assert not left_settings.items().isdisjoint([("ports", [80, 443])])
|
|
assert left_settings.items().isdisjoint({"ports": [8080]}.items())
|
|
assert left_settings.items().isdisjoint(())
|
|
assert left_settings.items().isdisjoint({"timeout": 30}.items())
|
|
assert left_settings.items() != frozenset()
|
|
assert left_settings.items() >= frozenset()
|
|
assert not left_settings.items() <= frozenset()
|
|
settings_view = left_settings.items()
|
|
assert not settings_view.isdisjoint(settings_view)
|
|
assert {}.items().isdisjoint({}.items())
|
|
|
|
|
|
def settings_then_error(setting):
|
|
yield setting
|
|
raise ValueError("remaining settings unavailable")
|
|
|
|
|
|
# A match stops consumption, while an error before a match still propagates.
|
|
assert not config.keys().isdisjoint(settings_then_error("host"))
|
|
assert not left_settings.items().isdisjoint(settings_then_error(("ports", [80, 443])))
|
|
with assert_raises(ValueError):
|
|
config.keys().isdisjoint(settings_then_error("missing"))
|
|
with assert_raises(TypeError):
|
|
config.keys().isdisjoint(42)
|
|
|
|
|
|
class ViewMembershipKey:
|
|
hash_calls = 0
|
|
|
|
def __hash__(self):
|
|
self.hash_calls += 1
|
|
return 42
|
|
|
|
|
|
# Successful item membership needs one dictionary lookup.
|
|
membership_key = ViewMembershipKey()
|
|
membership_dict = {membership_key: [1, 2]}
|
|
membership_key.hash_calls = 0
|
|
assert (membership_key, [1, 2]) in membership_dict.items()
|
|
assert membership_key.hash_calls == 1
|
|
|
|
for set_type in (set, frozenset):
|
|
|
|
class ViewSetSubclass(set_type):
|
|
events = []
|
|
|
|
def __iter__(self):
|
|
self.events.append("iter")
|
|
return super().__iter__()
|
|
|
|
def __contains__(self, item):
|
|
self.events.append("contains")
|
|
return super().__contains__(item)
|
|
|
|
# View operations preserve a set subclass's iteration and membership hooks.
|
|
schema = ViewSetSubclass({"host", "port"})
|
|
schema.events.clear()
|
|
assert config.keys() == schema
|
|
assert schema.events == ["contains", "contains"]
|
|
schema.events.clear()
|
|
assert config.keys() >= schema
|
|
assert schema.events == ["iter"]
|
|
schema.events.clear()
|
|
assert not config.keys().isdisjoint(schema)
|
|
assert schema.events == ["iter"]
|
|
larger_schema = ViewSetSubclass({"host", "port", "timeout"})
|
|
schema.events.clear()
|
|
assert not config.keys().isdisjoint(larger_schema)
|
|
assert schema.events == ["contains"]
|
|
|
|
# An empty view must still consume arbitrary iterables and validate their keys.
|
|
with assert_raises(TypeError):
|
|
{}.keys().isdisjoint([[]])
|
|
with assert_raises(ValueError):
|
|
{}.keys().isdisjoint(settings_then_error("missing"))
|
|
|
|
for set_type in (set, frozenset):
|
|
|
|
class ClearingLengthSet(set_type):
|
|
def __len__(self):
|
|
changing_config.clear()
|
|
return 1
|
|
|
|
def __iter__(self):
|
|
raise ValueError("settings unavailable")
|
|
|
|
# The view's size is captured before calling the other operand's __len__.
|
|
changing_config = {"host": "localhost", "port": 8080}
|
|
with assert_raises(ValueError):
|
|
changing_config.keys().isdisjoint(ClearingLengthSet({"host"}))
|
|
assert changing_config == {}
|
|
|
|
|
|
class ItemLookupDict(dict):
|
|
def __getitem__(self, key):
|
|
raise AssertionError("item membership called __getitem__")
|
|
|
|
def __missing__(self, key):
|
|
raise AssertionError("item membership called __missing__")
|
|
|
|
|
|
# Item views inspect dictionary storage without invoking subclass lookup hooks.
|
|
lookup_items = ItemLookupDict(ports=[80, 443]).items()
|
|
assert ("ports", [80, 443]) in lookup_items
|
|
assert ("ports", [8080]) not in lookup_items
|
|
assert ("missing", []) not in lookup_items
|
|
|
|
|
|
class LookupOrderKey:
|
|
calls = []
|
|
|
|
def __init__(self, name, equal):
|
|
self.name = name
|
|
self.equal = equal
|
|
|
|
def __hash__(self):
|
|
return 17
|
|
|
|
def __eq__(self, other):
|
|
self.calls.append(self.name)
|
|
return self.equal
|
|
|
|
|
|
stored = LookupOrderKey("stored", True)
|
|
lookup = LookupOrderKey("lookup", False)
|
|
mapping = {stored: 1}
|
|
assert mapping[lookup] == 1
|
|
mapping[lookup] = 2
|
|
assert len(mapping) == 1
|
|
assert next(iter(mapping)) is stored
|
|
del mapping[lookup]
|
|
assert not mapping
|
|
assert LookupOrderKey.calls == ["stored", "stored", "stored"]
|
|
|
|
LookupOrderKey.calls.clear()
|
|
stored = LookupOrderKey("stored", NotImplemented)
|
|
lookup = LookupOrderKey("lookup", True)
|
|
assert {stored: 1}[lookup] == 1
|
|
assert LookupOrderKey.calls == ["stored", "lookup"]
|
|
|
|
|
|
class LookupOrderSubclass(LookupOrderKey):
|
|
__hash__ = LookupOrderKey.__hash__
|
|
|
|
def __eq__(self, other):
|
|
return super().__eq__(other)
|
|
|
|
|
|
LookupOrderKey.calls.clear()
|
|
stored = LookupOrderKey("stored", False)
|
|
lookup = LookupOrderSubclass("lookup", True)
|
|
assert {stored: 1}[lookup] == 1
|
|
assert LookupOrderKey.calls == ["lookup"]
|
|
|
|
|
|
class MergeHashKey:
|
|
hash_disabled = False
|
|
|
|
def __init__(self, value):
|
|
self.value = value
|
|
|
|
def __hash__(self):
|
|
assert not self.hash_disabled, "dictionary merge rehashed a stored key"
|
|
return 42
|
|
|
|
def __eq__(self, other):
|
|
if not isinstance(other, MergeHashKey):
|
|
return NotImplemented
|
|
return self.value == other.value
|
|
|
|
|
|
def merge_with_update(source):
|
|
result = {}
|
|
result.update(source)
|
|
return result
|
|
|
|
|
|
def merge_with_ior(source):
|
|
result = {}
|
|
result |= source
|
|
return result
|
|
|
|
|
|
# Exact dictionary merges reuse hashes, including colliding keys and holes.
|
|
merge_keys = [MergeHashKey(i) for i in range(3)]
|
|
merge_source = dict(zip(merge_keys, ("first", "removed", "last")))
|
|
del merge_source[merge_keys[1]]
|
|
existing_merge_key = MergeHashKey(2)
|
|
merge_target = {existing_merge_key: "old"}
|
|
MergeHashKey.hash_disabled = True
|
|
for merge in (
|
|
dict,
|
|
merge_with_update,
|
|
merge_with_ior,
|
|
lambda source: {} | source,
|
|
lambda source: dict.__ror__(source, {}),
|
|
lambda source: {**source},
|
|
):
|
|
assert list(merge(merge_source).items()) == list(merge_source.items())
|
|
|
|
# Overwriting a matching key keeps its identity and position.
|
|
merge_target.update(merge_source)
|
|
assert next(iter(merge_target)) is existing_merge_key
|
|
assert list(merge_target.values()) == ["last", "first"]
|
|
merge_source.update(merge_source)
|
|
merge_source |= merge_source
|
|
assert list(merge_source.values()) == ["first", "last"]
|
|
MergeHashKey.hash_disabled = False
|
|
|
|
|
|
# Compact copies preserve order, key identity and stored hashes after deletions.
|
|
copy_keys = [MergeHashKey(i) for i in range(32)]
|
|
copy_source = {key: key.value for key in copy_keys}
|
|
for key in copy_keys[:-3]:
|
|
del copy_source[key]
|
|
MergeHashKey.hash_disabled = True
|
|
copy_result = copy_source.copy()
|
|
assert list(copy_result.values()) == [29, 30, 31]
|
|
assert all(actual is expected for actual, expected in zip(copy_result, copy_keys[-3:]))
|
|
copy_result.clear()
|
|
assert len(copy_source) == 3
|
|
MergeHashKey.hash_disabled = False
|
|
|
|
|
|
class MergeMapping(dict):
|
|
def __iter__(self):
|
|
return iter(("virtual",))
|
|
|
|
def keys(self):
|
|
return ["virtual"]
|
|
|
|
def __getitem__(self, key):
|
|
assert key == "virtual"
|
|
return 42
|
|
|
|
|
|
# Generic mappings retain their lookup hooks instead of exposing dict storage.
|
|
for merge in (dict, merge_with_update, merge_with_ior):
|
|
assert merge(MergeMapping(stored=0)) == {"virtual": 42}
|
|
|
|
# Test hashability of dict and OrderedDict views
|
|
import collections.abc
|
|
|
|
d = {"a": 1, "b": 2}
|
|
assert type(d.keys()).__hash__ is None
|
|
assert type(d.items()).__hash__ is None
|
|
assert type(d.values()).__hash__ is not None
|
|
assert not isinstance(d.keys(), collections.abc.Hashable)
|
|
assert not isinstance(d.items(), collections.abc.Hashable)
|
|
with assert_raises(TypeError):
|
|
hash(d.keys())
|
|
with assert_raises(TypeError):
|
|
hash(d.items())
|
|
|
|
od = collections.OrderedDict([("a", 1)])
|
|
assert type(od.keys()).__hash__ is None
|
|
assert type(od.items()).__hash__ is None
|
|
assert type(od.values()).__hash__ is not None
|
|
assert not isinstance(od.keys(), collections.abc.Hashable)
|
|
assert not isinstance(od.items(), collections.abc.Hashable)
|
|
with assert_raises(TypeError):
|
|
hash(od.keys())
|
|
with assert_raises(TypeError):
|
|
hash(od.items())
|
|
|
|
|
|
# A TypeError raised while *comparing* keys (e.g. from a colliding key's
|
|
# __eq__) must propagate unchanged, not be rewritten as an unhashable-key
|
|
# error. Only genuine hashing failures get the dict-specific "unhashable"
|
|
# wording.
|
|
class BadEq:
|
|
def __hash__(self):
|
|
return 42 # fixed hash forces a collision
|
|
|
|
def __eq__(self, other):
|
|
raise TypeError("nope")
|
|
|
|
|
|
bad = {}
|
|
bad[BadEq()] = 1
|
|
with assert_raises(TypeError) as cm:
|
|
bad[BadEq()] # hashes fine (42), then compares against the colliding key
|
|
assert "nope" in str(cm.exception), str(cm.exception)
|
|
assert "dict key" not in str(cm.exception), (
|
|
f"comparison error mislabeled as unhashable: {cm.exception}"
|
|
)
|
|
|
|
# A genuinely unhashable key still reports the dict-specific message.
|
|
with assert_raises(TypeError) as cm:
|
|
{}[[]]
|
|
assert "as a dict key" in str(cm.exception), str(cm.exception)
|
|
|
|
# The message reaches every insertion path, not just __setitem__: the
|
|
# constructor, update() and |= all go through the same wrapping.
|
|
for make in (
|
|
lambda: dict([([], 1)]),
|
|
lambda: {}.update([([], 1)]),
|
|
lambda: {}.__ior__([([], 1)]),
|
|
):
|
|
with assert_raises(TypeError) as cm:
|
|
make()
|
|
assert "as a dict key" in str(cm.exception), str(cm.exception)
|
|
|
|
|
|
# The key is hashed once up front, so a __hash__ that fails only on its first
|
|
# call is still reported (a re-hash on the error path would let it escape).
|
|
class FlakyHash:
|
|
_calls = 0
|
|
|
|
def __hash__(self):
|
|
FlakyHash._calls += 1
|
|
if FlakyHash._calls == 1:
|
|
raise TypeError("first call fails")
|
|
return 0
|
|
|
|
|
|
with assert_raises(TypeError) as cm:
|
|
{}[FlakyHash()]
|
|
assert "as a dict key" in str(cm.exception), str(cm.exception)
|
|
|
|
|
|
# The type name is the fully qualified one, like CPython's %T.
|
|
def _make_nested():
|
|
class Nested:
|
|
__hash__ = None
|
|
|
|
return Nested
|
|
|
|
|
|
with assert_raises(TypeError) as cm:
|
|
{}[_make_nested()()]
|
|
assert "_make_nested.<locals>.Nested" in str(cm.exception), str(cm.exception)
|
|
|
|
|
|
# A __hash__ raising a *subclass* of TypeError is left unchanged (CPython
|
|
# checks the exact type), so `except MySubclass` still catches it.
|
|
class MyTypeError(TypeError):
|
|
pass
|
|
|
|
|
|
class SubclassHash:
|
|
def __hash__(self):
|
|
raise MyTypeError("custom")
|
|
|
|
|
|
with assert_raises(MyTypeError) as cm:
|
|
{}[SubclassHash()]
|
|
assert "as a dict key" not in str(cm.exception), str(cm.exception)
|
|
|
|
|
|
# Every operation hashes the key exactly once (the hash is threaded into the
|
|
# inner map), so a __hash__ that would fail on a second call is never called
|
|
# twice. setdefault() and pop() went through their own lookup before.
|
|
class CountingHash:
|
|
calls = 0
|
|
|
|
def __hash__(self):
|
|
CountingHash.calls += 1
|
|
if CountingHash.calls >= 2:
|
|
raise TypeError("must not hash twice")
|
|
return 7
|
|
|
|
|
|
CountingHash.calls = 0
|
|
{}.setdefault(CountingHash(), 1)
|
|
assert CountingHash.calls == 1, CountingHash.calls
|
|
|
|
CountingHash.calls = 0
|
|
with assert_raises(KeyError):
|
|
# A non-empty dict so the lookup must hash the key (CPython skips hashing
|
|
# entirely when popping from an empty dict).
|
|
{1: 1}.pop(CountingHash())
|
|
assert CountingHash.calls == 1, CountingHash.calls
|
|
|
|
|
|
# PEP 814 regressions, validated against CPython 3.15.0.
|
|
import builtins
|
|
|
|
if hasattr(builtins, "frozendict"):
|
|
assert (
|
|
frozendict.__doc__
|
|
== """frozendict() -> new empty immutable dictionary
|
|
frozendict(mapping) -> new immutable dictionary initialized from a mapping
|
|
object's (key, value) pairs
|
|
frozendict(iterable) -> new immutable dictionary initialized as if via:
|
|
d = {}
|
|
for k, v in iterable:
|
|
d[k] = v
|
|
d = frozendict(d)
|
|
frozendict(**kwargs) -> new immutable dictionary initialized with the name=value
|
|
pairs in the keyword argument list. For example: frozendict(one=1, two=2)"""
|
|
)
|
|
|
|
import builtins
|
|
import copy
|
|
import gc
|
|
import pickle
|
|
import types
|
|
import unittest
|
|
|
|
class FrozenWithState(frozendict):
|
|
pass
|
|
|
|
class FrozenWithSlot(frozendict):
|
|
__slots__ = ("extra",)
|
|
|
|
class FrozenDictContractTests(unittest.TestCase):
|
|
def test_construction_identity_and_explicit_new(self):
|
|
original = frozendict(left=object())
|
|
self.assertIs(frozendict(original), original)
|
|
explicit = frozendict.__new__(frozendict, original)
|
|
self.assertIsNot(explicit, original)
|
|
self.assertEqual(explicit, original)
|
|
self.assertIsNot(FrozenWithState(original), original)
|
|
self.assertIsNot(frozendict(FrozenWithState(original)), original)
|
|
original.__init__(object(), object(), ignored=object())
|
|
self.assertEqual(list(original), ["left"])
|
|
self.assertFalse(isinstance(original, dict))
|
|
self.assertFalse(issubclass(frozendict, dict))
|
|
|
|
def test_call_ex_identity_preserves_explicit_type_call_distinction(self):
|
|
original = frozendict(answer=42)
|
|
self.assertIs(frozendict(*(original,)), original)
|
|
self.assertIs(frozendict(original, **{}), original) # noqa: PIE804
|
|
self.assertIs(frozendict(*(original,), **{}), original) # noqa: PIE804
|
|
self.assertIsNot(type.__call__(frozendict, original), original)
|
|
self.assertIsNot(frozendict.__call__(original), original)
|
|
|
|
def test_unrelated_descriptors_cannot_mutate_or_access(self):
|
|
frozen = frozendict(protected=23)
|
|
for method, args in (
|
|
(dict.__init__, ({"replacement": 8},)),
|
|
(dict.__setitem__, ("protected", 99)),
|
|
(dict.__delitem__, ("protected",)),
|
|
(dict.update, ({"replacement": 8},)),
|
|
(dict.clear, ()),
|
|
(dict.get, ("protected",)),
|
|
):
|
|
with self.subTest(method=method), self.assertRaises(TypeError):
|
|
method(frozen, *args)
|
|
with self.assertRaises(TypeError):
|
|
frozendict.get({"protected": 23}, "protected")
|
|
self.assertEqual(frozen, {"protected": 23})
|
|
for name in (
|
|
"clear",
|
|
"update",
|
|
"pop",
|
|
"popitem",
|
|
"setdefault",
|
|
"__setitem__",
|
|
"__delitem__",
|
|
"__ior__",
|
|
):
|
|
self.assertFalse(hasattr(frozen, name), name)
|
|
|
|
def test_subclass_missing_is_only_a_subscript_hook(self):
|
|
class Lookup(frozendict):
|
|
def __missing__(self, key):
|
|
return "missing:" + key
|
|
|
|
frozen = Lookup(present=23)
|
|
self.assertEqual(frozen["absent"], "missing:absent")
|
|
self.assertIsNone(frozen.get("absent"))
|
|
self.assertNotIn("absent", frozen)
|
|
|
|
def test_fromkeys_constructor_calls_and_source_independence(self):
|
|
calls = []
|
|
seed = frozendict(retained="old", replaced="old")
|
|
|
|
class Factory(frozendict):
|
|
def __new__(cls, *args):
|
|
calls.append(args)
|
|
if not args:
|
|
return seed
|
|
return super().__new__(cls, *args)
|
|
|
|
def __setitem__(self, key, value):
|
|
raise AssertionError("immutable construction called a mutator")
|
|
|
|
frozen = Factory.fromkeys(iter(["replaced", "added", "replaced"]), "new")
|
|
self.assertIs(type(frozen), Factory)
|
|
self.assertEqual(
|
|
list(frozen.items()),
|
|
[("retained", "old"), ("replaced", "new"), ("added", "new")],
|
|
)
|
|
self.assertEqual(seed, {"retained": "old", "replaced": "old"})
|
|
self.assertEqual(len(calls), 2)
|
|
self.assertEqual(calls[0], ())
|
|
self.assertIs(type(calls[1][0]), frozendict)
|
|
self.assertEqual(calls[1][0], frozen)
|
|
|
|
def test_fromkeys_preserves_custom_constructor_result(self):
|
|
target = {"seed": 1}
|
|
|
|
class MutableFactory(frozendict):
|
|
def __new__(cls):
|
|
return target
|
|
|
|
result = MutableFactory.fromkeys(["new"], 2)
|
|
self.assertIs(result, target)
|
|
self.assertEqual(target, {"seed": 1, "new": 2})
|
|
|
|
def test_fromkeys_subclass_initialization_is_two_phase(self):
|
|
calls = []
|
|
|
|
class Initialized(frozendict):
|
|
def __init__(self, *args):
|
|
calls.append(args)
|
|
|
|
result = Initialized.fromkeys(["first", "second"], 3)
|
|
self.assertIs(type(result), Initialized)
|
|
self.assertEqual(len(calls), 2)
|
|
self.assertEqual(calls[0], ())
|
|
self.assertIs(type(calls[1][0]), frozendict)
|
|
self.assertEqual(calls[1][0], result)
|
|
|
|
def test_fromkeys_constructs_before_rejecting_invalid_iterable(self):
|
|
calls = []
|
|
|
|
class Factory(frozendict):
|
|
def __new__(cls, *args):
|
|
calls.append(args)
|
|
return super().__new__(cls, *args)
|
|
|
|
with self.assertRaises(TypeError):
|
|
Factory.fromkeys(123)
|
|
self.assertEqual(calls, [()])
|
|
|
|
def test_hash_uses_stored_key_hash_and_caches_values(self):
|
|
class Counted:
|
|
def __init__(self, value):
|
|
self.value = value
|
|
self.calls = 0
|
|
|
|
def __hash__(self):
|
|
self.calls += 1
|
|
return self.value
|
|
|
|
key, value = Counted(41), Counted(73)
|
|
frozen = frozendict([(key, value)])
|
|
key_calls = key.calls
|
|
first = hash(frozen)
|
|
self.assertEqual(key.calls, key_calls)
|
|
self.assertEqual(value.calls, 1)
|
|
self.assertEqual(hash(frozen), first)
|
|
self.assertEqual(key.calls, key_calls)
|
|
self.assertEqual(value.calls, 1)
|
|
|
|
def test_hash_failure_does_not_poison_cache(self):
|
|
class EventuallyHashable:
|
|
def __init__(self):
|
|
self.calls = 0
|
|
|
|
def __hash__(self):
|
|
self.calls += 1
|
|
if self.calls == 1:
|
|
raise ValueError("not yet")
|
|
return 97
|
|
|
|
value = EventuallyHashable()
|
|
frozen = frozendict(retry=value)
|
|
with self.assertRaisesRegex(ValueError, "not yet"):
|
|
hash(frozen)
|
|
expected = hash(frozen)
|
|
self.assertEqual(hash(frozen), expected)
|
|
self.assertEqual(value.calls, 2)
|
|
|
|
def test_mapping_equality_reuses_hashes_but_views_rehash(self):
|
|
class Key:
|
|
forbidden = False
|
|
|
|
def __hash__(self):
|
|
if self.forbidden:
|
|
raise ValueError("rehash forbidden")
|
|
return 313
|
|
|
|
key = Key()
|
|
left = frozendict([(key, 7)])
|
|
right = frozendict.__new__(frozendict, left)
|
|
mutable = dict(left)
|
|
key.forbidden = True
|
|
self.assertTrue(left == left) # noqa: PLR0124
|
|
self.assertTrue(left.__eq__(left))
|
|
self.assertTrue(left == right)
|
|
self.assertTrue(left == mutable)
|
|
self.assertTrue(mutable == left)
|
|
for method in ("keys", "items"):
|
|
with self.assertRaisesRegex(ValueError, "rehash forbidden"):
|
|
_ = getattr(left, method)() == getattr(right, method)()
|
|
|
|
def test_value_hash_can_reenter_same_frozendict(self):
|
|
class Reentrant:
|
|
def __init__(self):
|
|
self.calls = 0
|
|
self.owner = None
|
|
self.nested = None
|
|
|
|
def __hash__(self):
|
|
self.calls += 1
|
|
if self.calls == 1:
|
|
self.nested = hash(self.owner)
|
|
return 127
|
|
|
|
value = Reentrant()
|
|
frozen = frozendict(recursive=value)
|
|
value.owner = frozen
|
|
result = hash(frozen)
|
|
self.assertEqual(result, value.nested)
|
|
self.assertEqual(hash(frozen), result)
|
|
self.assertEqual(value.calls, 2)
|
|
|
|
def test_union_result_type_order_and_identity(self):
|
|
frozen = frozendict(first=1, second=2)
|
|
result = frozen | {"first": 7, "last": 3}
|
|
self.assertIs(type(result), frozendict)
|
|
self.assertEqual(
|
|
list(result.items()), [("first", 7), ("second", 2), ("last", 3)]
|
|
)
|
|
self.assertIs(type({"start": 4} | frozen), dict)
|
|
self.assertIs(frozen | {}, frozen)
|
|
self.assertIs(frozendict() | frozen, frozen)
|
|
child = FrozenWithState(frozen)
|
|
self.assertIs(type(child | {}), frozendict)
|
|
self.assertIsNot(child | {}, child)
|
|
before = frozen
|
|
frozen |= {"third": 3}
|
|
self.assertIsNot(frozen, before)
|
|
self.assertNotIn("third", before)
|
|
with self.assertRaises(TypeError):
|
|
frozen |= [("fourth", 4)]
|
|
|
|
def test_explicit_reflected_union_uses_left_operand_type_and_order(self):
|
|
frozen = frozendict(left=1, shared="frozen")
|
|
mutable = {"right": 2, "shared": "mutable"}
|
|
result = dict.__ror__(mutable, frozen)
|
|
self.assertIs(type(result), frozendict)
|
|
self.assertEqual(
|
|
list(result.items()), [("left", 1), ("shared", "mutable"), ("right", 2)]
|
|
)
|
|
result = frozendict.__ror__(frozen, mutable)
|
|
self.assertIs(type(result), dict)
|
|
self.assertEqual(
|
|
list(result.items()), [("right", 2), ("shared", "frozen"), ("left", 1)]
|
|
)
|
|
|
|
def test_frozen_globals_and_separate_writable_locals(self):
|
|
observed = []
|
|
frozen = frozendict(answer=42, observed=observed, __builtins__=builtins)
|
|
local = {}
|
|
exec("observed.append(answer); local_answer = answer + 1", frozen, local)
|
|
self.assertEqual(observed, [42])
|
|
self.assertEqual(local, {"local_answer": 43})
|
|
self.assertEqual(eval("answer + 2", frozen), 44)
|
|
self.assertIs(eval("globals()", frozen), frozen)
|
|
self.assertNotIn("local_answer", frozen)
|
|
|
|
def test_frozen_globals_require_existing_builtins(self):
|
|
for execute, source in ((eval, "40 + 2"), (exec, "pass")):
|
|
for frozen in (frozendict(answer=42), FrozenWithState(answer=42)):
|
|
with self.subTest(execute=execute, frozen_type=type(frozen)):
|
|
with self.assertRaisesRegex(
|
|
TypeError,
|
|
"cannot assign __builtins__ to frozendict globals",
|
|
):
|
|
execute(source, frozen, {})
|
|
self.assertNotIn("__builtins__", frozen)
|
|
|
|
def test_functions_from_exec_keep_frozen_globals(self):
|
|
frozen = frozendict(answer=42, __builtins__=builtins)
|
|
local = {}
|
|
exec(
|
|
"def read():\n return answer\ndef assign():\n global answer\n answer = 99\ndef delete():\n global answer\n del answer",
|
|
frozen,
|
|
local,
|
|
)
|
|
self.assertIs(local["read"].__globals__, frozen)
|
|
self.assertEqual(local["read"](), 42)
|
|
for name, suffix in (("assign", "assignment"), ("delete", "deletion")):
|
|
with self.assertRaises(TypeError) as caught:
|
|
local[name]()
|
|
self.assertEqual(
|
|
str(caught.exception),
|
|
"frozendict object does not support item " + suffix,
|
|
)
|
|
with self.assertRaises(TypeError):
|
|
types.FunctionType(local["read"].__code__, frozen)
|
|
self.assertEqual(frozen["answer"], 42)
|
|
|
|
def test_frozen_builtins_and_type_namespace(self):
|
|
frozen_builtins = frozendict(len=len)
|
|
frozen = frozendict(__builtins__=frozen_builtins)
|
|
self.assertEqual(eval("len((10, 20))", frozen), 2)
|
|
namespace = frozendict(value=17)
|
|
cls = type("FrozenNamespace", (), namespace)
|
|
self.assertEqual(cls.value, 17)
|
|
cls.value = 19
|
|
self.assertEqual(namespace["value"], 17)
|
|
|
|
def test_frozen_subclass_globals_keep_lookup_semantics(self):
|
|
class Namespace(frozendict):
|
|
def __getitem__(self, key):
|
|
if key == "answer":
|
|
return 81
|
|
return super().__getitem__(key)
|
|
|
|
frozen = Namespace(answer=42, __builtins__=builtins)
|
|
self.assertEqual(eval("answer", frozen), 81)
|
|
self.assertEqual(eval("(lambda: answer)()", frozen), 81)
|
|
self.assertEqual(eval("answer", frozen, {}), 42)
|
|
|
|
def test_class_and_mapping_patterns(self):
|
|
frozen = frozendict(answer=42)
|
|
match frozen:
|
|
case frozendict(captured):
|
|
self.assertIs(captured, frozen)
|
|
case _:
|
|
self.fail("frozendict class pattern did not match")
|
|
match frozen:
|
|
case {"answer": captured, **remaining}:
|
|
self.assertEqual(captured, 42)
|
|
self.assertEqual(remaining, {})
|
|
self.assertIs(type(remaining), dict)
|
|
case _:
|
|
self.fail("frozendict mapping pattern did not match")
|
|
|
|
def test_hash_error_wrapper_preserves_exception_subclasses(self):
|
|
class SpecialTypeError(TypeError):
|
|
pass
|
|
|
|
class BadKey:
|
|
error = TypeError
|
|
|
|
def __hash__(self):
|
|
raise self.error("intentional hash failure")
|
|
|
|
key = BadKey()
|
|
frozen = frozendict(present=1)
|
|
operations = (
|
|
lambda: frozen[key],
|
|
lambda: frozen.get(key),
|
|
lambda: key in frozen,
|
|
lambda: frozendict([(key, 1)]),
|
|
lambda: frozendict.fromkeys([key]),
|
|
)
|
|
for error in (TypeError, SpecialTypeError, ValueError):
|
|
key.error = error
|
|
for operation in operations:
|
|
with self.subTest(error=error, operation=operation):
|
|
with self.assertRaises(error) as caught:
|
|
operation()
|
|
self.assertIs(type(caught.exception), error)
|
|
expected = "intentional hash failure"
|
|
if error is TypeError:
|
|
name = BadKey.__qualname__
|
|
if BadKey.__module__ not in ("builtins", "__main__"):
|
|
name = BadKey.__module__ + "." + name
|
|
expected = f"cannot use '{name}' as a frozendict key (intentional hash failure)"
|
|
self.assertEqual(str(caught.exception), expected)
|
|
|
|
def test_copy_protocol_and_getnewargs_do_not_expose_storage(self):
|
|
frozen = frozendict(present=object())
|
|
self.assertIs(copy.copy(frozen), frozen)
|
|
self.assertIs(frozen.copy(), frozen)
|
|
args = frozen.__getnewargs__()
|
|
self.assertIs(type(args), tuple)
|
|
self.assertIs(type(args[0]), dict)
|
|
args[0].clear()
|
|
self.assertEqual(list(frozen), ["present"])
|
|
child = FrozenWithState(frozen)
|
|
child.extra = "state"
|
|
shallow = copy.copy(child)
|
|
self.assertIs(type(shallow), FrozenWithState)
|
|
self.assertEqual(shallow.extra, "state")
|
|
self.assertIs(type(child.copy()), frozendict)
|
|
deep = copy.deepcopy(frozen)
|
|
self.assertIsNot(deep, frozen)
|
|
|
|
def test_subclass_copy_dispatch_tracks_iterator_override(self):
|
|
class NativeIteration(frozendict):
|
|
def keys(self):
|
|
return ["virtual"]
|
|
|
|
def __getitem__(self, key):
|
|
return 88
|
|
|
|
class CustomIteration(NativeIteration):
|
|
def __iter__(self):
|
|
return iter(["virtual"])
|
|
|
|
native = NativeIteration(stored=7)
|
|
custom = CustomIteration(stored=7)
|
|
self.assertEqual(native.copy(), {"stored": 7})
|
|
self.assertEqual(native | {"tail": 9}, {"stored": 7, "tail": 9})
|
|
self.assertEqual(NativeIteration.fromkeys(["tail"], 9), {"tail": 9})
|
|
self.assertEqual(custom.copy(), {"virtual": 88})
|
|
self.assertEqual(custom | {"tail": 9}, {"virtual": 88, "tail": 9})
|
|
self.assertEqual(
|
|
CustomIteration.fromkeys(["tail"], 9), {"virtual": 88, "tail": 9}
|
|
)
|
|
|
|
def test_deepcopy_recursive_aliases_from_both_roots(self):
|
|
loop = []
|
|
frozen = frozendict(left=loop, right=loop)
|
|
loop.extend([frozen, frozen])
|
|
for root in (frozen, loop):
|
|
result = copy.deepcopy(root)
|
|
copied_frozen = result if isinstance(result, frozendict) else result[0]
|
|
copied_loop = copied_frozen["left"]
|
|
self.assertIsNot(copied_frozen, frozen)
|
|
self.assertIsNot(copied_loop, loop)
|
|
self.assertIs(copied_frozen["right"], copied_loop)
|
|
self.assertIs(copied_loop[0], copied_frozen)
|
|
self.assertIs(copied_loop[1], copied_frozen)
|
|
|
|
def test_pickle_recursive_payload_and_subclass_state(self):
|
|
for cls in (frozendict, FrozenWithState, FrozenWithSlot):
|
|
loop = []
|
|
frozen = cls(left=loop, right=loop)
|
|
loop.extend([frozen, frozen])
|
|
if cls is not frozendict:
|
|
frozen.extra = loop
|
|
for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
|
|
with self.subTest(cls=cls, protocol=protocol):
|
|
if protocol < 2:
|
|
with self.assertRaises(TypeError):
|
|
pickle.dumps(frozen, protocol)
|
|
continue
|
|
result = pickle.loads(pickle.dumps(frozen, protocol))
|
|
self.assertIs(type(result), cls)
|
|
self.assertIsNot(result, frozen)
|
|
self.assertIs(result["left"], result["right"])
|
|
self.assertIs(result["left"][0], result)
|
|
self.assertIs(result["left"][1], result)
|
|
if cls is not frozendict:
|
|
self.assertIs(result.extra, result["left"])
|
|
|
|
def test_gc_does_not_expose_mutable_backing_mapping(self):
|
|
marker = object()
|
|
frozen = frozendict(private_payload=marker)
|
|
for referent in gc.get_referents(frozen):
|
|
self.assertFalse(isinstance(referent, dict))
|
|
self.assertIs(frozen["private_payload"], marker)
|
|
|
|
def test_gc_does_not_publish_partly_constructed_mapping(self):
|
|
marker = object()
|
|
observed = []
|
|
|
|
def source():
|
|
yield "private_payload", marker
|
|
observed.extend(
|
|
candidate
|
|
for candidate in gc.get_objects()
|
|
if type(candidate) is frozendict
|
|
and candidate.get("private_payload") is marker
|
|
)
|
|
yield "finished", True
|
|
|
|
frozen = frozendict(source())
|
|
self.assertEqual(observed, [])
|
|
self.assertIs(frozen["private_payload"], marker)
|
|
self.assertIs(frozen["finished"], True)
|
|
|
|
import gc
|
|
import operator
|
|
import unittest
|
|
import weakref
|
|
|
|
class FrozenDictEdgeCaseTests(unittest.TestCase):
|
|
def test_hash_matches_frozenset_of_items(self):
|
|
cases = (
|
|
frozendict(),
|
|
frozendict([(1, 2), (-3, 4), (2**130, -7)]),
|
|
frozendict(first=1, second=2),
|
|
frozendict(second=2, first=1),
|
|
frozendict([(1, "one"), ("two", 2), (("three",), frozenset({3}))]),
|
|
)
|
|
for frozen in cases:
|
|
with self.subTest(frozen=frozen):
|
|
self.assertEqual(hash(frozen), hash(frozenset(frozen.items())))
|
|
|
|
def test_gc_cycles_and_class_reassignment_cannot_expose_mutation(self):
|
|
frozen = frozendict(protected=23)
|
|
original_hash = hash(frozen)
|
|
with self.assertRaises(TypeError):
|
|
frozen.__class__ = dict
|
|
self.assertIs(type(frozen), frozendict)
|
|
self.assertEqual(frozen, {"protected": 23})
|
|
self.assertEqual(hash(frozen), original_hash)
|
|
|
|
class Marker:
|
|
pass
|
|
|
|
def make_cycle(include_views):
|
|
marker = Marker()
|
|
loop = [marker]
|
|
owner = frozendict(loop=loop)
|
|
loop.append(owner)
|
|
if include_views:
|
|
loop.extend(
|
|
(owner.keys(), owner.values(), owner.items(), iter(owner))
|
|
)
|
|
return weakref.ref(marker)
|
|
|
|
for include_views in (False, True):
|
|
with self.subTest(include_views=include_views):
|
|
marker_ref = make_cycle(include_views)
|
|
gc.collect()
|
|
self.assertIsNone(marker_ref())
|
|
|
|
def test_union_subclass_dispatch_matrix(self):
|
|
for base in (dict, frozendict):
|
|
|
|
class NativeIteration(base):
|
|
def keys(self):
|
|
return ["virtual"]
|
|
|
|
def __getitem__(self, key):
|
|
return 88
|
|
|
|
class CustomIteration(NativeIteration):
|
|
def __iter__(self):
|
|
return iter(["virtual"])
|
|
|
|
for cls in (NativeIteration, CustomIteration):
|
|
custom = cls is CustomIteration
|
|
for storage in ({}, {"stored": 7}):
|
|
subject = cls(storage)
|
|
copied = {"virtual": 88} if custom and storage else storage
|
|
merged = {"virtual": 88} if custom else storage
|
|
cases = (
|
|
("left_dict", lambda: subject | {}, base, copied),
|
|
(
|
|
"left_frozen",
|
|
lambda: subject | frozendict(),
|
|
base,
|
|
copied,
|
|
),
|
|
("right_dict", lambda: {} | subject, dict, merged),
|
|
(
|
|
"right_frozen",
|
|
lambda: frozendict() | subject,
|
|
frozendict,
|
|
merged,
|
|
),
|
|
(
|
|
"construction",
|
|
lambda: frozendict(subject),
|
|
frozendict,
|
|
merged,
|
|
),
|
|
)
|
|
for name, operation, expected_type, expected in cases:
|
|
with self.subTest(
|
|
base=base, cls=cls, storage=storage, operation=name
|
|
):
|
|
result = operation()
|
|
self.assertIs(type(result), expected_type)
|
|
self.assertEqual(result, expected)
|
|
self.assertEqual(
|
|
list(subject.items()), list(storage.items())
|
|
)
|
|
|
|
if base is frozendict:
|
|
with self.subTest(
|
|
base=base, cls=cls, storage=storage, operation="copy"
|
|
):
|
|
result = subject.copy()
|
|
self.assertIs(type(result), frozendict)
|
|
self.assertIsNot(result, subject)
|
|
self.assertEqual(result, copied)
|
|
|
|
def test_mapping_equality_uses_left_value_and_stored_key_hash(self):
|
|
trace = []
|
|
|
|
class Key:
|
|
def __init__(self, side):
|
|
self.side = side
|
|
|
|
def __hash__(self):
|
|
trace.append(("hash", self.side))
|
|
return 313
|
|
|
|
def __eq__(self, other):
|
|
trace.append(("key", self.side, other.side))
|
|
return True
|
|
|
|
class Value:
|
|
def __init__(self, side):
|
|
self.side = side
|
|
|
|
def __eq__(self, other):
|
|
trace.append(("value", self.side, other.side))
|
|
return self.side == "left"
|
|
|
|
for left_type in (dict, frozendict):
|
|
for right_type in (dict, frozendict):
|
|
left = left_type([(Key("left"), Value("left"))])
|
|
right = right_type([(Key("right"), Value("right"))])
|
|
with self.subTest(left_type=left_type, right_type=right_type):
|
|
trace.clear()
|
|
self.assertTrue(left == right)
|
|
self.assertEqual(
|
|
trace,
|
|
[("key", "right", "left"), ("value", "left", "right")],
|
|
)
|
|
trace.clear()
|
|
self.assertFalse(right == left)
|
|
self.assertEqual(
|
|
trace,
|
|
[("key", "left", "right"), ("value", "right", "left")],
|
|
)
|
|
|
|
def test_view_comparison_direction_and_asymmetric_values(self):
|
|
trace = []
|
|
|
|
class Key:
|
|
def __init__(self, side):
|
|
self.side = side
|
|
|
|
def __hash__(self):
|
|
trace.append(("hash", self.side))
|
|
return 313
|
|
|
|
def __eq__(self, other):
|
|
trace.append(("key", self.side, other.side))
|
|
return True
|
|
|
|
class Value:
|
|
def __init__(self, side):
|
|
self.side = side
|
|
|
|
def __eq__(self, other):
|
|
trace.append(("value", self.side, other.side))
|
|
return self.side == "left"
|
|
|
|
for left_type in (dict, frozendict):
|
|
for right_type in (dict, frozendict):
|
|
left = left_type([(Key("left"), Value("left"))])
|
|
right = right_type([(Key("right"), Value("right"))])
|
|
for method in ("keys", "items"):
|
|
for op in (
|
|
operator.eq,
|
|
operator.ne,
|
|
operator.lt,
|
|
operator.le,
|
|
operator.gt,
|
|
operator.ge,
|
|
):
|
|
with self.subTest(
|
|
left_type=left_type,
|
|
right_type=right_type,
|
|
method=method,
|
|
operation=op.__name__,
|
|
):
|
|
trace.clear()
|
|
result = op(
|
|
getattr(left, method)(), getattr(right, method)()
|
|
)
|
|
if op in (operator.lt, operator.gt):
|
|
self.assertFalse(result)
|
|
self.assertEqual(trace, [])
|
|
continue
|
|
reverse = op is operator.ge
|
|
needle, container = (
|
|
("right", "left") if reverse else ("left", "right")
|
|
)
|
|
expected_trace = [
|
|
("hash", needle),
|
|
("key", container, needle),
|
|
]
|
|
contained = True
|
|
if method == "items":
|
|
expected_trace.append(("value", container, needle))
|
|
contained = container == "left"
|
|
self.assertEqual(
|
|
result,
|
|
not contained if op is operator.ne else contained,
|
|
)
|
|
self.assertEqual(trace, expected_trace)
|
|
|
|
def test_view_hash_error_context_comes_from_containment_mapping(self):
|
|
class Key:
|
|
broken = False
|
|
|
|
def __hash__(self):
|
|
if self.broken:
|
|
raise TypeError("intentional view hash error")
|
|
return 313
|
|
|
|
for left_type in (dict, frozendict):
|
|
for right_type in (dict, frozendict):
|
|
key = Key()
|
|
left = left_type([(key, 1)])
|
|
right = right_type([(key, 1)])
|
|
key.broken = True
|
|
for method in ("keys", "items"):
|
|
for op in (operator.eq, operator.ne, operator.le, operator.ge):
|
|
with self.subTest(
|
|
left_type=left_type,
|
|
right_type=right_type,
|
|
method=method,
|
|
operation=op.__name__,
|
|
):
|
|
container = (
|
|
left_type if op is operator.ge else right_type
|
|
)
|
|
with self.assertRaises(TypeError) as caught:
|
|
op(
|
|
getattr(left, method)(),
|
|
getattr(right, method)(),
|
|
)
|
|
self.assertTrue(
|
|
str(caught.exception).endswith(
|
|
f"as a {container.__name__} key (intentional view hash error)"
|
|
),
|
|
str(caught.exception),
|
|
)
|
|
|
|
def test_view_equality_checks_mutation_of_the_iterated_mapping(self):
|
|
for op in (operator.eq, operator.le, operator.ge):
|
|
left = {"key": object()}
|
|
|
|
class Value:
|
|
def __eq__(self, other):
|
|
left.clear()
|
|
return True
|
|
|
|
right = frozendict(key=Value())
|
|
with self.subTest(operation=op.__name__):
|
|
if op is operator.ge:
|
|
self.assertTrue(op(left.items(), right.items()))
|
|
else:
|
|
with self.assertRaisesRegex(
|
|
RuntimeError, "dictionary changed size during iteration"
|
|
):
|
|
op(left.items(), right.items())
|
|
self.assertEqual(left, {})
|
|
|
|
def test_fromkeys_merges_constructor_result_before_iterable_validation(self):
|
|
calls = []
|
|
|
|
class Source(frozendict):
|
|
def __iter__(self):
|
|
return iter(())
|
|
|
|
def keys(self):
|
|
calls.append("keys")
|
|
return []
|
|
|
|
source = Source()
|
|
|
|
class Factory(frozendict):
|
|
def __new__(cls, *args):
|
|
calls.append("new")
|
|
return source
|
|
|
|
with self.assertRaises(TypeError):
|
|
Factory.fromkeys(123)
|
|
self.assertEqual(calls, ["new", "keys"])
|
|
self.assertEqual(source, {})
|
|
|
|
def test_fromkeys_source_error_precedes_invalid_iterable(self):
|
|
class Source(frozendict):
|
|
def __iter__(self):
|
|
return iter(())
|
|
|
|
def keys(self):
|
|
raise ValueError("source merge failed")
|
|
|
|
source = Source()
|
|
|
|
class Factory(frozendict):
|
|
def __new__(cls, *args):
|
|
return source
|
|
|
|
with self.assertRaisesRegex(ValueError, "source merge failed"):
|
|
Factory.fromkeys(123)
|
|
|
|
import builtins
|
|
import gc
|
|
import sys
|
|
import types
|
|
import unittest
|
|
|
|
class FrozenGlobalsTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.ns = frozendict(
|
|
__builtins__=builtins.__dict__,
|
|
__name__="frozen_test",
|
|
answer=42,
|
|
sys=sys,
|
|
)
|
|
|
|
def test_eval_and_implicit_scope(self):
|
|
self.assertIs(eval("globals()", self.ns), self.ns)
|
|
self.assertIs(eval("locals()", self.ns), self.ns)
|
|
self.assertEqual(eval("eval('answer')", self.ns), 42)
|
|
self.assertEqual(eval("(lambda: answer)()", self.ns), 42)
|
|
|
|
def test_function_and_materialized_frame(self):
|
|
local = {}
|
|
exec(
|
|
"def f():\n return sys._getframe(), globals(), answer\n", self.ns, local
|
|
)
|
|
f = local["f"]
|
|
self.assertIs(f.__globals__, self.ns)
|
|
self.assertEqual(f.__module__, "frozen_test")
|
|
for _ in range(300):
|
|
frame, ns, answer = f()
|
|
self.assertIs(frame.f_globals, self.ns)
|
|
self.assertIs(ns, self.ns)
|
|
self.assertEqual(answer, 42)
|
|
gc.collect()
|
|
self.assertIs(frame.f_globals, self.ns)
|
|
|
|
def test_generator_frames(self):
|
|
local = {}
|
|
exec(
|
|
"def gen():\n yield sys._getframe(), globals(), answer\n yield answer\n",
|
|
self.ns,
|
|
local,
|
|
)
|
|
gen = local["gen"]()
|
|
self.assertIs(gen.gi_frame.f_globals, self.ns)
|
|
frame, ns, answer = next(gen)
|
|
self.assertIs(frame.f_globals, self.ns)
|
|
self.assertIs(ns, self.ns)
|
|
self.assertEqual(answer, 42)
|
|
self.assertEqual(next(gen), 42)
|
|
with self.assertRaises(StopIteration):
|
|
next(gen)
|
|
gc.collect()
|
|
self.assertIs(frame.f_globals, self.ns)
|
|
|
|
def test_coroutine_frames(self):
|
|
local = {}
|
|
exec(
|
|
"async def coro():\n return sys._getframe(), globals(), answer\n",
|
|
self.ns,
|
|
local,
|
|
)
|
|
coro = local["coro"]()
|
|
self.assertIs(coro.cr_frame.f_globals, self.ns)
|
|
with self.assertRaises(StopIteration) as stopped:
|
|
coro.send(None)
|
|
frame, ns, answer = stopped.exception.value
|
|
self.assertIs(frame.f_globals, self.ns)
|
|
self.assertIs(ns, self.ns)
|
|
self.assertEqual(answer, 42)
|
|
|
|
def test_async_generator_frames(self):
|
|
local = {}
|
|
exec(
|
|
"async def agen():\n yield sys._getframe(), globals(), answer\n",
|
|
self.ns,
|
|
local,
|
|
)
|
|
agen = local["agen"]()
|
|
self.assertIs(agen.ag_frame.f_globals, self.ns)
|
|
with self.assertRaises(StopIteration) as stopped:
|
|
agen.asend(None).send(None)
|
|
frame, ns, answer = stopped.exception.value
|
|
self.assertIs(frame.f_globals, self.ns)
|
|
self.assertIs(ns, self.ns)
|
|
self.assertEqual(answer, 42)
|
|
with self.assertRaises(StopIteration):
|
|
agen.aclose().send(None)
|
|
|
|
def test_local_namespace_and_imports(self):
|
|
local = {}
|
|
exec(
|
|
"import math\nclass C:\n value = answer\ndef f():\n import math\n return math.sqrt(answer)\n",
|
|
self.ns,
|
|
local,
|
|
)
|
|
self.assertEqual(local["C"].value, 42)
|
|
self.assertEqual(local["C"].__module__, "frozen_test")
|
|
self.assertEqual(local["f"](), local["math"].sqrt(42))
|
|
self.assertNotIn("math", self.ns)
|
|
|
|
def test_missing_builtins(self):
|
|
for func, source in [(eval, ""), (exec, "")]:
|
|
with self.assertRaises(TypeError) as caught:
|
|
func(source, frozendict())
|
|
self.assertEqual(
|
|
str(caught.exception),
|
|
"cannot assign __builtins__ to frozendict globals",
|
|
)
|
|
|
|
def test_writes_and_deletes(self):
|
|
for source, error in [
|
|
("answer = 0", "'frozendict' object does not support item assignment"),
|
|
(
|
|
"global answer; answer = 0",
|
|
"frozendict object does not support item assignment",
|
|
),
|
|
(
|
|
"global answer; del answer",
|
|
"frozendict object does not support item deletion",
|
|
),
|
|
(
|
|
"global missing; del missing",
|
|
"frozendict object does not support item deletion",
|
|
),
|
|
]:
|
|
with self.assertRaises(TypeError) as caught:
|
|
exec(source, self.ns)
|
|
self.assertEqual(str(caught.exception), error)
|
|
self.assertEqual(self.ns["answer"], 42)
|
|
with self.assertRaises(NameError):
|
|
exec("del answer", self.ns)
|
|
|
|
def test_explicit_function_constructor_stays_dict_only(self):
|
|
with self.assertRaises(TypeError):
|
|
types.FunctionType((lambda: 42).__code__, self.ns)
|
|
|
|
def test_subclass_name_and_global_lookup(self):
|
|
class F(frozendict):
|
|
def __getitem__(self, key):
|
|
if key == "answer":
|
|
return 81
|
|
return super().__getitem__(key)
|
|
|
|
ns = F(self.ns)
|
|
self.assertEqual(eval("answer", ns), 81)
|
|
self.assertEqual(eval("(lambda: answer)()", ns), 81)
|
|
self.assertEqual(eval("answer", ns, {}), 42)
|
|
|
|
def test_subclass_function_metadata_reads_storage(self):
|
|
class F(frozendict):
|
|
def __getitem__(self, key):
|
|
if key == "__name__":
|
|
return "wrong_name"
|
|
if key == "__builtins__":
|
|
return {"len": lambda x: 99}
|
|
return super().__getitem__(key)
|
|
|
|
ns = F(self.ns)
|
|
f = eval("lambda: len([])", ns)
|
|
self.assertEqual(f.__module__, "frozen_test")
|
|
self.assertIs(f.__globals__, ns)
|
|
self.assertEqual(f(), 0)
|
|
|
|
def test_subclass_builtins_assignment_hook(self):
|
|
calls = []
|
|
|
|
class F(frozendict):
|
|
def __setitem__(self, key, value):
|
|
calls.append((key, value))
|
|
|
|
for operation, source, mode, expected in (
|
|
(eval, "1", "eval", 1),
|
|
(exec, "pass", "exec", None),
|
|
):
|
|
namespace = F()
|
|
calls.clear()
|
|
self.assertEqual(
|
|
operation(compile(source, "<frozen-globals>", mode), namespace),
|
|
expected,
|
|
)
|
|
self.assertEqual(calls, [("__builtins__", builtins.__dict__)])
|
|
self.assertNotIn("__builtins__", namespace)
|
|
calls.clear()
|
|
with self.assertRaisesRegex(
|
|
TypeError, "frozendict object does not support item assignment"
|
|
):
|
|
operation(source, namespace)
|
|
self.assertEqual(calls, [("__builtins__", builtins.__dict__)])
|
|
|
|
def test_implicit_frozen_builtins_namespace_counts(self):
|
|
import _testinternalcapi
|
|
|
|
function = eval(
|
|
"lambda: len(())",
|
|
frozendict(__builtins__=frozendict(len=len)),
|
|
)
|
|
counts = _testinternalcapi.get_code_var_counts(function)
|
|
self.assertEqual(
|
|
counts["unbound"]["globals"],
|
|
{"total": 1, "numglobal": 0, "numbuiltin": 1, "numunknown": 0},
|
|
)
|
|
for argument in ("globalsns", "builtinsns"):
|
|
with self.assertRaises(TypeError):
|
|
_testinternalcapi.get_code_var_counts(
|
|
function, **{argument: frozendict()}
|
|
)
|
|
|
|
def test_warning_context_reads_frozen_storage(self):
|
|
import warnings
|
|
|
|
class F(frozendict):
|
|
def __getitem__(self, key):
|
|
if key in ("__warningregistry__", "__name__"):
|
|
raise AssertionError("warning context must read stored entries")
|
|
return super().__getitem__(key)
|
|
|
|
for has_registry in (False, True):
|
|
entries = dict(self.ns, warn=warnings.warn)
|
|
if has_registry:
|
|
entries["__warningregistry__"] = {}
|
|
function = eval("lambda: warn('probe')", F(entries))
|
|
with warnings.catch_warnings(record=True) as caught:
|
|
warnings.simplefilter("always")
|
|
if has_registry:
|
|
function()
|
|
self.assertEqual(len(caught), 1)
|
|
self.assertEqual(str(caught[0].message), "probe")
|
|
else:
|
|
with self.assertRaisesRegex(
|
|
TypeError,
|
|
"frozendict object does not support item assignment",
|
|
):
|
|
function()
|
|
self.assertEqual(caught, [])
|
|
|
|
def test_frozen_builtins(self):
|
|
ns = frozendict(__builtins__=frozendict({"len": len}))
|
|
self.assertEqual(eval("(lambda: len([1, 2]))()", ns), 2)
|
|
|
|
def test_shared_code_does_not_keep_mutable_global_specialization(self):
|
|
code = compile("lambda: answer", "<globals-probe>", "eval")
|
|
mutable = {"__builtins__": builtins.__dict__, "answer": 19}
|
|
f = eval(code, mutable)
|
|
for _ in range(300):
|
|
self.assertEqual(f(), 19)
|
|
frozen_f = eval(code, self.ns)
|
|
for _ in range(300):
|
|
self.assertEqual(frozen_f(), 42)
|
|
self.assertEqual(f(), 19)
|
|
mutable["answer"] = 23
|
|
self.assertEqual(f(), 23)
|
|
self.assertEqual(frozen_f(), 42)
|
|
|
|
import types
|
|
import unittest
|
|
|
|
class FrozenTypeNamespaceTests(unittest.TestCase):
|
|
def test_exact_frozen_namespace_is_copied(self):
|
|
namespace = frozendict(value=17, __qualname__="Outer.FrozenNamespace")
|
|
original_hash = hash(namespace)
|
|
cls = type("FrozenNamespace", (), namespace)
|
|
self.assertEqual(cls.value, 17)
|
|
self.assertEqual(cls.__qualname__, "Outer.FrozenNamespace")
|
|
cls.value = 19
|
|
cls.added = 23
|
|
del cls.value
|
|
self.assertEqual(
|
|
namespace,
|
|
{"value": 17, "__qualname__": "Outer.FrozenNamespace"},
|
|
)
|
|
self.assertEqual(hash(namespace), original_hash)
|
|
self.assertNotIn("__module__", namespace)
|
|
self.assertNotIn("added", namespace)
|
|
|
|
def test_builtin_iterator_bypasses_subclass_mapping_overrides(self):
|
|
for base in (dict, frozendict):
|
|
|
|
class Namespace(base):
|
|
def keys(self):
|
|
raise AssertionError("keys must not be called")
|
|
|
|
def __getitem__(self, key):
|
|
raise AssertionError("__getitem__ must not be called")
|
|
|
|
def __len__(self):
|
|
raise AssertionError("__len__ must not be called")
|
|
|
|
with self.subTest(base=base):
|
|
namespace = Namespace(value=17, __qualname__="Outer.C")
|
|
cls = type("C", (), namespace)
|
|
self.assertEqual(cls.value, 17)
|
|
self.assertEqual(cls.__qualname__, "Outer.C")
|
|
self.assertEqual(base.__getitem__(namespace, "value"), 17)
|
|
|
|
def test_custom_iterator_uses_mapping_order_and_values(self):
|
|
for base in (dict, frozendict):
|
|
calls = []
|
|
|
|
class Namespace(base):
|
|
def __iter__(self):
|
|
raise AssertionError("the copy must use keys, not __iter__")
|
|
|
|
def keys(self):
|
|
calls.append("keys")
|
|
return ["second", "first"]
|
|
|
|
def __getitem__(self, key):
|
|
calls.append(key)
|
|
return {"first": 11, "second": 22}[key]
|
|
|
|
with self.subTest(base=base):
|
|
namespace = Namespace(stored=3)
|
|
cls = type("C", (), namespace)
|
|
self.assertEqual(calls, ["keys", "second", "first"])
|
|
self.assertEqual((cls.first, cls.second), (11, 22))
|
|
self.assertFalse(hasattr(cls, "stored"))
|
|
self.assertEqual(
|
|
[key for key in cls.__dict__ if not key.startswith("__")],
|
|
["second", "first"],
|
|
)
|
|
self.assertEqual(base.__getitem__(namespace, "stored"), 3)
|
|
|
|
def test_empty_subclass_copy_bypasses_mapping_overrides(self):
|
|
for base in (dict, frozendict):
|
|
|
|
class Namespace(base):
|
|
def __iter__(self):
|
|
raise AssertionError("__iter__ must not be called")
|
|
|
|
def keys(self):
|
|
raise AssertionError("keys must not be called")
|
|
|
|
def __getitem__(self, key):
|
|
raise AssertionError("__getitem__ must not be called")
|
|
|
|
def __len__(self):
|
|
return 1
|
|
|
|
with self.subTest(base=base):
|
|
namespace = Namespace()
|
|
cls = type("C", (), namespace)
|
|
self.assertEqual(cls.__name__, "C")
|
|
self.assertEqual(base.__len__(namespace), 0)
|
|
|
|
def test_class_cells_use_a_mutable_copy(self):
|
|
classcell = types.CellType()
|
|
dictcell = types.CellType()
|
|
namespace = frozendict(
|
|
value=17,
|
|
__classcell__=classcell,
|
|
__classdictcell__=dictcell,
|
|
)
|
|
cls = type("C", (), namespace)
|
|
self.assertIs(classcell.cell_contents, cls)
|
|
classdict = dictcell.cell_contents
|
|
self.assertIs(type(classdict), dict)
|
|
self.assertIsNot(classdict, namespace)
|
|
self.assertNotIn("__classcell__", classdict)
|
|
self.assertNotIn("__classdictcell__", classdict)
|
|
self.assertIs(namespace["__classcell__"], classcell)
|
|
self.assertIs(namespace["__classdictcell__"], dictcell)
|
|
cls.value = 19
|
|
self.assertEqual(classdict["value"], 19)
|
|
self.assertEqual(namespace["value"], 17)
|
|
|
|
def test_descriptor_initialization_does_not_change_source(self):
|
|
calls = []
|
|
|
|
class Descriptor:
|
|
def __set_name__(self, owner, name):
|
|
calls.append((owner, name))
|
|
owner.added = 23
|
|
|
|
descriptor = Descriptor()
|
|
namespace = frozendict(field=descriptor)
|
|
cls = type("C", (), namespace)
|
|
self.assertEqual(calls, [(cls, "field")])
|
|
self.assertEqual(cls.added, 23)
|
|
self.assertIs(namespace["field"], descriptor)
|
|
self.assertEqual(list(namespace), ["field"])
|
|
|
|
def test_winning_metaclass_receives_original_namespace(self):
|
|
for base in (dict, frozendict):
|
|
|
|
class Namespace(base):
|
|
def __iter__(self):
|
|
raise AssertionError("namespace must not be copied")
|
|
|
|
def keys(self):
|
|
raise AssertionError("namespace must not be copied")
|
|
|
|
namespace = Namespace(value=17)
|
|
|
|
class Meta(type):
|
|
def __new__(metacls, name, bases, ns):
|
|
if name == "Child":
|
|
return ns
|
|
return super().__new__(metacls, name, bases, ns)
|
|
|
|
class Base(metaclass=Meta):
|
|
pass
|
|
|
|
with self.subTest(base=base):
|
|
self.assertIs(type("Child", (Base,), namespace), namespace)
|
|
|
|
def test_keywords_and_explicit_type_new(self):
|
|
calls = []
|
|
|
|
class Base:
|
|
def __init_subclass__(cls, *, marker):
|
|
calls.append((cls, marker))
|
|
|
|
namespace = frozendict(value=17)
|
|
cls = type("C", (Base,), namespace, marker=23)
|
|
explicit = type.__new__(type, "D", (Base,), namespace, marker=29)
|
|
self.assertEqual(calls, [(cls, 23), (explicit, 29)])
|
|
self.assertEqual((cls.value, explicit.value), (17, 17))
|
|
self.assertEqual(namespace, {"value": 17})
|
|
|
|
def test_non_dictionary_mappings_remain_rejected(self):
|
|
class Mapping:
|
|
def keys(self):
|
|
return ["value"]
|
|
|
|
def __getitem__(self, key):
|
|
return 17
|
|
|
|
for namespace in (types.MappingProxyType({}), Mapping(), [], ()):
|
|
with self.subTest(namespace=namespace), self.assertRaises(TypeError):
|
|
type("C", (), namespace)
|
|
|
|
frozen_suite = unittest.TestSuite(
|
|
unittest.defaultTestLoader.loadTestsFromTestCase(case)
|
|
for case in (
|
|
FrozenDictContractTests,
|
|
FrozenDictEdgeCaseTests,
|
|
FrozenGlobalsTests,
|
|
FrozenTypeNamespaceTests,
|
|
)
|
|
)
|
|
assert frozen_suite.countTestCases() == 61
|
|
frozen_result = unittest.TextTestRunner(verbosity=2).run(frozen_suite)
|
|
assert frozen_result.wasSuccessful()
|
|
assert not frozen_result.skipped
|
|
|
|
|
|
# Final 3.15 rewrites exact hash TypeError without an explicit cause, and
|
|
# propagates formatting failures instead of replacing them with diagnostics.
|
|
def check_mapping_hash_error_context():
|
|
class SpecialTypeError(TypeError):
|
|
pass
|
|
|
|
for mapping_type in (dict, frozendict):
|
|
for case in (
|
|
"plain",
|
|
"empty",
|
|
"multiple",
|
|
"bad_str",
|
|
"bad_repr",
|
|
"subclass",
|
|
"subclass_bad_str",
|
|
"list",
|
|
):
|
|
for handled in (False, True):
|
|
for operation in ("getitem", "get", "contains"):
|
|
callback_error = RuntimeError("format failed")
|
|
callbacks = []
|
|
|
|
class BadText:
|
|
def __str__(self):
|
|
callbacks.append("str")
|
|
raise callback_error
|
|
|
|
def __repr__(self):
|
|
callbacks.append("repr")
|
|
raise callback_error
|
|
|
|
args = {
|
|
"plain": ("hash failed",),
|
|
"empty": (),
|
|
"multiple": ("one", 2),
|
|
"bad_str": (BadText(),),
|
|
"bad_repr": ("one", BadText()),
|
|
"subclass": ("hash failed",),
|
|
"subclass_bad_str": (BadText(),),
|
|
"list": (),
|
|
}[case]
|
|
error_type = (
|
|
SpecialTypeError if case.startswith("subclass") else TypeError
|
|
)
|
|
original = error_type(*args)
|
|
outer = ValueError("outer")
|
|
|
|
class Key:
|
|
def __hash__(self):
|
|
raise original
|
|
|
|
key = [] if case == "list" else Key()
|
|
mapping = mapping_type()
|
|
|
|
def invoke():
|
|
try:
|
|
if operation == "getitem":
|
|
mapping[key]
|
|
elif operation == "get":
|
|
mapping.get(key)
|
|
else:
|
|
key in mapping
|
|
except Exception as error:
|
|
assert error.__cause__ is None
|
|
assert error.__context__ is (outer if handled else None)
|
|
assert error.__suppress_context__ is False
|
|
if case.startswith("subclass"):
|
|
assert error is original
|
|
assert callbacks == []
|
|
elif case in ("bad_str", "bad_repr"):
|
|
assert error is callback_error
|
|
assert callbacks == [
|
|
"str" if case == "bad_str" else "repr"
|
|
]
|
|
else:
|
|
assert type(error) is TypeError
|
|
assert error is not original
|
|
name = "list" if case == "list" else Key.__qualname__
|
|
message = {
|
|
"plain": "hash failed",
|
|
"empty": "",
|
|
"multiple": "('one', 2)",
|
|
"list": "unhashable type: 'list'",
|
|
}[case]
|
|
assert str(error) == (
|
|
f"cannot use '{name}' as a {mapping_type.__name__} key ({message})"
|
|
)
|
|
else:
|
|
raise AssertionError("hash failure was not propagated")
|
|
|
|
if handled:
|
|
try:
|
|
raise outer
|
|
except ValueError:
|
|
invoke()
|
|
else:
|
|
invoke()
|
|
|
|
|
|
if hasattr(builtins, "frozendict"):
|
|
check_mapping_hash_error_context()
|