Merge pull request #3711 from davidhewitt/call-bound

add `call_bound` and `call_method_bound`
This commit is contained in:
David Hewitt 2024-01-03 13:43:30 +00:00 committed by GitHub
commit 58746bb4f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 93 additions and 34 deletions

View file

@ -41,7 +41,7 @@ impl PyCounter {
&self,
py: Python<'_>,
args: &PyTuple,
kwargs: Option<&PyDict>,
kwargs: Option<Bound<'_, PyDict>>,
) -> PyResult<Py<PyAny>> {
let old_count = self.count.get();
let new_count = old_count + 1;
@ -51,7 +51,7 @@ impl PyCounter {
println!("{} has been called {} time(s).", name, new_count);
// After doing something, we finally forward the call to the wrapped function
let ret = self.wraps.call(py, args, kwargs)?;
let ret = self.wraps.call_bound(py, args, kwargs.as_ref())?;
// We could do something with the return value of
// the function before returning it

View file

@ -10,13 +10,13 @@ Any Python-native object reference (such as `&PyAny`, `&PyList`, or `&PyCell<MyC
PyO3 offers two APIs to make function calls:
* [`call`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyAny.html#method.call) - call any callable Python object.
* [`call_method`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyAny.html#method.call_method) - call a method on the Python object.
* [`call`]({{#PYO3_DOCS_URL}}/pyo3/prelude/trait.PyAnyMethods#tymethod.call) - call any callable Python object.
* [`call_method`]({{#PYO3_DOCS_URL}}/pyo3/prelude/trait.PyAnyMethods#tymethod.call_method) - call a method on the Python object.
Both of these APIs take `args` and `kwargs` arguments (for positional and keyword arguments respectively). There are variants for less complex calls:
* [`call1`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyAny.html#method.call1) and [`call_method1`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyAny.html#method.call_method1) to call only with positional `args`.
* [`call0`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyAny.html#method.call0) and [`call_method0`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyAny.html#method.call_method0) to call with no arguments.
* [`call1`]({{#PYO3_DOCS_URL}}/pyo3/prelude/trait.PyAnyMethods#tymethod.call1) and [`call_method1`]({{#PYO3_DOCS_URL}}/pyo3/prelude/trait.PyAnyMethods#tymethod.call_method1) to call only with positional `args`.
* [`call0`]({{#PYO3_DOCS_URL}}/pyo3/prelude/trait.PyAnyMethods#tymethod.call0) and [`call_method0`]({{#PYO3_DOCS_URL}}/pyo3/prelude/trait.PyAnyMethods#tymethod.call_method0) to call with no arguments.
For convenience the [`Py<T>`](types.html#pyt-and-pyobject) smart pointer also exposes these same six API methods, but needs a `Python` token as an additional first argument to prove the GIL is held.
@ -95,22 +95,30 @@ fn main() -> PyResult<()> {
// call object with PyDict
let kwargs = [(key1, val1)].into_py_dict(py);
fun.call(py, (), Some(kwargs))?;
fun.call_bound(py, (), Some(&kwargs.as_borrowed()))?;
// pass arguments as Vec
let kwargs = vec![(key1, val1), (key2, val2)];
fun.call(py, (), Some(kwargs.into_py_dict(py)))?;
fun.call_bound(py, (), Some(&kwargs.into_py_dict(py).as_borrowed()))?;
// pass arguments as HashMap
let mut kwargs = HashMap::<&str, i32>::new();
kwargs.insert(key1, 1);
fun.call(py, (), Some(kwargs.into_py_dict(py)))?;
fun.call_bound(py, (), Some(&kwargs.into_py_dict(py).as_borrowed()))?;
Ok(())
})
}
```
<div class="warning">
During PyO3's [migration from "GIL Refs" to the `Bound<T>` smart pointer](./migration.md#migrating-from-the-gil-refs-api-to-boundt), [`Py<T>::call`]({{#PYO3_DOCS_URL}}/pyo3/struct.py#method.call) is temporarily named `call_bound` (and `call_method` is temporarily `call_method_bound`).
(This temporary naming is only the case for the `Py<T>` smart pointer. The methods on the `&PyAny` GIL Ref such as `call` have not been given replacements, and the methods on the `Bound<PyAny>` smart pointer such as [`Bound<PyAny>::call`]({#PYO3_DOCS_URL}}/pyo3/prelude/trait.pyanymethods#tymethod.call) already use follow the newest API conventions.)
</div>
## Executing existing Python code
If you already have some existing Python code that you need to execute from Rust, the following FAQs can help you select the right PyO3 functionality for your situation:

View file

@ -1016,14 +1016,30 @@ impl<T> Py<T> {
.setattr(attr_name, value.into_py(py).into_bound(py))
}
/// Deprecated form of [`call_bound`][Py::call_bound].
#[cfg_attr(
not(feature = "gil-refs"),
deprecated(
since = "0.21.0",
note = "`call` will be replaced by `call_bound` in a future PyO3 version"
)
)]
#[inline]
pub fn call<A>(&self, py: Python<'_>, args: A, kwargs: Option<&PyDict>) -> PyResult<PyObject>
where
A: IntoPy<Py<PyTuple>>,
{
self.call_bound(py, args, kwargs.map(PyDict::as_borrowed).as_deref())
}
/// Calls the object.
///
/// This is equivalent to the Python expression `self(*args, **kwargs)`.
pub fn call(
pub fn call_bound(
&self,
py: Python<'_>,
args: impl IntoPy<Py<PyTuple>>,
kwargs: Option<&PyDict>,
kwargs: Option<&Bound<'_, PyDict>>,
) -> PyResult<PyObject> {
self.bind(py).as_any().call(args, kwargs).map(Bound::unbind)
}
@ -1042,12 +1058,15 @@ impl<T> Py<T> {
self.bind(py).as_any().call0().map(Bound::unbind)
}
/// Calls a method on the object.
///
/// This is equivalent to the Python expression `self.name(*args, **kwargs)`.
///
/// To avoid repeated temporary allocations of Python strings, the [`intern!`](crate::intern)
/// macro can be used to intern `name`.
/// Deprecated form of [`call_method_bound`][Py::call_method_bound].
#[cfg_attr(
not(feature = "gil-refs"),
deprecated(
since = "0.21.0",
note = "`call_method` will be replaced by `call_method_bound` in a future PyO3 version"
)
)]
#[inline]
pub fn call_method<N, A>(
&self,
py: Python<'_>,
@ -1055,6 +1074,26 @@ impl<T> Py<T> {
args: A,
kwargs: Option<&PyDict>,
) -> PyResult<PyObject>
where
N: IntoPy<Py<PyString>>,
A: IntoPy<Py<PyTuple>>,
{
self.call_method_bound(py, name, args, kwargs.map(PyDict::as_borrowed).as_deref())
}
/// Calls a method on the object.
///
/// This is equivalent to the Python expression `self.name(*args, **kwargs)`.
///
/// To avoid repeated temporary allocations of Python strings, the [`intern!`](crate::intern)
/// macro can be used to intern `name`.
pub fn call_method_bound<N, A>(
&self,
py: Python<'_>,
name: N,
args: A,
kwargs: Option<&Bound<'_, PyDict>>,
) -> PyResult<PyObject>
where
N: IntoPy<Py<PyString>>,
A: IntoPy<Py<PyTuple>>,
@ -1490,23 +1529,34 @@ impl PyObject {
}
#[cfg(test)]
#[cfg_attr(not(feature = "gil-refs"), allow(deprecated))]
mod tests {
use super::{Bound, Py, PyObject};
use crate::types::{PyDict, PyString};
use crate::types::{dict::IntoPyDict, PyDict, PyString};
use crate::{PyAny, PyNativeType, PyResult, Python, ToPyObject};
#[test]
fn test_call0() {
fn test_call() {
Python::with_gil(|py| {
let obj = py.get_type::<PyDict>().to_object(py);
assert_eq!(
obj.call0(py)
let assert_repr = |obj: &PyAny, expected: &str| {
assert_eq!(obj.repr().unwrap().to_str().unwrap(), expected);
};
assert_repr(obj.call0(py).unwrap().as_ref(py), "{}");
assert_repr(obj.call1(py, ()).unwrap().as_ref(py), "{}");
assert_repr(obj.call(py, (), None).unwrap().as_ref(py), "{}");
assert_repr(
obj.call1(py, ((('x', 1),),)).unwrap().as_ref(py),
"{'x': 1}",
);
assert_repr(
obj.call(py, (), Some([('x', 1)].into_py_dict(py)))
.unwrap()
.as_ref(py)
.repr()
.unwrap()
.to_string_lossy(),
"{}"
.as_ref(py),
"{'x': 1}",
);
})
}

View file

@ -447,7 +447,7 @@ impl PyAny {
kwargs: Option<&PyDict>,
) -> PyResult<&PyAny> {
self.as_borrowed()
.call(args, kwargs)
.call(args, kwargs.map(PyDict::as_borrowed).as_deref())
.map(Bound::into_gil_ref)
}
@ -547,7 +547,7 @@ impl PyAny {
A: IntoPy<Py<PyTuple>>,
{
self.as_borrowed()
.call_method(name, args, kwargs)
.call_method(name, args, kwargs.map(PyDict::as_borrowed).as_deref())
.map(Bound::into_gil_ref)
}
@ -1322,7 +1322,7 @@ pub trait PyAnyMethods<'py> {
fn call(
&self,
args: impl IntoPy<Py<PyTuple>>,
kwargs: Option<&PyDict>,
kwargs: Option<&Bound<'_, PyDict>>,
) -> PyResult<Bound<'py, PyAny>>;
/// Calls the object without arguments.
@ -1415,7 +1415,7 @@ pub trait PyAnyMethods<'py> {
&self,
name: N,
args: A,
kwargs: Option<&PyDict>,
kwargs: Option<&Bound<'_, PyDict>>,
) -> PyResult<Bound<'py, PyAny>>
where
N: IntoPy<Py<PyString>>,
@ -1970,12 +1970,12 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> {
fn call(
&self,
args: impl IntoPy<Py<PyTuple>>,
kwargs: Option<&PyDict>,
kwargs: Option<&Bound<'_, PyDict>>,
) -> PyResult<Bound<'py, PyAny>> {
fn inner<'py>(
any: &Bound<'py, PyAny>,
args: Bound<'_, PyTuple>,
kwargs: Option<&PyDict>,
kwargs: Option<&Bound<'_, PyDict>>,
) -> PyResult<Bound<'py, PyAny>> {
unsafe {
ffi::PyObject_Call(
@ -2015,7 +2015,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> {
&self,
name: N,
args: A,
kwargs: Option<&PyDict>,
kwargs: Option<&Bound<'_, PyDict>>,
) -> PyResult<Bound<'py, PyAny>>
where
N: IntoPy<Py<PyString>>,
@ -2292,6 +2292,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> {
}
#[cfg(test)]
#[cfg_attr(not(feature = "gil-refs"), allow(deprecated))]
mod tests {
use crate::{
basic::CompareOp,

View file

@ -6,8 +6,8 @@ error[E0277]: the trait bound `PyErr: From<MyError>` is not satisfied
|
= help: the following other types implement trait `From<T>`:
<PyErr as From<PyBorrowError>>
<PyErr as From<PyBorrowMutError>>
<PyErr as From<std::io::Error>>
<PyErr as From<PyBorrowMutError>>
<PyErr as From<PyDowncastError<'a>>>
<PyErr as From<DowncastError<'_, '_>>>
<PyErr as From<DowncastIntoError<'_>>>