mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
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:
1 parent
c6ddfae40d
commit
7d06dd0859
2 files changed
+113
-17
No files matched your search
@@ -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
|
||||
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user