diffnc 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
diffnc/diff.py ADDED
@@ -0,0 +1,353 @@
1
+ """Structural diff engine.
2
+
3
+ The algorithm walks two :class:`~diffnc.ir.ConfigTree`'s in parallel, computing per-level
4
+ opcodes with :class:`difflib.SequenceMatcher` keyed on each node's ``line``. For matched
5
+ sections we recurse so that only their *changed descendants* surface; the matched section
6
+ header itself is emitted as a context line so the user can see the surrounding scope.
7
+
8
+ Two flavours are exposed, both modelled on :mod:`difflib`:
9
+
10
+ * :func:`unified_diff` — compact: only changed lines plus the section path leading to them
11
+ (and ``--- a`` / ``+++ b`` headers if file names are supplied).
12
+ * :func:`ndiff` — verbose: every line is shown, prefixed with ``- ``, ``+ `` or `` ``.
13
+
14
+ Both functions accept either ``str`` (raw config text) or ``list[str]`` (already-split
15
+ lines, joined internally) for parity with :mod:`difflib`.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from collections.abc import Iterable, Iterator, Sequence
21
+ from dataclasses import dataclass
22
+ from difflib import SequenceMatcher
23
+
24
+ from diffnc import vendors as _vendors
25
+ from diffnc.detect import detect_vendor
26
+ from diffnc.errors import VendorMismatchError
27
+ from diffnc.ir import ConfigNode, ConfigTree
28
+ from diffnc.vendors.base import VendorParser, is_order_sensitive_for, render_subtree
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class _Event:
33
+ op: str # one of ' ', '-', '+', '!'
34
+ text: str # already-indented line, no marker prefix
35
+
36
+
37
+ def unified_diff(
38
+ a: str | Iterable[str],
39
+ b: str | Iterable[str],
40
+ fromfile: str = "",
41
+ tofile: str = "",
42
+ lineterm: str = "",
43
+ *,
44
+ vendor: str | None = None,
45
+ ) -> Iterator[str]:
46
+ """Yield a structural unified diff of *a* vs *b*.
47
+
48
+ Equal leaves are omitted; only changed lines are shown, along with the path of
49
+ enclosing sections (as `` `` context lines) so the diff stays readable.
50
+
51
+ Args:
52
+ a, b: configuration text. ``str`` is taken verbatim; an iterable of strings is
53
+ joined with newlines before parsing.
54
+ fromfile, tofile: optional file names rendered as ``--- a`` / ``+++ b`` headers.
55
+ lineterm: line terminator appended to each yielded line. Defaults to the empty
56
+ string; callers that need newline-terminated lines should pass ``"\\n"``.
57
+ vendor: skip auto-detection and force a specific parser. Ignored when unset.
58
+
59
+ Raises:
60
+ VendorMismatchError, ParseError: see :mod:`diffnc.errors`.
61
+ """
62
+
63
+ events = _structural_events(a, b, vendor=vendor)
64
+ if not events:
65
+ return
66
+
67
+ if fromfile or tofile:
68
+ yield f"--- {fromfile}{lineterm}"
69
+ yield f"+++ {tofile}{lineterm}"
70
+ for ev in events:
71
+ yield f"{ev.op}{ev.text}{lineterm}"
72
+
73
+
74
+ def ndiff(
75
+ a: str | Iterable[str],
76
+ b: str | Iterable[str],
77
+ lineterm: str = "",
78
+ *,
79
+ vendor: str | None = None,
80
+ ) -> Iterator[str]:
81
+ """Yield a verbose, every-line diff using ``- ``/``+ ``/``! ``/`` `` markers.
82
+
83
+ ``lineterm`` defaults to the empty string; pass ``"\\n"`` for newline-terminated
84
+ output.
85
+ """
86
+
87
+ parser, tree_a, tree_b = _prepare(a, b, vendor=vendor)
88
+ events = list(
89
+ _diff_children(parser, tree_a.root, tree_b.root, depth=0, hide_equal=False, path=())
90
+ )
91
+ for ev in events:
92
+ marker = f"{ev.op} " if ev.op != " " else " "
93
+ yield f"{marker}{ev.text}{lineterm}"
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # internals
98
+ # ---------------------------------------------------------------------------
99
+
100
+
101
+ def _structural_events(
102
+ a: str | Iterable[str],
103
+ b: str | Iterable[str],
104
+ *,
105
+ vendor: str | None,
106
+ ) -> list[_Event]:
107
+ parser, tree_a, tree_b = _prepare(a, b, vendor=vendor)
108
+ return list(_diff_children(parser, tree_a.root, tree_b.root, depth=0, hide_equal=True, path=()))
109
+
110
+
111
+ def _prepare(
112
+ a: str | Iterable[str],
113
+ b: str | Iterable[str],
114
+ *,
115
+ vendor: str | None,
116
+ ) -> tuple[VendorParser, ConfigTree, ConfigTree]:
117
+ text_a = _coerce(a)
118
+ text_b = _coerce(b)
119
+
120
+ if vendor is None:
121
+ vendor_a = detect_vendor(text_a)
122
+ vendor_b = detect_vendor(text_b)
123
+ if vendor_a != vendor_b:
124
+ raise VendorMismatchError(vendor_a, vendor_b)
125
+ vendor_name = vendor_a
126
+ else:
127
+ vendor_name = vendor
128
+
129
+ parser = _vendors.get(vendor_name)
130
+ tree_a = parser.parse(text_a)
131
+ tree_b = parser.parse(text_b)
132
+ return parser, tree_a, tree_b
133
+
134
+
135
+ def _coerce(value: str | Iterable[str]) -> str:
136
+ if isinstance(value, str):
137
+ return value
138
+ return "\n".join(value)
139
+
140
+
141
+ def _diff_children(
142
+ parser: VendorParser,
143
+ node_a: ConfigNode,
144
+ node_b: ConfigNode,
145
+ depth: int,
146
+ *,
147
+ hide_equal: bool,
148
+ path: tuple[str, ...],
149
+ ) -> Iterator[_Event]:
150
+ if is_order_sensitive_for(parser, path):
151
+ yield from _diff_children_ordered(
152
+ parser, node_a, node_b, depth, hide_equal=hide_equal, path=path
153
+ )
154
+ else:
155
+ yield from _diff_children_unordered(
156
+ parser, node_a, node_b, depth, hide_equal=hide_equal, path=path
157
+ )
158
+
159
+
160
+ def _diff_children_ordered(
161
+ parser: VendorParser,
162
+ node_a: ConfigNode,
163
+ node_b: ConfigNode,
164
+ depth: int,
165
+ *,
166
+ hide_equal: bool,
167
+ path: tuple[str, ...],
168
+ ) -> Iterator[_Event]:
169
+ """Positional matching via :class:`difflib.SequenceMatcher`.
170
+
171
+ Used for parents whose children's evaluation order is semantically meaningful — e.g.
172
+ Junos ``firewall filter`` terms, Cisco ``ip access-list`` ACEs, ``policy-map`` class
173
+ blocks. Pure reorders (children whose rendered subtree is byte-identical on both
174
+ sides) surface once with the ``!`` marker; genuine changes still show as ``-``/``+``
175
+ pairs.
176
+ """
177
+
178
+ a_children = node_a.children
179
+ b_children = node_b.children
180
+ a_keys = [c.line for c in a_children]
181
+ b_keys = [c.line for c in b_children]
182
+ matcher = SequenceMatcher(a=a_keys, b=b_keys, autojunk=False)
183
+ opcodes = matcher.get_opcodes()
184
+
185
+ reordered_a, reordered_b = _collect_reorder_pairs(opcodes, a_children, b_children, parser)
186
+
187
+ for tag, i1, i2, j1, j2 in opcodes:
188
+ if tag == "equal":
189
+ for k in range(i2 - i1):
190
+ child_a = a_children[i1 + k]
191
+ child_b = b_children[j1 + k]
192
+ yield from _equal_pair(parser, child_a, child_b, depth, hide_equal, path)
193
+ elif tag == "delete":
194
+ for k in range(i1, i2):
195
+ op = "!" if k in reordered_a else "-"
196
+ yield from _emit_one_side(parser, a_children[k], depth, op)
197
+ elif tag == "insert":
198
+ for k in range(j1, j2):
199
+ if k in reordered_b:
200
+ continue
201
+ yield from _emit_one_side(parser, b_children[k], depth, "+")
202
+ elif tag == "replace":
203
+ a_indices = list(range(i1, i2))
204
+ b_indices = [k for k in range(j1, j2) if k not in reordered_b]
205
+ for step in range(max(len(a_indices), len(b_indices))):
206
+ if step < len(a_indices):
207
+ ka = a_indices[step]
208
+ op = "!" if ka in reordered_a else "-"
209
+ yield from _emit_one_side(parser, a_children[ka], depth, op)
210
+ if step < len(b_indices):
211
+ kb = b_indices[step]
212
+ yield from _emit_one_side(parser, b_children[kb], depth, "+")
213
+
214
+
215
+ def _collect_reorder_pairs(
216
+ opcodes: Sequence[tuple[str, int, int, int, int]],
217
+ a_children: list[ConfigNode],
218
+ b_children: list[ConfigNode],
219
+ parser: VendorParser,
220
+ ) -> tuple[set[int], set[int]]:
221
+ """Find delete/insert pairs whose subtrees render identically — i.e. pure reorders.
222
+
223
+ Returns ``(reordered_a_indices, reordered_b_indices)``. The A-side index is where
224
+ we will emit a single ``!`` line; the B-side index marks the matching insert that
225
+ should be suppressed.
226
+ """
227
+
228
+ a_indices: list[int] = []
229
+ b_indices: list[int] = []
230
+ for tag, i1, i2, j1, j2 in opcodes:
231
+ if tag in ("delete", "replace"):
232
+ a_indices.extend(range(i1, i2))
233
+ if tag in ("insert", "replace"):
234
+ b_indices.extend(range(j1, j2))
235
+
236
+ b_by_render: dict[tuple[str, ...], list[int]] = {}
237
+ for idx in b_indices:
238
+ key = tuple(render_subtree(parser, b_children[idx], 0))
239
+ b_by_render.setdefault(key, []).append(idx)
240
+
241
+ reordered_a: set[int] = set()
242
+ reordered_b: set[int] = set()
243
+ for idx in a_indices:
244
+ key = tuple(render_subtree(parser, a_children[idx], 0))
245
+ bucket = b_by_render.get(key)
246
+ if bucket:
247
+ reordered_a.add(idx)
248
+ reordered_b.add(bucket.pop(0))
249
+ return reordered_a, reordered_b
250
+
251
+
252
+ def _diff_children_unordered(
253
+ parser: VendorParser,
254
+ node_a: ConfigNode,
255
+ node_b: ConfigNode,
256
+ depth: int,
257
+ *,
258
+ hide_equal: bool,
259
+ path: tuple[str, ...],
260
+ ) -> Iterator[_Event]:
261
+ """Set-like matching keyed on ``line``, interleaved by position.
262
+
263
+ Within a parent, the IR guarantees each child has a unique ``line`` (same-named
264
+ siblings are merged on parse), so we can pair children by key. Matching stays
265
+ order-insensitive (pure reorders produce no diff), but the display walks ``a`` and
266
+ splices in ``b``-only children at their natural position — i.e. before the next
267
+ matched anchor — so ``-`` / ``+`` lines surface near each other instead of clumping
268
+ at the start and end.
269
+ """
270
+
271
+ a_children = node_a.children
272
+ b_children = node_b.children
273
+ b_by_line = {child.line: child for child in b_children}
274
+ common = {child.line for child in a_children} & set(b_by_line)
275
+ b_match_index = {
276
+ child.line: idx for idx, child in enumerate(b_children) if child.line in common
277
+ }
278
+
279
+ b_pointer = 0
280
+ for child_a in a_children:
281
+ if child_a.line not in common:
282
+ yield from _emit_one_side(parser, child_a, depth, "-")
283
+ continue
284
+ b_pos = b_match_index[child_a.line]
285
+ if b_pos >= b_pointer:
286
+ for k in range(b_pointer, b_pos):
287
+ child_b = b_children[k]
288
+ if child_b.line not in common:
289
+ yield from _emit_one_side(parser, child_b, depth, "+")
290
+ b_pointer = b_pos + 1
291
+ yield from _equal_pair(parser, child_a, b_by_line[child_a.line], depth, hide_equal, path)
292
+
293
+ for k in range(b_pointer, len(b_children)):
294
+ child_b = b_children[k]
295
+ if child_b.line not in common:
296
+ yield from _emit_one_side(parser, child_b, depth, "+")
297
+
298
+
299
+ def _equal_pair(
300
+ parser: VendorParser,
301
+ child_a: ConfigNode,
302
+ child_b: ConfigNode,
303
+ depth: int,
304
+ hide_equal: bool,
305
+ path: tuple[str, ...],
306
+ ) -> Iterator[_Event]:
307
+ a_is_section = not child_a.is_leaf
308
+ b_is_section = not child_b.is_leaf
309
+
310
+ if not a_is_section and not b_is_section:
311
+ # Equal leaf at this level. Suppress in the compact view, show as context in ndiff.
312
+ if not hide_equal:
313
+ yield _Event(" ", parser.render_leaf(child_a, depth))
314
+ return
315
+
316
+ if a_is_section != b_is_section:
317
+ # One side became a section while the other stayed a leaf — emit as replace.
318
+ yield from _emit_one_side(parser, child_a, depth, "-")
319
+ yield from _emit_one_side(parser, child_b, depth, "+")
320
+ return
321
+
322
+ # Both are sections with matching headers. Diff their children; only surface the section
323
+ # header if any descendant differs (compact mode) or always (ndiff).
324
+ inner = list(
325
+ _diff_children(
326
+ parser,
327
+ child_a,
328
+ child_b,
329
+ depth + 1,
330
+ hide_equal=hide_equal,
331
+ path=(*path, child_a.line),
332
+ )
333
+ )
334
+ has_change = any(ev.op != " " for ev in inner)
335
+
336
+ if not has_change and hide_equal:
337
+ return
338
+
339
+ yield _Event(" ", parser.render_open(child_a, depth))
340
+ yield from inner
341
+ close = parser.render_close(child_a, depth)
342
+ if close is not None:
343
+ yield _Event(" ", close)
344
+
345
+
346
+ def _emit_one_side(
347
+ parser: VendorParser,
348
+ node: ConfigNode,
349
+ depth: int,
350
+ op: str,
351
+ ) -> Iterator[_Event]:
352
+ for line in render_subtree(parser, node, depth):
353
+ yield _Event(op, line)
diffnc/errors.py ADDED
@@ -0,0 +1,22 @@
1
+ """Exception types used across diffnc."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class DiffncError(Exception):
7
+ """Base class for all diffnc errors."""
8
+
9
+
10
+ class ParseError(DiffncError):
11
+ """Raised when a configuration cannot be parsed or its vendor cannot be detected."""
12
+
13
+
14
+ class VendorMismatchError(DiffncError):
15
+ """Raised when two configurations belong to different vendors."""
16
+
17
+ def __init__(self, vendor_a: str, vendor_b: str) -> None:
18
+ super().__init__(
19
+ f"cannot diff configurations from different vendors: {vendor_a!r} vs {vendor_b!r}"
20
+ )
21
+ self.vendor_a = vendor_a
22
+ self.vendor_b = vendor_b
diffnc/ir.py ADDED
@@ -0,0 +1,58 @@
1
+ """Internal representation of a network configuration.
2
+
3
+ A configuration is modelled as a tree of :class:`ConfigNode`. Each node carries one logical
4
+ command line plus its (ordered) children. The whole document is wrapped in :class:`ConfigTree`
5
+ which records the originating vendor so that the diff engine can later format output back
6
+ in the input's flavour.
7
+
8
+ The constructors in this module also implement the normalisation rules described in the plan:
9
+
10
+ * Same-name non-leaf siblings get merged (their children concatenate).
11
+ * Duplicate leaf siblings collapse to one occurrence.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import dataclass, field
17
+
18
+
19
+ @dataclass
20
+ class ConfigNode:
21
+ """A single configuration statement, optionally with nested children."""
22
+
23
+ line: str
24
+ children: list[ConfigNode] = field(default_factory=list)
25
+
26
+ @property
27
+ def is_leaf(self) -> bool:
28
+ return not self.children
29
+
30
+ def add_child(self, child: ConfigNode) -> ConfigNode:
31
+ """Append *child* (or fold it into an existing same-named sibling) and return the
32
+ node that now represents it in the tree.
33
+
34
+ Indent-based vendors (e.g. NX-OS) can't tell at insertion time whether a line will
35
+ end up being a leaf or a section, so the merge rule is intentionally simple: any
36
+ sibling with the same ``line`` absorbs the new node's children. This collapses
37
+ repeated blocks (``interface eth1`` appearing twice) into one.
38
+ """
39
+
40
+ for sibling in self.children:
41
+ if sibling.line == child.line:
42
+ for grandchild in child.children:
43
+ sibling.add_child(grandchild)
44
+ return sibling
45
+ self.children.append(child)
46
+ return child
47
+
48
+
49
+ @dataclass
50
+ class ConfigTree:
51
+ """A parsed configuration document."""
52
+
53
+ root: ConfigNode
54
+ vendor: str
55
+
56
+ @classmethod
57
+ def empty(cls, vendor: str) -> ConfigTree:
58
+ return cls(root=ConfigNode(line=""), vendor=vendor)
diffnc/py.typed ADDED
File without changes
diffnc/reconcile.py ADDED
@@ -0,0 +1,132 @@
1
+ """Generate config-mode commands that transform config *A* into config *B*.
2
+
3
+ Walks two :class:`~diffnc.ir.ConfigTree`'s in parallel and emits a stream of
4
+ :class:`ReconcileEvent`'s — one per added subtree, deleted subtree, or order-sensitive
5
+ section that needs to be recreated wholesale. Each vendor's :meth:`render_reconcile` then
6
+ translates those events into its own CLI syntax (``no`` / ``set`` / ``delete`` ...).
7
+
8
+ Output is intentionally bare config-mode commands: no ``configure terminal`` / ``end`` /
9
+ ``commit`` wrappers, no indentation. The caller is expected to pipe the result into a
10
+ session that's already in config mode (e.g. ``... | ssh device 'configure terminal; ...'``).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from collections.abc import Iterable, Iterator
16
+ from dataclasses import dataclass
17
+
18
+ from diffnc.diff import _prepare
19
+ from diffnc.ir import ConfigNode
20
+ from diffnc.vendors.base import VendorParser, is_order_sensitive_for, render_subtree
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class ReconcileAdd:
25
+ """A subtree present in *B* but not in *A* at ``parent_path``."""
26
+
27
+ parent_path: tuple[str, ...]
28
+ node: ConfigNode
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class ReconcileDelete:
33
+ """A subtree present in *A* but not in *B* at ``parent_path``."""
34
+
35
+ parent_path: tuple[str, ...]
36
+ node: ConfigNode
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class ReconcileRecreate:
41
+ """An order-sensitive section whose children differ — wipe and recreate.
42
+
43
+ ``section_path`` is the full path *including* the section to recreate (its last
44
+ element is the section header). ``new_node`` is the B-side parent node, whose
45
+ children become the new contents of the section.
46
+ """
47
+
48
+ section_path: tuple[str, ...]
49
+ new_node: ConfigNode
50
+
51
+
52
+ ReconcileEvent = ReconcileAdd | ReconcileDelete | ReconcileRecreate
53
+
54
+
55
+ def reconcile(
56
+ a: str | Iterable[str],
57
+ b: str | Iterable[str],
58
+ *,
59
+ vendor: str | None = None,
60
+ ) -> Iterator[str]:
61
+ """Yield config-mode commands that, when entered on a device running config *A*,
62
+ bring it to the state described by config *B*.
63
+
64
+ Output lines do not include a trailing newline; callers that need newline-terminated
65
+ output should append one themselves (mirrors :func:`diffnc.unified_diff`).
66
+
67
+ Raises:
68
+ VendorMismatchError, ParseError: see :mod:`diffnc.errors`.
69
+ NotImplementedError: if the resolved vendor parser does not implement
70
+ :meth:`~diffnc.vendors.base.VendorParser.render_reconcile`.
71
+ """
72
+
73
+ parser, tree_a, tree_b = _prepare(a, b, vendor=vendor)
74
+ events = list(_collect_events(parser, tree_a.root, tree_b.root, parent_path=()))
75
+ if not events:
76
+ return
77
+ render = getattr(parser, "render_reconcile", None)
78
+ if render is None:
79
+ raise NotImplementedError(f"vendor {parser.name!r} does not support command generation")
80
+ yield from render(events)
81
+
82
+
83
+ # ---------------------------------------------------------------------------
84
+ # internals
85
+ # ---------------------------------------------------------------------------
86
+
87
+
88
+ def _collect_events(
89
+ parser: VendorParser,
90
+ node_a: ConfigNode,
91
+ node_b: ConfigNode,
92
+ parent_path: tuple[str, ...],
93
+ ) -> Iterator[ReconcileEvent]:
94
+ """Diff two parent nodes' children and yield reconcile events.
95
+
96
+ Mirrors the order-insensitive matching used by :func:`diffnc.diff._diff_children`
97
+ but emits semantic events instead of display lines, and short-circuits any
98
+ order-sensitive subsection into a single :class:`ReconcileRecreate`.
99
+ """
100
+
101
+ b_by_line = {c.line: c for c in node_b.children}
102
+ a_by_line = {c.line: c for c in node_a.children}
103
+
104
+ for child_a in node_a.children:
105
+ child_b = b_by_line.get(child_a.line)
106
+ if child_b is None:
107
+ yield ReconcileDelete(parent_path, child_a)
108
+ continue
109
+ if child_a.is_leaf and child_b.is_leaf:
110
+ continue
111
+ if child_a.is_leaf != child_b.is_leaf:
112
+ yield ReconcileDelete(parent_path, child_a)
113
+ yield ReconcileAdd(parent_path, child_b)
114
+ continue
115
+
116
+ next_path = (*parent_path, child_a.line)
117
+ if is_order_sensitive_for(parser, next_path):
118
+ if not _subtrees_equal(parser, child_a, child_b):
119
+ yield ReconcileRecreate(next_path, child_b)
120
+ continue
121
+
122
+ yield from _collect_events(parser, child_a, child_b, next_path)
123
+
124
+ for child_b in node_b.children:
125
+ if child_b.line not in a_by_line:
126
+ yield ReconcileAdd(parent_path, child_b)
127
+
128
+
129
+ def _subtrees_equal(parser: VendorParser, a: ConfigNode, b: ConfigNode) -> bool:
130
+ """True iff *a* and *b* render to identical line sequences."""
131
+
132
+ return render_subtree(parser, a, 0) == render_subtree(parser, b, 0)
@@ -0,0 +1,58 @@
1
+ """Vendor plugin registry.
2
+
3
+ Importing this package automatically registers the built-in vendors (Junos, Junos set,
4
+ NXOS, IOS, IOS-XE, IOS-XR, EOS). Third-party code can register additional vendors via
5
+ :func:`register`.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from diffnc.vendors.base import VendorParser
11
+
12
+ _REGISTRY: dict[str, VendorParser] = {}
13
+
14
+
15
+ def register(parser: VendorParser) -> None:
16
+ """Register *parser* under its :attr:`~VendorParser.name`."""
17
+
18
+ _REGISTRY[parser.name] = parser
19
+
20
+
21
+ def get(name: str) -> VendorParser:
22
+ """Look up the parser registered for *name*.
23
+
24
+ Raises:
25
+ KeyError: if no vendor with that name has been registered.
26
+ """
27
+
28
+ try:
29
+ return _REGISTRY[name]
30
+ except KeyError as exc:
31
+ raise KeyError(f"unknown vendor: {name!r}") from exc
32
+
33
+
34
+ def names() -> list[str]:
35
+ """Return all registered vendor names, sorted alphabetically."""
36
+
37
+ return sorted(_REGISTRY)
38
+
39
+
40
+ # Register built-in vendors. Imports are placed after the registry is defined to avoid
41
+ # circular-import surprises when sub-modules call back into this package at import time.
42
+ from diffnc.vendors import eos as _eos # noqa: E402
43
+ from diffnc.vendors import ios as _ios # noqa: E402
44
+ from diffnc.vendors import iosxe as _iosxe # noqa: E402
45
+ from diffnc.vendors import iosxr as _iosxr # noqa: E402
46
+ from diffnc.vendors import junos as _junos # noqa: E402
47
+ from diffnc.vendors import junos_set as _junos_set # noqa: E402
48
+ from diffnc.vendors import nxos as _nxos # noqa: E402
49
+
50
+ register(_junos.PARSER)
51
+ register(_junos_set.PARSER)
52
+ register(_nxos.PARSER)
53
+ register(_ios.PARSER)
54
+ register(_iosxe.PARSER)
55
+ register(_iosxr.PARSER)
56
+ register(_eos.PARSER)
57
+
58
+ __all__ = ["VendorParser", "get", "names", "register"]