Visitar URL original
Reject unsupported native keywords before argument binding · RustPython/RustPython@a0c0223 · GitHub
Skip to content

Commit a0c0223

Browse files
widehyo1youknowone
authored andcommitted
Reject unsupported native keywords before argument binding
Native functions and method descriptors raise TypeError for nonempty keywords when `flags` lacks `KEYWORDS`, before argument binding and conversion. Native functions report the unqualified name when `flags` contains `VARARGS` and the module-qualified `__qualname__` otherwise. Method descriptors report `Type.name()`. A method descriptor called without a receiver raises "unbound method Type.name() needs an argument" before the keyword check. Add C-API and snippet regression tests and remove the id doctest expected-failure marker. Assisted-by: Codex:GPT-6 Assisted-by: Claude Code:claude-opus-5-5
1 parent bb9ef5d commit a0c0223

5 files changed

Lines changed: 189 additions & 6 deletions

File tree

‎Lib/test/test_extcall.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@
442442
... False
443443
True
444444
445-
>>> id(1, **{'foo': 1}) # TODO: RUSTPYTHON # doctest:+EXPECTED_FAILURE
445+
>>> id(1, **{'foo': 1})
446446
Traceback (most recent call last):
447447
...
448448
TypeError: id() takes no keyword arguments

‎crates/capi/src/methodobject.rs‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,4 +385,26 @@ mod tests {
385385
func.call0().unwrap();
386386
});
387387
}
388+
389+
#[test]
390+
fn no_keywords_before_arity() {
391+
Python::attach(|py| {
392+
unsafe extern "C" fn c_fn(_self: *mut PyObject, _args: *mut PyObject) -> *mut PyObject {
393+
unsafe { PyLong_FromLong(42) }
394+
}
395+
let f = PyCFunction::new(py, c_fn, c"no_keywords", c"", None).unwrap();
396+
let kwargs = pyo3::types::PyDict::new(py);
397+
kwargs.set_item("unexpected", 1).unwrap();
398+
let err = f.call((1,), Some(&kwargs)).unwrap_err();
399+
assert_eq!(
400+
err.value(py).str().unwrap().to_str().unwrap(),
401+
"no_keywords() takes no keyword arguments"
402+
);
403+
kwargs.clear();
404+
assert_eq!(
405+
f.call((), Some(&kwargs)).unwrap().extract::<i32>().unwrap(),
406+
42
407+
);
408+
});
409+
}
388410
}

