Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion crates/derive-impl/src/from_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,13 @@ pub(crate) fn impl_from_args(input: DeriveInput) -> Result<TokenStream> {
.map(ArgAttribute::try_from)
.collect::<Result<Vec<_>>>()?;
let (min_arity, max_arity) = compute_arity_bounds(&field_attrs);
// A flattened field adds its own type's upper bound. An unbounded one
// such as `PosArgs` is `usize::MAX`, so the sum saturates.
let flattened = struct_fields
.iter()
.zip(&field_attrs)
.filter(|(_, attr)| attr.kind == ParameterKind::Flatten)
.map(|(field, _)| &field.ty);

let mut params = Vec::new();
for field in &struct_fields {
Expand All @@ -619,7 +626,9 @@ pub(crate) fn impl_from_args(input: DeriveInput) -> Result<TokenStream> {
Some(&[#(#params),*]);

fn arity() -> ::std::ops::RangeInclusive<usize> {
#min_arity..=#max_arity
#min_arity..=#max_arity #(
.saturating_add(*<#flattened as ::rustpython_vm::function::FromArgs>::arity().end())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you tell me why saturating_add is required here? I am missing something.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PosArgs doesn't override arity(), so with this change it gets the default 0..=usize::MAX. A struct like PyMapNewArgs has two positional fields plus a flattened PosArgs, so the derived bound becomes 2 + usize::MAX. With plain + that panics in debug builds and wraps to 1 in release, and then the new check in bind_for would reject map(f, it). Saturating keeps it at usize::MAX, meaning "no upper bound". The tuple impl uses saturating_add for the same reason.

Happy to add a short comment there if that makes it clearer.

)*
}

fn from_args(
Expand Down
4 changes: 4 additions & 0 deletions crates/stdlib/src/csv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,10 @@ mod _csv {
Param::var_keyword("fmtparams"),
]);

fn arity() -> core::ops::RangeInclusive<usize> {
0..=1
}

fn from_args(vm: &VirtualMachine, args: &mut FuncArgs) -> Result<Self, ArgumentError> {
let res = Self {
dialect: args.take_positional_keyword("dialect"),
Expand Down
22 changes: 19 additions & 3 deletions crates/vm/src/function/argument.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,12 @@ impl FuncArgs {
let arity = arity.start().saturating_sub(instance)..=arity.end().saturating_sub(instance);
let num_given = self.args.len().saturating_sub(instance);

// Too many positional arguments are reported before any argument is
// converted, so a conversion cannot run user code first.
if num_given > *arity.end() {
return Err(ArgumentError::TooManyArgs.into_exception(&arity, num_given, callee, vm));
}

// A keyword that fills a parameter already given by position is
// reported before leftover-keyword and conversion errors.
if let Some(params) = T::PARAMS
Expand Down Expand Up @@ -581,10 +587,11 @@ impl ArgumentError {
pub trait FromArgs: Sized {
/// The range of positional arguments permitted by the function signature.
///
/// Returns an empty range if not applicable.
/// The default has no upper bound, so a type that takes positional
/// arguments itself is never rejected before `from_args` runs.
#[must_use]
fn arity() -> RangeInclusive<usize> {
0..=0
0..=usize::MAX
}

/// Parameters this type contributes to a text signature.
Expand Down Expand Up @@ -799,6 +806,10 @@ where
{
const PARAMS: Option<&'static [Param]> = Some(&[Param::var_keyword(Name::NAME)]);

fn arity() -> RangeInclusive<usize> {
0..=0
}

fn from_args(vm: &VirtualMachine, args: &mut FuncArgs) -> Result<Self, ArgumentError> {
let mut kwargs = KwArgsMap::default();
for (name, value) in args.remaining_keywords() {
Expand Down Expand Up @@ -989,6 +1000,10 @@ where
impl FromArgs for () {
const PARAMS: Option<&'static [Param]> = Some(&[]);

fn arity() -> RangeInclusive<usize> {
0..=0
}

fn from_args(_vm: &VirtualMachine, _args: &mut FuncArgs) -> Result<Self, ArgumentError> {
Ok(())
}
Expand Down Expand Up @@ -1020,7 +1035,8 @@ macro_rules! tuple_from_py_func_args {
$(
let (start, end) = $T::arity().into_inner();
min += start;
max += end;
// An unbounded `end` is `usize::MAX`, so the sum saturates.
max = usize::saturating_add(max, end);
)+
min..=max
}
Expand Down
8 changes: 8 additions & 0 deletions crates/vm/src/stdlib/os.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ impl<'fd, KW: DirFdKeyword> DirFd<'fd, 1, KW> {
impl<const AVAILABLE: usize, KW: DirFdKeyword> FromArgs for DirFd<'_, AVAILABLE, KW> {
const PARAMS: Option<&'static [Param]> = Some(KW::PARAMS);

fn arity() -> core::ops::RangeInclusive<usize> {
0..=0
}

fn from_args(vm: &VirtualMachine, args: &mut FuncArgs) -> Result<Self, ArgumentError> {
let fd = match args.take_keyword(KW::NAME) {
Some(o) if vm.is_none(&o) => Ok(DEFAULT_DIR_FD),
Expand Down Expand Up @@ -290,6 +294,10 @@ pub(super) mod _os {
impl FromArgs for OpenDirFd {
const PARAMS: Option<&'static [Param]> = Some(DefaultDirFd::PARAMS);

fn arity() -> core::ops::RangeInclusive<usize> {
0..=0
}

fn from_args(vm: &VirtualMachine, args: &mut FuncArgs) -> Result<Self, ArgumentError> {
let fd = match args.take_keyword("dir_fd") {
Some(o) if vm.is_none(&o) => AT_FDCWD,
Expand Down
20 changes: 19 additions & 1 deletion extra_tests/snippets/vm_argument_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import typing
import weakref

from testutils import assert_raises
from testutils import TestFailingIter, assert_raises


def check(exc_type, message, fn, *args, **kwargs):
Expand Down Expand Up @@ -150,6 +150,23 @@ def test_a_keyword_check_of_its_own_says_the_same_thing():
)


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()
Expand All @@ -158,3 +175,4 @@ def test_a_keyword_check_of_its_own_says_the_same_thing():
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()
Loading