Commit Graph
2 Commits
Author SHA1 Message Date
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
Jeong, YunWon 516a116f9a Name the function a failed argument binding was binding for (#8630)
* Name the function a failed argument binding was binding for

A built-in call that binds badly reported neither the function nor, for a
method, the right numbers: `self` was prepended before binding, so it was
counted as both an expected parameter and a given argument. `[].append()`
read `expected at least 2 arguments, got 1`.

`Callee` carries what a message says about the function: the name, and
whether the leading argument fills the instance parameter. `PyNativeFn`
takes one, `PyNativeFunction` and `PyMethodDescriptor` supply it, and the
four method-descriptor call specializations in `frame.rs` do the same.
`bind_for` subtracts the instance from both numbers.

The messages now read as their counterparts do:

- `_PyArg_CheckPositional`: `find expected at least 1 argument, got 0`,
  with the singular when the count is one.
- `_PyArg_UnpackKeywords`: `split() got an unexpected keyword argument
  'bogus'`, and `cast() missing required argument 'format' (pos 1)` for a
  parameter a call may pass by name. A positional-only parameter is only
  ever counted, so it keeps the first form.

A binding that happens where the name isn't known leaves it off, the way
`_PyArg_Parser.fname` is NULL.

`test_find_etc_raise_correct_error_messages` now passes in both
`test_bytes` and `string_tests`.

Assisted-by: Claude

* Name the type whose constructor or initializer bound the arguments

`Constructor::slot_new` and `Initializer::slot_init` bound their arguments
without a name, and so did the slots that override them, so a constructor
that was called wrongly said only `expected 1 argument, got 0`.

Both defaults and every override now name the type the slot was written
for, not the subclass being constructed and not the module `tp_name`
carries: `float expected at most 1 argument, got 2`, and `deque expected
at most 2 arguments, got 3` for a `deque` subclass.

`slice()` and `range()` counted their own arguments and raised messages of
their own; they now raise the one `_PyArg_CheckPositional` raises, which
makes `test_range_constructor_error_messages` pass.

Assisted-by: Claude
2026-09-01 06:57:58 +09:00