From 532466c7522b0b510a032238cf77e8536a004a55 Mon Sep 17 00:00:00 2001 From: Brandon Minnix Date: Fri, 15 May 2026 11:58:09 -0400 Subject: [PATCH] Updating the find_children_w_parents method and tests --- changes/836.added | 1 + changes/836.fixed | 1 + netutils/config/parser.py | 71 +++++++++++++------ .../ios_descendant_depth_limited_args.json | 6 ++ .../ios_descendant_depth_limited_received.txt | 3 + .../ios_descendant_depth_limited_sent.txt | 8 +++ .../ios_descendant_depth_zero_args.json | 6 ++ .../ios_descendant_depth_zero_received.txt | 2 + .../ios_descendant_depth_zero_sent.txt | 7 ++ .../cisco_ios/ios_duplicate_parents_args.json | 5 ++ .../ios_duplicate_parents_received.txt | 3 + .../cisco_ios/ios_duplicate_parents_sent.txt | 7 ++ .../cisco_ios/ios_full_received.txt | 1 - .../ios_match_type_overrides_args.json | 6 ++ .../ios_match_type_overrides_received.txt | 3 + .../ios_match_type_overrides_sent.txt | 4 ++ 16 files changed, 113 insertions(+), 21 deletions(-) create mode 100644 changes/836.added create mode 100644 changes/836.fixed create mode 100644 tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_descendant_depth_limited_args.json create mode 100644 tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_descendant_depth_limited_received.txt create mode 100644 tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_descendant_depth_limited_sent.txt create mode 100644 tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_descendant_depth_zero_args.json create mode 100644 tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_descendant_depth_zero_received.txt create mode 100644 tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_descendant_depth_zero_sent.txt create mode 100644 tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_duplicate_parents_args.json create mode 100644 tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_duplicate_parents_received.txt create mode 100644 tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_duplicate_parents_sent.txt create mode 100644 tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_match_type_overrides_args.json create mode 100644 tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_match_type_overrides_received.txt create mode 100644 tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_match_type_overrides_sent.txt diff --git a/changes/836.added b/changes/836.added new file mode 100644 index 00000000..7214def5 --- /dev/null +++ b/changes/836.added @@ -0,0 +1 @@ +Added `parent_match_type`, `child_match_type`, and `descendant_depth` parameters to `find_children_w_parents`. diff --git a/changes/836.fixed b/changes/836.fixed new file mode 100644 index 00000000..f4d4ac6d --- /dev/null +++ b/changes/836.fixed @@ -0,0 +1 @@ +Fixed `find_children_w_parents` to no longer include sibling lines that do not match the child pattern. diff --git a/netutils/config/parser.py b/netutils/config/parser.py index b38248f1..90a70248 100644 --- a/netutils/config/parser.py +++ b/netutils/config/parser.py @@ -390,18 +390,36 @@ def find_all_children(self, pattern: str, match_type: str = "exact") -> t.List[s config.append(cfg_line.config_line) return config - def find_children_w_parents( - self, parent_pattern: str, child_pattern: str, match_type: str = "exact" + def find_children_w_parents( # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals + self, + parent_pattern: str, + child_pattern: str, + match_type: str = "exact", + parent_match_type: t.Optional[str] = None, + child_match_type: t.Optional[str] = None, + descendant_depth: int = -1, ) -> t.List[str]: - """Returns configuration part for a specific pattern including parents and children. + """Returns lines matching ``child_pattern`` under a top-level parent matching ``parent_pattern``, plus their descendants. Args: - parent_pattern: pattern that describes parent. - child_pattern: pattern that describes child. - match_type (optional): Exact or regex. Defaults to "exact". + parent_pattern: pattern that describes the top-level parent of candidate lines. + child_pattern: pattern that describes the line to match. + match_type (optional): Default match type used for both patterns when a specific + override is not provided. One of ``exact``, ``startswith``, ``endswith``, ``regex``. + Defaults to ``exact``. + parent_match_type (optional): Match type override for ``parent_pattern``. When + ``None`` (default), falls back to ``match_type``. + child_match_type (optional): Match type override for ``child_pattern``. When + ``None`` (default), falls back to ``match_type``. + descendant_depth (optional): How many levels of descendants below a matched line + to include. ``-1`` (default) includes all descendants. ``0`` returns only the + matched lines themselves. ``N`` includes ``N`` levels of nesting beneath each + matched line. Returns: - configuration under that parent pattern. + Lines matching ``child_pattern`` (whose top-level parent matches ``parent_pattern``) + plus descendants of those matched lines, limited by ``descendant_depth``. Sibling + lines that do not themselves match ``child_pattern`` are not included. Examples: >>> from netutils.config.parser import BaseSpaceConfigParser @@ -414,20 +432,33 @@ def find_children_w_parents( >>> print(bgp_conf) [' address-family ipv4 unicast', ' neighbor 192.168.1.2 activate', ' network 172.17.1.0 mask'] """ - config = [] - potential_parents = [ - elem.parents[0] - for elem in self.build_config_relationship() - if self._match_type_check(elem.config_line, child_pattern, match_type) - ] - for cfg_line in self.build_config_relationship(): - parents = cfg_line.parents[0] if cfg_line.parents else None - if parents in potential_parents and self._match_type_check( - parents, # type: ignore[arg-type] - parent_pattern, - match_type, - ): + p_match = parent_match_type if parent_match_type is not None else match_type + c_match = child_match_type if child_match_type is not None else match_type + relationship = self.build_config_relationship() + matched_indices = { + i + for i, elem in enumerate(relationship) + if elem.parents + and self._match_type_check(elem.parents[0], parent_pattern, p_match) + and self._match_type_check(elem.config_line, child_pattern, c_match) + } + config: t.List[str] = [] + # Stack of (index, depth) for matched lines whose subtree we are still inside. + # Identifying matches by index (rather than config_line text) keeps duplicate + # line text under different parents from being treated as the same match. + active_matches: t.List[t.Tuple[int, int]] = [] + for i, cfg_line in enumerate(relationship): + depth = len(cfg_line.parents) + while active_matches and depth <= active_matches[-1][1]: + active_matches.pop() + if i in matched_indices: config.append(cfg_line.config_line) + active_matches.append((i, depth)) + continue + if active_matches: + descendant_level = depth - active_matches[-1][1] + if descendant_depth < 0 or descendant_level <= descendant_depth: + config.append(cfg_line.config_line) return config diff --git a/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_descendant_depth_limited_args.json b/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_descendant_depth_limited_args.json new file mode 100644 index 00000000..c8e9de43 --- /dev/null +++ b/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_descendant_depth_limited_args.json @@ -0,0 +1,6 @@ +{ + "parent_pattern": "policy-map PM_OUT", + "child_pattern": " class CM_VOICE", + "match_type": "exact", + "descendant_depth": 1 +} diff --git a/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_descendant_depth_limited_received.txt b/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_descendant_depth_limited_received.txt new file mode 100644 index 00000000..7d7a12a9 --- /dev/null +++ b/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_descendant_depth_limited_received.txt @@ -0,0 +1,3 @@ + class CM_VOICE + priority percent 20 + police 1000000 \ No newline at end of file diff --git a/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_descendant_depth_limited_sent.txt b/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_descendant_depth_limited_sent.txt new file mode 100644 index 00000000..918813ee --- /dev/null +++ b/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_descendant_depth_limited_sent.txt @@ -0,0 +1,8 @@ +policy-map PM_OUT + class CM_VOICE + priority percent 20 + police 1000000 + conform-action transmit + exceed-action drop + class class-default + fair-queue \ No newline at end of file diff --git a/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_descendant_depth_zero_args.json b/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_descendant_depth_zero_args.json new file mode 100644 index 00000000..f0a21c76 --- /dev/null +++ b/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_descendant_depth_zero_args.json @@ -0,0 +1,6 @@ +{ + "parent_pattern": "router bgp 65001", + "child_pattern": " address-family", + "child_match_type": "startswith", + "descendant_depth": 0 +} diff --git a/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_descendant_depth_zero_received.txt b/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_descendant_depth_zero_received.txt new file mode 100644 index 00000000..a02b0482 --- /dev/null +++ b/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_descendant_depth_zero_received.txt @@ -0,0 +1,2 @@ + address-family ipv4 unicast + address-family ipv6 unicast \ No newline at end of file diff --git a/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_descendant_depth_zero_sent.txt b/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_descendant_depth_zero_sent.txt new file mode 100644 index 00000000..64efac77 --- /dev/null +++ b/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_descendant_depth_zero_sent.txt @@ -0,0 +1,7 @@ +router bgp 65001 + neighbor 10.0.0.1 remote-as 65002 + address-family ipv4 unicast + neighbor 10.0.0.1 activate + network 192.168.1.0 + address-family ipv6 unicast + neighbor 2001::1 activate \ No newline at end of file diff --git a/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_duplicate_parents_args.json b/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_duplicate_parents_args.json new file mode 100644 index 00000000..d60b22f6 --- /dev/null +++ b/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_duplicate_parents_args.json @@ -0,0 +1,5 @@ +{ + "parent_pattern": "router bgp 65001", + "child_pattern": " address-family ipv4 unicast", + "match_type": "exact" +} diff --git a/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_duplicate_parents_received.txt b/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_duplicate_parents_received.txt new file mode 100644 index 00000000..0788f855 --- /dev/null +++ b/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_duplicate_parents_received.txt @@ -0,0 +1,3 @@ + address-family ipv4 unicast + neighbor 10.0.0.1 activate + network 192.168.1.0 \ No newline at end of file diff --git a/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_duplicate_parents_sent.txt b/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_duplicate_parents_sent.txt new file mode 100644 index 00000000..b9abc233 --- /dev/null +++ b/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_duplicate_parents_sent.txt @@ -0,0 +1,7 @@ +router bgp 65001 + address-family ipv4 unicast + neighbor 10.0.0.1 activate + network 192.168.1.0 +router bgp 65002 + address-family ipv4 unicast + neighbor 10.0.0.2 activate \ No newline at end of file diff --git a/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_full_received.txt b/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_full_received.txt index e7a4b1be..4fe99aa6 100644 --- a/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_full_received.txt +++ b/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_full_received.txt @@ -1,4 +1,3 @@ - contact-email-addr sch-smart-licensing@cisco.com profile "CiscoTAC-1" active destination transport-method http \ No newline at end of file diff --git a/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_match_type_overrides_args.json b/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_match_type_overrides_args.json new file mode 100644 index 00000000..913e6208 --- /dev/null +++ b/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_match_type_overrides_args.json @@ -0,0 +1,6 @@ +{ + "parent_pattern": "^router bgp", + "child_pattern": " address-family ipv4 unicast", + "match_type": "exact", + "parent_match_type": "regex" +} diff --git a/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_match_type_overrides_received.txt b/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_match_type_overrides_received.txt new file mode 100644 index 00000000..0788f855 --- /dev/null +++ b/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_match_type_overrides_received.txt @@ -0,0 +1,3 @@ + address-family ipv4 unicast + neighbor 10.0.0.1 activate + network 192.168.1.0 \ No newline at end of file diff --git a/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_match_type_overrides_sent.txt b/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_match_type_overrides_sent.txt new file mode 100644 index 00000000..5b5fde40 --- /dev/null +++ b/tests/unit/mock/config/parser/find_children_w_parents/cisco_ios/ios_match_type_overrides_sent.txt @@ -0,0 +1,4 @@ +router bgp 65001 + address-family ipv4 unicast + neighbor 10.0.0.1 activate + network 192.168.1.0 \ No newline at end of file