Skip to content
Draft
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
1 change: 1 addition & 0 deletions newsfragments/5951.added.md
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.

It's not interesting to users if we add test coverage, can delete this file (I added label in CI).

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added tests for `PyCallArgs` impls `()`, `&Bound<'py, PyTuple>`, `Py<PyTuple>`, `&Py<PyTuple>`, and `Borrowed<'_, 'py, PyTuple>`, and for `call_method_positional` default impl and error paths in `src/call.rs`.
1 change: 1 addition & 0 deletions newsfragments/5951.fixed.md
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.

Same here.

Original file line number Diff line number Diff line change
@@ -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.
91 changes: 85 additions & 6 deletions src/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,11 +245,8 @@ mod tests {

macro_rules! check_call {
($args:expr, $kwargs:expr) => {
let (a, k): (Py<PyTuple>, Py<PyDict>) = f
.call(args.clone(), Some(kwargs))
.unwrap()
.extract()
.unwrap();
let (a, k): (Py<PyTuple>, Py<PyDict>) =
f.call($args, Some($kwargs)).unwrap().extract().unwrap();
assert!(a.is(&args));
assert!(k.is(kwargs));
};
Expand Down Expand Up @@ -287,7 +284,7 @@ mod tests {
macro_rules! check_call {
($args:expr, $kwargs:expr) => {
Comment on lines 284 to 285
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.

It looks like $kwargs is not used in this macro at all, should be removed?

let (a, k): (Py<PyTuple>, Py<PyNone>) =
f.call1(args.clone()).unwrap().extract().unwrap();
f.call1($args).unwrap().extract().unwrap();
assert!(a.is(&args));
assert!(k.is_none(py));
};
Expand All @@ -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::{
Comment on lines +310 to +315
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.

This test possibly reveals something interesting - PyCallArgs<'py> for () can potentially use PyObject_VectorcallDict with empty array to avoid needing to pass the intermediate tuple unless the callee wants it.

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<PyTuple>, Option<Py<PyDict>>) =
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<PyTuple>, Option<Py<PyDict>>) =
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::<i32>().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::<PyAttributeError>(py));
})
}
Comment on lines +336 to +362
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.

This reads like AI slop test which is heavily coupled to the current implementation (e.g. referencing implementation function names, line numbers). If there is merit in having such a test, I would rather it be rewritten to cover the possible error pathways presented by the API, not from the implementation.


#[test]
fn test_call_error_paths() {
// Exercises the NULL-return error paths in Borrowed::call and
// Borrowed::call_positional when PyObject_Call fails.
Comment on lines +364 to +367
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.

Similar comment as above RE ai slop.

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::<PyTypeError>(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::<PyTypeError>(py));
})
}
}
Loading