mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
* Count positional arguments before converting any FuncArgs::bind_for rejected surplus positional arguments only after FromArgs::from_args had converted every declared parameter. A conversion can run user code (__iter__ for PyIter, __index__ for ArgIndex), so filter(None, it, 0), hex(x, 1) and pairwise(it, 0) raised whatever that code raised instead of the TypeError CPython reports before it touches any argument. Check the count against FromArgs::arity() before from_args runs. For that bound to be safe: - The default arity() has no upper bound, since a manual impl may take positional arguments itself. KwArgs, (), DirFd, OpenDirFd and csv's FormatOptions declare the bound they actually have. - A derived struct adds the upper bound of each flattened field. - A tuple sums upper bounds with saturating_add. Assisted-by: Claude Code:claude-opus-5-5 * Note why arity upper bounds saturate Comment that an unbounded upper bound is usize::MAX, which is why the tuple and derived arity() sums use saturating_add, and drop the CPython mention from the positional count check comment. Assisted-by: Claude Code:claude-opus-5-5 --------- Co-authored-by: Jeong YunWon <jeong@youknowone.org>
179 lines
5.8 KiB
Python
179 lines
5.8 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 TestFailingIter, 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,
|
|
)
|
|
|
|
|
|
def test_too_many_arguments_are_counted_before_any_is_converted():
|
|
class FailingIndex:
|
|
def __index__(self):
|
|
raise RuntimeError
|
|
|
|
check(
|
|
TypeError,
|
|
"filter expected 2 arguments, got 3",
|
|
filter,
|
|
None,
|
|
TestFailingIter(),
|
|
0,
|
|
)
|
|
with assert_raises(TypeError):
|
|
hex(FailingIndex(), 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()
|
|
test_too_many_arguments_are_counted_before_any_is_converted()
|