From d0c073014a9d7be669ba8b385253acda6abe8f15 Mon Sep 17 00:00:00 2001 From: Jiangzhou He Date: Thu, 27 Nov 2025 16:46:55 -0800 Subject: [PATCH] chore: upgrade pyo3 to v0.27 --- Cargo.lock | 33 +++++++------- Cargo.toml | 8 ++-- rust/cocoindex/src/builder/flow_builder.rs | 10 ++--- rust/cocoindex/src/ops/py_factory.rs | 50 +++++++++++----------- rust/cocoindex/src/py/convert.rs | 4 +- rust/cocoindex/src/py/mod.rs | 20 ++++----- rust/py_utils/src/convert.rs | 11 +++-- rust/py_utils/src/future.rs | 8 ++-- 8 files changed, 73 insertions(+), 71 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e27ed11e..8d02cd43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3538,9 +3538,9 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "numpy" -version = "0.25.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29f1dee9aa8d3f6f8e8b9af3803006101bb3653866ef056d530d53ae68587191" +checksum = "0fa24ffc88cf9d43f7269d6b6a0d0a00010924a8cc90604a21ef9c433b66998d" dependencies = [ "libc", "ndarray", @@ -4008,9 +4008,9 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.25.1" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8970a78afe0628a3e3430376fc5fd76b6b45c4d43360ffd6cdd40bdde72b682a" +checksum = "37a6df7eab65fc7bee654a421404947e10a0f7085b6951bf2ea395f4659fb0cf" dependencies = [ "chrono", "indoc", @@ -4027,9 +4027,9 @@ dependencies = [ [[package]] name = "pyo3-async-runtimes" -version = "0.25.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d73cc6b1b7d8b3cef02101d37390dbdfe7e450dfea14921cae80a9534ba59ef2" +checksum = "57ddb5b570751e93cc6777e81fee8087e59cd53b5043292f2a6d59d5bd80fdfd" dependencies = [ "futures", "once_cell", @@ -4040,19 +4040,18 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.25.1" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458eb0c55e7ece017adeba38f2248ff3ac615e53660d7c71a238d7d2a01c7598" +checksum = "f77d387774f6f6eec64a004eac0ed525aab7fa1966d94b42f743797b3e395afb" dependencies = [ - "once_cell", "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.25.1" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7114fe5457c61b276ab77c5055f206295b812608083644a5c5b2640c3102565c" +checksum = "2dd13844a4242793e02df3e2ec093f540d948299a6a77ea9ce7afd8623f542be" dependencies = [ "libc", "pyo3-build-config", @@ -4060,9 +4059,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.25.1" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8725c0a622b374d6cb051d11a0983786448f7785336139c3c94f5aa6bef7e50" +checksum = "eaf8f9f1108270b90d3676b8679586385430e5c0bb78bb5f043f95499c821a71" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -4072,9 +4071,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.25.1" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4109984c22491085343c05b0dbc54ddc405c3cf7b4374fc533f5c3313a572ccc" +checksum = "70a3b2274450ba5288bc9b8c1b69ff569d1d61189d4bff38f8d22e03d17f932b" dependencies = [ "heck", "proc-macro2", @@ -4085,9 +4084,9 @@ dependencies = [ [[package]] name = "pythonize" -version = "0.25.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597907139a488b22573158793aa7539df36ae863eba300c75f3a0d65fc475e27" +checksum = "a3a8f29db331e28c332c63496cfcbb822aca3d7320bc08b655d7fd0c29c50ede" dependencies = [ "pyo3", "serde", diff --git a/Cargo.toml b/Cargo.toml index d5d95332..9fec1f59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,14 +9,15 @@ rust-version = "1.89" license = "Apache-2.0" [workspace.dependencies] -pyo3 = { version = "0.25.1", features = [ +pyo3 = { version = "0.27.1", features = [ "abi3-py311", "auto-initialize", "chrono", "uuid", ] } -pythonize = "0.25.0" -pyo3-async-runtimes = { version = "0.25.0", features = ["tokio-runtime"] } +pythonize = "0.27.0" +pyo3-async-runtimes = { version = "0.27.0", features = ["tokio-runtime"] } +numpy = "0.27.0" anyhow = { version = "1.0.100", features = ["std"] } async-trait = "0.1.89" @@ -89,7 +90,6 @@ aws-config = "1.8.11" aws-sdk-s3 = "1.115.0" aws-sdk-sqs = "1.90.0" time = { version = "0.3", features = ["macros", "serde"] } -numpy = "0.25.0" infer = "0.19.0" serde_with = { version = "3.16.0", features = ["base64"] } google-cloud-aiplatform-v1 = { version = "0.4.5", default-features = false, features = [ diff --git a/rust/cocoindex/src/builder/flow_builder.rs b/rust/cocoindex/src/builder/flow_builder.rs index c60c88f4..665e7be0 100644 --- a/rust/cocoindex/src/builder/flow_builder.rs +++ b/rust/cocoindex/src/builder/flow_builder.rs @@ -252,7 +252,7 @@ impl FlowBuilder { #[new] pub fn new(py: Python<'_>, name: &str, py_event_loop: Py) -> PyResult { let lib_context = py - .allow_threads(|| -> anyhow::Result> { + .detach(|| -> anyhow::Result> { get_runtime().block_on(get_lib_context()) }) .into_py_result()?; @@ -331,7 +331,7 @@ impl FlowBuilder { flow_ctx: self.flow_inst_context.clone(), }; let analyzed = py - .allow_threads(|| { + .detach(|| { get_runtime().block_on( analyzer_ctx.analyze_import_op(&self.root_op_scope, import_op.clone()), ) @@ -486,7 +486,7 @@ impl FlowBuilder { flow_ctx: self.flow_inst_context.clone(), }; let analyzed = py - .allow_threads(|| { + .detach(|| { get_runtime().block_on(analyzer_ctx.analyze_reactive_op(op_scope, &reactive_op)) }) .into_py_result()?; @@ -536,7 +536,7 @@ impl FlowBuilder { flow_ctx: self.flow_inst_context.clone(), }; let analyzed = py - .allow_threads(|| { + .detach(|| { get_runtime().block_on(analyzer_ctx.analyze_reactive_op(common_scope, &reactive_op)) }) .into_py_result()?; @@ -645,7 +645,7 @@ impl FlowBuilder { }; let flow_instance_ctx = self.flow_inst_context.clone(); let flow_ctx = py - .allow_threads(|| { + .detach(|| { get_runtime().block_on(async move { let analyzed_flow = super::AnalyzedFlow::from_flow_instance(spec, flow_instance_ctx).await?; diff --git a/rust/cocoindex/src/ops/py_factory.rs b/rust/cocoindex/src/ops/py_factory.rs index dc8804cb..1ae4fa4b 100644 --- a/rust/cocoindex/src/ops/py_factory.rs +++ b/rust/cocoindex/src/ops/py_factory.rs @@ -91,14 +91,14 @@ impl PyFunctionExecutor { impl interface::SimpleFunctionExecutor for Arc { async fn evaluate(&self, input: Vec) -> Result { let self = self.clone(); - let result_fut = Python::with_gil(|py| -> Result<_> { + let result_fut = Python::attach(|py| -> Result<_> { let result_coro = self.call_py_fn(py, input)?; let task_locals = pyo3_async_runtimes::TaskLocals::new(self.py_exec_ctx.event_loop.bind(py).clone()); Ok(from_py_future(py, &task_locals, result_coro)?) })?; let result = result_fut.await; - Python::with_gil(|py| -> Result<_> { + Python::attach(|py| -> Result<_> { let result = result.to_result_with_py_trace(py)?; Ok(py::value_from_py_object( &self.result_type.typ, @@ -129,7 +129,7 @@ struct PyBatchedFunctionExecutor { #[async_trait] impl BatchedFunctionExecutor for PyBatchedFunctionExecutor { async fn evaluate_batch(&self, args: Vec>) -> Result> { - let result_fut = Python::with_gil(|py| -> pyo3::PyResult<_> { + let result_fut = Python::attach(|py| -> pyo3::PyResult<_> { let py_args = PyList::new( py, args.into_iter() @@ -155,7 +155,7 @@ impl BatchedFunctionExecutor for PyBatchedFunctionExecutor { )?) })?; let result = result_fut.await; - Python::with_gil(|py| -> Result<_> { + Python::attach(|py| -> Result<_> { let result = result.to_result_with_py_trace(py)?; let result_bound = result.into_bound(py); let result_list = result_bound.extract::>>()?; @@ -189,7 +189,7 @@ impl interface::SimpleFunctionFactory for PyFunctionFactory { context: Arc, ) -> Result { let (result_type, executor, kw_args_names, num_positional_args, behavior_version) = - Python::with_gil(|py| -> anyhow::Result<_> { + Python::attach(|py| -> anyhow::Result<_> { let mut args = vec![pythonize(py, &spec)?]; let mut kwargs = vec![]; let mut num_positional_args = 0; @@ -246,7 +246,7 @@ impl interface::SimpleFunctionFactory for PyFunctionFactory { .ok_or_else(|| anyhow!("Python execution context is missing"))? .clone(); let (prepare_fut, enable_cache, timeout, batching_options) = - Python::with_gil(|py| -> anyhow::Result<_> { + Python::attach(|py| -> anyhow::Result<_> { let prepare_coro = executor .call_method(py, "prepare", (), None) .to_result_with_py_trace(py) @@ -342,10 +342,10 @@ impl interface::SourceExecutor for PySourceExecutor { options: &interface::SourceExecutorReadOptions, ) -> Result>>> { let py_exec_ctx = self.py_exec_ctx.clone(); - let py_source_executor = Python::with_gil(|py| self.py_source_executor.clone_ref(py)); + let py_source_executor = Python::attach(|py| self.py_source_executor.clone_ref(py)); // Get the Python async iterator - let py_async_iter = Python::with_gil(|py| { + let py_async_iter = Python::attach(|py| { py_source_executor .call_method(py, "list_async", (pythonize(py, options)?,), None) .to_result_with_py_trace(py) @@ -375,10 +375,10 @@ impl interface::SourceExecutor for PySourceExecutor { options: &interface::SourceExecutorReadOptions, ) -> Result { let py_exec_ctx = self.py_exec_ctx.clone(); - let py_source_executor = Python::with_gil(|py| self.py_source_executor.clone_ref(py)); + let py_source_executor = Python::attach(|py| self.py_source_executor.clone_ref(py)); let key_clone = key.clone(); - let py_result = Python::with_gil(|py| -> Result<_> { + let py_result = Python::attach(|py| -> Result<_> { let result_coro = py_source_executor .call_method( py, @@ -406,7 +406,7 @@ impl interface::SourceExecutor for PySourceExecutor { })? .await; - Python::with_gil(|py| -> Result<_> { + Python::attach(|py| -> Result<_> { let result = py_result.to_result_with_py_trace(py)?; let result_bound = result.into_bound(py); let data = self.parse_partial_source_row_data(py, &result_bound)?; @@ -432,7 +432,7 @@ impl PySourceExecutor { py_exec_ctx: &Arc, ) -> Result> { // Call the Python method to get the next item, avoiding storing Python objects across await points - let next_item_coro = Python::with_gil(|py| -> Result<_> { + let next_item_coro = Python::attach(|py| -> Result<_> { let coro = py_async_iter .call_method0(py, "__anext__") .to_result_with_py_trace(py) @@ -446,7 +446,7 @@ impl PySourceExecutor { let py_item_result = next_item_coro.await; // Handle StopAsyncIteration and convert to Rust data immediately to avoid Send issues - Python::with_gil(|py| -> Result> { + Python::attach(|py| -> Result> { match py_item_result { Ok(item) => { let bound_item = item.into_bound(py); @@ -472,7 +472,7 @@ impl PySourceExecutor { ) -> Result { // Each item should be a tuple of (key, data) let tuple = bound_item - .downcast::() + .cast::() .map_err(|e| anyhow!("Failed to downcast to PyTuple: {}", e))?; if tuple.len() != 2 { api_bail!("Expected tuple of length 2 from Python source iterator"); @@ -583,7 +583,7 @@ impl interface::SourceFactory for PySourceConnectorFactory { .clone(); // First get the table type (this doesn't require executor) - let table_type = Python::with_gil(|py| -> Result<_> { + let table_type = Python::attach(|py| -> Result<_> { let value_type_result = self .py_source_connector .call_method(py, "get_table_type", (), None) @@ -625,7 +625,7 @@ impl interface::SourceFactory for PySourceConnectorFactory { let source_name = source_name.to_string(); let executor_fut = async move { // Create the executor using the async create_executor method - let create_future = Python::with_gil(|py| -> Result<_> { + let create_future = Python::attach(|py| -> Result<_> { let create_coro = self .py_source_connector .call_method(py, "create_executor", (pythonize(py, &spec)?,), None) @@ -645,7 +645,7 @@ impl interface::SourceFactory for PySourceConnectorFactory { let py_executor_context_result = create_future.await; let (py_source_executor_context, provides_ordinal) = - Python::with_gil(|py| -> Result<_> { + Python::attach(|py| -> Result<_> { let executor_context = py_executor_context_result .to_result_with_py_trace(py) .with_context(|| { @@ -748,7 +748,7 @@ impl interface::TargetFactory for PyExportTargetFactory { .ok_or_else(|| anyhow!("Python execution context is missing"))? .clone(); for data_collection in data_collections.into_iter() { - let (py_export_ctx, persistent_key, setup_state) = Python::with_gil(|py| { + let (py_export_ctx, persistent_key, setup_state) = Python::attach(|py| { // Deserialize the spec to Python object. let py_export_ctx = self .py_target_connector @@ -805,7 +805,7 @@ impl interface::TargetFactory for PyExportTargetFactory { let py_exec_ctx = py_exec_ctx.clone(); let build_output = interface::ExportDataCollectionBuildOutput { export_context: Box::pin(async move { - Python::with_gil(|py| { + Python::attach(|py| { let prepare_coro = factory .py_target_connector .call_method(py, "prepare_async", (&py_export_ctx,), None) @@ -872,7 +872,7 @@ impl interface::TargetFactory for PyExportTargetFactory { desired_state: &serde_json::Value, existing_state: &serde_json::Value, ) -> Result { - let compatibility = Python::with_gil(|py| -> Result<_> { + let compatibility = Python::attach(|py| -> Result<_> { let result = self .py_target_connector .call_method( @@ -895,7 +895,7 @@ impl interface::TargetFactory for PyExportTargetFactory { } fn describe_resource(&self, key: &serde_json::Value) -> Result { - Python::with_gil(|py| -> Result { + Python::attach(|py| -> Result { let result = self .py_target_connector .call_method(py, "describe_resource", (pythonize(py, key)?,), None) @@ -950,7 +950,7 @@ impl interface::TargetFactory for PyExportTargetFactory { .as_ref() .ok_or_else(|| anyhow!("Python execution context is missing"))? .clone(); - let py_result = Python::with_gil(move |py| -> Result<_> { + let py_result = Python::attach(move |py| -> Result<_> { let result_coro = self .py_target_connector .call_method( @@ -972,7 +972,7 @@ impl interface::TargetFactory for PyExportTargetFactory { )?) })? .await; - Python::with_gil(move |py| { + Python::attach(move |py| { py_result .to_result_with_py_trace(py) .with_context(|| format!("while applying setup changes in user-configured target")) @@ -991,7 +991,7 @@ impl interface::TargetFactory for PyExportTargetFactory { return Ok(()); } - let py_result = Python::with_gil(|py| -> Result<_> { + let py_result = Python::attach(|py| -> Result<_> { // Create a `list[tuple[export_ctx, list[tuple[key, value | None]]]]` for Python, and collect `py_exec_ctx`. let mut py_args = Vec::with_capacity(mutations.len()); let mut py_exec_ctx: Option<&Arc> = None; @@ -1039,7 +1039,7 @@ impl interface::TargetFactory for PyExportTargetFactory { })? .await; - Python::with_gil(move |py| { + Python::attach(move |py| { py_result .to_result_with_py_trace(py) .with_context(|| format!("while applying mutations in user-configured target")) diff --git a/rust/cocoindex/src/py/convert.rs b/rust/cocoindex/src/py/convert.rs index 27f6ead0..122a38da 100644 --- a/rust/cocoindex/src/py/convert.rs +++ b/rust/cocoindex/src/py/convert.rs @@ -193,7 +193,7 @@ fn handle_ndarray_from_py<'py>( ) -> PyResult> { macro_rules! try_convert { ($t:ty, $cast:expr) => { - if let Ok(array) = v.downcast::>() { + if let Ok(array) = v.cast::>() { let data = array.readonly().as_slice()?.to_vec(); let vec = data.into_iter().map($cast).collect::>(); return Ok(Some(value::BasicValue::Vector(Arc::from(vec)))); @@ -357,7 +357,7 @@ mod tests { use std::sync::Arc; fn assert_roundtrip_conversion(original_value: &value::Value, value_type: &schema::ValueType) { - Python::with_gil(|py| { + Python::attach(|py| { // Convert Rust value to Python object using value_to_py_object let py_object = value_to_py_object(py, original_value) .expect("Failed to convert Rust value to Python object"); diff --git a/rust/cocoindex/src/py/mod.rs b/rust/cocoindex/src/py/mod.rs index 18f79389..d4653fe4 100644 --- a/rust/cocoindex/src/py/mod.rs +++ b/rust/cocoindex/src/py/mod.rs @@ -25,7 +25,7 @@ pub(crate) use py_utils::*; #[pyfunction] fn set_settings_fn(get_settings_fn: Py) -> PyResult<()> { let get_settings_closure = move || { - Python::with_gil(|py| { + Python::attach(|py| { let obj = get_settings_fn .bind(py) .call0() @@ -47,7 +47,7 @@ fn init_pyo3_runtime() { #[pyfunction] fn init(py: Python<'_>, settings: Pythonized>) -> PyResult<()> { - py.allow_threads(|| -> anyhow::Result<()> { + py.detach(|| -> anyhow::Result<()> { get_runtime().block_on(async move { init_lib_context(settings.into_inner()).await }) }) .into_py_result() @@ -55,7 +55,7 @@ fn init(py: Python<'_>, settings: Pythonized>) -> PyResult<()> #[pyfunction] fn start_server(py: Python<'_>, settings: Pythonized) -> PyResult<()> { - py.allow_threads(|| -> anyhow::Result<()> { + py.detach(|| -> anyhow::Result<()> { let server = get_runtime().block_on(async move { server::init_server(get_lib_context().await?, settings.into_inner()).await })?; @@ -67,7 +67,7 @@ fn start_server(py: Python<'_>, settings: Pythonized) -> PyResul #[pyfunction] fn stop(py: Python<'_>) -> PyResult<()> { - py.allow_threads(|| get_runtime().block_on(clear_lib_context())); + py.detach(|| get_runtime().block_on(clear_lib_context())); Ok(()) } @@ -216,7 +216,7 @@ impl Flow { py: Python<'_>, options: Pythonized, ) -> PyResult<()> { - py.allow_threads(|| { + py.detach(|| { get_runtime() .block_on(async { let exec_plan = self.0.flow.get_execution_plan().await?; @@ -391,7 +391,7 @@ impl Flow { flow_ctx: &interface::FlowInstanceContext, ) -> Result { // Call the Python async function on the flow's event loop - let result_fut = Python::with_gil(|py| -> Result<_> { + let result_fut = Python::attach(|py| -> Result<_> { let handler = self.handler.clone_ref(py); // Build args: pass a dict with the query input let args = pyo3::types::PyTuple::new(py, [input.query])?; @@ -413,7 +413,7 @@ impl Flow { let py_obj = result_fut.await; // Convert Python result to Rust type with proper traceback handling - let output = Python::with_gil(|py| -> Result<_> { + let output = Python::attach(|py| -> Result<_> { let output_any = py_obj.to_result_with_py_trace(py)?; let output: crate::py::Pythonized = output_any.extract(py)?; @@ -466,7 +466,7 @@ impl TransientFlow { let result = evaluate_transient_flow(&flow, &input_values) .await .into_py_result()?; - Python::with_gil(|py| value_to_py_object(py, &result)?.into_py_any(py)) + Python::attach(|py| value_to_py_object(py, &result)?.into_py_any(py)) }) } } @@ -545,7 +545,7 @@ fn make_drop_bundle(flow_names: Vec) -> PyResult { #[pyfunction] fn remove_flow_context(py: Python<'_>, flow_name: String) -> PyResult<()> { - py.allow_threads(|| -> anyhow::Result<()> { + py.detach(|| -> anyhow::Result<()> { get_runtime().block_on(async move { let lib_context = get_lib_context().await.into_py_result()?; lib_context.remove_flow_context(&flow_name); @@ -580,7 +580,7 @@ fn get_auth_entry(key: String) -> PyResult> { #[pyfunction] fn get_app_namespace(py: Python<'_>) -> PyResult { let app_namespace = py - .allow_threads(|| -> anyhow::Result<_> { + .detach(|| -> anyhow::Result<_> { get_runtime().block_on(async move { let lib_context = get_lib_context().await?; Ok(lib_context.app_namespace.clone()) diff --git a/rust/py_utils/src/convert.rs b/rust/py_utils/src/convert.rs index 2c727dfd..eeb7102b 100644 --- a/rust/py_utils/src/convert.rs +++ b/rust/py_utils/src/convert.rs @@ -1,4 +1,4 @@ -use pyo3::prelude::*; +use pyo3::{BoundObject, prelude::*}; use pythonize::{depythonize, pythonize}; use serde::{Serialize, de::DeserializeOwned}; use std::ops::Deref; @@ -8,9 +8,12 @@ use crate::error::IntoPyResult; #[derive(Debug)] pub struct Pythonized(pub T); -impl<'py, T: DeserializeOwned> FromPyObject<'py> for Pythonized { - fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { - Ok(Pythonized(depythonize(obj).into_py_result()?)) +impl<'py, T: DeserializeOwned> FromPyObject<'_, '_> for Pythonized { + type Error = PyErr; + + fn extract(obj: Borrowed<'_, '_, PyAny>) -> PyResult { + let bound = obj.into_bound(); + Ok(Pythonized(depythonize(&bound).into_py_result()?)) } } diff --git a/rust/py_utils/src/future.rs b/rust/py_utils/src/future.rs index c4b0b1b7..d7de38d5 100644 --- a/rust/py_utils/src/future.rs +++ b/rust/py_utils/src/future.rs @@ -12,7 +12,7 @@ use std::{ }; struct CancelOnDropPy { - inner: BoxFuture<'static, pyo3::PyResult>, + inner: BoxFuture<'static, PyResult>>, task: Py, event_loop: Py, ctx: Py, @@ -20,7 +20,7 @@ struct CancelOnDropPy { } impl Future for CancelOnDropPy { - type Output = pyo3::PyResult; + type Output = PyResult>; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { match Pin::new(&mut self.inner).poll(cx) { Poll::Ready(out) => { @@ -37,7 +37,7 @@ impl Drop for CancelOnDropPy { if self.done.load(Ordering::SeqCst) { return; } - Python::with_gil(|py| { + Python::attach(|py| { let kwargs = PyDict::new(py); let result = || -> PyResult<()> { // pass context so cancellation runs under the right contextvars @@ -61,7 +61,7 @@ pub fn from_py_future<'py, 'fut>( py: Python<'py>, locals: &TaskLocals, awaitable: Bound<'py, PyAny>, -) -> pyo3::PyResult> + Send + use<'fut>> { +) -> pyo3::PyResult>> + Send + use<'fut>> { // 1) Capture loop + context from TaskLocals for thread-safe cancellation let event_loop: Bound<'py, PyAny> = locals.event_loop(py).into(); let ctx: Bound<'py, PyAny> = locals.context(py);