diff --git a/newsfragments/5951.added.md b/newsfragments/5951.added.md new file mode 100644 index 00000000000..3b15d147edc --- /dev/null +++ b/newsfragments/5951.added.md @@ -0,0 +1 @@ +Added tests for `PyCallArgs` impls `()`, `&Bound<'py, PyTuple>`, `Py`, `&Py`, and `Borrowed<'_, 'py, PyTuple>`, and for `call_method_positional` default impl and error paths in `src/call.rs`. \ No newline at end of file diff --git a/newsfragments/5951.fixed.md b/newsfragments/5951.fixed.md new file mode 100644 index 00000000000..43c01ef4b2e --- /dev/null +++ b/newsfragments/5951.fixed.md @@ -0,0 +1 @@ +Fixed `check_call!` test macros in `src/call.rs` that ignored their `$args` parameter, causing all five `PyCallArgs` dispatch arms to test only the `Bound<'py, PyTuple>` path. \ No newline at end of file diff --git a/src/call.rs b/src/call.rs index b621e83a9e1..0fdd63ceee6 100644 --- a/src/call.rs +++ b/src/call.rs @@ -245,11 +245,8 @@ mod tests { macro_rules! check_call { ($args:expr, $kwargs:expr) => { - let (a, k): (Py, Py) = f - .call(args.clone(), Some(kwargs)) - .unwrap() - .extract() - .unwrap(); + let (a, k): (Py, Py) = + f.call($args, Some($kwargs)).unwrap().extract().unwrap(); assert!(a.is(&args)); assert!(k.is(kwargs)); }; @@ -287,7 +284,7 @@ mod tests { macro_rules! check_call { ($args:expr, $kwargs:expr) => { let (a, k): (Py, Py) = - f.call1(args.clone()).unwrap().extract().unwrap(); + f.call1($args).unwrap().extract().unwrap(); assert!(a.is(&args)); assert!(k.is_none(py)); }; @@ -309,4 +306,86 @@ mod tests { check_call!(args.as_borrowed(), kwargs); }) } + + #[test] + fn test_call_unit_args() { + // Exercises ()::call and ()::call_positional — both routes through + // into_pyobject_or_pyerr to produce an empty PyTuple. + use crate::types::PyDict; + use crate::{ + types::{IntoPyDict, PyAnyMethods, PyTupleMethods}, + wrap_pyfunction, Py, Python, + }; + + Python::attach(|py| { + let f = wrap_pyfunction!(args_kwargs, py).unwrap(); + let kwargs = &[("x", 1i32)].into_py_dict(py).unwrap(); + + // ()::call — empty positional args dispatched via args.call(…) branch + let (a, _k): (Py, Option>) = + f.call((), Some(kwargs)).unwrap().extract().unwrap(); + assert_eq!(a.bind(py).len(), 0); + + // ()::call_positional — empty positional args dispatched via call1 → args.call_positional(…) + let (a, _k): (Py, Option>) = + f.call1(()).unwrap().extract().unwrap(); + assert_eq!(a.bind(py).len(), 0); + }) + } + + #[test] + fn test_call_method_positional_default() { + // Exercises the default call_method_positional impl on the PyCallArgs trait + // (lines 68–76 of call.rs): getattr + call1. Reached via call_method1. + use crate::{ + exceptions::PyAttributeError, + types::{PyAnyMethods, PyList, PyListMethods, PyTuple}, + Python, + }; + + Python::attach(|py| { + let list = PyList::new(py, [1i32, 2, 3]).unwrap(); + let args = PyTuple::new(py, [4i32]).unwrap(); + + // Happy path: getattr("append") succeeds, call1 appends the element. + list.call_method1("append", args).unwrap(); + assert_eq!(list.len(), 4); + // Verify the value is the integer 4, not a wrapped tuple. + assert_eq!(list.get_item(3).unwrap().extract::().unwrap(), 4i32); + + // Error path: getattr fails → call_method_positional returns Err. + let err = list + .call_method1("nonexistent_method_xyz", PyTuple::empty(py)) + .unwrap_err(); + assert!(err.is_instance_of::(py)); + }) + } + + #[test] + fn test_call_error_paths() { + // Exercises the NULL-return error paths in Borrowed::call and + // Borrowed::call_positional when PyObject_Call fails. + use crate::{ + exceptions::PyTypeError, + types::{IntoPyDict, PyAnyMethods, PyDict}, + Python, + }; + + Python::attach(|py| { + // A PyDict is not callable — invoking it raises TypeError. + let not_callable = PyDict::new(py).into_any(); + let args = PyTuple::empty(py); + let kwargs = &[("x", 1i32)].into_py_dict(py).unwrap(); + + // Borrowed::call error path: kwargs Some → args.call(…) branch → + // PyObject_Call(function=dict, …) returns NULL. + let err = not_callable.call(args.clone(), Some(kwargs)).unwrap_err(); + assert!(err.is_instance_of::(py)); + + // Borrowed::call_positional error path: kwargs None → args.call_positional(…) branch → + // PyObject_Call(…, kwargs=null_ptr) returns NULL. + let err = not_callable.call(args, None).unwrap_err(); + assert!(err.is_instance_of::(py)); + }) + } }