‎crates/vm/src/builtins/builtin_func.rs‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ impl Callable for PyNativeFunction {
8787
type Args = FuncArgs;
8888
#[inline]
8989
fn call(zelf: &Py<Self>, mut args: FuncArgs, vm: &VirtualMachine) -> PyResult {
90+
if !args.kwargs.is_empty() && !zelf.value.flags.contains(PyMethodFlags::KEYWORDS) {
91+
return Err(native_no_keywords_error(zelf, vm)?);
92+
}
9093
let mut callee = Callee::named(zelf.value.name);
9194
if let Some(z) = &zelf.zelf {
9295
// STATIC methods store the class in zelf for qualname/repr purposes,
@@ -272,6 +275,12 @@ fn vectorcall_native_function(
272275
) -> PyResult {
273276
let zelf: &Py<PyNativeFunction> = zelf_obj.downcast_ref().unwrap();
274277

278+
if kwnames.is_some_and(|names| !names.is_empty())
279+
&& !zelf.value.flags.contains(PyMethodFlags::KEYWORDS)
280+
{
281+
return Err(native_no_keywords_error(zelf, vm)?);
282+
}
283+
275284
// Build FuncArgs with self already at position 0 (no insert(0) needed)
276285
let needs_self = zelf
277286
.zelf
@@ -292,6 +301,30 @@ fn vectorcall_native_function(
292301
(zelf.value.func)(vm, func_args, callee)
293302
}
294303

304+
#[cold]
305+
fn native_no_keywords_error(
306+
zelf: &Py<PyNativeFunction>,
307+
vm: &VirtualMachine,
308+
) -> PyResult<crate::exceptions::types::PyBaseExceptionRef> {
309+
if zelf.value.flags.contains(PyMethodFlags::VARARGS) {
310+
return Ok(vm.new_type_error(format!("{}() takes no keyword arguments", zelf.value.name)));
311+
}
312+
let qualname = PyNativeFunction::__qualname__(NativeFunctionOrMethod(zelf.to_owned()), vm)?;
313+
let module = zelf
314+
.module
315+
.load_owned()
316+
.filter(|module| !vm.is_none(module));
317+
let module = module.map(|module| module.str(vm)).transpose()?;
318+
let name = if let Some(module) = module
319+
&& module.to_string() != "builtins"
320+
{
321+
format!("{module}.{qualname}")
322+
} else {
323+
qualname.to_string()
324+
};
325+
Ok(vm.new_type_error(format!("{name}() takes no keyword arguments")))
326+
}
327+
295328
pub(crate) fn init(context: &'static Context) {
296329
PyNativeFunction::extend_class(context, context.types.builtin_function_or_method_type);
297330
context

‎crates/vm/src/builtins/descriptor.rs‎

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,9 @@ impl Callable for PyMethodDescriptor {
112112
type Args = FuncArgs;
113113
#[inline]
114114
fn call(zelf: &Py<Self>, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
115-
if let Some(obj) = args.args.first() {
116-
method_descr_typecheck(zelf, obj, vm)?;
115+
method_descr_check_self(zelf, args.args.first().map(|obj| &**obj), vm)?;
116+
if !args.kwargs.is_empty() && !zelf.method.flags.contains(PyMethodFlags::KEYWORDS) {
117+
return Err(method_descr_no_keywords_error(zelf, vm));
117118
}
118119
(zelf.method.func)(
119120
vm,
@@ -1556,6 +1557,40 @@ fn method_descr_typecheck(
15561557
)))
15571558
}
15581559

1560+
#[inline]
1561+
fn method_descr_check_self(
1562+
descr: &PyMethodDescriptor,
1563+
obj: Option<&PyObject>,
1564+
vm: &VirtualMachine,
1565+
) -> PyResult<()> {
1566+
let obj = obj.ok_or_else(|| method_descr_missing_self_error(descr, vm))?;
1567+
method_descr_typecheck(descr, obj, vm)
1568+
}
1569+
1570+
#[cold]
1571+
fn method_descr_missing_self_error(
1572+
descr: &PyMethodDescriptor,
1573+
vm: &VirtualMachine,
1574+
) -> crate::exceptions::types::PyBaseExceptionRef {
1575+
vm.new_type_error(format!(
1576+
"unbound method {}.{}() needs an argument",
1577+
descr.common.typ.name(),
1578+
descr.common.name
1579+
))
1580+
}
1581+
1582+
#[cold]
1583+
fn method_descr_no_keywords_error(
1584+
descr: &PyMethodDescriptor,
1585+
vm: &VirtualMachine,
1586+
) -> crate::exceptions::types::PyBaseExceptionRef {
1587+
vm.new_type_error(format!(
1588+
"{}.{}() takes no keyword arguments",
1589+
descr.common.typ.name(),
1590+
descr.common.name
1591+
))
1592+
}
1593+
15591594
/// Vectorcall for method_descriptor: calls native method directly
15601595
fn vectorcall_method_descriptor(
15611596
zelf_obj: &PyObject,
@@ -1565,10 +1600,19 @@ fn vectorcall_method_descriptor(
15651600
vm: &VirtualMachine,
15661601
) -> PyResult {
15671602
let zelf: &Py<PyMethodDescriptor> = zelf_obj.downcast_ref().unwrap();
1568-
if nargs > 0
1569-
&& let Some(obj) = args.first()
1603+
method_descr_check_self(
1604+
zelf,
1605+
if nargs > 0 {
1606+
args.first().map(|obj| &**obj)
1607+
} else {
1608+
None
1609+
},
1610+
vm,
1611+
)?;
1612+
if kwnames.is_some_and(|names| !names.is_empty())
1613+
&& !zelf.method.flags.contains(PyMethodFlags::KEYWORDS)
15701614
{
1571-
method_descr_typecheck(zelf, obj, vm)?;
1615+
return Err(method_descr_no_keywords_error(zelf, vm));
15721616
}
15731617
let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames);
15741618
(zelf.method.func)(

‎extra_tests/snippets/syntax_function_args.py‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,90 @@
11
from testutils import assert_raises
22

33

4+
def error(call, expected):
5+
try:
6+
call()
7+
except TypeError as exc:
8+
assert str(exc) == expected, (str(exc), expected)
9+
else:
10+
raise AssertionError("expected TypeError")
11+
12+
13+
error(lambda: id(obj=1), "id() takes no keyword arguments")
14+
error(lambda: id(**{"obj": 1}), "id() takes no keyword arguments")
15+
error(lambda: id(1, 2, obj=1), "id() takes no keyword arguments")
16+
error(lambda: list.append(x=1), "unbound method list.append() needs an argument")
17+
error(lambda: list.append(**{"x": 1}), "unbound method list.append() needs an argument")
18+
error(
19+
lambda: list.append(1, x=1),
20+
"descriptor 'append' for 'list' objects doesn't apply to a 'int' object",
21+
)
22+
error(lambda: list.append([], x=1), "list.append() takes no keyword arguments")
23+
error(lambda: list.append([], **{"x": 1}), "list.append() takes no keyword arguments")
24+
error(lambda: [].append(x=1), "list.append() takes no keyword arguments")
25+
error(lambda: [].clear(1, x=1), "list.clear() takes no keyword arguments")
26+
27+
28+
class Index:
29+
def __index__(self):
30+
raise AssertionError("conversion ran before keyword check")
31+
32+
33+
error(lambda: bin(Index(), x=1), "bin() takes no keyword arguments")
34+
assert id(1, **{}) == id(1)
35+
assert round(1.25, ndigits=1) == 1.2
36+
assert pow(2, exp=3) == 8
37+
assert sorted([2, 1], reverse=True) == [2, 1]
38+
assert sum([1, 2], start=4) == 7
39+
values = []
40+
list.append(values, 1)
41+
values.append(2)
42+
assert values == [1, 2]
43+
list.clear(values)
44+
assert values == []
45+
46+
# Keyword metadata must forward raw arguments without changing body validation.
47+
for choose in (min, max):
48+
assert choose([], default=42) == 42
49+
assert choose([1, 2], key=lambda value: -value) == (2 if choose is min else 1)
50+
try:
51+
choose(bogus=1)
52+
except TypeError as exc:
53+
assert str(exc) == f"{choose.__name__} expected at least 1 argument, got 0"
54+
else:
55+
raise AssertionError("missing positional argument was accepted")
56+
try:
57+
choose(1, 2, default=0)
58+
except TypeError as exc:
59+
assert str(exc) == (
60+
f"Cannot specify a default for {choose.__name__}() "
61+
"with multiple positional arguments"
62+
)
63+
else:
64+
raise AssertionError("default with multiple arguments was accepted")
65+
66+
# Named fields in argument structs must override positional-only arguments.
67+
assert eval("value", globals={"value": 42}) == 42
68+
namespace = {}
69+
exec("value = 42", globals=namespace)
70+
assert namespace["value"] == 42
71+
assert compile(source="42", filename="<inference>", mode="eval")
72+
assert __import__(name="sys").__name__ == "sys"
73+
74+
from io import StringIO
75+
76+
output = StringIO()
77+
print(1, 2, sep=":", end="!", file=output)
78+
assert output.getvalue() == "1:2!"
79+
80+
# Methods using a keyword-only argument struct must keep accepting keywords.
81+
values = [1, 2]
82+
values.sort(reverse=True)
83+
assert values == [2, 1]
84+
85+
# Definitions built without the macros infer their calling convention too.
86+
assert int.__new__(int, "11", base=2) == 3
87+
488
def sum(x, y):
589
return x+y
690

0 commit comments

Comments
 (0)