Files
RustPython/extra_tests/snippets/vm_argument_errors.py
T
Jeong, YunWon 39682562b3 Raise the shared argument-binding errors from the slots that count for themselves (#8634)
* Raise the shared argument-binding errors from the slots that count for themselves

Sites all over the tree checked their own argument counts and keywords and
wrote the message by hand, so they drifted: `attrgetter expected 1 argument,
got 0.` carried a full stop, `TypeVar() got unexpected keyword argument(s):
bogus` named neither the keyword the way its counterpart does nor the
function the way `typevar()` is named, and `_csv`'s dialect parser handed a
whole formatted sentence to `InvalidKeywordArgument`, which then wrapped it
in another one.

`Callee::arity_error` and `Callee::unexpected_keyword` are now public, and
these sites raise through them, so there is one place the wording lives.
`FrameLocalsProxy` counts its arguments before reading its keywords, and
`min`/`max` do too, which is the order `framelocalsproxy_new` and `min_max`
check in. `weakref.ref`, `GenericAlias`, `frozenset`, `attrgetter`,
`itemgetter`, `islice`, `start_new_thread`, `TextIOWrapper`, `AttributeError`,
`NameError`, `TypeVar`, `ParamSpec`, `TypeVarTuple` and `TypeAliasType` all
follow.

Of 25 calls measured against CPython 3.14.6, 21 now produce the identical
string. The four left over need what `Callee` cannot carry: `weakref.proxy`
is a function of its own there rather than a type, `frozenset` names the
subclass being constructed, and `TextIOWrapper` names its first parameter
and counts before converting.

Assisted-by: Claude

* Give the argument-binding errors the constructors the rest of them have

Raising one of these meant writing `Callee::of::<Self>(vm).arity_error(1..=3,
0, vm)` — the vm twice, and a type to know about before you could say what
you meant. Every other error in the tree is a `vm.new_*_error`.

So the message texts move out of `Callee` into functions the vm can call, and
the vm gets `new_arity_type_error` and `new_unexpected_keyword_type_error`
alongside `new_unsupported_bin_op_error` and the rest. A slot names itself
with the `NAME` its class already carries:

    return Err(vm.new_arity_type_error(Self::NAME, 1..=3, 0));
    return Err(vm.new_unexpected_keyword_type_error(Some("typevar"), &key));

The keyword one takes the name as an `Option` because the parser sometimes
has none of its own, which is what `_PyArg_Parser.fname` being NULL means and
what `_csv`'s dialect parser wants.

`bind_for` and `check_kwargs_empty_for` take anything a `Callee` converts
from, so the slots pass `Self::NAME` there too. `Callee` itself is left to the
binding machinery, which is the only place that needs to carry a name around
rather than use one.

No message changes: 22 of 26 calls still match CPython 3.14.6 exactly, and the
43-call set still stands at 23.

Assisted-by: Claude
2026-09-01 22:16:12 +09:00

161 lines
5.4 KiB
Python

# A call that binds badly says which function it was binding for, counts only
# the arguments the caller wrote, and agrees with itself about singular and
# plural. Every message here is the one CPython raises, so this file is run
# against both.
import array
import collections
import csv
import itertools
import operator
import typing
import weakref
from testutils import assert_raises
def check(exc_type, message, fn, *args, **kwargs):
try:
fn(*args, **kwargs)
except exc_type as e:
assert str(e) == message, f"{str(e)!r} != {message!r}"
else:
raise AssertionError(f"{fn} did not raise {exc_type.__name__}")
def test_the_message_names_the_function():
check(TypeError, "find expected at least 1 argument, got 0", "".find)
check(TypeError, "center expected at least 1 argument, got 0", "a".center)
check(TypeError, "fromkeys expected at least 1 argument, got 0", dict.fromkeys)
check(TypeError, "divmod expected 2 arguments, got 1", divmod, 1)
check(TypeError, "insert expected 2 arguments, got 1", [].insert, 1)
check(TypeError, "sorted expected 1 argument, got 0", sorted)
check(TypeError, "min expected at least 1 argument, got 0", min)
def test_a_method_does_not_count_its_instance():
# `self` fills the instance parameter, so it is in neither number, whether
# the call bound it or wrote it.
check(TypeError, "insert expected 2 arguments, got 1", [].insert, 1)
check(TypeError, "insert expected 2 arguments, got 1", list.insert, [], 1)
def test_one_argument_is_singular():
check(TypeError, "sorted expected 1 argument, got 0", sorted)
check(TypeError, "divmod expected 2 arguments, got 1", divmod, 1)
check(TypeError, "float expected at most 1 argument, got 2", float, 1, 2)
check(TypeError, "int expected at most 2 arguments, got 3", int, 1, 2, 3)
def test_a_keyword_it_did_not_expect_is_quoted():
check(
TypeError,
"split() got an unexpected keyword argument 'bogus'",
"".split,
bogus=1,
)
check(
TypeError,
"round() got an unexpected keyword argument 'bogus'",
round,
1,
bogus=2,
)
def test_a_parameter_a_call_may_name_is_named_back():
check(
TypeError,
"memoryview() missing required argument 'object' (pos 1)",
memoryview,
)
check(
TypeError,
"cast() missing required argument 'format' (pos 1)",
memoryview(b"a").cast,
)
def test_a_type_is_named_by_the_type_it_builds():
# The name is the type the slot was written for, without the module and
# without the subclass being constructed.
class Deque(collections.deque):
pass
check(TypeError, "range expected at least 1 argument, got 0", range)
check(TypeError, "slice expected at least 1 argument, got 0", slice)
check(TypeError, "filter expected 2 arguments, got 0", filter)
check(TypeError, "set expected at most 1 argument, got 2", set, 1, 2)
check(TypeError, "tuple expected at most 1 argument, got 2", tuple, 1, 2)
check(TypeError, "staticmethod expected 1 argument, got 0", staticmethod)
check(TypeError, "classmethod expected 1 argument, got 0", classmethod)
with assert_raises(TypeError):
Deque(1, 2, 3)
with assert_raises(TypeError):
array.array()
def test_a_slot_that_counts_its_own_arguments_says_the_same_thing():
# Sites that check the count themselves raise what binding would have.
class Obj:
pass
check(TypeError, "__new__ expected at least 1 argument, got 0", weakref.ref)
check(
TypeError,
"__new__ expected at most 2 arguments, got 3",
weakref.ref,
Obj(),
1,
2,
)
check(TypeError, "GenericAlias expected 2 arguments, got 0", type(list[int]))
check(TypeError, "frozenset expected at most 1 argument, got 2", frozenset, 1, 2)
check(TypeError, "attrgetter expected 1 argument, got 0", operator.attrgetter)
check(TypeError, "itemgetter expected 1 argument, got 0", operator.itemgetter)
check(
TypeError, "islice expected at least 2 arguments, got 1", itertools.islice, []
)
# The count is read before the keywords are.
check(TypeError, "min expected at least 1 argument, got 0", min, bogus=1)
check(TypeError, "max expected at least 1 argument, got 0", max, bogus=1)
def test_a_keyword_check_of_its_own_says_the_same_thing():
check(
TypeError,
"AttributeError() got an unexpected keyword argument 'bogus'",
AttributeError,
bogus=1,
)
check(
TypeError,
"NameError() got an unexpected keyword argument 'bogus'",
NameError,
bogus=1,
)
check(
TypeError,
"typevar() got an unexpected keyword argument 'bogus'",
typing.TypeVar,
"T",
bogus=1,
)
# The dialect is parsed by a parser of its own, which has no name to give.
check(
TypeError,
"this function got an unexpected keyword argument 'bogus'",
csv.reader,
[],
bogus=1,
)
test_the_message_names_the_function()
test_a_method_does_not_count_its_instance()
test_one_argument_is_singular()
test_a_keyword_it_did_not_expect_is_quoted()
test_a_parameter_a_call_may_name_is_named_back()
test_a_type_is_named_by_the_type_it_builds()
test_a_slot_that_counts_its_own_arguments_says_the_same_thing()
test_a_keyword_check_of_its_own_says_the_same_thing()