Skip to content

Commit 21170c4

Browse files
SILIZ4IvanIsCoding
andauthored
Add flags multigraph and selfloops to read_edge_list methods (#1526)
* add flags mutligraph and selfloops to read_edge_list * Apply suggestions from code review * Use macos-15-intel to unblock CI --------- Co-authored-by: Ivan Carvalho <[email protected]> Co-authored-by: Ivan Carvalho <[email protected]>
1 parent 04780a5 commit 21170c4

File tree

7 files changed

+110
-8
lines changed

7 files changed

+110
-8
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ jobs:
6363
rust: [stable]
6464
python-version: [3.9, "3.10", "3.11", "3.12", "3.13"]
6565
platform: [
66-
{ os: "macOS-13", python-architecture: "x64", rust-target: "x86_64-apple-darwin" },
66+
{ os: "macos-15-intel", python-architecture: "x64", rust-target: "x86_64-apple-darwin" },
6767
{ os: "macOS-14", python-architecture: "arm64", rust-target: "aarch64-apple-darwin" },
6868
{ os: "ubuntu-latest", python-architecture: "x64", rust-target: "x86_64-unknown-linux-gnu" },
6969
{ os: "ubuntu-24.04-arm", python-architecture: "arm64", rust-target: "aarch64-unknown-linux-gnu" },
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
features:
3+
- |
4+
Adds the flags ``multigraph`` and ``allow_self_loops`` to the ``read_edge_list`` methods of :class:`rustwork.PyGraph` and :class:`rustworkx.PyDiGraph`.
5+
When the flag ``multigraph`` is set to ``False``, no parallel edge is created, but the edge weight is updated.
6+
The edge weight is the last one found in the edge list file.
7+
When flag ``allow_self_loops` is set to ``False``, the self-loops in the edge list file are ignored.

rustworkx/rustworkx.pyi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1387,6 +1387,8 @@ class PyGraph(Generic[_S, _T]):
13871387
comment: str | None = ...,
13881388
deliminator: str | None = ...,
13891389
labels: bool = ...,
1390+
multigraph: bool = ...,
1391+
allow_self_loops: bool = ...,
13901392
) -> PyGraph: ...
13911393
def remove_edge(self, node_a: int, node_b: int, /) -> None: ...
13921394
def remove_edge_from_index(self, edge: int, /) -> None: ...
@@ -1581,6 +1583,8 @@ class PyDiGraph(Generic[_S, _T]):
15811583
comment: str | None = ...,
15821584
deliminator: str | None = ...,
15831585
labels: bool = ...,
1586+
multigraph: bool = ...,
1587+
allow_self_loops: bool = ...,
15841588
) -> PyDiGraph: ...
15851589
def remove_edge(self, parent: int, child: int, /) -> None: ...
15861590
def remove_edge_from_index(self, edge: int, /) -> None: ...

src/digraph.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2458,6 +2458,10 @@ impl PyDiGraph {
24582458
/// :param bool labels: If set to ``True`` the first two separated fields
24592459
/// will be treated as string labels uniquely identifying a node
24602460
/// instead of node indices.
2461+
/// :param bool multigraph: If ``False``, duplicate edges do not create
2462+
/// parallel edges. Instead, repeated edges overwrite the previous
2463+
/// weight, with the last one in the input determining the final weight.
2464+
/// :param bool selfloops: If set to ``False``, self-loops are not created.
24612465
///
24622466
/// For example:
24632467
///
@@ -2480,14 +2484,18 @@ impl PyDiGraph {
24802484
/// mpl_draw(graph)
24812485
///
24822486
#[staticmethod]
2483-
#[pyo3(signature=(path, comment=None, deliminator=None, labels=false))]
2484-
#[pyo3(text_signature = "(path, /, comment=None, deliminator=None, labels=False)")]
2487+
#[pyo3(signature=(path, comment=None, deliminator=None, labels=false, multigraph=true, allow_self_loops=true))]
2488+
#[pyo3(
2489+
text_signature = "(path, /, comment=None, deliminator=None, labels=False, multigraph=True, allow_self_loops=True)"
2490+
)]
24852491
pub fn read_edge_list(
24862492
py: Python,
24872493
path: &str,
24882494
comment: Option<String>,
24892495
deliminator: Option<String>,
24902496
labels: bool,
2497+
multigraph: bool,
2498+
allow_self_loops: bool,
24912499
) -> PyResult<PyDiGraph> {
24922500
let file = File::open(path)?;
24932501
let buf_reader = BufReader::new(file);
@@ -2555,14 +2563,20 @@ impl PyDiGraph {
25552563
} else {
25562564
py.None()
25572565
};
2558-
out_graph.add_edge(NodeIndex::new(src), NodeIndex::new(target), weight);
2566+
if allow_self_loops || src != target {
2567+
if multigraph {
2568+
out_graph.add_edge(NodeIndex::new(src), NodeIndex::new(target), weight);
2569+
} else {
2570+
out_graph.update_edge(NodeIndex::new(src), NodeIndex::new(target), weight);
2571+
}
2572+
}
25592573
}
25602574
Ok(PyDiGraph {
25612575
graph: out_graph,
25622576
cycle_state: algo::DfsSpace::default(),
25632577
check_cycle: false,
25642578
node_removed: false,
2565-
multigraph: true,
2579+
multigraph,
25662580
attrs: py.None(),
25672581
})
25682582
}

src/graph.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1347,6 +1347,10 @@ impl PyGraph {
13471347
/// :param bool labels: If set to ``True`` the first two separated fields
13481348
/// will be treated as string labels uniquely identifying a node
13491349
/// instead of node indices
1350+
/// :param bool multigraph: If ``False``, duplicate edges do not create
1351+
/// parallel edges. Instead, repeated edges overwrite the previous
1352+
/// weight, with the last one in the input determining the final weight.
1353+
/// :param bool selfloops: If set to ``False``, self-loops are not created.
13501354
///
13511355
/// For example:
13521356
///
@@ -1369,13 +1373,18 @@ impl PyGraph {
13691373
/// mpl_draw(graph)
13701374
///
13711375
#[staticmethod]
1372-
#[pyo3(signature=(path, comment=None, deliminator=None, labels=false), text_signature = "(path, /, comment=None, deliminator=None, labels=False)")]
1376+
#[pyo3(signature=(path, comment=None, deliminator=None, labels=false, multigraph=true, allow_self_loops=true))]
1377+
#[pyo3(
1378+
text_signature = "(path, /, comment=None, deliminator=None, labels=False, multigraph=True, allow_self_loops=True)"
1379+
)]
13731380
pub fn read_edge_list(
13741381
py: Python,
13751382
path: &str,
13761383
comment: Option<String>,
13771384
deliminator: Option<String>,
13781385
labels: bool,
1386+
multigraph: bool,
1387+
allow_self_loops: bool,
13791388
) -> PyResult<PyGraph> {
13801389
let file = File::open(path)?;
13811390
let buf_reader = BufReader::new(file);
@@ -1443,12 +1452,18 @@ impl PyGraph {
14431452
} else {
14441453
py.None()
14451454
};
1446-
out_graph.add_edge(NodeIndex::new(src), NodeIndex::new(target), weight);
1455+
if allow_self_loops || src != target {
1456+
if multigraph {
1457+
out_graph.add_edge(NodeIndex::new(src), NodeIndex::new(target), weight);
1458+
} else {
1459+
out_graph.update_edge(NodeIndex::new(src), NodeIndex::new(target), weight);
1460+
}
1461+
}
14471462
}
14481463
Ok(PyGraph {
14491464
graph: out_graph,
14501465
node_removed: false,
1451-
multigraph: true,
1466+
multigraph,
14521467
attrs: py.None(),
14531468
})
14541469
}

tests/digraph/test_edgelist.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,37 @@ def test_labels_digraph_target_existing(self):
147147
self.assertTrue(graph.has_edge(0, 2))
148148
self.assertEqual(graph.edges(), ["0", "1", None])
149149

150+
def test_remove_parallel_edges_digraph(self):
151+
with tempfile.NamedTemporaryFile("wt") as fd:
152+
fd.write("0 1\n")
153+
fd.write("1 1 a\n")
154+
fd.write("1 1 b\n")
155+
fd.write("1 2 c\n")
156+
fd.write("1 2 d\n")
157+
fd.flush()
158+
graph = rustworkx.PyDiGraph.read_edge_list(fd.name, multigraph=False)
159+
self.assertTrue(graph.node_indexes(), [0, 1, 2])
160+
self.assertTrue(graph.has_edge(0, 1))
161+
self.assertTrue(graph.has_edge(1, 2))
162+
self.assertTrue(graph.has_edge(1, 1))
163+
self.assertEqual(graph.edges(), [None, "b", "d"])
164+
self.assertFalse(graph.multigraph)
165+
166+
def test_remove_selfloops_digraph(self):
167+
with tempfile.NamedTemporaryFile("wt") as fd:
168+
fd.write("0 1\n")
169+
fd.write("1 1 a\n")
170+
fd.write("1 1 b\n")
171+
fd.write("1 2 c\n")
172+
fd.write("1 2 d\n")
173+
fd.flush()
174+
graph = rustworkx.PyDiGraph.read_edge_list(fd.name, allow_self_loops=False)
175+
self.assertTrue(graph.node_indexes(), [0, 1, 2])
176+
self.assertTrue(graph.has_edge(0, 1))
177+
self.assertTrue(graph.has_edge(1, 2))
178+
self.assertFalse(graph.has_edge(1, 1))
179+
self.assertEqual(graph.edges(), [None, "c", "d"])
180+
150181
def test_write_edge_list_empty_digraph(self):
151182
path = os.path.join(tempfile.gettempdir(), "empty.txt")
152183
graph = rustworkx.PyDiGraph()

tests/graph/test_edgelist.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,37 @@ def test_labels_graph_target_existing(self):
143143
self.assertTrue(graph.has_edge(0, 2))
144144
self.assertEqual(graph.edges(), ["0", "1", None])
145145

146+
def test_remove_parallel_edges_graph(self):
147+
with tempfile.NamedTemporaryFile("wt") as fd:
148+
fd.write("0 1\n")
149+
fd.write("1 1 a\n")
150+
fd.write("1 1 b\n")
151+
fd.write("2 1 c\n")
152+
fd.write("1 2 d\n")
153+
fd.flush()
154+
graph = rustworkx.PyGraph.read_edge_list(fd.name, multigraph=False)
155+
self.assertTrue(graph.node_indexes(), [0, 1, 2])
156+
self.assertTrue(graph.has_edge(0, 1))
157+
self.assertTrue(graph.has_edge(1, 2))
158+
self.assertTrue(graph.has_edge(1, 1))
159+
self.assertEqual(graph.edges(), [None, "b", "d"])
160+
self.assertFalse(graph.multigraph)
161+
162+
def test_remove_selfloops_graph(self):
163+
with tempfile.NamedTemporaryFile("wt") as fd:
164+
fd.write("0 1\n")
165+
fd.write("1 1 a\n")
166+
fd.write("1 1 b\n")
167+
fd.write("2 1 c\n")
168+
fd.write("1 2 d\n")
169+
fd.flush()
170+
graph = rustworkx.PyGraph.read_edge_list(fd.name, allow_self_loops=False)
171+
self.assertTrue(graph.node_indexes(), [0, 1, 2])
172+
self.assertTrue(graph.has_edge(0, 1))
173+
self.assertTrue(graph.has_edge(1, 2))
174+
self.assertFalse(graph.has_edge(1, 1))
175+
self.assertEqual(graph.edges(), [None, "c", "d"])
176+
146177
def test_write_edge_list_empty_digraph(self):
147178
path = os.path.join(tempfile.gettempdir(), "empty.txt")
148179
graph = rustworkx.PyGraph()

0 commit comments

Comments
 (0)