From 7776749cb953b8239687095011e135bdeb6b0c4d Mon Sep 17 00:00:00 2001 From: Andrey G Date: Thu, 7 Aug 2025 17:21:26 +0100 Subject: [PATCH] Support attribute-based node grouping --- ngraph/network.py | 26 +++++++++++++++++++------- ngraph/network_view.py | 4 ++-- tests/test_network_selection.py | 15 +++++++++++++++ tests/test_network_view.py | 17 +++++++++++++++++ 4 files changed, 53 insertions(+), 9 deletions(-) diff --git a/ngraph/network.py b/ngraph/network.py index f5c89d9..48e0921 100644 --- a/ngraph/network.py +++ b/ngraph/network.py @@ -224,19 +224,31 @@ def _build_graph( return graph def select_node_groups_by_path(self, path: str) -> Dict[str, List[Node]]: - """Select and group nodes whose names match a given regular expression. + """Select and group nodes using a regex pattern or attribute directive. - Uses re.match(), so the pattern is anchored at the start of the node name. - If the pattern includes capturing groups, the group label is formed by - joining all non-None captures with '|'. If no capturing groups exist, - the group label is the original pattern string. + If ``path`` begins with ``"attr:"``, the remainder specifies an attribute + name. Nodes are grouped by the value of this attribute; nodes without the + attribute are ignored. Otherwise, ``path`` is treated as a regular + expression anchored at the start of each node name. If the pattern + includes capturing groups, the group label joins captures with ``"|"``. + Without capturing groups, the label is the pattern string itself. Args: - path (str): A Python regular expression pattern (e.g., "^foo", "bar(\\d+)", etc.). + path: Regular expression pattern or ``"attr:"`` directive. Returns: - Dict[str, List[Node]]: A mapping from group label -> list of matching nodes. + Mapping from group label to list of matching nodes. """ + if path.startswith("attr:"): + attr_name = path[5:] + groups_map: Dict[str, List[Node]] = {} + for node in self.nodes.values(): + value = node.attrs.get(attr_name) + if value is not None: + label = str(value) + groups_map.setdefault(label, []).append(node) + return groups_map + pattern = re.compile(path) groups_map: Dict[str, List[Node]] = {} diff --git a/ngraph/network_view.py b/ngraph/network_view.py index 01612f7..f4e1e2f 100644 --- a/ngraph/network_view.py +++ b/ngraph/network_view.py @@ -160,10 +160,10 @@ def to_strict_multidigraph(self, add_reverse: bool = True) -> "StrictMultiDiGrap return cache[add_reverse] def select_node_groups_by_path(self, path: str) -> Dict[str, List["Node"]]: - """Select and group visible nodes matching a regex pattern. + """Select and group visible nodes using regex or attribute directive. Args: - path: Regular expression pattern to match node names. + path: Regular expression pattern or ``"attr:"`` directive. Returns: Dictionary mapping group labels to lists of matching visible nodes. diff --git a/tests/test_network_selection.py b/tests/test_network_selection.py index 57bbf18..cc0511a 100644 --- a/tests/test_network_selection.py +++ b/tests/test_network_selection.py @@ -97,6 +97,21 @@ def test_select_node_groups_multiple_capture_groups(self, complex_network): # Should have groups for each combination found assert len(node_groups) >= 2 + def test_select_node_groups_by_attr(self): + """Test grouping nodes by attribute value.""" + net = Network() + net.add_node(Node("r1", attrs={"metro": "SEA"})) + net.add_node(Node("r2", attrs={"metro": "SEA"})) + net.add_node(Node("r3", attrs={"metro": "SFO"})) + + groups = net.select_node_groups_by_path("attr:metro") + + assert set(groups) == {"SEA", "SFO"} + sea = {n.name for n in groups["SEA"]} + assert sea == {"r1", "r2"} + sfo = {n.name for n in groups["SFO"]} + assert sfo == {"r3"} + class TestLinkUtilities: """Tests for link utility methods.""" diff --git a/tests/test_network_view.py b/tests/test_network_view.py index a6b7db6..7672690 100644 --- a/tests/test_network_view.py +++ b/tests/test_network_view.py @@ -428,6 +428,23 @@ def test_select_empty_after_filtering(self): # Should return empty dict since all dc1 nodes are hidden assert len(groups) == 0 + def test_select_by_attribute(self): + """Test grouping visible nodes by attribute value.""" + net = Network() + net.add_node(Node("a", attrs={"role": "core"})) + net.add_node(Node("b", attrs={"role": "core"})) + net.add_node(Node("c", attrs={"role": "edge"})) + net.nodes["b"].disabled = True + view = NetworkView(_base=net) + + groups = view.select_node_groups_by_path("attr:role") + + assert set(groups) == {"core", "edge"} + core = {n.name for n in groups["core"]} + assert core == {"a"} + edge = {n.name for n in groups["edge"]} + assert edge == {"c"} + class TestNetworkViewEdgeCases: """Test NetworkView edge cases and error conditions."""