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()?))? }