From da52e9e2e5c8777269e2c41eff77c95cbfc0dea1 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 15 Jun 2026 13:20:07 -0400 Subject: [PATCH] Bump PyO3 and rust-numpy to latest 0.29 release This commit updates the rustworkx crate's usage of PyO3 and rust-numpy to use the latest release of both 0.29. This release introduces a few API changes that need to be adapted to. Overall these changes are fairly mechanical and mostly are just about adapting our pyo3 usage. The details of these changes can be found in the migration guide: https://pyo3.rs/v0.29.0/migration.html --- Cargo.lock | 59 +++++++++-------------------------------- Cargo.toml | 4 +-- src/coloring.rs | 2 +- src/connectivity/mod.rs | 5 ++-- src/digraph.rs | 38 ++++++++++++++------------ src/graph.rs | 28 ++++++++++--------- src/graphml.rs | 12 +++++---- src/iterators.rs | 25 ++++++++--------- src/lib.rs | 4 +-- src/toposort.rs | 2 +- 10 files changed, 79 insertions(+), 100 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 647339b518..c91a49e43c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -322,15 +322,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - [[package]] name = "itertools" version = "0.13.0" @@ -415,15 +406,6 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -574,9 +556,9 @@ dependencies = [ [[package]] name = "numpy" -version = "0.26.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2dba356160b54f5371b550575b78130a54718b4c6e46b3f33a6da74a27e78b" +checksum = "6a5b15d63a5ff39e378daed0e1340d3a5964703ea9712eb09a0dc66fade996f4" dependencies = [ "libc", "ndarray", @@ -711,39 +693,37 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.26.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba0117f4212101ee6544044dae45abe1083d30ce7b29c4b5cbdfa2354e07383" +checksum = "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c" dependencies = [ "hashbrown 0.15.5", "indexmap", - "indoc", "libc", - "memoffset", "num-bigint", "num-complex", + "num-traits", "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", - "unindent", ] [[package]] name = "pyo3-build-config" -version = "0.26.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fc6ddaf24947d12a9aa31ac65431fb1b851b8f4365426e182901eabfb87df5f" +checksum = "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078" dependencies = [ "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.26.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "025474d3928738efb38ac36d4744a74a400c901c7596199e20e45d98eb194105" +checksum = "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b" dependencies = [ "libc", "pyo3-build-config", @@ -751,9 +731,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.26.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e64eb489f22fe1c95911b77c44cc41e7c19f3082fc81cce90f657cdc42ffded" +checksum = "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -763,13 +743,12 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.26.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "100246c0ecf400b475341b8455a9213344569af29a3c841d29270e53102e0fcf" +checksum = "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362" dependencies = [ "heck", "proc-macro2", - "pyo3-build-config", "quote", "syn", ] @@ -992,12 +971,6 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - [[package]] name = "rustworkx" version = "0.17.1" @@ -1215,12 +1188,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "unindent" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" - [[package]] name = "version_check" version = "0.9.5" diff --git a/Cargo.toml b/Cargo.toml index fa6d6bda7b..87634324aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ indexmap = { version = ">=1.9, <3", features = ["rayon"] } ndarray = { version = "0.16.1", features = ["rayon"] } num-traits = "0.2" petgraph = "0.8" -numpy = "0.26" +numpy = "0.29" rand = "0.9" rand_distr = "0.5" rand_pcg = "0.9" @@ -71,7 +71,7 @@ version = "0.15" # Implicitly required to match PyO3's and `petgraph`'s via cro features = ["rayon"] [dependencies.pyo3] -version = "0.26" +version = "0.29" features = ["abi3-py310", "extension-module", "hashbrown", "num-bigint", "num-complex", "indexmap", "py-clone"] [dependencies.sprs] diff --git a/src/coloring.rs b/src/coloring.rs index 73590ecbe7..77a579735e 100644 --- a/src/coloring.rs +++ b/src/coloring.rs @@ -42,7 +42,7 @@ pub use rustworkx_core::coloring::ColoringStrategy as ColoringStrategyCore; /// - `GIS` strategy in [1] (section 1.2.2.9) /// /// [1] Adrian Kosowski, and Krzysztof Manuszewski, Classical Coloring of Graphs, Graph Colorings, 2-19, 2004. ISBN 0-8218-3458-4. -#[pyclass(module = "rustworkx", eq, eq_int)] +#[pyclass(module = "rustworkx", eq, eq_int, from_py_object)] #[derive(Clone, PartialEq)] pub enum ColoringStrategy { Degree, diff --git a/src/connectivity/mod.rs b/src/connectivity/mod.rs index 335f87e070..b6d05e1006 100644 --- a/src/connectivity/mod.rs +++ b/src/connectivity/mod.rs @@ -1025,8 +1025,9 @@ pub enum TargetNodes { Multiple(HashSet), } -impl<'py> FromPyObject<'py> for TargetNodes { - fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { +impl<'py> FromPyObject<'_, 'py> for TargetNodes { + type Error = PyErr; + fn extract(ob: Borrowed<'_, 'py, PyAny>) -> PyResult { if let Ok(int) = ob.extract::() { Ok(Self::Single(NodeIndex::new(int))) } else { diff --git a/src/digraph.rs b/src/digraph.rs index fa4139b771..f454dee2f9 100644 --- a/src/digraph.rs +++ b/src/digraph.rs @@ -32,7 +32,7 @@ use smallvec::SmallVec; use pyo3::IntoPyObjectExt; use pyo3::PyTraverseError; use pyo3::Python; -use pyo3::exceptions::PyIndexError; +use pyo3::exceptions::{PyIndexError, PyUnicodeDecodeError}; use pyo3::gc::PyVisit; use pyo3::prelude::*; use pyo3::types::{IntoPyDict, PyBool, PyDict, PyGenericAlias, PyList, PyString, PyTuple, PyType}; @@ -187,7 +187,7 @@ use super::dag_algo::is_directed_acyclic_graph; /// many edges before needing to grow. This does not prepopulate any edges with data, it is /// only a potential performance optimization if the complete size of the graph is known in /// advance. -#[pyclass(mapping, module = "rustworkx", subclass)] +#[pyclass(mapping, module = "rustworkx", subclass, from_py_object)] #[derive(Clone)] pub struct PyDiGraph { pub graph: StablePyGraph, @@ -367,17 +367,17 @@ impl PyDiGraph { } fn __setstate__(&mut self, py: Python, state: Py) -> PyResult<()> { - let dict_state = state.downcast_bound::(py)?; + let dict_state = state.bind(py).cast::()?; let binding = dict_state.get_item("nodes")?.unwrap(); - let nodes_lst = binding.downcast::()?; + let nodes_lst = binding.cast::()?; let binding = dict_state.get_item("edges")?.unwrap(); - let edges_lst = binding.downcast::()?; + let edges_lst = binding.cast::()?; self.graph = StablePyGraph::::new(); - let dict_state = state.downcast_bound::(py)?; + let dict_state = state.bind(py).cast::()?; self.node_removed = dict_state .get_item("nodes_removed")? .unwrap() - .downcast::()? + .cast::()? .extract()?; // graph is empty, stop early @@ -388,7 +388,7 @@ impl PyDiGraph { if !self.node_removed { for item in nodes_lst.iter() { let node_w = item - .downcast::() + .cast::() .unwrap() .get_item(1) .unwrap() @@ -399,7 +399,7 @@ impl PyDiGraph { } else if nodes_lst.len() == 1 { // graph has only one node, handle logic here to save one if in the loop later let binding = nodes_lst.get_item(0).unwrap(); - let item = binding.downcast::().unwrap(); + let item = binding.cast::().unwrap(); let node_idx: usize = item.get_item(0).unwrap().extract().unwrap(); let node_w = item.get_item(1).unwrap().extract().unwrap(); @@ -412,7 +412,7 @@ impl PyDiGraph { } } else { let binding = nodes_lst.get_item(nodes_lst.len() - 1).unwrap(); - let last_item = binding.downcast::().unwrap(); + let last_item = binding.cast::().unwrap(); // list of temporary nodes that will be removed later to re-create holes let node_bound_1: usize = last_item.get_item(0).unwrap().extract().unwrap(); @@ -420,7 +420,7 @@ impl PyDiGraph { Vec::with_capacity(node_bound_1 + 1 - nodes_lst.len()); for item in nodes_lst { - let item = item.downcast::().unwrap(); + let item = item.cast::().unwrap(); let next_index: usize = item.get_item(0).unwrap().extract().unwrap(); let weight: Py = item.get_item(1).unwrap().extract().unwrap(); while next_index > self.graph.node_bound() { @@ -445,7 +445,7 @@ impl PyDiGraph { // add a temporary edge that will be deleted later to re-create the hole self.graph.add_edge(tmp_node, tmp_node, py.None()); } else { - let triple = item.downcast::().unwrap(); + let triple = item.cast::().unwrap(); let edge_p: usize = triple.get_item(0).unwrap().extract().unwrap(); let edge_c: usize = triple.get_item(1).unwrap().extract().unwrap(); let edge_w = triple.get_item(2).unwrap().extract().unwrap(); @@ -1275,7 +1275,7 @@ impl PyDiGraph { edge.weight().clone_ref(py) }; if let Some(edge_data) = in_edges.get_item(key_value.bind(py))? { - let edge_data = edge_data.downcast::()?; + let edge_data = edge_data.cast::()?; edge_data.borrow_mut().nodes.push(edge.source()); } else { in_edges.set_item( @@ -1302,7 +1302,7 @@ impl PyDiGraph { edge.weight().clone_ref(py) }; if let Some(edge_data) = out_edges.get_item(key_value.bind(py))? { - let edge_data = edge_data.downcast::()?; + let edge_data = edge_data.cast::()?; edge_data.borrow_mut().nodes.push(edge.target()); } else { out_edges.set_item( @@ -1319,9 +1319,9 @@ impl PyDiGraph { }; for (in_key, in_edge_data) in in_edges { - let in_edge_data = in_edge_data.downcast::()?.borrow(); + let in_edge_data = in_edge_data.cast::()?.borrow(); let out_edge_data = match out_edges.get_item(in_key)? { - Some(out_edge_data) => out_edge_data.downcast::()?.borrow(), + Some(out_edge_data) => out_edge_data.cast::()?.borrow(), None => continue, }; for source in in_edge_data.nodes.iter() { @@ -2447,7 +2447,11 @@ impl PyDiGraph { None => { let mut file = Vec::::new(); build_dot(py, &self.graph, &mut file, graph_attr, node_attr, edge_attr)?; - Ok(Some(PyString::new(py, str::from_utf8(&file)?))) + Ok(Some(PyString::new( + py, + str::from_utf8(&file) + .map_err(|e| PyUnicodeDecodeError::new_err_from_utf8(py, &file, e))?, + ))) } } } diff --git a/src/graph.rs b/src/graph.rs index 02fbda2d4b..d9cd17611d 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -26,7 +26,7 @@ use rustworkx_core::graph_ext::*; use pyo3::IntoPyObjectExt; use pyo3::PyTraverseError; use pyo3::Python; -use pyo3::exceptions::PyIndexError; +use pyo3::exceptions::{PyIndexError, PyUnicodeDecodeError}; use pyo3::gc::PyVisit; use pyo3::prelude::*; use pyo3::types::{IntoPyDict, PyBool, PyDict, PyGenericAlias, PyList, PyString, PyTuple, PyType}; @@ -147,7 +147,7 @@ use petgraph::visit::{ /// many edges before needing to grow. This does not prepopulate any edges with data, it is /// only a potential performance optimization if the complete size of the graph is known in /// advance. -#[pyclass(mapping, module = "rustworkx", subclass)] +#[pyclass(mapping, module = "rustworkx", subclass, from_py_object)] #[derive(Clone)] pub struct PyGraph { pub graph: StablePyGraph, @@ -258,16 +258,16 @@ impl PyGraph { } fn __setstate__(&mut self, py: Python, state: Py) -> PyResult<()> { - let dict_state = state.downcast_bound::(py)?; + let dict_state = state.bind(py).cast::()?; let binding = dict_state.get_item("nodes")?.unwrap(); - let nodes_lst = binding.downcast::()?; + let nodes_lst = binding.cast::()?; let binding = dict_state.get_item("edges")?.unwrap(); - let edges_lst = binding.downcast::()?; + let edges_lst = binding.cast::()?; self.node_removed = dict_state .get_item("nodes_removed")? .unwrap() - .downcast::()? + .cast::()? .extract()?; // graph is empty, stop early if nodes_lst.is_empty() { @@ -277,7 +277,7 @@ impl PyGraph { if !self.node_removed { for item in nodes_lst.iter() { let node_w = item - .downcast::() + .cast::() .unwrap() .get_item(1) .unwrap() @@ -288,7 +288,7 @@ impl PyGraph { } else if nodes_lst.len() == 1 { // graph has only one node, handle logic here to save one if in the loop later let binding = nodes_lst.get_item(0).unwrap(); - let item = binding.downcast::().unwrap(); + let item = binding.cast::().unwrap(); let node_idx: usize = item.get_item(0).unwrap().extract().unwrap(); let node_w = item.get_item(1).unwrap().extract().unwrap(); @@ -301,7 +301,7 @@ impl PyGraph { } } else { let binding = nodes_lst.get_item(nodes_lst.len() - 1).unwrap(); - let last_item = binding.downcast::().unwrap(); + let last_item = binding.cast::().unwrap(); // list of temporary nodes that will be removed later to re-create holes let node_bound_1: usize = last_item.get_item(0).unwrap().extract().unwrap(); @@ -309,7 +309,7 @@ impl PyGraph { Vec::with_capacity(node_bound_1 + 1 - nodes_lst.len()); for item in nodes_lst { - let item = item.downcast::().unwrap(); + let item = item.cast::().unwrap(); let next_index: usize = item.get_item(0).unwrap().extract().unwrap(); let weight: Py = item.get_item(1).unwrap().extract().unwrap(); while next_index > self.graph.node_bound() { @@ -334,7 +334,7 @@ impl PyGraph { // add a temporary edge that will be deleted later to re-create the hole self.graph.add_edge(tmp_node, tmp_node, py.None()); } else { - let triple = item.downcast::().unwrap(); + let triple = item.cast::().unwrap(); let edge_p: usize = triple.get_item(0).unwrap().extract().unwrap(); let edge_c: usize = triple.get_item(1).unwrap().extract().unwrap(); let edge_w = triple.get_item(2).unwrap().extract().unwrap(); @@ -1336,7 +1336,11 @@ impl PyGraph { None => { let mut file = Vec::::new(); build_dot(py, &self.graph, &mut file, graph_attr, node_attr, edge_attr)?; - Ok(Some(PyString::new(py, str::from_utf8(&file)?))) + Ok(Some(PyString::new( + py, + str::from_utf8(&file) + .map_err(|e| PyUnicodeDecodeError::new_err_from_utf8(py, &file, e))?, + ))) } } } diff --git a/src/graphml.rs b/src/graphml.rs index 8fc444016a..aa73164086 100644 --- a/src/graphml.rs +++ b/src/graphml.rs @@ -127,7 +127,7 @@ fn xml_attribute<'a>(element: &'a BytesStart<'a>, key: &[u8]) -> Result for Domain { } } -#[pyclass(eq, name = "GraphMLType")] +#[pyclass(eq, name = "GraphMLType", from_py_object)] #[derive(Clone, Copy, Debug, PartialEq)] pub enum Type { Boolean, @@ -250,8 +250,10 @@ impl Value { } } -impl<'py> FromPyObject<'py> for Value { - fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { +impl<'py> FromPyObject<'_, 'py> for Value { + type Error = PyErr; + + fn extract(ob: Borrowed<'_, 'py, PyAny>) -> PyResult { if let Ok(value) = ob.extract::() { return Ok(Value::Boolean(value)); } @@ -1265,7 +1267,7 @@ pub fn read_graphml<'py>( } /// Key definition: id, domain, name of the key, type, default value. -#[pyclass(name = "GraphMLKey")] +#[pyclass(name = "GraphMLKey", skip_from_py_object)] pub struct KeySpec { #[pyo3(get)] id: String, diff --git a/src/iterators.rs b/src/iterators.rs index 0de255c3de..2922dc25fa 100644 --- a/src/iterators.rs +++ b/src/iterators.rs @@ -285,7 +285,7 @@ where impl PyEq> for T where - for<'p> T: PyEq + Clone + FromPyObject<'p>, + for<'a, 'p> T: PyEq + Clone + FromPyObject<'a, 'p, Error = PyErr>, { #[inline] fn eq(&self, other: &Bound, py: Python) -> PyResult { @@ -419,14 +419,15 @@ pub enum PySequenceIndex<'py> { Slice(Bound<'py, PySlice>), } -impl<'py> FromPyObject<'py> for PySequenceIndex<'py> { - fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { +impl<'py> FromPyObject<'_, 'py> for PySequenceIndex<'py> { + type Error = PyErr; + fn extract(ob: Borrowed<'_, 'py, PyAny>) -> PyResult { // `slice` can't be subclassed in Python, so it's safe (and faster) to check for it exactly. // The `downcast_exact` check is just a pointer comparison, so while `slice` is the less // common input, doing that first has little-to-no impact on the speed of the `isize` path, // while the reverse makes `slice` inputs significantly slower. - if let Ok(slice) = ob.downcast_exact::() { - return Ok(Self::Slice(slice.clone())); + if let Ok(slice) = ob.cast_exact::() { + return Ok(Self::Slice(slice.to_owned())); } Ok(Self::Int(ob.extract()?)) } @@ -495,7 +496,7 @@ impl PyConvertToPyArray for Vec<(usize, usize, Py)> { macro_rules! custom_vec_iter_impl { ($name:ident, $iter:ident, $reversed:ident, $data:ident, $T:ty, $doc:literal) => { #[doc = $doc] - #[pyclass(module = "rustworkx", sequence)] + #[pyclass(module = "rustworkx", sequence, from_py_object)] #[derive(Clone)] pub struct $name { pub $data: Vec<$T>, @@ -658,7 +659,7 @@ macro_rules! custom_vec_iter_impl { #[doc = concat!("Custom iterator class for :class:`.", stringify!($name), "`")] // No module because this isn't constructable from Python space, and is only exposed as an // implementation detail. - #[pyclass] + #[pyclass(skip_from_py_object)] pub struct $iter { inner: Option>, index: usize, @@ -708,7 +709,7 @@ macro_rules! custom_vec_iter_impl { #[doc = concat!("Custom reversed iterator class for :class:`.", stringify!($name), "`")] // No module because this isn't constructable from Python space, and is only exposed as an // implementation detail. - #[pyclass] + #[pyclass(skip_from_py_object)] pub struct $reversed { inner: Option>, index: usize, @@ -1171,7 +1172,7 @@ impl PyGCProtocol for RelationalCoarsestPartition {} macro_rules! py_iter_protocol_impl { ($name:ident, $data:ident, $T:ty) => { - #[pyclass(module = "rustworkx")] + #[pyclass(module = "rustworkx", skip_from_py_object)] pub struct $name { pub $data: Vec<$T>, iter_pos: usize, @@ -1202,7 +1203,7 @@ macro_rules! custom_hash_map_iter_impl { $K:ty, $V:ty, $doc:literal ) => { #[doc = $doc] - #[pyclass(mapping, module = "rustworkx")] + #[pyclass(mapping, module = "rustworkx", from_py_object)] #[derive(Clone)] pub struct $name { pub $data: DictMap<$K, $V>, @@ -1404,7 +1405,7 @@ impl PyGCProtocol for EdgeIndexMap { /// second_target = next(edges_iter) /// second_path = edges[second_target] /// -#[pyclass(mapping, module = "rustworkx")] +#[pyclass(mapping, module = "rustworkx", from_py_object)] #[derive(Clone)] pub struct PathMapping { pub paths: DictMap>, @@ -1550,7 +1551,7 @@ impl PyDisplay for PathMapping { /// return a mapping of target nodes and paths. It implements the Python /// mapping protocol. So you can treat the return as a read-only /// mapping/dict. -#[pyclass(mapping, module = "rustworkx")] +#[pyclass(mapping, module = "rustworkx", from_py_object)] #[derive(Clone)] pub struct MultiplePathMapping { pub paths: DictMap>>, diff --git a/src/lib.rs b/src/lib.rs index 9a333075d2..32f7b942b7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -262,7 +262,7 @@ fn weight_callable<'p, T>( default: T, ) -> PyResult where - T: FromPyObject<'p>, + T: FromPyObjectOwned<'p, Error = PyErr>, { match weight_fn { Some(weight_fn) => { @@ -280,7 +280,7 @@ pub fn edge_weights_from_callable<'p, T, Ty: EdgeType>( default_weight: T, ) -> PyResult>> where - T: FromPyObject<'p> + Copy, + T: FromPyObjectOwned<'p, Error = PyErr> + Copy, { let mut edge_weights: Vec> = Vec::with_capacity(graph.edge_bound()); for index in 0..=graph.edge_bound() { diff --git a/src/toposort.rs b/src/toposort.rs index b241387299..5024edb959 100644 --- a/src/toposort.rs +++ b/src/toposort.rs @@ -222,7 +222,7 @@ impl TopologicalSorter { fn done(&mut self, nodes: &Bound) -> PyResult<()> { if let Ok(node) = nodes.extract::() { self.done_single(nodes.py(), NodeIndex::new(node)) - } else if let Ok(nodes) = nodes.downcast::() { + } else if let Ok(nodes) = nodes.cast::() { for node in nodes { self.done_single(nodes.py(), NodeIndex::new(node.extract()?))? }