From d11ce531cd2ab74d6a6ed59c89477cd0ce36a822 Mon Sep 17 00:00:00 2001 From: Krishn Parasar <76171905+Krishn1412@users.noreply.github.com> Date: Sat, 2 Aug 2025 17:02:47 +0530 Subject: [PATCH 1/6] First draft Built a basic grammar and wired it up with functions to parse it and added it in the pymodule. Next steps: 1) Fix the grammar, make it robust. 2) Add tests and documentations --- Cargo.toml | 2 + src/dot_parser/dot.pest | 26 +++++++++ src/dot_parser/mod.rs | 120 ++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 3 + tests/graph/test_dot.py | 14 +++++ 5 files changed, 165 insertions(+) create mode 100644 src/dot_parser/dot.pest create mode 100644 src/dot_parser/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 2b2c09c0b0..2c901676f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,6 +64,8 @@ serde_json = "1.0" smallvec = { version = "1.0", features = ["union"] } rustworkx-core = { path = "rustworkx-core", version = "=0.17.0" } flate2 = "1.0.35" +pest = "2.7" +pest_derive = "2.7" [dependencies.pyo3] version = "0.24" diff --git a/src/dot_parser/dot.pest b/src/dot_parser/dot.pest new file mode 100644 index 0000000000..47f5ee4a8b --- /dev/null +++ b/src/dot_parser/dot.pest @@ -0,0 +1,26 @@ +WHITESPACE = _{ " " | "\t" | "\r" | "\n" } + +graph_file = { SOI ~ graph_decl ~ EOI } // <-- New top-level rule + +graph_decl = { graph_type ~ ident? ~ "{" ~ stmt_list ~ "}" } + +graph_type = { "graph" | "digraph" } + +stmt_list = { stmt* } +stmt = _{ node_stmt | edge_stmt | attr_stmt | ";" } + +ident = @{ (ASCII_ALPHANUMERIC | "_" | ".")+ } + +node_stmt = { ident ~ attr_list? ~ ";"? } + +edge_stmt = { ident ~ edge_op ~ ident ~ attr_list? ~ ";"? } + +edge_op = { "--" | "->" } + +attr_stmt = { ident ~ "=" ~ ident ~ ";"? } + +attr_list = { "[" ~ attr_pair ~ ("," ~ attr_pair)* ~ "]" } + +attr_pair = { ident ~ "=" ~ (ident | quoted_string) } + +quoted_string = @{ "\"" ~ (!( "\"" ) ~ ANY)* ~ "\"" } diff --git a/src/dot_parser/mod.rs b/src/dot_parser/mod.rs new file mode 100644 index 0000000000..8ca1a05d68 --- /dev/null +++ b/src/dot_parser/mod.rs @@ -0,0 +1,120 @@ +use pest::Parser; +use pest_derive::Parser; +use pyo3::prelude::*; +use pyo3::types::PyString; + +use crate::StablePyGraph; +use crate::digraph::PyDiGraph; +use crate::graph::PyGraph; +// use crate::dot_parser::Rule; + +use rustworkx_core::petgraph::Directed; +use rustworkx_core::petgraph::Undirected; + +#[derive(Parser)] +#[grammar = "dot_parser/dot.pest"] +pub struct DotParser; + +#[pyfunction] +pub fn from_dot(py: Python<'_>, dot_str: &str) -> PyResult { + let pairs = DotParser::parse(Rule::graph_file, dot_str) + .map_err(|e| PyErr::new::(format!("DOT parse error: {}", e)))?; + + let mut is_directed = false; + let mut node_map = std::collections::HashMap::new(); + + // We'll populate these after we know graph type + let mut di_graph: Option = None; + let mut undi_graph: Option = None; + + for pair in pairs { + if pair.as_rule() != Rule::graph_file { + continue; + } + let mut inner = pair.into_inner(); + let gtype = inner.next().unwrap().as_str(); + is_directed = gtype == "digraph"; + + if is_directed { + // Create a directed StablePyGraph with some initial capacity + let mut graph = StablePyGraph::::with_capacity(0, 0); + di_graph = Some(PyDiGraph { + graph, + cycle_state: rustworkx_core::petgraph::algo::DfsSpace::default(), + check_cycle: false, + node_removed: false, + multigraph: true, + attrs: py.None() + }); + } else { + // Create an undirected StablePyGraph with some initial capacity + let mut graph = StablePyGraph::::with_capacity(0, 0); + undi_graph = Some(PyGraph { + graph, + node_removed: false, + multigraph: true, + attrs: py.None() + }); + } + + // Now parse statements + for stmt in inner { + if stmt.as_rule() != Rule::stmt_list { + continue; + } + for s in stmt.into_inner() { + match s.as_rule() { + Rule::node_stmt => { + let mut tokens = s.into_inner(); + let name = tokens.next().unwrap().as_str().to_string(); + let py_node = PyString::new(py, &name).to_object(py); + + let idx = if is_directed { + di_graph.as_mut().unwrap().graph.add_node(py_node) + } else { + undi_graph.as_mut().unwrap().graph.add_node(py_node) + }; + node_map.insert(name, idx); + } + Rule::edge_stmt => { + let mut tokens = s.into_inner(); + let source = tokens.next().unwrap().as_str().to_string(); + let _arrow = tokens.next().unwrap(); + let target = tokens.next().unwrap().as_str().to_string(); + + let source_idx = *node_map.entry(source.clone()).or_insert_with(|| { + let py_node = PyString::new(py, &source).to_object(py); + if is_directed { + di_graph.as_mut().unwrap().graph.add_node(py_node) + } else { + undi_graph.as_mut().unwrap().graph.add_node(py_node) + } + }); + let target_idx = *node_map.entry(target.clone()).or_insert_with(|| { + let py_node = PyString::new(py, &target).to_object(py); + if is_directed { + di_graph.as_mut().unwrap().graph.add_node(py_node) + } else { + undi_graph.as_mut().unwrap().graph.add_node(py_node) + } + }); + + let edge_weight = PyString::new(py, "1").to_object(py); + if is_directed { + di_graph.as_mut().unwrap().graph.add_edge(source_idx, target_idx, edge_weight); + } else { + undi_graph.as_mut().unwrap().graph.add_edge(source_idx, target_idx, edge_weight); + } + } + _ => {} + } + } + } + } + + if is_directed { + Ok(di_graph.unwrap().into_py(py)) + } else { + Ok(undi_graph.unwrap().into_py(py)) + } +} diff --git a/src/lib.rs b/src/lib.rs index 8da3e7f18f..6a9bea20a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,6 +39,7 @@ mod token_swapper; mod toposort; mod transitivity; mod traversal; +mod dot_parser; mod tree; mod union; @@ -65,6 +66,7 @@ use tensor_product::*; use token_swapper::*; use transitivity::*; use traversal::*; +use dot_parser::*; use tree::*; use union::*; @@ -679,6 +681,7 @@ fn rustworkx(py: Python<'_>, m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(parse_node_link_json))?; m.add_wrapped(wrap_pyfunction!(pagerank))?; m.add_wrapped(wrap_pyfunction!(hits))?; + m.add_wrapped(wrap_pyfunction!(from_dot))?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/tests/graph/test_dot.py b/tests/graph/test_dot.py index 63c98804ee..c20902c8a3 100644 --- a/tests/graph/test_dot.py +++ b/tests/graph/test_dot.py @@ -128,3 +128,17 @@ def test_graph_no_args(self): graph = rustworkx.undirected_gnp_random_graph(3, 0.95, seed=24) dot_str = graph.to_dot() self.assertEqual("graph {\n0 ;\n1 ;\n2 ;\n2 -- 0 ;\n2 -- 1 ;\n}\n", dot_str) + + +def test_from_dot(): + expected = ( + 'graph {\n0 [color=black, fillcolor=green, label="a", ' + 'style=filled];\n1 [color=black, fillcolor=red, label="a", ' + 'style=filled];\n0 -- 1 [label="1", name=1];\n}\n' + ) + g = rustworkx.from_dot(expected) + print(g) + print(g.nodes()) + print(g.edges()) + +test_from_dot() From 06273db9e13c1da389dc0685bfa451c5d6dcb35c Mon Sep 17 00:00:00 2001 From: Krishn1412 Date: Fri, 8 Aug 2025 23:13:59 +0530 Subject: [PATCH 2/6] Updating grammar --- src/dot_parser/dot.pest | 44 +++++++---- src/dot_parser/mod.rs | 164 +++++++++++++++++++++++++++++----------- 2 files changed, 150 insertions(+), 58 deletions(-) diff --git a/src/dot_parser/dot.pest b/src/dot_parser/dot.pest index 47f5ee4a8b..d8ba191336 100644 --- a/src/dot_parser/dot.pest +++ b/src/dot_parser/dot.pest @@ -1,26 +1,44 @@ WHITESPACE = _{ " " | "\t" | "\r" | "\n" } +COMMENT = _{ "#" ~ (!NEWLINE ~ ANY)* | "//" ~ (!NEWLINE ~ ANY)* | "/*" ~ (!"*/" ~ ANY)* ~ "*/" } +NEWLINE = _{ "\n" | "\r\n" } -graph_file = { SOI ~ graph_decl ~ EOI } // <-- New top-level rule - -graph_decl = { graph_type ~ ident? ~ "{" ~ stmt_list ~ "}" } +graph_file = { SOI ~ strict? ~ graph_type ~ id? ~ "{" ~ stmt_list? ~ "}" ~ EOI } +strict = { "strict" } graph_type = { "graph" | "digraph" } -stmt_list = { stmt* } -stmt = _{ node_stmt | edge_stmt | attr_stmt | ";" } +id = _{ number | identifier | quoted_id | html_id } + +number = @{ ASCII_DIGIT+ } +identifier = @{ (ASCII_ALPHANUMERIC | "_" | "." | "/" | "\\") + } +quoted_id = @{ "\"" ~ (("\\\"" | (!"\"" ~ ANY))*) ~ "\"" } +html_id = @{ "<" ~ (!">" ~ ANY)* ~ ">" } + +stmt_list = { (stmt ~ ";"?)* } + +stmt = _{ + node_stmt + | edge_stmt + | attr_stmt + | assignment + | subgraph +} + +node_stmt = { node_id ~ attr_list* } -ident = @{ (ASCII_ALPHANUMERIC | "_" | ".")+ } +edge_stmt = { edge_point ~ (edge_op ~ edge_point)+ ~ attr_list* } -node_stmt = { ident ~ attr_list? ~ ";"? } +edge_op = { "->" | "--" } -edge_stmt = { ident ~ edge_op ~ ident ~ attr_list? ~ ";"? } +edge_point = { node_id | subgraph } -edge_op = { "--" | "->" } +attr_stmt = { ("graph" | "node" | "edge") ~ attr_list+ } -attr_stmt = { ident ~ "=" ~ ident ~ ";"? } +attr_list = { "[" ~ a_list? ~ "]" } +a_list = { (id ~ ("=" ~ id)? ~ (","?))* } -attr_list = { "[" ~ attr_pair ~ ("," ~ attr_pair)* ~ "]" } +assignment = { id ~ "=" ~ id } -attr_pair = { ident ~ "=" ~ (ident | quoted_string) } +subgraph = { "subgraph" ~ id? ~ "{" ~ stmt_list? ~ "}" } -quoted_string = @{ "\"" ~ (!( "\"" ) ~ ANY)* ~ "\"" } +node_id = { id ~ (":" ~ id)? ~ (":" ~ id)? } diff --git a/src/dot_parser/mod.rs b/src/dot_parser/mod.rs index 8ca1a05d68..68daec2378 100644 --- a/src/dot_parser/mod.rs +++ b/src/dot_parser/mod.rs @@ -1,15 +1,15 @@ use pest::Parser; use pest_derive::Parser; use pyo3::prelude::*; -use pyo3::types::PyString; +use pyo3::types::{PyDict, PyString}; +use pyo3::IntoPy; use crate::StablePyGraph; use crate::digraph::PyDiGraph; use crate::graph::PyGraph; -// use crate::dot_parser::Rule; -use rustworkx_core::petgraph::Directed; -use rustworkx_core::petgraph::Undirected; +use rustworkx_core::petgraph::prelude::{NodeIndex, Directed, Undirected}; +use std::collections::HashMap; #[derive(Parser)] #[grammar = "dot_parser/dot.pest"] @@ -21,91 +21,151 @@ pub fn from_dot(py: Python<'_>, dot_str: &str) -> PyResult { .map_err(|e| PyErr::new::(format!("DOT parse error: {}", e)))?; let mut is_directed = false; - let mut node_map = std::collections::HashMap::new(); + let mut node_map: HashMap = HashMap::new(); - // We'll populate these after we know graph type let mut di_graph: Option = None; let mut undi_graph: Option = None; + // Initialize an empty dict of attributes for the graph + let mut graph_attrs = PyDict::new(py).into_py(py); + for pair in pairs { if pair.as_rule() != Rule::graph_file { continue; } + let mut inner = pair.into_inner(); - let gtype = inner.next().unwrap().as_str(); - is_directed = gtype == "digraph"; + // Handle optional 'strict' keyword + let first = inner.next().unwrap(); + let graph_type = if first.as_rule() == Rule::strict { + inner.next().unwrap().as_str() + } else { + first.as_str() + }; + is_directed = graph_type == "digraph"; + + // Create appropriate graph wrapper if is_directed { - // Create a directed StablePyGraph with some initial capacity - let mut graph = StablePyGraph::::with_capacity(0, 0); + let graph = StablePyGraph::::with_capacity(0, 0); di_graph = Some(PyDiGraph { graph, cycle_state: rustworkx_core::petgraph::algo::DfsSpace::default(), check_cycle: false, node_removed: false, multigraph: true, - attrs: py.None() + attrs: graph_attrs.clone_ref(py), }); } else { - // Create an undirected StablePyGraph with some initial capacity - let mut graph = StablePyGraph::::with_capacity(0, 0); + let graph = StablePyGraph::::with_capacity(0, 0); undi_graph = Some(PyGraph { graph, node_removed: false, multigraph: true, - attrs: py.None() + attrs: graph_attrs.clone_ref(py), }); } - // Now parse statements - for stmt in inner { - if stmt.as_rule() != Rule::stmt_list { + for stmt_pair in inner { + if stmt_pair.as_rule() != Rule::stmt_list { continue; } - for s in stmt.into_inner() { - match s.as_rule() { - Rule::node_stmt => { - let mut tokens = s.into_inner(); - let name = tokens.next().unwrap().as_str().to_string(); - let py_node = PyString::new(py, &name).to_object(py); + for stmt in stmt_pair.into_inner() { + match stmt.as_rule() { + Rule::node_stmt => { + let mut parts = stmt.into_inner(); + let name = parts.next().unwrap().as_str().to_string(); + let py_node = PyString::new(py, &name).into_py(py); let idx = if is_directed { di_graph.as_mut().unwrap().graph.add_node(py_node) } else { undi_graph.as_mut().unwrap().graph.add_node(py_node) }; - node_map.insert(name, idx); + node_map.insert(name.clone(), idx); + + if let Some(attr_list) = parts.next() { + let _attrs = parse_attrs(py, attr_list.into_inner()); + // Future: Use or store node attributes here + } } + Rule::edge_stmt => { - let mut tokens = s.into_inner(); - let source = tokens.next().unwrap().as_str().to_string(); - let _arrow = tokens.next().unwrap(); - let target = tokens.next().unwrap().as_str().to_string(); + let mut parts = stmt.into_inner(); + let mut endpoints = vec![]; - let source_idx = *node_map.entry(source.clone()).or_insert_with(|| { - let py_node = PyString::new(py, &source).to_object(py); - if is_directed { - di_graph.as_mut().unwrap().graph.add_node(py_node) - } else { - undi_graph.as_mut().unwrap().graph.add_node(py_node) + // Collect node identifiers + while let Some(token) = parts.peek() { + match token.as_rule() { + Rule::edge_op => { parts.next(); } + Rule::node_id | Rule::subgraph => { + endpoints.push(parts.next().unwrap()); + } + _ => break, } - }); - let target_idx = *node_map.entry(target.clone()).or_insert_with(|| { - let py_node = PyString::new(py, &target).to_object(py); + } + + // Edge attributes or default empty dict + let attrs = parts + .next() + .map(|p| parse_attrs(py, p.into_inner())) + .unwrap_or_else(|| PyDict::new(py).into_py(py)); + + for pair in endpoints.windows(2) { + let src = pair[0].as_str().to_string(); + let dst = pair[1].as_str().to_string(); + + let src_idx = *node_map.entry(src.clone()).or_insert_with(|| { + let py_node = PyString::new(py, &src).into_py(py); + if is_directed { + di_graph.as_mut().unwrap().graph.add_node(py_node) + } else { + undi_graph.as_mut().unwrap().graph.add_node(py_node) + } + }); + + let dst_idx = *node_map.entry(dst.clone()).or_insert_with(|| { + let py_node = PyString::new(py, &dst).into_py(py); + if is_directed { + di_graph.as_mut().unwrap().graph.add_node(py_node) + } else { + undi_graph.as_mut().unwrap().graph.add_node(py_node) + } + }); + if is_directed { - di_graph.as_mut().unwrap().graph.add_node(py_node) + di_graph.as_mut().unwrap().graph.add_edge(src_idx, dst_idx, attrs.clone_ref(py)); } else { - undi_graph.as_mut().unwrap().graph.add_node(py_node) + undi_graph.as_mut().unwrap().graph.add_edge(src_idx, dst_idx, attrs.clone_ref(py)); } - }); + } + } - let edge_weight = PyString::new(py, "1").to_object(py); - if is_directed { - di_graph.as_mut().unwrap().graph.add_edge(source_idx, target_idx, edge_weight); - } else { - undi_graph.as_mut().unwrap().graph.add_edge(source_idx, target_idx, edge_weight); + Rule::attr_stmt => { + let mut parts = stmt.into_inner(); + let target = parts.next().unwrap().as_str(); + let attrs = parse_attrs(py, parts.flat_map(|p| p.into_inner())); + if target == "graph" { + graph_attrs = attrs; } } + + Rule::assignment => { + let mut parts = stmt.into_inner(); + let key = parts.next().unwrap().as_str(); + let val = parts.next().unwrap().as_str(); + + let any = graph_attrs.bind(py); // Returns Bound<'_, PyAny> + let dict = any.downcast::()?; // Downcast to PyDict + + dict.set_item(key, val)?; // Set key-value in dict + } + + + Rule::subgraph => { + // TODO: Add subgraph handling if needed + } + _ => {} } } @@ -118,3 +178,17 @@ pub fn from_dot(py: Python<'_>, dot_str: &str) -> PyResult { Ok(undi_graph.unwrap().into_py(py)) } } + +fn parse_attrs<'a>( + py: Python<'a>, + pairs: impl Iterator>, +) -> PyObject { + let dict = PyDict::new(py); + for pair in pairs { + let mut kv = pair.into_inner(); + if let (Some(k), Some(v)) = (kv.next(), kv.next()) { + dict.set_item(k.as_str(), v.as_str()).unwrap(); + } + } + dict.into_py(py) +} From 7d849dee71eb56272a2e0b27847709d102f2fa94 Mon Sep 17 00:00:00 2001 From: Krishn Parasar Date: Sun, 10 Aug 2025 02:53:43 +0530 Subject: [PATCH 3/6] from_dot feature --- src/dot_parser/dot.pest | 13 +- src/dot_parser/mod.rs | 261 ++++++++++++++++++++++++++++------------ src/lib.rs | 4 +- tests/graph/test_dot.py | 30 +++-- 4 files changed, 209 insertions(+), 99 deletions(-) diff --git a/src/dot_parser/dot.pest b/src/dot_parser/dot.pest index d8ba191336..dad44a31d3 100644 --- a/src/dot_parser/dot.pest +++ b/src/dot_parser/dot.pest @@ -10,22 +10,21 @@ graph_type = { "graph" | "digraph" } id = _{ number | identifier | quoted_id | html_id } number = @{ ASCII_DIGIT+ } -identifier = @{ (ASCII_ALPHANUMERIC | "_" | "." | "/" | "\\") + } +identifier = @{ (ASCII_ALPHANUMERIC | "_" | "." | "/" | "\\" | "-")+ } quoted_id = @{ "\"" ~ (("\\\"" | (!"\"" ~ ANY))*) ~ "\"" } html_id = @{ "<" ~ (!">" ~ ANY)* ~ ">" } -stmt_list = { (stmt ~ ";"?)* } +stmt_list = { (stmt ~ (";" | NEWLINE)*)* } stmt = _{ - node_stmt - | edge_stmt + edge_stmt + | node_stmt | attr_stmt | assignment | subgraph } node_stmt = { node_id ~ attr_list* } - edge_stmt = { edge_point ~ (edge_op ~ edge_point)+ ~ attr_list* } edge_op = { "->" | "--" } @@ -35,10 +34,10 @@ edge_point = { node_id | subgraph } attr_stmt = { ("graph" | "node" | "edge") ~ attr_list+ } attr_list = { "[" ~ a_list? ~ "]" } -a_list = { (id ~ ("=" ~ id)? ~ (","?))* } +a_list = { (id ~ ("=" ~ id)? ~ ("," | ";")?)* } assignment = { id ~ "=" ~ id } subgraph = { "subgraph" ~ id? ~ "{" ~ stmt_list? ~ "}" } -node_id = { id ~ (":" ~ id)? ~ (":" ~ id)? } +node_id = { id ~ (":" ~ id)? ~ (":" ~ id)? } \ No newline at end of file diff --git a/src/dot_parser/mod.rs b/src/dot_parser/mod.rs index 68daec2378..094194a977 100644 --- a/src/dot_parser/mod.rs +++ b/src/dot_parser/mod.rs @@ -2,23 +2,70 @@ use pest::Parser; use pest_derive::Parser; use pyo3::prelude::*; use pyo3::types::{PyDict, PyString}; -use pyo3::IntoPy; -use crate::StablePyGraph; use crate::digraph::PyDiGraph; use crate::graph::PyGraph; +use crate::StablePyGraph; -use rustworkx_core::petgraph::prelude::{NodeIndex, Directed, Undirected}; +use rustworkx_core::petgraph::prelude::{Directed, NodeIndex, Undirected}; use std::collections::HashMap; #[derive(Parser)] #[grammar = "dot_parser/dot.pest"] pub struct DotParser; +/// Unquote a quoted string +fn unquote_str(s: &str) -> String { + let t = s.trim(); + if t.starts_with('"') && t.ends_with('"') && t.len() >= 2 { + t[1..t.len() - 1] + .replace("\\\"", "\"") + .replace("\\\\", "\\") + } else { + t.to_string() + } +} + +/// Parse an `attr_list` pair into a Rust HashMap +fn parse_attr_list_to_map(pair: pest::iterators::Pair) -> HashMap { + let mut map = HashMap::new(); + for a_list in pair.into_inner() { + if a_list.as_rule() != Rule::a_list { + continue; + } + let tokens: Vec<_> = a_list.into_inner().collect(); + let mut i = 0usize; + while i < tokens.len() { + // tokens are (id) (maybe "=") (maybe id), but pyparsing/pest flattening depends on grammar. + // The simple approach here: take token i as key; if token i+1 exists and is not a comma (we filtered commas out in grammar), + // treat it as value. + let key = tokens[i].as_str().trim().to_string(); + if i + 1 < tokens.len() { + let val = tokens[i + 1].as_str().trim().to_string(); + map.insert(key, unquote_str(&val)); + i += 2; + } else { + map.insert(key, String::new()); + i += 1; + } + } + } + map +} + +/// Extract the first inner token of node_id +fn node_id_to_string(pair: pest::iterators::Pair) -> String { + if let Some(child) = pair.into_inner().next() { + return unquote_str(child.as_str().trim()); + } + String::new() +} + #[pyfunction] pub fn from_dot(py: Python<'_>, dot_str: &str) -> PyResult { - let pairs = DotParser::parse(Rule::graph_file, dot_str) - .map_err(|e| PyErr::new::(format!("DOT parse error: {}", e)))?; + let pairs = DotParser::parse(Rule::graph_file, dot_str).map_err(|e| { + PyErr::new::(format!("DOT parse error: {}", e)) + })?; let mut is_directed = false; let mut node_map: HashMap = HashMap::new(); @@ -26,8 +73,12 @@ pub fn from_dot(py: Python<'_>, dot_str: &str) -> PyResult { let mut di_graph: Option = None; let mut undi_graph: Option = None; - // Initialize an empty dict of attributes for the graph - let mut graph_attrs = PyDict::new(py).into_py(py); + let graph_attrs = PyDict::new(py); + + let mut default_node_attrs: HashMap = HashMap::new(); + let mut default_edge_attrs: HashMap = HashMap::new(); + + let mut node_attrs_map: HashMap = HashMap::new(); for pair in pairs { if pair.as_rule() != Rule::graph_file { @@ -35,17 +86,16 @@ pub fn from_dot(py: Python<'_>, dot_str: &str) -> PyResult { } let mut inner = pair.into_inner(); - // Handle optional 'strict' keyword + + // handle graph_type let first = inner.next().unwrap(); - let graph_type = if first.as_rule() == Rule::strict { + let graph_type_str = if first.as_rule() == Rule::strict { inner.next().unwrap().as_str() } else { first.as_str() }; + is_directed = graph_type_str == "digraph"; - is_directed = graph_type == "digraph"; - - // Create appropriate graph wrapper if is_directed { let graph = StablePyGraph::::with_capacity(0, 0); di_graph = Some(PyDiGraph { @@ -54,7 +104,7 @@ pub fn from_dot(py: Python<'_>, dot_str: &str) -> PyResult { check_cycle: false, node_removed: false, multigraph: true, - attrs: graph_attrs.clone_ref(py), + attrs: graph_attrs.clone().into(), }); } else { let graph = StablePyGraph::::with_capacity(0, 0); @@ -62,61 +112,90 @@ pub fn from_dot(py: Python<'_>, dot_str: &str) -> PyResult { graph, node_removed: false, multigraph: true, - attrs: graph_attrs.clone_ref(py), + attrs: graph_attrs.clone().into(), }); } - for stmt_pair in inner { - if stmt_pair.as_rule() != Rule::stmt_list { + // handle stmt_list + for rest in inner { + if rest.as_rule() != Rule::stmt_list { continue; } - for stmt in stmt_pair.into_inner() { + for stmt in rest.into_inner() { match stmt.as_rule() { Rule::node_stmt => { - let mut parts = stmt.into_inner(); - let name = parts.next().unwrap().as_str().to_string(); - let py_node = PyString::new(py, &name).into_py(py); + let mut it = stmt.into_inner(); + let nid = it.next().unwrap(); + let name = node_id_to_string(nid); + // create python node object + let py_node_obj = PyString::new(py, &name).into(); + + // add node to graph let idx = if is_directed { - di_graph.as_mut().unwrap().graph.add_node(py_node) + di_graph.as_mut().unwrap().graph.add_node(py_node_obj) } else { - undi_graph.as_mut().unwrap().graph.add_node(py_node) + undi_graph.as_mut().unwrap().graph.add_node(py_node_obj) }; node_map.insert(name.clone(), idx); - if let Some(attr_list) = parts.next() { - let _attrs = parse_attrs(py, attr_list.into_inner()); - // Future: Use or store node attributes here + // produce merged attrs + let merged = PyDict::new(py); + for (k, v) in default_node_attrs.iter() { + merged.set_item(k, v)?; } + for maybe_attr in it { + if maybe_attr.as_rule() == Rule::attr_list { + let map = parse_attr_list_to_map(maybe_attr); + for (k, v) in map { + merged.set_item(k, v)?; + } + } + } + node_attrs_map.insert(name.clone(), merged.into()); } Rule::edge_stmt => { - let mut parts = stmt.into_inner(); - let mut endpoints = vec![]; - - // Collect node identifiers - while let Some(token) = parts.peek() { - match token.as_rule() { - Rule::edge_op => { parts.next(); } - Rule::node_id | Rule::subgraph => { - endpoints.push(parts.next().unwrap()); + // edge_stmt + let mut endpoints: Vec = Vec::new(); + let mut operators: Vec = Vec::new(); + + // start collected edge attrs from defaults + let collected = PyDict::new(py); + for (k, v) in default_edge_attrs.iter() { + collected.set_item(k, v)?; + } + + for child in stmt.into_inner() { + match child.as_rule() { + Rule::edge_point => { + for ep_child in child.into_inner() { + if ep_child.as_rule() == Rule::node_id { + let n = node_id_to_string(ep_child); + endpoints.push(n); + } + } + } + Rule::edge_op => { + operators.push(child.as_str().trim().to_string()); + } + Rule::attr_list => { + let map = parse_attr_list_to_map(child); + for (k, v) in map { + collected.set_item(k, v)?; + } } - _ => break, + _ => {} } } - // Edge attributes or default empty dict - let attrs = parts - .next() - .map(|p| parse_attrs(py, p.into_inner())) - .unwrap_or_else(|| PyDict::new(py).into_py(py)); - - for pair in endpoints.windows(2) { - let src = pair[0].as_str().to_string(); - let dst = pair[1].as_str().to_string(); + // create pairwise edges and add them to the graph + for i in 0..endpoints.len().saturating_sub(1) { + let src = endpoints[i].clone(); + let dst = endpoints[i + 1].clone(); let src_idx = *node_map.entry(src.clone()).or_insert_with(|| { - let py_node = PyString::new(py, &src).into_py(py); + let py_node = PyString::new(py, &src).into(); if is_directed { di_graph.as_mut().unwrap().graph.add_node(py_node) } else { @@ -125,7 +204,7 @@ pub fn from_dot(py: Python<'_>, dot_str: &str) -> PyResult { }); let dst_idx = *node_map.entry(dst.clone()).or_insert_with(|| { - let py_node = PyString::new(py, &dst).into_py(py); + let py_node = PyString::new(py, &dst).into(); if is_directed { di_graph.as_mut().unwrap().graph.add_node(py_node) } else { @@ -133,37 +212,69 @@ pub fn from_dot(py: Python<'_>, dot_str: &str) -> PyResult { } }); + let edge_attrs_obj: PyObject = collected.clone().into(); + if is_directed { - di_graph.as_mut().unwrap().graph.add_edge(src_idx, dst_idx, attrs.clone_ref(py)); + di_graph.as_mut().unwrap().graph.add_edge( + src_idx, + dst_idx, + edge_attrs_obj.clone(), + ); } else { - undi_graph.as_mut().unwrap().graph.add_edge(src_idx, dst_idx, attrs.clone_ref(py)); + undi_graph.as_mut().unwrap().graph.add_edge( + src_idx, + dst_idx, + edge_attrs_obj.clone(), + ); } } } Rule::attr_stmt => { - let mut parts = stmt.into_inner(); - let target = parts.next().unwrap().as_str(); - let attrs = parse_attrs(py, parts.flat_map(|p| p.into_inner())); - if target == "graph" { - graph_attrs = attrs; + let mut it = stmt.into_inner(); + if let Some(target_pair) = it.next() { + let target = target_pair.as_str(); + for rest in it { + if rest.as_rule() == Rule::attr_list { + let map = parse_attr_list_to_map(rest); + match target { + "graph" => { + for (k, v) in map { + graph_attrs.set_item(k, v)?; + } + if let Some(dg) = di_graph.as_mut() { + dg.attrs = graph_attrs.clone().into(); + } + if let Some(ug) = undi_graph.as_mut() { + ug.attrs = graph_attrs.clone().into(); + } + } + "node" => { + for (k, v) in map { + default_node_attrs.insert(k, v); + } + } + "edge" => { + for (k, v) in map { + default_edge_attrs.insert(k, v); + } + } + _ => {} + } + } + } } } Rule::assignment => { let mut parts = stmt.into_inner(); - let key = parts.next().unwrap().as_str(); - let val = parts.next().unwrap().as_str(); - - let any = graph_attrs.bind(py); // Returns Bound<'_, PyAny> - let dict = any.downcast::()?; // Downcast to PyDict - - dict.set_item(key, val)?; // Set key-value in dict + let key = parts.next().map(|p| p.as_str()).unwrap_or(""); + let val = parts.next().map(|p| p.as_str()).unwrap_or(""); + graph_attrs.set_item(key, val)?; } - Rule::subgraph => { - // TODO: Add subgraph handling if needed + // TODO: subgraph handling } _ => {} @@ -173,22 +284,16 @@ pub fn from_dot(py: Python<'_>, dot_str: &str) -> PyResult { } if is_directed { - Ok(di_graph.unwrap().into_py(py)) + let dg = di_graph + .take() + .expect("directed graph was created but now missing"); + let py_obj = Py::new(py, dg)?.into(); + Ok(py_obj) } else { - Ok(undi_graph.unwrap().into_py(py)) - } -} - -fn parse_attrs<'a>( - py: Python<'a>, - pairs: impl Iterator>, -) -> PyObject { - let dict = PyDict::new(py); - for pair in pairs { - let mut kv = pair.into_inner(); - if let (Some(k), Some(v)) = (kv.next(), kv.next()) { - dict.set_item(k.as_str(), v.as_str()).unwrap(); - } + let ug = undi_graph + .take() + .expect("undirected graph was created but now missing"); + let py_obj = Py::new(py, ug)?.into(); + Ok(py_obj) } - dict.into_py(py) } diff --git a/src/lib.rs b/src/lib.rs index 6a9bea20a8..5fa9fc5117 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,7 @@ mod connectivity; mod dag_algo; mod digraph; mod dominance; +mod dot_parser; mod dot_utils; mod generators; mod graph; @@ -39,7 +40,6 @@ mod token_swapper; mod toposort; mod transitivity; mod traversal; -mod dot_parser; mod tree; mod union; @@ -57,6 +57,7 @@ use layout::*; use line_graph::*; use link_analysis::*; +use dot_parser::*; use matching::*; use planar::*; use random_graph::*; @@ -66,7 +67,6 @@ use tensor_product::*; use token_swapper::*; use transitivity::*; use traversal::*; -use dot_parser::*; use tree::*; use union::*; diff --git a/tests/graph/test_dot.py b/tests/graph/test_dot.py index c20902c8a3..3d51d4ba0c 100644 --- a/tests/graph/test_dot.py +++ b/tests/graph/test_dot.py @@ -129,16 +129,22 @@ def test_graph_no_args(self): dot_str = graph.to_dot() self.assertEqual("graph {\n0 ;\n1 ;\n2 ;\n2 -- 0 ;\n2 -- 1 ;\n}\n", dot_str) + def test_from_dot_graph(self): + dot_str = """graph { + 0 [color=black, fillcolor=green, label="a", style=filled]; + 1 [color=black, fillcolor=red, label="a", style=filled]; + 0 -- 1 [label="1", name=1]; + }""" + g = rustworkx.from_dot(dot_str) + self.assertEqual(len(g.nodes()), 2) + self.assertEqual(len(g.edges()), 1) -def test_from_dot(): - expected = ( - 'graph {\n0 [color=black, fillcolor=green, label="a", ' - 'style=filled];\n1 [color=black, fillcolor=red, label="a", ' - 'style=filled];\n0 -- 1 [label="1", name=1];\n}\n' - ) - g = rustworkx.from_dot(expected) - print(g) - print(g.nodes()) - print(g.edges()) - -test_from_dot() + def test_from_dot_digraph(self): + dot_str = """digraph { + 0 [color=black, fillcolor=green, label="a", style=filled]; + 1 [color=black, fillcolor=red, label="a", style=filled]; + 0 -> 1 [label="1", name=1]; + }""" + g = rustworkx.from_dot(dot_str) + self.assertEqual(len(g.nodes()), 2) + self.assertEqual(len(g.edges()), 1) \ No newline at end of file From a40b782be2710ca73c4fa5ea37ca7acd333d1739 Mon Sep 17 00:00:00 2001 From: Krishn Parasar Date: Sat, 23 Aug 2025 23:47:41 +0530 Subject: [PATCH 4/6] Updates based on comments 1) Added a new function to use stable graph with is_directed as a parameter to avoid creating python object every time. 2) Added more tests to include round trip from to_dot and from_dot for graph and digraph. --- src/dot_parser/mod.rs | 214 +++++++++++++++++++++------------------- tests/graph/test_dot.py | 58 ++++++++++- 2 files changed, 171 insertions(+), 101 deletions(-) diff --git a/src/dot_parser/mod.rs b/src/dot_parser/mod.rs index 094194a977..3708244927 100644 --- a/src/dot_parser/mod.rs +++ b/src/dot_parser/mod.rs @@ -14,6 +14,46 @@ use std::collections::HashMap; #[grammar = "dot_parser/dot.pest"] pub struct DotParser; +/// Keep a single graph value that can be either directed or undirected. This avoids generic return-type mismatches. +enum DotGraph { + Directed(StablePyGraph), + Undirected(StablePyGraph), +} + +impl DotGraph { + fn new_directed() -> Self { + DotGraph::Directed(StablePyGraph::::with_capacity(0, 0)) + } + fn new_undirected() -> Self { + DotGraph::Undirected(StablePyGraph::::with_capacity(0, 0)) + } + fn add_node(&mut self, w: PyObject) -> NodeIndex { + match self { + DotGraph::Directed(g) => g.add_node(w), + DotGraph::Undirected(g) => g.add_node(w), + } + } + fn add_edge(&mut self, a: NodeIndex, b: NodeIndex, w: PyObject) { + match self { + DotGraph::Directed(g) => { + g.add_edge(a, b, w); + } + DotGraph::Undirected(g) => { + g.add_edge(a, b, w); + } + } + } + fn is_directed(&self) -> bool { + matches!(self, DotGraph::Directed(_)) + } + fn into_inner(self) -> Result, StablePyGraph> { + match self { + DotGraph::Directed(g) => Ok(g), + DotGraph::Undirected(g) => Err(g), + } + } +} + /// Unquote a quoted string fn unquote_str(s: &str) -> String { let t = s.trim(); @@ -36,9 +76,6 @@ fn parse_attr_list_to_map(pair: pest::iterators::Pair) -> HashMap = a_list.into_inner().collect(); let mut i = 0usize; while i < tokens.len() { - // tokens are (id) (maybe "=") (maybe id), but pyparsing/pest flattening depends on grammar. - // The simple approach here: take token i as key; if token i+1 exists and is not a comma (we filtered commas out in grammar), - // treat it as value. let key = tokens[i].as_str().trim().to_string(); if i + 1 < tokens.len() { let val = tokens[i + 1].as_str().trim().to_string(); @@ -67,12 +104,32 @@ pub fn from_dot(py: Python<'_>, dot_str: &str) -> PyResult { PyErr::new::(format!("DOT parse error: {}", e)) })?; + // Detect directedness from a clone of the iterator so we don't consume it. let mut is_directed = false; - let mut node_map: HashMap = HashMap::new(); + for pair in pairs.clone() { + if pair.as_rule() != Rule::graph_file { + continue; + } + let mut inner = pair.into_inner(); + let first = inner.next().unwrap(); + let graph_type_str = if first.as_rule() == Rule::strict { + inner.next().unwrap().as_str() + } else { + first.as_str() + }; + is_directed = graph_type_str == "digraph"; + break; + } - let mut di_graph: Option = None; - let mut undi_graph: Option = None; + build_graph_enum(py, pairs, is_directed) +} +fn build_graph_enum( + py: Python<'_>, + pairs: pest::iterators::Pairs, + is_directed: bool, +) -> PyResult { + let mut node_map: HashMap = HashMap::new(); let graph_attrs = PyDict::new(py); let mut default_node_attrs: HashMap = HashMap::new(); @@ -80,43 +137,22 @@ pub fn from_dot(py: Python<'_>, dot_str: &str) -> PyResult { let mut node_attrs_map: HashMap = HashMap::new(); + let mut graph = if is_directed { + DotGraph::new_directed() + } else { + DotGraph::new_undirected() + }; + for pair in pairs { if pair.as_rule() != Rule::graph_file { continue; } - let mut inner = pair.into_inner(); - - // handle graph_type let first = inner.next().unwrap(); - let graph_type_str = if first.as_rule() == Rule::strict { - inner.next().unwrap().as_str() - } else { - first.as_str() - }; - is_directed = graph_type_str == "digraph"; - - if is_directed { - let graph = StablePyGraph::::with_capacity(0, 0); - di_graph = Some(PyDiGraph { - graph, - cycle_state: rustworkx_core::petgraph::algo::DfsSpace::default(), - check_cycle: false, - node_removed: false, - multigraph: true, - attrs: graph_attrs.clone().into(), - }); - } else { - let graph = StablePyGraph::::with_capacity(0, 0); - undi_graph = Some(PyGraph { - graph, - node_removed: false, - multigraph: true, - attrs: graph_attrs.clone().into(), - }); + if first.as_rule() == Rule::strict { + inner.next(); } - // handle stmt_list for rest in inner { if rest.as_rule() != Rule::stmt_list { continue; @@ -128,27 +164,21 @@ pub fn from_dot(py: Python<'_>, dot_str: &str) -> PyResult { let mut it = stmt.into_inner(); let nid = it.next().unwrap(); let name = node_id_to_string(nid); - // create python node object - let py_node_obj = PyString::new(py, &name).into(); - - // add node to graph - let idx = if is_directed { - di_graph.as_mut().unwrap().graph.add_node(py_node_obj) - } else { - undi_graph.as_mut().unwrap().graph.add_node(py_node_obj) - }; + let py_node_obj: PyObject = PyString::new(py, &name).into(); + + let idx = graph.add_node(py_node_obj); node_map.insert(name.clone(), idx); - // produce merged attrs + // Merge default node attrs + node's attr_list let merged = PyDict::new(py); for (k, v) in default_node_attrs.iter() { - merged.set_item(k, v)?; + merged.set_item(k.as_str(), v.as_str())?; } for maybe_attr in it { if maybe_attr.as_rule() == Rule::attr_list { let map = parse_attr_list_to_map(maybe_attr); for (k, v) in map { - merged.set_item(k, v)?; + merged.set_item(k.as_str(), v.as_str())?; } } } @@ -156,14 +186,12 @@ pub fn from_dot(py: Python<'_>, dot_str: &str) -> PyResult { } Rule::edge_stmt => { - // edge_stmt let mut endpoints: Vec = Vec::new(); - let mut operators: Vec = Vec::new(); - // start collected edge attrs from defaults + // Start collected edge attrs from defaults let collected = PyDict::new(py); for (k, v) in default_edge_attrs.iter() { - collected.set_item(k, v)?; + collected.set_item(k.as_str(), v.as_str())?; } for child in stmt.into_inner() { @@ -177,60 +205,40 @@ pub fn from_dot(py: Python<'_>, dot_str: &str) -> PyResult { } } Rule::edge_op => { - operators.push(child.as_str().trim().to_string()); + // we already know directedness } Rule::attr_list => { let map = parse_attr_list_to_map(child); for (k, v) in map { - collected.set_item(k, v)?; + collected.set_item(k.as_str(), v.as_str())?; } } _ => {} } } - // create pairwise edges and add them to the graph + // Pairwise edges along the chain for i in 0..endpoints.len().saturating_sub(1) { let src = endpoints[i].clone(); let dst = endpoints[i + 1].clone(); let src_idx = *node_map.entry(src.clone()).or_insert_with(|| { - let py_node = PyString::new(py, &src).into(); - if is_directed { - di_graph.as_mut().unwrap().graph.add_node(py_node) - } else { - undi_graph.as_mut().unwrap().graph.add_node(py_node) - } + let py_node: PyObject = PyString::new(py, &src).into(); + graph.add_node(py_node) }); let dst_idx = *node_map.entry(dst.clone()).or_insert_with(|| { - let py_node = PyString::new(py, &dst).into(); - if is_directed { - di_graph.as_mut().unwrap().graph.add_node(py_node) - } else { - undi_graph.as_mut().unwrap().graph.add_node(py_node) - } + let py_node: PyObject = PyString::new(py, &dst).into(); + graph.add_node(py_node) }); let edge_attrs_obj: PyObject = collected.clone().into(); - - if is_directed { - di_graph.as_mut().unwrap().graph.add_edge( - src_idx, - dst_idx, - edge_attrs_obj.clone(), - ); - } else { - undi_graph.as_mut().unwrap().graph.add_edge( - src_idx, - dst_idx, - edge_attrs_obj.clone(), - ); - } + graph.add_edge(src_idx, dst_idx, edge_attrs_obj); } } Rule::attr_stmt => { + // attr_stmt = ("graph" | "node" | "edge") ~ attr_list+ let mut it = stmt.into_inner(); if let Some(target_pair) = it.next() { let target = target_pair.as_str(); @@ -240,13 +248,7 @@ pub fn from_dot(py: Python<'_>, dot_str: &str) -> PyResult { match target { "graph" => { for (k, v) in map { - graph_attrs.set_item(k, v)?; - } - if let Some(dg) = di_graph.as_mut() { - dg.attrs = graph_attrs.clone().into(); - } - if let Some(ug) = undi_graph.as_mut() { - ug.attrs = graph_attrs.clone().into(); + graph_attrs.set_item(k.as_str(), v.as_str())?; } } "node" => { @@ -274,7 +276,9 @@ pub fn from_dot(py: Python<'_>, dot_str: &str) -> PyResult { } Rule::subgraph => { - // TODO: subgraph handling + return Err(PyErr::new::( + "subgraph parsing is not supported", + )); } _ => {} @@ -283,17 +287,27 @@ pub fn from_dot(py: Python<'_>, dot_str: &str) -> PyResult { } } - if is_directed { - let dg = di_graph - .take() - .expect("directed graph was created but now missing"); - let py_obj = Py::new(py, dg)?.into(); - Ok(py_obj) - } else { - let ug = undi_graph - .take() - .expect("undirected graph was created but now missing"); - let py_obj = Py::new(py, ug)?.into(); - Ok(py_obj) + // Wrap into the a Python class + match graph.into_inner() { + Ok(directed_graph) => { + let dg = PyDiGraph { + graph: directed_graph, + cycle_state: rustworkx_core::petgraph::algo::DfsSpace::default(), + check_cycle: false, + node_removed: false, + multigraph: true, + attrs: graph_attrs.clone().into(), + }; + Ok(Py::new(py, dg)?.into()) + } + Err(undirected_graph) => { + let ug = PyGraph { + graph: undirected_graph, + node_removed: false, + multigraph: true, + attrs: graph_attrs.clone().into(), + }; + Ok(Py::new(py, ug)?.into()) + } } } diff --git a/tests/graph/test_dot.py b/tests/graph/test_dot.py index 3d51d4ba0c..e3ab9158a7 100644 --- a/tests/graph/test_dot.py +++ b/tests/graph/test_dot.py @@ -147,4 +147,60 @@ def test_from_dot_digraph(self): }""" g = rustworkx.from_dot(dot_str) self.assertEqual(len(g.nodes()), 2) - self.assertEqual(len(g.edges()), 1) \ No newline at end of file + self.assertEqual(len(g.edges()), 1) + + def test_graph_roundtrip_with_attrs(self): + + graph = rustworkx.PyGraph() + graph.add_node( + { + "color": "black", + "fillcolor": "green", + "label": "a", + "style": "filled", + } + ) + graph.add_node( + { + "color": "black", + "fillcolor": "red", + "label": "a", + "style": "filled", + } + ) + graph.add_edge(0, 1, dict(label="1", name="1")) + + res = graph.to_dot(lambda node: node, lambda edge: edge) + + g2 = rustworkx.from_dot(res) + + self.assertEqual(len(g2.nodes()), 2) + self.assertEqual(len(g2.edges()), 1) + + def test_digraph_roundtrip_with_attrs(self): + graph = rustworkx.PyGraph() + graph.add_node( + { + "color": "black", + "fillcolor": "green", + "label": "a", + "style": "filled", + } + ) + graph.add_node( + { + "color": "black", + "fillcolor": "red", + "label": "a", + "style": "filled", + } + ) + graph.add_edge(0, 1, dict(label="1", name="1")) + graph.add_edge(1, 0, dict(label="2", name="2")) + + res = graph.to_dot(lambda node: node, lambda edge: edge) + + g2 = rustworkx.from_dot(res) + + self.assertEqual(len(g2.nodes()), 2) + self.assertEqual(len(g2.edges()), 2) From 2ac3ec13b20fe795d8beeaafecf163481ba7e43d Mon Sep 17 00:00:00 2001 From: Krishn Parasar Date: Sat, 23 Aug 2025 23:57:40 +0530 Subject: [PATCH 5/6] fixing clippy --- src/dot_parser/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/dot_parser/mod.rs b/src/dot_parser/mod.rs index 3708244927..ddad2ff2fd 100644 --- a/src/dot_parser/mod.rs +++ b/src/dot_parser/mod.rs @@ -43,9 +43,12 @@ impl DotGraph { } } } + + #[allow(dead_code)] fn is_directed(&self) -> bool { matches!(self, DotGraph::Directed(_)) } + fn into_inner(self) -> Result, StablePyGraph> { match self { DotGraph::Directed(g) => Ok(g), From dbe10e7bae9c201021cf425a8b3eb240e1fd70ef Mon Sep 17 00:00:00 2001 From: Krishn Parasar Date: Thu, 11 Sep 2025 23:51:38 +0530 Subject: [PATCH 6/6] Updates based on review --- Cargo.lock | 371 ++++++++++++------ Cargo.toml | 4 +- .../notes/from-dot-b9f092537c7bce94.yaml | 21 + src/dot_parser/mod.rs | 12 +- 4 files changed, 293 insertions(+), 115 deletions(-) create mode 100644 releasenotes/notes/from-dot-b9f092537c7bce94.yaml diff --git a/Cargo.lock b/Cargo.lock index fd443490c0..396a69392b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" @@ -25,31 +25,35 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" dependencies = [ "derive_arbitrary", ] [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] -name = "bitflags" -version = "2.9.0" +name = "block-buffer" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] [[package]] name = "cc" -version = "1.2.18" +version = "1.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c" +checksum = "5252b3d2648e5eedbc1a6f501e3c795e07025c1e93bbf8bbdd6eef7f447a6d54" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -57,15 +61,24 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "cpufeatures" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -95,17 +108,37 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "derive_arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "either" version = "1.15.0" @@ -128,6 +161,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "find-msvc-tools" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d" + [[package]] name = "fixedbitset" version = "0.5.7" @@ -136,9 +175,9 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", "miniz_oxide", @@ -150,34 +189,44 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] name = "getrandom" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasi 0.14.5+wasi-0.2.4", ] [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", @@ -193,15 +242,15 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.3.9" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "indexmap" -version = "2.9.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "206a8042aec68fa4a62e8d3f7aa4ceb508177d9324faf261e1959e495b7a1921" dependencies = [ "equivalent", "hashbrown", @@ -240,25 +289,25 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.3", "libc", ] [[package]] name = "libc" -version = "0.2.171" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libfuzzer-sys" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75" +checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" dependencies = [ "arbitrary", "cc", @@ -272,15 +321,15 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "matrixmultiply" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9380b911e3e96d10c1f415da0876389aaf1b56759054eeb0de7df940c456ba1a" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" dependencies = [ "autocfg", "rawpointer", @@ -288,9 +337,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memoffset" @@ -303,9 +352,9 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.8.7" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] @@ -390,9 +439,9 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ "hermit-abi", "libc", @@ -420,11 +469,56 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "pest" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + [[package]] name = "petgraph" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a98c6720655620a521dcc722d0ad66cd8afd5d86e34a89ef691c50b7b24de06" +checksum = "54acf3a685220b533e437e264e4d932cfbdc4cc7ec0cd232ed73c08d03b8a7ca" dependencies = [ "fixedbitset", "hashbrown", @@ -434,9 +528,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portable-atomic-util" @@ -458,9 +552,9 @@ dependencies = [ [[package]] name = "priority-queue" -version = "2.3.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef08705fa1589a1a59aa924ad77d14722cb0cd97b67dd5004ed5f4a4873fce8d" +checksum = "5676d703dda103cbb035b653a9f11448c0a7216c7926bd35fcb5865475d0c970" dependencies = [ "autocfg", "equivalent", @@ -469,18 +563,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] [[package]] name = "pyo3" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17da310086b068fbdcefbba30aeb3721d5bb9af8db4987d6735b2183ca567229" +checksum = "e5203598f366b11a02b13aa20cab591229ff0a89fd121a308a5df751d5fc9219" dependencies = [ "cfg-if", "hashbrown", @@ -500,9 +594,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e27165889bd793000a098bb966adc4300c312497ea25cf7a690a9f0ac5aa5fc1" +checksum = "99636d423fa2ca130fa5acde3059308006d46f98caac629418e53f7ebb1e9999" dependencies = [ "once_cell", "target-lexicon", @@ -510,9 +604,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05280526e1dbf6b420062f3ef228b78c0c54ba94e157f5cb724a609d0f2faabc" +checksum = "78f9cf92ba9c409279bc3305b5409d90db2d2c22392d443a87df3a1adad59e33" dependencies = [ "libc", "pyo3-build-config", @@ -520,9 +614,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3ce5686aa4d3f63359a5100c62a127c9f15e8398e5fdeb5deef1fed5cd5f44" +checksum = "0b999cb1a6ce21f9a6b147dcf1be9ffedf02e0043aec74dc390f3007047cecd9" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -532,9 +626,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4cf6faa0cbfb0ed08e89beb8103ae9724eb4750e3a78084ba4017cbe94f3855" +checksum = "822ece1c7e1012745607d5cf0bcb2874769f0f7cb34c4cde03b9358eb9ef911a" dependencies = [ "heck", "proc-macro2", @@ -545,9 +639,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.37.4" +version = "0.37.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ce8c88de324ff838700f36fb6ab86c96df0e3c4ab6ef3a9b2044465cce1369" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" dependencies = [ "memchr", ] @@ -585,9 +679,9 @@ dependencies = [ [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" @@ -602,9 +696,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", @@ -636,7 +730,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", ] [[package]] @@ -645,7 +739,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.3", ] [[package]] @@ -655,7 +749,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463" dependencies = [ "num-traits", - "rand 0.9.1", + "rand 0.9.2", ] [[package]] @@ -675,9 +769,9 @@ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -696,9 +790,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -706,9 +800,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", @@ -718,9 +812,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" dependencies = [ "aho-corasick", "memchr", @@ -729,9 +823,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "rustc-hash" @@ -754,10 +848,12 @@ dependencies = [ "num-complex", "num-traits", "numpy", + "pest", + "pest_derive", "petgraph", "pyo3", "quick-xml", - "rand 0.9.1", + "rand 0.9.2", "rand_distr", "rand_pcg", "rayon", @@ -782,7 +878,7 @@ dependencies = [ "priority-queue", "quickcheck", "quickcheck_macros", - "rand 0.9.1", + "rand 0.9.2", "rand_distr", "rand_pcg", "rayon", @@ -826,9 +922,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ "itoa", "memchr", @@ -836,6 +932,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -844,9 +951,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "sprs" @@ -864,9 +971,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.100" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -875,15 +982,47 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" +checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" + +[[package]] +name = "thiserror" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unindent" @@ -891,44 +1030,56 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.2+wasi-0.2.4" +version = "0.14.5+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "a4494f6290a82f5fe584817a676a34b9d6763e8d9d18204009fb31dceca98fd4" dependencies = [ - "wit-bindgen-rt", + "wasip2", ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wasip2" +version = "1.0.0+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +checksum = "03fa2761397e5bd52002cd7e73110c71af2109aca4e521a9f40473fe685b0a24" dependencies = [ - "bitflags", + "wit-bindgen", ] +[[package]] +name = "wit-bindgen" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36" + [[package]] name = "zerocopy" -version = "0.8.24" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.24" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 2c901676f3..842ae4eeea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,8 +64,8 @@ serde_json = "1.0" smallvec = { version = "1.0", features = ["union"] } rustworkx-core = { path = "rustworkx-core", version = "=0.17.0" } flate2 = "1.0.35" -pest = "2.7" -pest_derive = "2.7" +pest = "=2.8.0" +pest_derive = "=2.8.0" [dependencies.pyo3] version = "0.24" diff --git a/releasenotes/notes/from-dot-b9f092537c7bce94.yaml b/releasenotes/notes/from-dot-b9f092537c7bce94.yaml new file mode 100644 index 0000000000..f8f5076c61 --- /dev/null +++ b/releasenotes/notes/from-dot-b9f092537c7bce94.yaml @@ -0,0 +1,21 @@ +features: + - | + Added feature for importing graphs from the GraphViz DOT format via the new + :func:`~rustworkx.from_dot` function. This function takes a DOT string and + constructs either a :class:`~rustworkx.PyGraph` or + :class:`~rustworkx.PyDiGraph` object, automatically detecting the graph type + from the DOT input. Node attributes, edge attributes, and graph-level + attributes are preserved.For example:: + + import rustworkx + + dot_str = ''' + digraph { + 0 [label="a", color=red]; + 1 [label="b", color=blue]; + 0 -> 1 [weight=1]; + } + ''' + g = rustworkx.from_dot(dot_str) + assert len(g.nodes()) == 2 + assert len(g.edges()) == 1 diff --git a/src/dot_parser/mod.rs b/src/dot_parser/mod.rs index ddad2ff2fd..6281e08f4a 100644 --- a/src/dot_parser/mod.rs +++ b/src/dot_parser/mod.rs @@ -7,8 +7,8 @@ use crate::digraph::PyDiGraph; use crate::graph::PyGraph; use crate::StablePyGraph; +use hashbrown::HashMap; use rustworkx_core::petgraph::prelude::{Directed, NodeIndex, Undirected}; -use std::collections::HashMap; #[derive(Parser)] #[grammar = "dot_parser/dot.pest"] @@ -151,7 +151,9 @@ fn build_graph_enum( continue; } let mut inner = pair.into_inner(); - let first = inner.next().unwrap(); + let first = inner.next().ok_or_else(|| { + PyErr::new::("Missing graph type in DOT") + })?; if first.as_rule() == Rule::strict { inner.next(); } @@ -165,7 +167,11 @@ fn build_graph_enum( match stmt.as_rule() { Rule::node_stmt => { let mut it = stmt.into_inner(); - let nid = it.next().unwrap(); + let nid = it.next().ok_or_else(|| { + PyErr::new::( + "Missing node id in DOT", + ) + })?; let name = node_id_to_string(nid); let py_node_obj: PyObject = PyString::new(py, &name).into();