diff --git a/Lib/test/test_extcall.py b/Lib/test/test_extcall.py index 483d5ad5f2b..c816d0135a1 100644 --- a/Lib/test/test_extcall.py +++ b/Lib/test/test_extcall.py @@ -442,7 +442,7 @@ ... False True - >>> id(1, **{'foo': 1}) # TODO: RUSTPYTHON # doctest:+EXPECTED_FAILURE + >>> id(1, **{'foo': 1}) Traceback (most recent call last): ... TypeError: id() takes no keyword arguments diff --git a/crates/capi/src/methodobject.rs b/crates/capi/src/methodobject.rs index 9868db36cac..2e82c5e9daa 100644 --- a/crates/capi/src/methodobject.rs +++ b/crates/capi/src/methodobject.rs @@ -385,4 +385,26 @@ mod tests { func.call0().unwrap(); }); } + + #[test] + fn no_keywords_before_arity() { + Python::attach(|py| { + unsafe extern "C" fn c_fn(_self: *mut PyObject, _args: *mut PyObject) -> *mut PyObject { + unsafe { PyLong_FromLong(42) } + } + let f = PyCFunction::new(py, c_fn, c"no_keywords", c"", None).unwrap(); + let kwargs = pyo3::types::PyDict::new(py); + kwargs.set_item("unexpected", 1).unwrap(); + let err = f.call((1,), Some(&kwargs)).unwrap_err(); + assert_eq!( + err.value(py).str().unwrap().to_str().unwrap(), + "no_keywords() takes no keyword arguments" + ); + kwargs.clear(); + assert_eq!( + f.call((), Some(&kwargs)).unwrap().extract::().unwrap(), + 42 + ); + }); + } } diff --git a/crates/vm/src/builtins/builtin_func.rs b/crates/vm/src/builtins/builtin_func.rs index 17e3e342485..e0399987049 100644 --- a/crates/vm/src/builtins/builtin_func.rs +++ b/crates/vm/src/builtins/builtin_func.rs @@ -87,6 +87,9 @@ impl Callable for PyNativeFunction { type Args = FuncArgs; #[inline] fn call(zelf: &Py, mut args: FuncArgs, vm: &VirtualMachine) -> PyResult { + if !args.kwargs.is_empty() && !zelf.value.flags.contains(PyMethodFlags::KEYWORDS) { + return Err(native_no_keywords_error(zelf, vm)?); + } let mut callee = Callee::named(zelf.value.name); if let Some(z) = &zelf.zelf { // STATIC methods store the class in zelf for qualname/repr purposes, @@ -272,6 +275,12 @@ fn vectorcall_native_function( ) -> PyResult { let zelf: &Py = zelf_obj.downcast_ref().unwrap(); + if kwnames.is_some_and(|names| !names.is_empty()) + && !zelf.value.flags.contains(PyMethodFlags::KEYWORDS) + { + return Err(native_no_keywords_error(zelf, vm)?); + } + // Build FuncArgs with self already at position 0 (no insert(0) needed) let needs_self = zelf .zelf @@ -292,6 +301,30 @@ fn vectorcall_native_function( (zelf.value.func)(vm, func_args, callee) } +#[cold] +fn native_no_keywords_error( + zelf: &Py, + vm: &VirtualMachine, +) -> PyResult { + if zelf.value.flags.contains(PyMethodFlags::VARARGS) { + return Ok(vm.new_type_error(format!("{}() takes no keyword arguments", zelf.value.name))); + } + let qualname = PyNativeFunction::__qualname__(NativeFunctionOrMethod(zelf.to_owned()), vm)?; + let module = zelf + .module + .load_owned() + .filter(|module| !vm.is_none(module)); + let module = module.map(|module| module.str(vm)).transpose()?; + let name = if let Some(module) = module + && module.to_string() != "builtins" + { + format!("{module}.{qualname}") + } else { + qualname.to_string() + }; + Ok(vm.new_type_error(format!("{name}() takes no keyword arguments"))) +} + pub(crate) fn init(context: &'static Context) { PyNativeFunction::extend_class(context, context.types.builtin_function_or_method_type); context diff --git a/crates/vm/src/builtins/descriptor.rs b/crates/vm/src/builtins/descriptor.rs index 301448654a6..55c2275f5bb 100644 --- a/crates/vm/src/builtins/descriptor.rs +++ b/crates/vm/src/builtins/descriptor.rs @@ -112,8 +112,9 @@ impl Callable for PyMethodDescriptor { type Args = FuncArgs; #[inline] fn call(zelf: &Py, args: FuncArgs, vm: &VirtualMachine) -> PyResult { - if let Some(obj) = args.args.first() { - method_descr_typecheck(zelf, obj, vm)?; + method_descr_check_self(zelf, args.args.first().map(|obj| &**obj), vm)?; + if !args.kwargs.is_empty() && !zelf.method.flags.contains(PyMethodFlags::KEYWORDS) { + return Err(method_descr_no_keywords_error(zelf, vm)); } (zelf.method.func)( vm, @@ -1556,6 +1557,40 @@ fn method_descr_typecheck( ))) } +#[inline] +fn method_descr_check_self( + descr: &PyMethodDescriptor, + obj: Option<&PyObject>, + vm: &VirtualMachine, +) -> PyResult<()> { + let obj = obj.ok_or_else(|| method_descr_missing_self_error(descr, vm))?; + method_descr_typecheck(descr, obj, vm) +} + +#[cold] +fn method_descr_missing_self_error( + descr: &PyMethodDescriptor, + vm: &VirtualMachine, +) -> crate::exceptions::types::PyBaseExceptionRef { + vm.new_type_error(format!( + "unbound method {}.{}() needs an argument", + descr.common.typ.name(), + descr.common.name + )) +} + +#[cold] +fn method_descr_no_keywords_error( + descr: &PyMethodDescriptor, + vm: &VirtualMachine, +) -> crate::exceptions::types::PyBaseExceptionRef { + vm.new_type_error(format!( + "{}.{}() takes no keyword arguments", + descr.common.typ.name(), + descr.common.name + )) +} + /// Vectorcall for method_descriptor: calls native method directly fn vectorcall_method_descriptor( zelf_obj: &PyObject, @@ -1565,10 +1600,19 @@ fn vectorcall_method_descriptor( vm: &VirtualMachine, ) -> PyResult { let zelf: &Py = zelf_obj.downcast_ref().unwrap(); - if nargs > 0 - && let Some(obj) = args.first() + method_descr_check_self( + zelf, + if nargs > 0 { + args.first().map(|obj| &**obj) + } else { + None + }, + vm, + )?; + if kwnames.is_some_and(|names| !names.is_empty()) + && !zelf.method.flags.contains(PyMethodFlags::KEYWORDS) { - method_descr_typecheck(zelf, obj, vm)?; + return Err(method_descr_no_keywords_error(zelf, vm)); } let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames); (zelf.method.func)( diff --git a/extra_tests/snippets/syntax_function_args.py b/extra_tests/snippets/syntax_function_args.py index eb4f94b8a36..96cbaf37272 100644 --- a/extra_tests/snippets/syntax_function_args.py +++ b/extra_tests/snippets/syntax_function_args.py @@ -1,6 +1,90 @@ from testutils import assert_raises +def error(call, expected): + try: + call() + except TypeError as exc: + assert str(exc) == expected, (str(exc), expected) + else: + raise AssertionError("expected TypeError") + + +error(lambda: id(obj=1), "id() takes no keyword arguments") +error(lambda: id(**{"obj": 1}), "id() takes no keyword arguments") +error(lambda: id(1, 2, obj=1), "id() takes no keyword arguments") +error(lambda: list.append(x=1), "unbound method list.append() needs an argument") +error(lambda: list.append(**{"x": 1}), "unbound method list.append() needs an argument") +error( + lambda: list.append(1, x=1), + "descriptor 'append' for 'list' objects doesn't apply to a 'int' object", +) +error(lambda: list.append([], x=1), "list.append() takes no keyword arguments") +error(lambda: list.append([], **{"x": 1}), "list.append() takes no keyword arguments") +error(lambda: [].append(x=1), "list.append() takes no keyword arguments") +error(lambda: [].clear(1, x=1), "list.clear() takes no keyword arguments") + + +class Index: + def __index__(self): + raise AssertionError("conversion ran before keyword check") + + +error(lambda: bin(Index(), x=1), "bin() takes no keyword arguments") +assert id(1, **{}) == id(1) +assert round(1.25, ndigits=1) == 1.2 +assert pow(2, exp=3) == 8 +assert sorted([2, 1], reverse=True) == [2, 1] +assert sum([1, 2], start=4) == 7 +values = [] +list.append(values, 1) +values.append(2) +assert values == [1, 2] +list.clear(values) +assert values == [] + +# Keyword metadata must forward raw arguments without changing body validation. +for choose in (min, max): + assert choose([], default=42) == 42 + assert choose([1, 2], key=lambda value: -value) == (2 if choose is min else 1) + try: + choose(bogus=1) + except TypeError as exc: + assert str(exc) == f"{choose.__name__} expected at least 1 argument, got 0" + else: + raise AssertionError("missing positional argument was accepted") + try: + choose(1, 2, default=0) + except TypeError as exc: + assert str(exc) == ( + f"Cannot specify a default for {choose.__name__}() " + "with multiple positional arguments" + ) + else: + raise AssertionError("default with multiple arguments was accepted") + +# Named fields in argument structs must override positional-only arguments. +assert eval("value", globals={"value": 42}) == 42 +namespace = {} +exec("value = 42", globals=namespace) +assert namespace["value"] == 42 +assert compile(source="42", filename="", mode="eval") +assert __import__(name="sys").__name__ == "sys" + +from io import StringIO + +output = StringIO() +print(1, 2, sep=":", end="!", file=output) +assert output.getvalue() == "1:2!" + +# Methods using a keyword-only argument struct must keep accepting keywords. +values = [1, 2] +values.sort(reverse=True) +assert values == [2, 1] + +# Definitions built without the macros infer their calling convention too. +assert int.__new__(int, "11", base=2) == 3 + def sum(x, y): return x+y