From 3c746c668c2b031158eadac38426ea1eacc2607b Mon Sep 17 00:00:00 2001 From: JuanPBedoya Date: Sun, 30 Aug 2026 20:22:33 -0500 Subject: [PATCH] Raise RuntimeError instead of PanicException when pruning on finish_vertex Raising PruneSearch from the finish_vertex event of bfs_search, dfs_search or dijkstra_search reached rustworkx-core, which panics because there is nothing left to prune once a vertex is finished. The resulting PanicException derives from BaseException, so it escaped a plain 'except Exception', and it also printed a panic message to stderr next to the Python traceback. The event is now checked in the PyO3 handlers, before the control flow value reaches the core, and a RuntimeError is raised instead. The core keeps its panic as an assertion for Rust callers. The docstrings for the three searches already noted that an exception was raised in that event but did not name the type, which is now RuntimeError. They also referred to finish_vertex with :class: rather than :meth:. Fixes #1383 --- ...-vertex-runtimeerror-9c4a1e7b2f83d605.yaml | 13 ++++++ rustworkx/__init__.py | 12 ++--- src/traversal/bfs_visit.rs | 14 +++++- src/traversal/dfs_visit.rs | 14 +++++- src/traversal/dijkstra_visit.rs | 14 +++++- src/traversal/mod.rs | 24 +++++----- tests/digraph/test_bfs_search.py | 45 ++++++++++++++++++ tests/digraph/test_dfs_search.py | 46 +++++++++++++++++++ tests/digraph/test_dijkstra_search.py | 45 ++++++++++++++++++ tests/graph/test_bfs_search.py | 45 ++++++++++++++++++ tests/graph/test_dfs_search.py | 46 +++++++++++++++++++ tests/graph/test_dijkstra_search.py | 45 ++++++++++++++++++ 12 files changed, 342 insertions(+), 21 deletions(-) create mode 100644 releasenotes/notes/prunesearch-finish-vertex-runtimeerror-9c4a1e7b2f83d605.yaml diff --git a/releasenotes/notes/prunesearch-finish-vertex-runtimeerror-9c4a1e7b2f83d605.yaml b/releasenotes/notes/prunesearch-finish-vertex-runtimeerror-9c4a1e7b2f83d605.yaml new file mode 100644 index 0000000000..9a67f0f8d6 --- /dev/null +++ b/releasenotes/notes/prunesearch-finish-vertex-runtimeerror-9c4a1e7b2f83d605.yaml @@ -0,0 +1,13 @@ +--- +fixes: + - | + Fixed :func:`~rustworkx.bfs_search`, :func:`~rustworkx.dfs_search` and + :func:`~rustworkx.dijkstra_search` raising a + ``pyo3_runtime.PanicException`` when a visitor raises + :class:`~rustworkx.visit.PruneSearch` from its ``finish_vertex`` event. + Pruning is not possible once a vertex is finished, so this was always an + error, but ``PanicException`` inherits from ``BaseException`` instead of + ``Exception``, so it was not caught by an ``except Exception`` block, and a + panic message was printed to ``stderr`` alongside the Python traceback. A + ``RuntimeError`` is now raised instead. Refer to + `#1383 `__ for more details. diff --git a/rustworkx/__init__.py b/rustworkx/__init__.py index 24660f76c5..3a737647c3 100644 --- a/rustworkx/__init__.py +++ b/rustworkx/__init__.py @@ -1834,8 +1834,8 @@ def tree_edge(self, edge): .. note:: - An exception is raised if the :class:`~rustworkx.visit.PruneSearch` is - raised in the :class:`~rustworkx.visit.BFSVisitor.finish_vertex` event. + A ``RuntimeError`` is raised if :class:`~rustworkx.visit.PruneSearch` is + raised in the :meth:`~rustworkx.visit.BFSVisitor.finish_vertex` event. :param graph: The graph to be used. This can be a :class:`~rustworkx.PyGraph` @@ -1930,8 +1930,8 @@ def tree_edge(self, edge): .. note:: - An exception is raised if the :class:`~rustworkx.visit.PruneSearch` is - raised in the :class:`~rustworkx.visit.DFSVisitor.finish_vertex` event. + A ``RuntimeError`` is raised if :class:`~rustworkx.visit.PruneSearch` is + raised in the :meth:`~rustworkx.visit.DFSVisitor.finish_vertex` event. :param graph: The graph to be used. This can be a :class:`~rustworkx.PyGraph` or a :class:`~rustworkx.PyDiGraph` @@ -2042,8 +2042,8 @@ def edge_relaxed(self, edge): .. note:: - An exception is raised if the :class:`~rustworkx.visit.PruneSearch` is - raised in the :class:`~rustworkx.visit.DijkstraVisitor.finish_vertex` event. + A ``RuntimeError`` is raised if :class:`~rustworkx.visit.PruneSearch` is + raised in the :meth:`~rustworkx.visit.DijkstraVisitor.finish_vertex` event. :param graph: The graph to be used. This can be a :class:`~rustworkx.PyGraph` or a :class:`~rustworkx.PyDiGraph`. diff --git a/src/traversal/bfs_visit.rs b/src/traversal/bfs_visit.rs index ef7f87ce27..16b7220436 100644 --- a/src/traversal/bfs_visit.rs +++ b/src/traversal/bfs_visit.rs @@ -10,6 +10,7 @@ // License for the specific language governing permissions and limitations // under the License. +use pyo3::exceptions::PyRuntimeError; use pyo3::prelude::*; use petgraph::stable_graph::NodeIndex; @@ -33,6 +34,11 @@ pub fn bfs_handler( vis: &PyBfsVisitor, event: BfsEvent>, ) -> PyResult> { + // There is nothing left to prune once a vertex is finished, and the core + // traversal panics on a `Prune` for that event. Catch it here instead, so + // Python callers get a regular exception. + let is_finish = matches!(&event, BfsEvent::Finish(..)); + let res = match event { BfsEvent::Discover(u) => vis.discover_vertex.call1(py, (u.index(),)), BfsEvent::TreeEdge(u, v, weight) => { @@ -57,7 +63,13 @@ pub fn bfs_handler( match res { Err(e) => { if e.is_instance_of::(py) { - Ok(Control::Prune) + if is_finish { + Err(PyRuntimeError::new_err( + "Pruning on the `finish_vertex` event is not supported", + )) + } else { + Ok(Control::Prune) + } } else if e.is_instance_of::(py) { Ok(Control::Break(())) } else { diff --git a/src/traversal/dfs_visit.rs b/src/traversal/dfs_visit.rs index b70d3263db..aea2b271ff 100644 --- a/src/traversal/dfs_visit.rs +++ b/src/traversal/dfs_visit.rs @@ -10,6 +10,7 @@ // License for the specific language governing permissions and limitations // under the License. +use pyo3::exceptions::PyRuntimeError; use pyo3::prelude::*; use petgraph::stable_graph::NodeIndex; @@ -32,6 +33,11 @@ pub fn dfs_handler( vis: &PyDfsVisitor, event: DfsEvent>, ) -> PyResult> { + // There is nothing left to prune once a vertex is finished, and the core + // traversal panics on a `Prune` for that event. Catch it here instead, so + // Python callers get a regular exception. + let is_finish = matches!(&event, DfsEvent::Finish(..)); + let res = match event { DfsEvent::Discover(u, Time(t)) => vis.discover_vertex.call1(py, (u.index(), t)), DfsEvent::TreeEdge(u, v, weight) => { @@ -52,7 +58,13 @@ pub fn dfs_handler( match res { Err(e) => { if e.is_instance_of::(py) { - Ok(Control::Prune) + if is_finish { + Err(PyRuntimeError::new_err( + "Pruning on the `finish_vertex` event is not supported", + )) + } else { + Ok(Control::Prune) + } } else { Err(e) } diff --git a/src/traversal/dijkstra_visit.rs b/src/traversal/dijkstra_visit.rs index 8b44ade6c4..47005a91c6 100644 --- a/src/traversal/dijkstra_visit.rs +++ b/src/traversal/dijkstra_visit.rs @@ -10,6 +10,7 @@ // License for the specific language governing permissions and limitations // under the License. +use pyo3::exceptions::PyRuntimeError; use pyo3::prelude::*; use petgraph::stable_graph::NodeIndex; @@ -32,6 +33,11 @@ pub fn dijkstra_handler( vis: &PyDijkstraVisitor, event: DijkstraEvent, f64>, ) -> PyResult> { + // There is nothing left to prune once a vertex is finished, and the core + // traversal panics on a `Prune` for that event. Catch it here instead, so + // Python callers get a regular exception. + let is_finish = matches!(&event, DijkstraEvent::Finish(..)); + let res = match event { DijkstraEvent::Discover(u, score) => vis.discover_vertex.call1(py, (u.index(), score)), DijkstraEvent::ExamineEdge(u, v, weight) => { @@ -52,7 +58,13 @@ pub fn dijkstra_handler( match res { Err(e) => { if e.is_instance_of::(py) { - Ok(Control::Prune) + if is_finish { + Err(PyRuntimeError::new_err( + "Pruning on the `finish_vertex` event is not supported", + )) + } else { + Ok(Control::Prune) + } } else if e.is_instance_of::(py) { Ok(Control::Break(())) } else { diff --git a/src/traversal/mod.rs b/src/traversal/mod.rs index 33db7361a7..cb7b7e700d 100644 --- a/src/traversal/mod.rs +++ b/src/traversal/mod.rs @@ -416,8 +416,8 @@ pub fn descendants(graph: &digraph::PyDiGraph, node: usize) -> PyResult