Preserve final dictionary hash-error context and formatting

Share exact-TypeError rewriting between dict and frozendict without attaching a cause. Propagate string and repr callback failures using final BaseException argument formatting, leaving global diagnostic behavior unchanged. Add bounded Python coverage for context, identity, and formatting failures.

Assisted-by: OpenAI Codex:model identifier unavailable
This commit is contained in:
dot committed 2026-10-12 08:12:32 +09:00
1 parent c6ddfae40d
commit 7d06dd0859
2 files changed
+113 -17

No files matched your search

+11 -17
View File
@@ -41,9 +41,18 @@ pub(crate) fn key_hash<K: DictKey + ?Sized>(
vm: &VirtualMachine,
) -> PyResult<PyHash> {
match key.key_hash(vm) {
// Exact `TypeError` only: a `__hash__` raising a subclass must
// propagate unchanged, matching CPython's exact-type check.
Err(exc) if exc.class().is(vm.ctx.exceptions.type_error) => {
let name = key.to_pyobject(vm).class().fully_qualified_name(vm)?;
let message = exc.as_object().str(vm)?;
// Match BaseException_str without the diagnostic formatter's
// fallback: an argument's failing __str__/__repr__ must propagate.
let args = exc.args();
let message = match args.as_slice() {
[] => vm.ctx.empty_str.to_owned(),
[arg] => arg.str(vm)?,
_ => args.as_object().str(vm)?,
};
Err(vm.new_type_error(format!(
"cannot use '{name}' as a {mapping_name} key ({message})"
)))
@@ -520,22 +529,7 @@ impl PyDict {
/// Hashing up front also means a `__hash__` that fails only intermittently
/// is still reported.
fn hash_or_unhashable<K: DictKey + ?Sized>(key: &K, vm: &VirtualMachine) -> PyResult<PyHash> {
match key.key_hash(vm) {
Ok(hash) => Ok(hash),
// Exact `TypeError` only: a `__hash__` raising a *subclass* of
// `TypeError` (e.g. a user `MyTypeError`) must propagate unchanged,
// matching CPython's exact-type check.
Err(cause) if cause.class().is(vm.ctx.exceptions.type_error) => {
let message = cause.as_object().str(vm)?;
let key = key.to_pyobject(vm);
let name = key.class().fully_qualified_name(vm)?;
let err =
vm.new_type_error(format!("cannot use '{name}' as a dict key ({message})"));
err.set_cause(Some(cause));
Err(err)
}
Err(other) => Err(other),
}
key_hash(key, "dict", vm)
}
/// Set item variant which can be called with multiple
+102
View File
@@ -2125,3 +2125,105 @@ frozendict(**kwargs) -> new immutable dictionary initialized with the name=value
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()
check_mapping_hash_error_context()