diff --git a/.cargo/config b/.cargo/config.toml similarity index 100% rename from .cargo/config rename to .cargo/config.toml diff --git a/.github/workflows/preview-deployments.yml b/.github/workflows/preview-deployments.yml index 908337467..9aba299f6 100644 --- a/.github/workflows/preview-deployments.yml +++ b/.github/workflows/preview-deployments.yml @@ -18,6 +18,7 @@ jobs: macos: runs-on: macos-latest strategy: + fail-fast: false matrix: python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: @@ -48,11 +49,20 @@ jobs: windows: runs-on: windows-latest strategy: + fail-fast: false matrix: python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] target: [x64, x86] steps: - uses: actions/checkout@v4 + # The Microsoft Store python alias stubs intermittently raise EACCES when a + # Node-based action probes PATH for python3, failing the whole job. Remove + # them; setup-python below installs the real interpreter on PATH. + - name: Remove Windows Store python aliases (avoids flaky EACCES) + shell: pwsh + run: | + Remove-Item "$env:LOCALAPPDATA\Microsoft\WindowsApps\python.exe" -Force -ErrorAction SilentlyContinue + Remove-Item "$env:LOCALAPPDATA\Microsoft\WindowsApps\python3.exe" -Force -ErrorAction SilentlyContinue - name: Install uv uses: astral-sh/setup-uv@v3 - uses: actions/setup-python@v6 @@ -74,6 +84,7 @@ jobs: linux: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] target: [x86_64, i686] diff --git a/.github/workflows/python-CI.yml b/.github/workflows/python-CI.yml index 96173853a..09dc9b7c3 100644 --- a/.github/workflows/python-CI.yml +++ b/.github/workflows/python-CI.yml @@ -41,7 +41,7 @@ jobs: name: ${{ matrix.os }} tests with python ${{ matrix.python-version }} runs-on: ${{ matrix.os }}-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: @@ -53,4 +53,23 @@ jobs: with: python-versions: ${{ matrix.python-version }} - name: Test with Nox + if: runner.os != 'Windows' run: nox --non-interactive --error-on-missing-interpreter -p ${{ matrix.python-version }} + # Windows extension-module builds intermittently hit a transient linker + # failure (LNK1181: cannot open input file 'pythonXY.lib') from the hosted + # toolchain. Retry so a one-off linker hiccup doesn't fail the whole run. + - name: Test with Nox (Windows, retry transient linker failures) + if: runner.os == 'Windows' + shell: bash + run: | + for attempt in 1 2 3; do + echo "::group::nox attempt $attempt" + if nox --non-interactive --error-on-missing-interpreter -p ${{ matrix.python-version }}; then + echo "::endgroup::" + exit 0 + fi + echo "::endgroup::" + echo "nox attempt $attempt failed; retrying..." + done + echo "nox failed after 3 attempts" + exit 1 diff --git a/.github/workflows/release-CI.yml b/.github/workflows/release-CI.yml index a2612d037..f8785b403 100644 --- a/.github/workflows/release-CI.yml +++ b/.github/workflows/release-CI.yml @@ -18,6 +18,7 @@ jobs: macos: runs-on: macos-latest strategy: + fail-fast: false matrix: python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: @@ -52,11 +53,20 @@ jobs: windows: runs-on: windows-latest strategy: + fail-fast: false matrix: python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] target: [x64, x86] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + # The Microsoft Store python alias stubs intermittently raise EACCES when a + # Node-based action probes PATH for python3, failing the whole job. Remove + # them; setup-python below installs the real interpreter on PATH. + - name: Remove Windows Store python aliases (avoids flaky EACCES) + shell: pwsh + run: | + Remove-Item "$env:LOCALAPPDATA\Microsoft\WindowsApps\python.exe" -Force -ErrorAction SilentlyContinue + Remove-Item "$env:LOCALAPPDATA\Microsoft\WindowsApps\python3.exe" -Force -ErrorAction SilentlyContinue - name: Install uv uses: astral-sh/setup-uv@v3 - uses: actions/setup-python@v6 @@ -82,6 +92,7 @@ jobs: linux: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] target: [x86_64, i686] @@ -113,6 +124,7 @@ jobs: runs-on: ubuntu-latest if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.enable_linux_cross) strategy: + fail-fast: false matrix: python: [ diff --git a/src/asyncio.rs b/src/asyncio.rs index 8967a5b22..39e8acf92 100644 --- a/src/asyncio.rs +++ b/src/asyncio.rs @@ -3,6 +3,10 @@ // Licensed under the BSD 3-Clause License // See: https://github.com/emmett-framework/granian/blob/master/LICENSE +// Adapted runtime scaffolding: some helpers aren't wired up on every code path +// yet, so allow dead code at the module level rather than scattering attributes. +#![allow(dead_code)] + use pyo3::{prelude::*, sync::PyOnceLock}; use std::convert::Into; use std::ffi::CString; diff --git a/src/callbacks.rs b/src/callbacks.rs index eaa10a894..fc1c29ec0 100644 --- a/src/callbacks.rs +++ b/src/callbacks.rs @@ -3,6 +3,10 @@ // Licensed under the BSD 3-Clause License // See: https://github.com/emmett-framework/granian/blob/master/LICENSE +// Adapted runtime scaffolding: some helpers aren't wired up on every code path +// yet, so allow dead code at the module level rather than scattering attributes. +#![allow(dead_code)] + use pyo3::{exceptions::PyStopIteration, prelude::*, IntoPyObjectExt}; use std::sync::{atomic, Arc, OnceLock, RwLock}; use tokio::sync::Notify; diff --git a/src/conversion.rs b/src/conversion.rs index cc9d6569e..599dcf650 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -3,6 +3,9 @@ // Licensed under the BSD 3-Clause License // See: https://github.com/emmett-framework/granian/blob/master/LICENSE +// Adapted scaffolding: not every conversion variant is constructed yet. +#![allow(dead_code)] + use pyo3::prelude::*; pub(crate) enum FutureResultToPy { None, diff --git a/src/executors/mod.rs b/src/executors/mod.rs index b7531d487..daa36b259 100644 --- a/src/executors/mod.rs +++ b/src/executors/mod.rs @@ -87,10 +87,10 @@ fn extract_response_type_fast(output: &Bound<'_, PyAny>) -> PyResult() { + if let Ok(py_resp) = output.cast_exact::() { let borrowed = py_resp.borrow(); let description = crate::types::get_description_from_pyobject(borrowed.description.bind(py))?; @@ -110,10 +110,10 @@ fn extract_response_type_fast(output: &Bound<'_, PyAny>) -> PyResult().is_ok() || output.downcast::().is_ok() { + if output.cast::().is_ok() || output.cast::().is_ok() { let dumps = orjson_dumps(py)?; let encoded = dumps.call1((output,))?; - let bytes = encoded.downcast::()?.as_bytes().to_vec(); + let bytes = encoded.cast::()?.as_bytes().to_vec(); return Ok(ResponseType::Standard(response_from_bytes( bytes, json_headers(), @@ -122,14 +122,14 @@ fn extract_response_type_fast(output: &Bound<'_, PyAny>) -> PyResult() { + if let Ok(s) = output.cast::() { let bytes = s.to_string().into_bytes(); return Ok(ResponseType::Standard(response_from_bytes( bytes, text_plain_headers(), ))); } - if let Ok(b) = output.downcast::() { + if let Ok(b) = output.cast::() { let bytes = b.as_bytes().to_vec(); return Ok(ResponseType::Standard(response_from_bytes( bytes, @@ -162,7 +162,7 @@ where T: Clone + for<'py> IntoPyObject<'py>, for<'py> >::Error: std::fmt::Debug, { - let handler = function.handler.bind(py).downcast()?; + let handler = function.handler.bind(py).cast()?; // 0-param handlers: skip Request→Python conversion entirely if function.number_of_params == 0 { @@ -279,7 +279,7 @@ where for<'py> >::Error: std::fmt::Debug, { if function.is_async { - let output: Py = Python::with_gil(|py| -> PyResult<_> { + let output: Py = Python::attach(|py| -> PyResult<_> { let coroutine = get_function_output(function, py, input)?; let awaitable = match context { Some(ctx) => wrap_coro_in_context(py, ctx.bind(py), coroutine)?, @@ -289,7 +289,7 @@ where })? .await?; - Python::with_gil(|py| -> Result { + Python::attach(|py| -> Result { // Try response extraction first, then request match output.extract::(py) { Ok(response) => Ok(MiddlewareReturn::Response(response)), @@ -300,7 +300,7 @@ where } }) } else { - Python::with_gil(|py| -> Result { + Python::attach(|py| -> Result { let output = match context { Some(ctx) => get_function_output_in_context(function, py, ctx.bind(py), input)?, None => get_function_output(function, py, input)?, @@ -332,7 +332,7 @@ where for<'py> >::Error: std::fmt::Debug, for<'py> >::Error: std::fmt::Debug, { - let handler = function.handler.bind(py).downcast()?; + let handler = function.handler.bind(py).cast()?; if function.number_of_params == 0 { return handler.call0(); @@ -471,7 +471,7 @@ pub async fn execute_after_middleware_function( context: Option<&Py>, ) -> Result { if function.is_async { - let output: Py = Python::with_gil(|py| -> PyResult<_> { + let output: Py = Python::attach(|py| -> PyResult<_> { let coroutine = get_function_output_with_two_args(function, py, request, response)?; let awaitable = match context { Some(ctx) => wrap_coro_in_context(py, ctx.bind(py), coroutine)?, @@ -481,7 +481,7 @@ pub async fn execute_after_middleware_function( })? .await?; - Python::with_gil(|py| -> Result { + Python::attach(|py| -> Result { // Try response extraction first, then request match output.extract::(py) { Ok(response) => Ok(MiddlewareReturn::Response(response)), @@ -492,7 +492,7 @@ pub async fn execute_after_middleware_function( } }) } else { - Python::with_gil(|py| -> Result { + Python::attach(|py| -> Result { let output = match context { Some(ctx) => get_function_output_with_two_args_in_context( function, @@ -522,7 +522,7 @@ pub async fn execute_http_function( context: Option<&Py>, ) -> PyResult { if function.is_async { - let output = Python::with_gil(|py| -> PyResult<_> { + let output = Python::attach(|py| -> PyResult<_> { let coroutine = get_function_output(function, py, request)?; let awaitable = match context { Some(ctx) => wrap_coro_in_context(py, ctx.bind(py), coroutine)?, @@ -532,9 +532,9 @@ pub async fn execute_http_function( })? .await?; - Python::with_gil(|py| extract_response_type(output, py)) + Python::attach(|py| extract_response_type(output, py)) } else { - Python::with_gil(|py| { + Python::attach(|py| { let output = match context { Some(ctx) => get_function_output_in_context(function, py, ctx.bind(py), request)?, None => get_function_output(function, py, request)?, @@ -560,7 +560,7 @@ pub async fn execute_startup_handler( ) -> Result<()> { if let Some(function) = event_handler { if function.is_async { - Python::with_gil(|py| { + Python::attach(|py| { pyo3_async_runtimes::into_future_with_locals( task_locals, function.handler.bind(py).call0()?, @@ -568,7 +568,7 @@ pub async fn execute_startup_handler( })? .await?; } else { - Python::with_gil(|py| function.handler.call0(py))?; + Python::attach(|py| function.handler.call0(py))?; } } Ok(()) diff --git a/src/executors/web_socket_executors.rs b/src/executors/web_socket_executors.rs index 82bbc2dd2..1f4683ae8 100644 --- a/src/executors/web_socket_executors.rs +++ b/src/executors/web_socket_executors.rs @@ -13,8 +13,8 @@ pub fn execute_ws_function( ws: &WebSocketConnector, ) { if function.is_async { - let fut = match Python::with_gil(|py| { - let handler = function.handler.bind(py).downcast()?; + let fut = match Python::attach(|py| { + let handler = function.handler.bind(py).cast()?; let result = handler.call1((ws.clone(),))?; pyo3_async_runtimes::into_future_with_locals(task_locals, result) }) { @@ -26,7 +26,7 @@ pub fn execute_ws_function( }; let f = async move { match fut.await { - Ok(output) => Python::with_gil(|py| match output.extract::>(py) { + Ok(output) => Python::attach(|py| match output.extract::>(py) { Ok(msg) => msg, Err(e) => { log::error!("Failed to extract WebSocket handler result: {}", e); @@ -47,8 +47,8 @@ pub fn execute_ws_function( }); ctx.spawn(f); } else { - Python::with_gil(|py| { - let handler = match function.handler.bind(py).downcast() { + Python::attach(|py| { + let handler = match function.handler.bind(py).cast() { Ok(h) => h, Err(e) => { log::error!("Failed to get sync WebSocket handler: {}", e); diff --git a/src/routers/http_router.rs b/src/routers/http_router.rs index b877b19af..0d3e844f1 100644 --- a/src/routers/http_router.rs +++ b/src/routers/http_router.rs @@ -50,7 +50,7 @@ impl Router<(FunctionInfo, HashMap), HttpMethod> for HttpRouter route_params.insert(key.to_string(), value.to_string()); } - let function_info = Python::with_gil(|_| res.value.to_owned()); + let function_info = Python::attach(|_| res.value.to_owned()); return Some((function_info, route_params)); } diff --git a/src/routers/middleware_router.rs b/src/routers/middleware_router.rs index 48a89bf22..7a68064bf 100644 --- a/src/routers/middleware_router.rs +++ b/src/routers/middleware_router.rs @@ -60,7 +60,7 @@ impl Router<(Vec, HashMap), MiddlewareType> for Mi route_params.insert(key.to_string(), value.to_string()); } - let functions = Python::with_gil(|_| res.value.to_owned()); + let functions = Python::attach(|_| res.value.to_owned()); Some((functions, route_params)) } diff --git a/src/runtime.rs b/src/runtime.rs index fffaca3d1..10c9799df 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -3,6 +3,10 @@ // Licensed under the BSD 3-Clause License // See: https://github.com/emmett-framework/granian/blob/master/LICENSE +// Adapted runtime scaffolding: some helpers aren't wired up on every code path +// yet, so allow dead code at the module level rather than scattering attributes. +#![allow(dead_code)] + use futures::FutureExt; use pyo3::{prelude::*, IntoPyObjectExt}; use std::{future::Future, sync::Arc, sync::OnceLock}; diff --git a/src/server.rs b/src/server.rs index 668aced7a..54bccb48a 100644 --- a/src/server.rs +++ b/src/server.rs @@ -118,7 +118,7 @@ impl Server { let excluded_response_headers_paths = self.excluded_response_headers_paths.clone(); let _ = TASK_LOCALS.get_or_try_init(|| { - Python::with_gil(|py| { + Python::attach(|py| { pyo3_async_runtimes::TaskLocals::new(event_loop.clone().into()).copy_context(py) }) }); @@ -135,7 +135,7 @@ impl Server { thread::spawn(move || { actix_web::rt::System::new().block_on(async move { - let task_locals = Python::with_gil(|py| TASK_LOCALS.get().unwrap().clone_ref(py)); + let task_locals = Python::attach(|_py| TASK_LOCALS.get().unwrap().clone()); execute_startup_handler(startup_handler, &task_locals) .await .unwrap(); @@ -203,7 +203,7 @@ impl Server { web::get().to(move |stream: web::Payload, req: HttpRequest| { let endpoint_copy = endpoint_for_closure.clone(); let task_locals = - Python::with_gil(|py| TASK_LOCALS.get().unwrap().clone_ref(py)); + Python::attach(|_py| TASK_LOCALS.get().unwrap().clone()); start_web_socket( req, stream, @@ -253,7 +253,7 @@ impl Server { // Normal path: dynamic routes (and const routes when middlewares exist) require Python let req_ref = req.clone(); let task_locals = - Python::with_gil(|py| TASK_LOCALS.get().unwrap().clone_ref(py)); + Python::attach(|_py| TASK_LOCALS.get().unwrap().clone()); let response = pyo3_async_runtimes::tokio::scope_local( task_locals, async move { @@ -290,20 +290,19 @@ impl Server { if event_loop.is_err() { if let Some(function) = shutdown_handler { if function.is_async { - let task_locals = - Python::with_gil(|py| TASK_LOCALS.get().unwrap().clone_ref(py)); + let task_locals = Python::attach(|_py| TASK_LOCALS.get().unwrap().clone()); pyo3_async_runtimes::tokio::run_until_complete( task_locals.event_loop(_py), pyo3_async_runtimes::into_future_with_locals( - &task_locals.clone_ref(_py), + &task_locals.clone(), function.handler.bind(_py).call0()?, ) .unwrap(), ) .unwrap(); } else { - Python::with_gil(|py| function.handler.call0(py))?; + Python::attach(|py| function.handler.call0(py))?; } } @@ -429,7 +428,7 @@ impl Server { endpoint_prefixed_with_method.push_str(route); - Python::with_gil(|py| { + Python::attach(|py| { self.middleware_router .add_route( py, @@ -512,7 +511,7 @@ async fn index( // (which run via `ctx.run(...)`): without a fresh context, a `ContextVar` // written inside a handler would persist in the worker thread's current // context and leak into the next request on that thread. - let request_context: Py = match Python::with_gil(crate::asyncio::new_context) { + let request_context: Py = match Python::attach(crate::asyncio::new_context) { Ok(ctx) => ctx, Err(e) => { error!("Failed to create request contextvars context: {}", e); @@ -644,7 +643,7 @@ async fn index( } fn get_traceback(error: &PyErr) -> String { - Python::with_gil(|py| -> String { + Python::attach(|py| -> String { if let Some(traceback) = error.traceback(py) { let msg = match traceback.format() { Ok(msg) => format!("\n{msg} {error}"), diff --git a/src/types/function_info.rs b/src/types/function_info.rs index 72ca1d922..75e67332a 100644 --- a/src/types/function_info.rs +++ b/src/types/function_info.rs @@ -61,7 +61,7 @@ impl FunctionInfo { impl Clone for FunctionInfo { fn clone(&self) -> Self { - Python::with_gil(|py| Self { + Python::attach(|py| Self { handler: self.handler.clone_ref(py), is_async: self.is_async, number_of_params: self.number_of_params, diff --git a/src/types/headers.rs b/src/types/headers.rs index 57eb16d6a..39276e7e5 100644 --- a/src/types/headers.rs +++ b/src/types/headers.rs @@ -21,7 +21,7 @@ impl Headers { for (key, value) in default_headers { let key = key.to_string().to_lowercase(); - let new_value = value.downcast::(); + let new_value = value.cast::(); if let Ok(new_value) = new_value { let value: Vec = new_value.iter().map(|x| x.to_string()).collect(); @@ -101,7 +101,7 @@ impl Headers { pub fn populate_from_dict(&mut self, headers: &Bound) { for (key, value) in headers.iter() { let key = key.to_string().to_lowercase(); - let new_value = value.downcast::(); + let new_value = value.cast::(); if let Ok(new_value) = new_value { let value: Vec = new_value.iter().map(|x| x.to_string()).collect(); diff --git a/src/types/mod.rs b/src/types/mod.rs index e04361fe5..60d6b08a2 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -107,9 +107,9 @@ impl Url { } pub fn get_body_from_pyobject(body: &Bound<'_, PyAny>) -> PyResult> { - if let Ok(s) = body.downcast::() { + if let Ok(s) = body.cast::() { Ok(s.to_string().into_bytes()) - } else if let Ok(b) = body.downcast::() { + } else if let Ok(b) = body.cast::() { Ok(b.as_bytes().to_vec()) } else { debug!("Could not convert specified body to bytes"); @@ -118,9 +118,9 @@ pub fn get_body_from_pyobject(body: &Bound<'_, PyAny>) -> PyResult> { } pub fn get_description_from_pyobject(description: &Bound<'_, PyAny>) -> PyResult> { - if let Ok(s) = description.downcast::() { + if let Ok(s) = description.cast::() { Ok(s.to_string().into_bytes()) - } else if let Ok(b) = description.downcast::() { + } else if let Ok(b) = description.cast::() { Ok(b.as_bytes().to_vec()) } else { debug!("Could not convert specified response description to bytes"); @@ -129,7 +129,7 @@ pub fn get_description_from_pyobject(description: &Bound<'_, PyAny>) -> PyResult } pub fn check_body_type(py: Python, body: &Py) -> PyResult<()> { - if body.downcast_bound::(py).is_err() && body.downcast_bound::(py).is_err() { + if body.cast_bound::(py).is_err() && body.cast_bound::(py).is_err() { return Err(PyValueError::new_err( "Could not convert specified body to bytes", )); @@ -138,7 +138,7 @@ pub fn check_body_type(py: Python, body: &Py) -> PyResult<()> { } pub fn check_description_type(py: Python, body: &Py) -> PyResult<()> { - if body.downcast_bound::(py).is_err() && body.downcast_bound::(py).is_err() { + if body.cast_bound::(py).is_err() && body.cast_bound::(py).is_err() { return Err(PyValueError::new_err( "Could not convert specified response description to bytes", )); diff --git a/src/types/request.rs b/src/types/request.rs index 5fcaccdf6..c0e4f421c 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -256,9 +256,9 @@ impl PyRequest { } pub fn json(&self, py: Python) -> PyResult> { - let parsed: Value = if let Ok(python_string) = self.body.downcast_bound::(py) { + let parsed: Value = if let Ok(python_string) = self.body.cast_bound::(py) { serde_json::from_str(python_string.extract()?) - } else if let Ok(python_bytes) = self.body.downcast_bound::(py) { + } else if let Ok(python_bytes) = self.body.cast_bound::(py) { serde_json::from_slice(python_bytes.as_bytes()) } else { return Err(PyValueError::new_err("Invalid JSON body")); diff --git a/src/types/response.rs b/src/types/response.rs index 57144d923..bcdfdb2ba 100644 --- a/src/types/response.rs +++ b/src/types/response.rs @@ -125,7 +125,7 @@ fn create_python_stream( ) -> Pin> + Send>> { Box::pin(futures::stream::unfold(generator, |generator| async move { match tokio::task::spawn_blocking(move || { - Python::with_gil(|py| { + Python::attach(|py| { let gen = generator.bind(py); match gen.call_method0("__next__") { @@ -283,7 +283,7 @@ impl PyStreamingResponse { let media_type = media_type.unwrap_or_else(|| "text/event-stream".to_string()); let headers_output: Py = if let Some(headers) = headers { - if let Ok(headers_dict) = headers.downcast::() { + if let Ok(headers_dict) = headers.cast::() { let headers = Headers::new(Some(headers_dict)); Py::new(py, headers)? } else if let Ok(headers) = headers.extract::>() { @@ -351,7 +351,7 @@ impl PyResponse { check_body_type(py, &description)?; let headers_output: Py = if let Some(headers) = headers { - if let Ok(headers_dict) = headers.downcast::() { + if let Ok(headers_dict) = headers.cast::() { // Here you'd have logic to create a Headers instance from a PyDict // For simplicity, let's assume you have a method `from_dict` on Headers for this let headers = Headers::new(Some(headers_dict)); // Hypothetical method diff --git a/src/websockets/mod.rs b/src/websockets/mod.rs index b133eb980..cdfec9efc 100644 --- a/src/websockets/mod.rs +++ b/src/websockets/mod.rs @@ -75,7 +75,7 @@ impl Actor for WebSocketConnector { let (tx, rx) = mpsc::unbounded_channel::>(); self.message_sender = Some(tx); - self.message_channel = Python::with_gil(|py| { + self.message_channel = Python::attach(|py| { Some( Py::new( py, @@ -108,7 +108,7 @@ impl Actor for WebSocketConnector { impl Clone for WebSocketConnector { fn clone(&self) -> Self { - let task_locals_clone = Python::with_gil(|py| self.task_locals.clone_ref(py)); + let task_locals_clone = Python::attach(|_py| self.task_locals.clone()); Self { id: self.id, @@ -117,7 +117,7 @@ impl Clone for WebSocketConnector { registry_addr: self.registry_addr.clone(), query_params: self.query_params.clone(), message_sender: self.message_sender.clone(), - message_channel: Python::with_gil(|py| { + message_channel: Python::attach(|py| { self.message_channel.as_ref().map(|c| c.clone_ref(py)) }), }