Files
RustPython/extra_tests/snippets/builtin_dict.py
youknowdot 1889ee627f Track live entries in reverse dictionary iterators (#9046)
* Track live entries in reverse dictionary iterators

Extract the Python 3.14-compatible remaining-count and same-size storage handling from #8954. Preserve the older deferred exhaustion and extra-yield contract, using explicit wrapping arithmetic rather than the Python 3.15 extra-yield error and cursor clamp. Cover deleted-slot hints and mutation after the last yield in the existing Python snippet.

Assisted-by: OpenAI Codex:model identifier unavailable

* Enable the repaired reverse-dict restoration regression

The patched native interpreter unexpectedly passes the existing reverse-dict clear/restore test. Remove only its obsolete expected-failure decorator, preserving the canonical assertions and all other markers.

Assisted-by: OpenAI Codex:model identifier unavailable

* Preserve reverse iterator invalidation on atomic relocation

Assisted-by: OpenAI Codex:model identifier unavailable
2026-10-10 12:44:03 +00:00

876 lines
23 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