diff --git a/newsfragments/3610.added.md b/newsfragments/3610.added.md new file mode 100644 index 00000000000..3b1493c29c0 --- /dev/null +++ b/newsfragments/3610.added.md @@ -0,0 +1 @@ +Add `#[pyo3(allow_threads)]` to release the GIL in (async) functions \ No newline at end of file diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index e91b3b8d9a2..000ec4b5979 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -9,6 +9,7 @@ use syn::{ }; pub mod kw { + syn::custom_keyword!(allow_threads); syn::custom_keyword!(annotation); syn::custom_keyword!(attribute); syn::custom_keyword!(cancel_handle); diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 982cf62946e..49448c017f1 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -6,6 +6,7 @@ use syn::{ext::IdentExt, spanned::Spanned, Ident, Result}; use crate::utils::Ctx; use crate::{ + attributes, attributes::{FromPyWithAttribute, TextSignatureAttribute, TextSignatureAttributeValue}, deprecations::{Deprecation, Deprecations}, params::{impl_arg_params, Holders}, @@ -379,6 +380,7 @@ pub struct FnSpec<'a> { pub asyncness: Option, pub unsafety: Option, pub deprecations: Deprecations<'a>, + pub allow_threads: Option, } pub fn parse_method_receiver(arg: &syn::FnArg) -> Result { @@ -416,6 +418,7 @@ impl<'a> FnSpec<'a> { text_signature, name, signature, + allow_threads, .. } = options; @@ -461,6 +464,7 @@ impl<'a> FnSpec<'a> { asyncness: sig.asyncness, unsafety: sig.unsafety, deprecations, + allow_threads, }) } @@ -603,6 +607,21 @@ impl<'a> FnSpec<'a> { bail_spanned!(name.span() => "`cancel_handle` may only be specified once"); } } + if let Some(FnArg::Py(py_arg)) = self + .signature + .arguments + .iter() + .find(|arg| matches!(arg, FnArg::Py(_))) + { + ensure_spanned!( + self.asyncness.is_none(), + py_arg.ty.span() => "GIL token cannot be passed to async function" + ); + ensure_spanned!( + self.allow_threads.is_none(), + py_arg.ty.span() => "GIL cannot be held in function annotated with `allow_threads`" + ); + } if self.asyncness.is_some() { ensure_spanned!( @@ -612,8 +631,21 @@ impl<'a> FnSpec<'a> { } let rust_call = |args: Vec, holders: &mut Holders| { - let mut self_arg = || self.tp.self_arg(cls, ExtractErrorMode::Raise, holders, ctx); - + let allow_threads = self.allow_threads.is_some(); + let mut self_arg = || { + let self_arg = self.tp.self_arg(cls, ExtractErrorMode::Raise, holders, ctx); + if self_arg.is_empty() { + self_arg + } else { + let self_checker = holders.push_gil_refs_checker(self_arg.span()); + quote! { + #pyo3_path::impl_::deprecations::inspect_type(#self_arg &#self_checker), + } + } + }; + let arg_names = (0..args.len()) + .map(|i| format_ident!("arg_{}", i)) + .collect::>(); let call = if self.asyncness.is_some() { let throw_callback = if cancel_handle.is_some() { quote! { Some(__throw_callback) } @@ -625,9 +657,6 @@ impl<'a> FnSpec<'a> { Some(cls) => quote!(Some(<#cls as #pyo3_path::PyTypeInfo>::NAME)), None => quote!(None), }; - let arg_names = (0..args.len()) - .map(|i| format_ident!("arg_{}", i)) - .collect::>(); let future = match self.tp { FnType::Fn(SelfType::Receiver { mutable: false, .. }) => { quote! {{ @@ -645,18 +674,7 @@ impl<'a> FnSpec<'a> { } _ => { let self_arg = self_arg(); - if self_arg.is_empty() { - quote! { function(#(#args),*) } - } else { - let self_checker = holders.push_gil_refs_checker(self_arg.span()); - quote! { - function( - // NB #self_arg includes a comma, so none inserted here - #pyo3_path::impl_::deprecations::inspect_type(#self_arg &#self_checker), - #(#args),* - ) - } - } + quote!(function(#self_arg #(#args),*)) } }; let mut call = quote! {{ @@ -665,6 +683,7 @@ impl<'a> FnSpec<'a> { #pyo3_path::intern!(py, stringify!(#python_name)), #qualname_prefix, #throw_callback, + #allow_threads, async move { #pyo3_path::impl_::wrap::OkWrap::wrap(future.await) }, ) }}; @@ -676,20 +695,21 @@ impl<'a> FnSpec<'a> { }}; } call - } else { + } else if allow_threads { let self_arg = self_arg(); - if self_arg.is_empty() { - quote! { function(#(#args),*) } + let (self_arg_name, self_arg_decl) = if self_arg.is_empty() { + (quote!(), quote!()) } else { - let self_checker = holders.push_gil_refs_checker(self_arg.span()); - quote! { - function( - // NB #self_arg includes a comma, so none inserted here - #pyo3_path::impl_::deprecations::inspect_type(#self_arg &#self_checker), - #(#args),* - ) - } - } + (quote!(__self,), quote! { let (__self,) = (#self_arg); }) + }; + quote! {{ + #self_arg_decl + #(let #arg_names = #args;)* + py.allow_threads(|| function(#self_arg_name #(#arg_names),*)) + }} + } else { + let self_arg = self_arg(); + quote!(function(#self_arg #(#args),*)) }; quotes::map_result_into_ptr(quotes::ok_wrap(call, ctx), ctx) }; diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index d9c84655b42..8ee1821e954 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1174,6 +1174,7 @@ fn complex_enum_struct_variant_new<'a>( asyncness: None, unsafety: None, deprecations: Deprecations::new(ctx), + allow_threads: None, }; crate::pymethod::impl_py_method_def_new(&variant_cls_type, &spec, ctx) @@ -1199,6 +1200,7 @@ fn complex_enum_variant_field_getter<'a>( asyncness: None, unsafety: None, deprecations: Deprecations::new(ctx), + allow_threads: None, }; let property_type = crate::pymethod::PropertyType::Function { diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index 7c355533b83..f65a0597b58 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -91,6 +91,7 @@ pub struct PyFunctionOptions { pub signature: Option, pub text_signature: Option, pub krate: Option, + pub allow_threads: Option, } impl Parse for PyFunctionOptions { @@ -99,7 +100,8 @@ impl Parse for PyFunctionOptions { while !input.is_empty() { let lookahead = input.lookahead1(); - if lookahead.peek(attributes::kw::name) + if lookahead.peek(attributes::kw::allow_threads) + || lookahead.peek(attributes::kw::name) || lookahead.peek(attributes::kw::pass_module) || lookahead.peek(attributes::kw::signature) || lookahead.peek(attributes::kw::text_signature) @@ -121,6 +123,7 @@ impl Parse for PyFunctionOptions { } pub enum PyFunctionOption { + AllowThreads(attributes::kw::allow_threads), Name(NameAttribute), PassModule(attributes::kw::pass_module), Signature(SignatureAttribute), @@ -131,7 +134,9 @@ pub enum PyFunctionOption { impl Parse for PyFunctionOption { fn parse(input: ParseStream<'_>) -> Result { let lookahead = input.lookahead1(); - if lookahead.peek(attributes::kw::name) { + if lookahead.peek(attributes::kw::allow_threads) { + input.parse().map(PyFunctionOption::AllowThreads) + } else if lookahead.peek(attributes::kw::name) { input.parse().map(PyFunctionOption::Name) } else if lookahead.peek(attributes::kw::pass_module) { input.parse().map(PyFunctionOption::PassModule) @@ -171,6 +176,7 @@ impl PyFunctionOptions { } for attr in attrs { match attr { + PyFunctionOption::AllowThreads(allow_threads) => set_option!(allow_threads), PyFunctionOption::Name(name) => set_option!(name), PyFunctionOption::PassModule(pass_module) => set_option!(pass_module), PyFunctionOption::Signature(signature) => set_option!(signature), @@ -198,6 +204,7 @@ pub fn impl_wrap_pyfunction( ) -> syn::Result { check_generic(&func.sig)?; let PyFunctionOptions { + allow_threads, pass_module, name, signature, @@ -247,6 +254,7 @@ pub fn impl_wrap_pyfunction( python_name, signature, text_signature, + allow_threads, asyncness: func.sig.asyncness, unsafety: func.sig.unsafety, deprecations: Deprecations::new(ctx), diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index 64756a1c73b..65456334ac1 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -121,6 +121,7 @@ pub fn pymethods(attr: TokenStream, input: TokenStream) -> TokenStream { /// | `#[pyo3(name = "...")]` | Defines the name of the function in Python. | /// | `#[pyo3(text_signature = "...")]` | Defines the `__text_signature__` attribute of the function in Python. | /// | `#[pyo3(pass_module)]` | Passes the module containing the function as a `&PyModule` first argument to the function. | +/// | `#[pyo3(allow_threads)]` | Release the GIL in the function body, or each time the returned future is polled for `async fn` | /// /// For more on exposing functions see the [function section of the guide][1]. /// diff --git a/src/coroutine.rs b/src/coroutine.rs index f2feab4af16..a31d864e826 100644 --- a/src/coroutine.rs +++ b/src/coroutine.rs @@ -21,17 +21,54 @@ use crate::{ pub(crate) mod cancel; mod waker; +use crate::marker::Ungil; pub use cancel::CancelHandle; const COROUTINE_REUSED_ERROR: &str = "cannot reuse already awaited coroutine"; +trait CoroutineFuture: Send { + fn poll(self: Pin<&mut Self>, py: Python<'_>, waker: &Waker) -> Poll>; +} + +impl CoroutineFuture for F +where + F: Future> + Send, + T: IntoPy + Send, + E: Into + Send, +{ + fn poll(self: Pin<&mut Self>, py: Python<'_>, waker: &Waker) -> Poll> { + self.poll(&mut Context::from_waker(waker)) + .map_ok(|obj| obj.into_py(py)) + .map_err(Into::into) + } +} + +struct AllowThreads { + future: F, +} + +impl CoroutineFuture for AllowThreads +where + F: Future> + Send + Ungil, + T: IntoPy + Send + Ungil, + E: Into + Send + Ungil, +{ + fn poll(self: Pin<&mut Self>, py: Python<'_>, waker: &Waker) -> Poll> { + // SAFETY: future field is pinned when self is + let future = unsafe { self.map_unchecked_mut(|a| &mut a.future) }; + py.allow_threads(|| future.poll(&mut Context::from_waker(waker))) + .map_ok(|obj| obj.into_py(py)) + .map_err(Into::into) + } +} + /// Python coroutine wrapping a [`Future`]. #[pyclass(crate = "crate")] pub struct Coroutine { name: Option>, qualname_prefix: Option<&'static str>, throw_callback: Option, - future: Option> + Send>>>, + future: Option>>, waker: Option>, } @@ -46,23 +83,23 @@ impl Coroutine { name: Option>, qualname_prefix: Option<&'static str>, throw_callback: Option, + allow_threads: bool, future: F, ) -> Self where - F: Future> + Send + 'static, - T: IntoPy, - E: Into, + F: Future> + Send + Ungil + 'static, + T: IntoPy + Send + Ungil, + E: Into + Send + Ungil, { - let wrap = async move { - let obj = future.await.map_err(Into::into)?; - // SAFETY: GIL is acquired when future is polled (see `Coroutine::poll`) - Ok(obj.into_py(unsafe { Python::assume_gil_acquired() })) - }; Self { name, qualname_prefix, throw_callback, - future: Some(Box::pin(wrap)), + future: Some(if allow_threads { + Box::pin(AllowThreads { future }) + } else { + Box::pin(future) + }), waker: None, } } @@ -88,10 +125,10 @@ impl Coroutine { } else { self.waker = Some(Arc::new(AsyncioWaker::new())); } - let waker = Waker::from(self.waker.clone().unwrap()); - // poll the Rust future and forward its results if ready + // poll the future and forward its results if ready // polling is UnwindSafe because the future is dropped in case of panic - let poll = || future_rs.as_mut().poll(&mut Context::from_waker(&waker)); + let waker = Waker::from(self.waker.clone().unwrap()); + let poll = || future_rs.as_mut().poll(py, &waker); match panic::catch_unwind(panic::AssertUnwindSafe(poll)) { Ok(Poll::Ready(res)) => { self.close(); diff --git a/src/coroutine/cancel.rs b/src/coroutine/cancel.rs index 2b10fb9a438..de0943c9ede 100644 --- a/src/coroutine/cancel.rs +++ b/src/coroutine/cancel.rs @@ -1,9 +1,13 @@ -use crate::{Py, PyAny, PyObject}; +use std::{ + future::Future, + pin::Pin, + sync::Arc, + task::{Context, Poll, Waker}, +}; + use parking_lot::Mutex; -use std::future::Future; -use std::pin::Pin; -use std::sync::Arc; -use std::task::{Context, Poll, Waker}; + +use crate::PyObject; #[derive(Debug, Default)] struct Inner { @@ -45,6 +49,15 @@ impl CancelHandle { /// Retrieve the exception thrown in the associated coroutine. pub async fn cancelled(&mut self) -> PyObject { + // TODO use `std::future::poll_fn` with MSRV 1.64+ + struct Cancelled<'a>(&'a mut CancelHandle); + + impl Future for Cancelled<'_> { + type Output = PyObject; + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + self.0.poll_cancelled(cx) + } + } Cancelled(self).await } @@ -54,21 +67,11 @@ impl CancelHandle { } } -// Because `poll_fn` is not available in MSRV -struct Cancelled<'a>(&'a mut CancelHandle); - -impl Future for Cancelled<'_> { - type Output = PyObject; - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - self.0.poll_cancelled(cx) - } -} - #[doc(hidden)] pub struct ThrowCallback(Arc>); impl ThrowCallback { - pub(super) fn throw(&self, exc: Py) { + pub(super) fn throw(&self, exc: PyObject) { let mut inner = self.0.lock(); inner.exception = Some(exc); if let Some(waker) = inner.waker.take() { diff --git a/src/gil.rs b/src/gil.rs index e2f36037755..64c82d50221 100644 --- a/src/gil.rs +++ b/src/gil.rs @@ -1,14 +1,18 @@ //! Interaction with Python's global interpreter lock -use crate::impl_::not_send::{NotSend, NOT_SEND}; -use crate::{ffi, Python}; -use parking_lot::{const_mutex, Mutex, Once}; -use std::cell::Cell; #[cfg(debug_assertions)] use std::cell::RefCell; #[cfg(not(debug_assertions))] use std::cell::UnsafeCell; -use std::{mem, ptr::NonNull}; +use std::{cell::Cell, mem, ptr::NonNull}; + +use parking_lot::{const_mutex, Mutex, Once}; + +use crate::{ + ffi, + impl_::not_send::{NotSend, NOT_SEND}, + Python, +}; static START: Once = Once::new(); @@ -799,9 +803,10 @@ mod tests { #[test] #[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled fn test_clone_without_gil() { - use crate::{Py, PyAny}; use std::{sync::Arc, thread}; + use crate::{Py, PyAny}; + // Some events for synchronizing static GIL_ACQUIRED: Event = Event::new(); static OBJECT_CLONED: Event = Event::new(); @@ -864,9 +869,10 @@ mod tests { #[test] #[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled fn test_clone_in_other_thread() { - use crate::Py; use std::{sync::Arc, thread}; + use crate::Py; + // Some events for synchronizing static OBJECT_CLONED: Event = Event::new(); @@ -939,4 +945,47 @@ mod tests { POOL.update_counts(py); }) } + + #[cfg(feature = "macros")] + #[test] + fn allow_threads_fn() { + #[crate::pyfunction(allow_threads, crate = "crate")] + fn without_gil(_arg1: PyObject, _arg2: PyObject) { + GIL_COUNT.with(|c| assert_eq!(c.get(), 0)); + } + Python::with_gil(|gil| { + let without_gil = crate::wrap_pyfunction_bound!(without_gil, gil).unwrap(); + crate::py_run!(gil, without_gil, "without_gil(..., ...)"); + }) + } + + #[cfg(feature = "experimental-async")] + #[cfg(not(target_arch = "wasm32"))] + #[test] + fn allow_threads_async_fn() { + #[crate::pyfunction(allow_threads, crate = "crate")] + async fn without_gil(_arg1: PyObject, _arg2: PyObject) { + use std::task::Poll; + GIL_COUNT.with(|c| assert_eq!(c.get(), 0)); + let mut ready = false; + futures::future::poll_fn(|cx| { + if ready { + return Poll::Ready(()); + } + ready = true; + cx.waker().wake_by_ref(); + Poll::Pending + }) + .await; + GIL_COUNT.with(|c| assert_eq!(c.get(), 0)); + } + Python::with_gil(|gil| { + let without_gil = crate::wrap_pyfunction_bound!(without_gil, gil).unwrap(); + crate::py_run!( + gil, + without_gil, + "import asyncio; asyncio.run(without_gil(..., ...))" + ); + }) + } } diff --git a/src/impl_/coroutine.rs b/src/impl_/coroutine.rs index 1d3119400a0..da2066553df 100644 --- a/src/impl_/coroutine.rs +++ b/src/impl_/coroutine.rs @@ -6,6 +6,7 @@ use std::{ use crate::{ coroutine::{cancel::ThrowCallback, Coroutine}, instance::Bound, + marker::Ungil, pycell::impl_::PyClassBorrowChecker, pyclass::boolean_struct::False, types::{PyAnyMethods, PyString}, @@ -16,17 +17,19 @@ pub fn new_coroutine( name: &Bound<'_, PyString>, qualname_prefix: Option<&'static str>, throw_callback: Option, + allow_threads: bool, future: F, ) -> Coroutine where - F: Future> + Send + 'static, - T: IntoPy, - E: Into, + F: Future> + Send + Ungil + 'static, + T: IntoPy + Send + Ungil, + E: Into + Send + Ungil, { Coroutine::new( Some(name.clone().into()), qualname_prefix, throw_callback, + allow_threads, future, ) } diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 44049620598..48069a59d75 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -52,6 +52,6 @@ fn test_compile_errors() { t.compile_fail("tests/ui/invalid_pymodule_two_pymodule_init.rs"); #[cfg(feature = "experimental-async")] #[cfg(any(not(Py_LIMITED_API), Py_3_10))] // to avoid PyFunctionArgument for &str - t.compile_fail("tests/ui/invalid_cancel_handle.rs"); + t.compile_fail("tests/ui/invalid_async_pyfunction.rs"); t.pass("tests/ui/pymodule_missing_docs.rs"); } diff --git a/tests/test_methods.rs b/tests/test_methods.rs index 2b5396e9ee4..f343dd3eb98 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -1158,3 +1158,23 @@ fn test_issue_2988() { ) { } } + +#[pyclass] +struct NoGILCounter(usize); + +#[pymethods] +impl NoGILCounter { + #[pyo3(allow_threads, signature = (other = 1))] + fn inc_no_gil(&mut self, other: usize) -> usize { + self.0 += other; + self.0 + } +} + +#[test] +fn test_method_allow_threads() { + Python::with_gil(|py| { + let counter = Py::new(py, NoGILCounter(42)).unwrap(); + py_run!(py, counter, "assert counter.inc_no_gil() == 43") + }) +} diff --git a/tests/ui/invalid_cancel_handle.rs b/tests/ui/invalid_async_pyfunction.rs similarity index 60% rename from tests/ui/invalid_cancel_handle.rs rename to tests/ui/invalid_async_pyfunction.rs index cff6c5dcbad..d39c017c0e7 100644 --- a/tests/ui/invalid_cancel_handle.rs +++ b/tests/ui/invalid_async_pyfunction.rs @@ -1,20 +1,26 @@ use pyo3::prelude::*; +#[pyfunction(allow_threads)] +async fn async_with_gil(_py: Python<'_>) {} + +#[pyfunction(allow_threads)] +async fn async_with_bound(_obj: &Bound<'_, PyAny>) {} + #[pyfunction] -async fn cancel_handle_repeated(#[pyo3(cancel_handle, cancel_handle)] _param: String) {} +async fn cancel_handle_repeated(#[pyo3(cancel_handle, cancel_handle)] _param: i32) {} #[pyfunction] async fn cancel_handle_repeated2( - #[pyo3(cancel_handle)] _param: String, - #[pyo3(cancel_handle)] _param2: String, + #[pyo3(cancel_handle)] _param: i32, + #[pyo3(cancel_handle)] _param2: i32, ) { } #[pyfunction] -fn cancel_handle_synchronous(#[pyo3(cancel_handle)] _param: String) {} +fn cancel_handle_synchronous(#[pyo3(cancel_handle)] _param: i32) {} #[pyfunction] -async fn cancel_handle_wrong_type(#[pyo3(cancel_handle)] _param: String) {} +async fn cancel_handle_wrong_type(#[pyo3(cancel_handle)] _param: i32) {} #[pyfunction] async fn missing_cancel_handle_attribute(_param: pyo3::coroutine::CancelHandle) {} diff --git a/tests/ui/invalid_async_pyfunction.stderr b/tests/ui/invalid_async_pyfunction.stderr new file mode 100644 index 00000000000..bec522cdeba --- /dev/null +++ b/tests/ui/invalid_async_pyfunction.stderr @@ -0,0 +1,141 @@ +error: GIL token cannot be passed to async function + --> tests/ui/invalid_async_pyfunction.rs:4:30 + | +4 | async fn async_with_gil(_py: Python<'_>) {} + | ^^^^^^ + +error: `cancel_handle` may only be specified once per argument + --> tests/ui/invalid_async_pyfunction.rs:10:55 + | +10 | async fn cancel_handle_repeated(#[pyo3(cancel_handle, cancel_handle)] _param: i32) {} + | ^^^^^^^^^^^^^ + +error: `cancel_handle` may only be specified once + --> tests/ui/invalid_async_pyfunction.rs:15:28 + | +15 | #[pyo3(cancel_handle)] _param2: i32, + | ^^^^^^^ + +error: `cancel_handle` attribute can only be used with `async fn` + --> tests/ui/invalid_async_pyfunction.rs:20:53 + | +20 | fn cancel_handle_synchronous(#[pyo3(cancel_handle)] _param: i32) {} + | ^^^^^^ + +error: `from_py_with` and `cancel_handle` cannot be specified together + --> tests/ui/invalid_async_pyfunction.rs:30:12 + | +30 | #[pyo3(cancel_handle, from_py_with = "cancel_handle")] _param: pyo3::coroutine::CancelHandle, + | ^^^^^^^^^^^^^ + +error[E0277]: `*mut pyo3::Python<'static>` cannot be shared between threads safely + --> tests/ui/invalid_async_pyfunction.rs:6:1 + | +6 | #[pyfunction(allow_threads)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `*mut pyo3::Python<'static>` cannot be shared between threads safely + | + = help: within `pyo3::Bound<'_, pyo3::PyAny>`, the trait `Sync` is not implemented for `*mut pyo3::Python<'static>`, which is required by `{async block@$DIR/tests/ui/invalid_async_pyfunction.rs:6:1: 6:29}: Send` +note: required because it appears within the type `PhantomData<*mut pyo3::Python<'static>>` + --> $RUST/core/src/marker.rs + | + | pub struct PhantomData; + | ^^^^^^^^^^^ +note: required because it appears within the type `impl_::not_send::NotSend` + --> src/impl_/not_send.rs + | + | pub(crate) struct NotSend(PhantomData<*mut Python<'static>>); + | ^^^^^^^ + = note: required because it appears within the type `(&pyo3::gil::GILGuard, impl_::not_send::NotSend)` +note: required because it appears within the type `PhantomData<(&pyo3::gil::GILGuard, impl_::not_send::NotSend)>` + --> $RUST/core/src/marker.rs + | + | pub struct PhantomData; + | ^^^^^^^^^^^ +note: required because it appears within the type `pyo3::Python<'_>` + --> src/marker.rs + | + | pub struct Python<'py>(PhantomData<(&'py GILGuard, NotSend)>); + | ^^^^^^ +note: required because it appears within the type `pyo3::Bound<'_, pyo3::PyAny>` + --> src/instance.rs + | + | pub struct Bound<'py, T>(Python<'py>, ManuallyDrop>); + | ^^^^^ + = note: required for `&pyo3::Bound<'_, pyo3::PyAny>` to implement `Send` +note: required because it's used within this `async` fn body + --> tests/ui/invalid_async_pyfunction.rs:7:52 + | +7 | async fn async_with_bound(_obj: &Bound<'_, PyAny>) {} + | ^^ +note: required because it's used within this `async` block + --> tests/ui/invalid_async_pyfunction.rs:6:1 + | +6 | #[pyfunction(allow_threads)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +note: required by a bound in `new_coroutine` + --> src/impl_/coroutine.rs + | + | pub fn new_coroutine( + | ------------- required by a bound in this function +... + | F: Future> + Send + Ungil + 'static, + | ^^^^ required by this bound in `new_coroutine` + = note: this error originates in the attribute macro `pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0308]: mismatched types + --> tests/ui/invalid_async_pyfunction.rs:22:1 + | +22 | #[pyfunction] + | ^^^^^^^^^^^^^ + | | + | expected `i32`, found `CancelHandle` + | arguments to this function are incorrect + | +note: function defined here + --> tests/ui/invalid_async_pyfunction.rs:23:10 + | +23 | async fn cancel_handle_wrong_type(#[pyo3(cancel_handle)] _param: i32) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^ ----------- + = note: this error originates in the attribute macro `pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `CancelHandle: PyClass` is not satisfied + --> tests/ui/invalid_async_pyfunction.rs:26:50 + | +26 | async fn missing_cancel_handle_attribute(_param: pyo3::coroutine::CancelHandle) {} + | ^^^^ the trait `PyClass` is not implemented for `CancelHandle`, which is required by `CancelHandle: PyFunctionArgument<'_, '_>` + | + = help: the trait `PyClass` is implemented for `pyo3::coroutine::Coroutine` + = note: required for `CancelHandle` to implement `FromPyObject<'_>` + = note: required for `CancelHandle` to implement `FromPyObjectBound<'_, '_>` + = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_>` +note: required by a bound in `extract_argument` + --> src/impl_/extract_argument.rs + | + | pub fn extract_argument<'a, 'py, T>( + | ---------------- required by a bound in this function +... + | T: PyFunctionArgument<'a, 'py>, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` + +error[E0277]: the trait bound `CancelHandle: Clone` is not satisfied + --> tests/ui/invalid_async_pyfunction.rs:26:50 + | +26 | async fn missing_cancel_handle_attribute(_param: pyo3::coroutine::CancelHandle) {} + | ^^^^ the trait `Clone` is not implemented for `CancelHandle`, which is required by `CancelHandle: PyFunctionArgument<'_, '_>` + | + = help: the following other types implement trait `PyFunctionArgument<'a, 'py>`: + Option<&'a pyo3::Bound<'py, T>> + &'a pyo3::Bound<'py, T> + &'a pyo3::coroutine::Coroutine + &'a mut pyo3::coroutine::Coroutine + = note: required for `CancelHandle` to implement `FromPyObject<'_>` + = note: required for `CancelHandle` to implement `FromPyObjectBound<'_, '_>` + = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_>` +note: required by a bound in `extract_argument` + --> src/impl_/extract_argument.rs + | + | pub fn extract_argument<'a, 'py, T>( + | ---------------- required by a bound in this function +... + | T: PyFunctionArgument<'a, 'py>, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` diff --git a/tests/ui/invalid_cancel_handle.stderr b/tests/ui/invalid_cancel_handle.stderr index 41a2c0854b7..e69de29bb2d 100644 --- a/tests/ui/invalid_cancel_handle.stderr +++ b/tests/ui/invalid_cancel_handle.stderr @@ -1,81 +0,0 @@ -error: `cancel_handle` may only be specified once per argument - --> tests/ui/invalid_cancel_handle.rs:4:55 - | -4 | async fn cancel_handle_repeated(#[pyo3(cancel_handle, cancel_handle)] _param: String) {} - | ^^^^^^^^^^^^^ - -error: `cancel_handle` may only be specified once - --> tests/ui/invalid_cancel_handle.rs:9:28 - | -9 | #[pyo3(cancel_handle)] _param2: String, - | ^^^^^^^ - -error: `cancel_handle` attribute can only be used with `async fn` - --> tests/ui/invalid_cancel_handle.rs:14:53 - | -14 | fn cancel_handle_synchronous(#[pyo3(cancel_handle)] _param: String) {} - | ^^^^^^ - -error: `from_py_with` and `cancel_handle` cannot be specified together - --> tests/ui/invalid_cancel_handle.rs:24:12 - | -24 | #[pyo3(cancel_handle, from_py_with = "cancel_handle")] _param: pyo3::coroutine::CancelHandle, - | ^^^^^^^^^^^^^ - -error[E0308]: mismatched types - --> tests/ui/invalid_cancel_handle.rs:16:1 - | -16 | #[pyfunction] - | ^^^^^^^^^^^^^ - | | - | expected `String`, found `CancelHandle` - | arguments to this function are incorrect - | -note: function defined here - --> tests/ui/invalid_cancel_handle.rs:17:10 - | -17 | async fn cancel_handle_wrong_type(#[pyo3(cancel_handle)] _param: String) {} - | ^^^^^^^^^^^^^^^^^^^^^^^^ -------------- - = note: this error originates in the attribute macro `pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) - -error[E0277]: the trait bound `CancelHandle: PyClass` is not satisfied - --> tests/ui/invalid_cancel_handle.rs:20:50 - | -20 | async fn missing_cancel_handle_attribute(_param: pyo3::coroutine::CancelHandle) {} - | ^^^^ the trait `PyClass` is not implemented for `CancelHandle`, which is required by `CancelHandle: PyFunctionArgument<'_, '_>` - | - = help: the trait `PyClass` is implemented for `pyo3::coroutine::Coroutine` - = note: required for `CancelHandle` to implement `FromPyObject<'_>` - = note: required for `CancelHandle` to implement `FromPyObjectBound<'_, '_>` - = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_>` -note: required by a bound in `extract_argument` - --> src/impl_/extract_argument.rs - | - | pub fn extract_argument<'a, 'py, T>( - | ---------------- required by a bound in this function -... - | T: PyFunctionArgument<'a, 'py>, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` - -error[E0277]: the trait bound `CancelHandle: Clone` is not satisfied - --> tests/ui/invalid_cancel_handle.rs:20:50 - | -20 | async fn missing_cancel_handle_attribute(_param: pyo3::coroutine::CancelHandle) {} - | ^^^^ the trait `Clone` is not implemented for `CancelHandle`, which is required by `CancelHandle: PyFunctionArgument<'_, '_>` - | - = help: the following other types implement trait `PyFunctionArgument<'a, 'py>`: - Option<&'a pyo3::Bound<'py, T>> - &'a pyo3::Bound<'py, T> - &'a pyo3::coroutine::Coroutine - &'a mut pyo3::coroutine::Coroutine - = note: required for `CancelHandle` to implement `FromPyObject<'_>` - = note: required for `CancelHandle` to implement `FromPyObjectBound<'_, '_>` - = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_>` -note: required by a bound in `extract_argument` - --> src/impl_/extract_argument.rs - | - | pub fn extract_argument<'a, 'py, T>( - | ---------------- required by a bound in this function -... - | T: PyFunctionArgument<'a, 'py>, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` diff --git a/tests/ui/invalid_pyfunction_signatures.stderr b/tests/ui/invalid_pyfunction_signatures.stderr index dbca169d8ea..d48e21fa1ea 100644 --- a/tests/ui/invalid_pyfunction_signatures.stderr +++ b/tests/ui/invalid_pyfunction_signatures.stderr @@ -16,7 +16,7 @@ error: expected argument from function definition `y` but got argument `x` 13 | #[pyo3(signature = (x))] | ^ -error: expected one of: `name`, `pass_module`, `signature`, `text_signature`, `crate` +error: expected one of: `allow_threads`, `name`, `pass_module`, `signature`, `text_signature`, `crate` --> tests/ui/invalid_pyfunction_signatures.rs:18:14 | 18 | #[pyfunction(x)] diff --git a/tests/ui/invalid_pyfunctions.rs b/tests/ui/invalid_pyfunctions.rs index 1a95c9e4a34..4c09ffd5608 100644 --- a/tests/ui/invalid_pyfunctions.rs +++ b/tests/ui/invalid_pyfunctions.rs @@ -35,4 +35,10 @@ fn first_argument_not_module<'a, 'py>( module.name() } +#[pyfunction(allow_threads)] +fn allow_threads_with_gil(_py: Python<'_>) {} + +#[pyfunction(allow_threads)] +fn allow_threads_with_bound(_obj: &Bound<'_, PyAny>) {} + fn main() {} diff --git a/tests/ui/invalid_pyfunctions.stderr b/tests/ui/invalid_pyfunctions.stderr index 893d7cbec2c..af49195e0de 100644 --- a/tests/ui/invalid_pyfunctions.stderr +++ b/tests/ui/invalid_pyfunctions.stderr @@ -47,6 +47,12 @@ error: expected `&PyModule` or `Py` as first argument with `pass_modul 28 | fn pass_module_but_no_arguments<'py>() {} | ^^ +error: GIL cannot be held in function annotated with `allow_threads` + --> tests/ui/invalid_pyfunctions.rs:39:32 + | +39 | fn allow_threads_with_gil(_py: Python<'_>) {} + | ^^^^^^ + error[E0277]: the trait bound `&str: From>` is not satisfied --> tests/ui/invalid_pyfunctions.rs:32:13 | @@ -61,3 +67,54 @@ error[E0277]: the trait bound `&str: From> > = note: required for `BoundRef<'_, '_, pyo3::prelude::PyModule>` to implement `Into<&str>` + +error[E0277]: `*mut pyo3::Python<'static>` cannot be shared between threads safely + --> tests/ui/invalid_pyfunctions.rs:41:1 + | +41 | #[pyfunction(allow_threads)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `*mut pyo3::Python<'static>` cannot be shared between threads safely + | + = help: within `&pyo3::Bound<'_, pyo3::PyAny>`, the trait `Sync` is not implemented for `*mut pyo3::Python<'static>`, which is required by `{closure@$DIR/tests/ui/invalid_pyfunctions.rs:41:1: 41:29}: Ungil` +note: required because it appears within the type `PhantomData<*mut pyo3::Python<'static>>` + --> $RUST/core/src/marker.rs + | + | pub struct PhantomData; + | ^^^^^^^^^^^ +note: required because it appears within the type `impl_::not_send::NotSend` + --> src/impl_/not_send.rs + | + | pub(crate) struct NotSend(PhantomData<*mut Python<'static>>); + | ^^^^^^^ + = note: required because it appears within the type `(&pyo3::gil::GILGuard, impl_::not_send::NotSend)` +note: required because it appears within the type `PhantomData<(&pyo3::gil::GILGuard, impl_::not_send::NotSend)>` + --> $RUST/core/src/marker.rs + | + | pub struct PhantomData; + | ^^^^^^^^^^^ +note: required because it appears within the type `pyo3::Python<'_>` + --> src/marker.rs + | + | pub struct Python<'py>(PhantomData<(&'py GILGuard, NotSend)>); + | ^^^^^^ +note: required because it appears within the type `pyo3::Bound<'_, pyo3::PyAny>` + --> src/instance.rs + | + | pub struct Bound<'py, T>(Python<'py>, ManuallyDrop>); + | ^^^^^ + = note: required because it appears within the type `&pyo3::Bound<'_, pyo3::PyAny>` + = note: required for `&&pyo3::Bound<'_, pyo3::PyAny>` to implement `Send` +note: required because it's used within this closure + --> tests/ui/invalid_pyfunctions.rs:41:1 + | +41 | #[pyfunction(allow_threads)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: required for `{closure@$DIR/tests/ui/invalid_pyfunctions.rs:41:1: 41:29}` to implement `Ungil` +note: required by a bound in `pyo3::Python::<'py>::allow_threads` + --> src/marker.rs + | + | pub fn allow_threads(self, f: F) -> T + | ------------- required by a bound in this associated function + | where + | F: Ungil + FnOnce() -> T, + | ^^^^^ required by this bound in `Python::<'py>::allow_threads` + = note: this error originates in the attribute macro `pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info)