Rust rewrite - #302
Draft
aedwardstx wants to merge 32 commits into
Draft
Conversation
Bring across every file the rewrite added that upstream/next never touched: the hier_config_core and hier_config_py crates, the generated config corpus under testdata/, the Rust-native and parity test suites, the benchmark regression gate, and the cargo tooling config. These 446 files are disjoint from next's own changes, so they land wholesale. The 70 files both branches touched are re-derived from next's versions in the following commits rather than overwritten. Python packaging is deliberately left on next's pure-Python layout here; this commit only establishes that the Rust workspace builds, lints and tests clean against the new base. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Phase 3a of docs/plans/rust-rewrite-onto-next.md. Convert next's tree modules into re-export shims over the Rust core and rebuild the exception hierarchy so the native classes stay canonical. HierConfigError and DuplicateChildError are raised from Rust, so redefining them in Python would stop `except DuplicateChildError:` from matching errors the core raises; they are now imported from the extension and next's three Python-only errors subclass the native base. Repoint the benchmark helper imports at next's relocated tests/benchmarks/test_benchmarks.py. Record the Phase 3b sizing finding in the plan: diffing next against the merge base rather than against the rewrite shows the per-driver deltas are 7-32 mechanical lines, that the Rust rules.json needs no change under next's unified negation model, and that the overridable negate_with() hook is the one real API incompatibility left. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Phase 3b of docs/plans/rust-rewrite-onto-next.md. next (netdevops#220) collapsed NegationDefaultWhenRule, NegationDefaultWithRule and NegationSubRule into a single ordered `negation` list carrying a NegationStrategy. The Python driver rules are handed to the core by serializing them and deserializing into DriverRules, and DriverRules did not declare that field. serde is not configured to deny unknown fields, so every v4-spelled negation rule was being dropped on the floor: a driver using the new spelling would silently negate with a bare prefix swap instead of its declared strategy, producing wrong remediation with no error to point at it. Teach DriverRules about the unified list and evaluate it the way the Python contract describes: REPLACE is consulted first regardless of position, then the remaining strategies are tried in declaration order rather than grouped by kind. The three v3 lists are still accepted and are folded in after the unified list, matching all_negation_rules(), so the committed platform rules.json files keep validating unchanged and drivers may mix both spellings. Keep a fast path for the common case: when no unified rules are present the original grouped walk runs and nothing is allocated. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Reconcile the Rust rewrite with `next` across four seams that the two branches had diverged on. Views: `next` restructured config views into a shared Python base plus four prefix-constant-driven mixins, which the Rust view layer never mirrored -- and the Rust Arista and generic views were never more than stubs. The view layer is not on the remediation hot path, so adopt `next`'s Python views wholesale; all 277 of its view tests pass unmodified against the native tree. `parse_ipv4_interface` comes along because the views need it. Ingestion/serialization: `future()` and `remediation()` build native trees, so any method defined only on a Python `HConfig` subclass silently vanished from the objects they returned. Move the seven v4 classmethods into the Rust class -- delegating back into Python for the actual work -- so every tree the core hands back carries the full API, and `root.py` becomes a plain shim. Negation prefixes: `sync_rules()` reads `negation_prefix` from the Python driver, so the thin set-style drivers that had dropped the override were silently resetting `delete `/`undo `/`unset ` back to `no `. Restore the overrides and sync `declaration_prefix` alongside it. Workflows: add the `plugins` argument (netdevops#181), run the driver's `remediation_transform_callbacks` (netdevops#180), add `remediation_json` (netdevops#287) and `remediation_netconf_xml` (netdevops#232), and raise `IncompatibleDriverError` rather than `ValueError` on a driver mismatch. Suite: 360 -> 33 failures. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add FutureReport plumbing through the Rust remediation engine and expose future_with_report() natively, with future() delegating to it. Restore the swap_negation / idempotent_for / negate_with / sectional_exit overrides that next's drivers rely on. The v4 engine never calls these hooks during remediation, but they remain part of the public driver API, so mark the core-mirrored ones @core_owned to satisfy the removed-hook guard while keeping direct calls correct. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The report's nodes must be identical objects to those the caller reaches through get_child(), so build them with the interning path rather than fresh handles. Also restore the config_preprocessor staticmethods that next's set-style drivers expose as public API. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
PyO3 exposes __init__ as an ordinary method rather than the tp_init slot, so the native constructor could not record the object it had just built. The first accessor to ask for the tree's root therefore won the race and every later caller got a different Python object for the same tree. Claim the handle at the library's construction sites, hand recursive traversals back through a real generator using interned child handles, keep transform callbacks out of the rules JSON bridge, and raise the Python-side InvalidConfigError for unterminated banners.
Four behaviors from next were still being overridden by the Rust core. Post-load callbacks are removable by identity in next (netdevops#286), but the core dispatched its own implementations unconditionally, so dropping the Python callable changed nothing. DriverRules now carries an optional enabled_post_load allow-list that sync_rules populates from the driver's surviving callbacks; None keeps every callback on for pure-Rust callers. is_duplicate_child_allowed returned early for the root node, which pre-empted rule evaluation entirely. A rule with empty match_rules describes root's empty lineage and must match it (netdevops#215). get_hconfig_from_dump ran callbacks against the still-empty tree. Callbacks normalize parsed configuration, so they now run once the dumped lines are in place. This is a deliberate behavior change from v3. The extension-surface test also now enumerates the unified negation rule field and the remediation transform callbacks. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
next's Python view layer is the one that ships: it is complete, fully tested, and referenced everywhere. The Rust port never got past stubs (66 unimplemented methods for Arista, 38 for generic) and nothing -- Python or Rust -- imported it, so keeping it only invited drift between two view implementations. Removes crates/hier_config_core/src/view/, the seven per-platform view.rs files, the PyO3 wrapper, and the NativeHConfigView / NativeConfigViewInterface exports and their stub declarations. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The pyproject graft that brought over next's packaging tail dropped the tooling configuration the Rust rewrite depends on, so every Python linter failed for reasons unrelated to the code: ruff lost its per-file-ignores, mypy and pyright lost the `stubs/` search path that resolves the compiled `_hier_config_rust` extension, and pylint lost the extension allow-list. Restore all of them, translating the paths through next's test renames. Regenerate the four tree stubs. The generator now knows about v4: `depth` is a property rather than a method, `__eq__` and `__hash__` are emitted (object defines them too, so the "not inherited" filter was dropping them and every `config == other` assertion read as a non-overlapping equality check), and the private native loaders `hier_config.constructors` drives are declared. Its `from_json`/`from_xml`/`from_text` signatures and the non-existent `HConfig.from_file` were fabricated; correct them against the live extension. Reconcile `_instantiate_rules` back to next's staticmethod contract. The rewrite had made it a classmethod so built-in drivers could inherit a platform-driven implementation, but that silently broke the v4 extension point next published: custom drivers written against next declare a staticmethod. Give the nine drivers that were inheriting an explicit one-line override instead. Make `core_owned` generic. It marks both post-load callbacks and the core-owned hook overrides, which have unrelated signatures, so a fixed `Callable[[HConfig], None]` annotation erased the type of every hook it decorated. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The workflows on next still installed poetry and ran `poetry install`, which cannot build this project any more: the extension module is compiled by maturin and `[project.version]` is dynamic. Restore the maturin pipeline — Rust fmt/clippy/doc/MSRV/coverage, cargo-deny, a wheel-in-clean-venv packaging check, and the calibrated performance gate — and add `next` to the branch triggers so it runs on the v4 integration branch too. `poetry version` was the release workflow's version bumper. The number now lives on `[workspace.package]` in Cargo.toml, so replace it with scripts/bump_version.py, which emits both the SemVer form Cargo requires and the PEP 440 form maturin gives the wheel. The tag follows the wheel. Relocate the Rust-specific test modules under tests/native/ rather than leaving them loose at the root of next's restructured tests/ tree, and move the two remaining flat driver tests into tests/integration/ alongside their eleven siblings. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
next restructured docs/ and wrote its own user/migrating-from-v3.md covering the v4 API renames. The Rust branch had brought a separate page with a nearly identical title covering a different subject — the behavior differences that fall out of the engine swap. Two pages both called "migrating to v4" is worse than either, so rename this one to user/rust-core-changes.md, scope it explicitly to the engine, and point it at next's page as the prerequisite. Three of its sections had gone stale against the reconciled implementation and were actively misleading: - The hook matrix said idempotent_for/negate_with/sectional_exit were removed and could not be overridden. They can, when the override carries the @core_owned marker, which is how the in-tree drivers declare them. - It claimed HConfigDriverBase drops ABC and that _instantiate_rules became a classmethod. Neither is true: next's staticmethod contract was kept, and only the @AbstractMethod decorator was dropped. - It claimed every traversal returns a tuple. all_children and all_children_sorted are real generators; only the two that cannot produce a result before walking the whole tree return tuples. Repoint the links the restructure broke, add nav entries for the two new developer pages, and add a redirect for the renamed migration page. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
griffe reads the .pyi stubs, not the compiled extension, so a member whose signature comes from RAW_OVERRIDES rendered as a bare signature on the API reference page even when the Rust source documented it. That hit the five v4 ingestion constructors -- from_text, from_lines, from_dump, from_json, from_xml -- which are the first block on that page. Splice the live __doc__ into a raw override block when the block does not already spell one out, so a docstring written once in Rust reaches both help() and the rendered docs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add the compiled-wheel packaging change, the core_owned marker, the poetry-to-maturin move, the from_dump callback-timing change, and the two removals the rewrite makes. Qualify the netdevops#222 design-decision bullet: the three engine-resolved hooks are no longer freely overridable. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The performance gate caught iteration at roughly twice its budget. Two causes, both introduced while reconciling with next: lazy_children() materialized every node through get_or_create_child(), the interning lookup, which costs a lock, a hash, a weakref allocation and an upgrade per node. That path exists to keep handle identity stable for single-node lookups; a whole-tree walk needs none of it. Use the documented bulk materializer instead, as all_children_sorted_by_tags() already did. all_children_sorted() also wrapped its result in a Python generator, adding a frame resume per node. Sorting has to see the whole tree before it can yield anything, so the generator only misrepresented the cost. Return the sequence. Only all_children() is contractually a generator. A full all_children_sorted() walk over a 7,400-node config drops from 0.70ms to 0.11ms; the gate goes from 0.1158 to 0.0378 calibration units against a 0.06 ceiling. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
migrating-from-v3.md covered the rename surface and stopped there; the engine-level differences lived in a page nothing pointed at. Name it as the required next read, and summarize the six core-driven behavior changes -- most importantly the driver-hook audit -- in the section where a reader is already looking for things that will break. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Mark all six phases complete and correct the two Phase 3 notes that execution overturned: the unified negation list did need a core change (DriverRules has no deny_unknown_fields, so next's rules would have been silently discarded), and the negate_with divergence was resolved with the core_owned marker rather than left open. Add an execution record covering the rename trap, the seven reconciliations that needed judgement, the lost-tooling-config lesson, and the two defects the Phase 6 audit surfaced. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The built-in drivers were already Rust-native -- their rules live as JSON inside the core and Python reads them, so there is a single source of truth. The constructors were already there too. What blocked a Rust-only consumer was the view layer: 1,368 lines of Python with no native equivalent. Port it. Views are traits with default method bodies rather than the mixin union that sank the first attempt (abf3d6f), so a platform implements only the hooks its Python sibling overrides; optional behaviour is gated behind supports_vlan/supports_nac/supports_physical/bundle_prefix. Six platforms have a Python view.py and so get a port; the other seven are listed explicitly in the dispatch match, making a new Platform variant a compile error until someone decides. The Python view layer is deliberately untouched. Instead both sides are pinned to a shared corpus under testdata/views/, generated from Python and asserted from Rust, with a Python-side --check that fails when the snapshots go stale. That caught three real bugs in the port on its first run. Add constructors.rs for one-call Rust entry points and a standalone_usage integration test that walks driver -> load -> remediate -> view using only hier_config_core. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
scripts/gen_stubs.py has supported --check since it was written, but nothing ever invoked it. The .pyi stubs for the four Rust-backed tree types are the only type information mypy, pyright, and griffe have for the compiled extension, so a PyO3 signature change that was not mirrored into the stubs would ship wrong types with a fully green gate. Add a check-stubs build command and wire it into both lint and lint-and-test, alongside the existing check_displacement_markers step. Verified it exits 1 on injected drift and 0 on a clean tree. Also document the generator in docs/dev/architecture.md, which the stub file headers already pointed at but which had never mentioned .pyi files. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The Rust core resolves negation, idempotency, and sectional exiting from each driver's rule data. The Python implementations of idempotent_for(), negate_with(), sectional_exit(), and swap_negation() were therefore never called at runtime -- a monkeypatch probe across every platform confirmed none of the four hooks executes during remediation. Leaving ~560 lines of unreachable code in driver_base.py and the platform drivers made the Python surface look authoritative when it was not, and invited contributors to fix bugs in code that cannot run. The @core_owned escape hatch existed only to let those dead overrides past the guard, so it goes too; __init_subclass__ now rejects all four hooks unconditionally and core_owned() reverts to marking post-load callbacks. Rule data, negation_prefix, and declaration_prefix are unchanged and stay the supported way to shape these behaviors. Remediation and rollback output is byte-identical before and after across nine platforms. Tests that drove the deleted methods directly are rewritten to assert the live behavior through child.negate() and end-to-end remediation, so the semantics they covered -- including the FortiOS unset handling from netdevops#225 -- remain pinned. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
`crates/hier_config_py/src/workflow.rs` called `py.import("hier_config.formats")`
to render NETCONF and gNMI payloads, so the Rust core depended on the Python
package it is supposed to back. A pure-Rust consumer got no structured output
at all, and the 552-line Python implementation was untyped surface area that
no longer earned its keep in a Rust-first v4.
Move all six conversions into `hier_config_core::formats` and reduce
`hier_config/formats.py` to a 157-line documented wrapper. Both interpreter
call-backs in `workflow.rs` are gone.
Behavior is pinned by a 385-case parity corpus captured from the previous
Python implementation before any of it was touched; the corpus is generated
from Python and made to pass in Rust, never the reverse.
The port surfaced two real defects that the corpus did not cover but
`tests/unit/test_formats.py` did: duplicate list entries were silently
de-duplicated instead of raising `DuplicateChildError`, and `FormatError`
flattened `TreeError` to a string, degrading the exception type at the Python
boundary. Both are fixed.
Wire two anti-drift guards into the lint gate, since the hand-written
extension stub is what gives mypy and pyright their view of the native
surface: `gen_stubs.py --check` now also diffs the live `_hier_config_rust`
surface against `stubs/_hier_config_rust.pyi`, and `gen_formats_corpus.py
--check` joins the gate.
Adds `quick-xml` as a Rust build dependency; it compiles into the wheel and
does not change the package's Python dependencies.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The view layer existed twice: ~1,321 lines of pure Python under hier_config/platforms/*/view.py, and a complete Rust implementation in hier_config_core/src/view/ that no Python caller could reach. Keeping the two honest required a generated corpus (scripts/gen_view_corpus.py, testdata/views/, plus a Rust and a Python test harness) that only existed because the duplication existed. Expose the Rust view through PyO3 (crates/hier_config_py/src/view.rs) and reduce the Python modules to a facade that re-exports the native classes. The duplicate implementation and its entire anti-drift apparatus are gone. Two isinstance protocols had to survive the collapse: the netdevops#227 capability mixins, which tests assert *negatively*, and the per-platform interface classes used for type narrowing. A @runtime_checkable Protocol cannot serve either, because the native class carries every attribute and so would satisfy every negative assertion. ViewMarkerMeta.__instancecheck__ answers both from the native `capabilities` and `platform` data instead. Three properties that raised in Python now return None or () — a v4 breaking change, documented in docs/user/rust-core-changes.md. scripts/gen_stubs.py --check now also diffs each stub class's declared members against the runtime pyclass, so a #[getter] added without a stub entry fails the lint gate rather than silently degrading callers to Unknown under pyright strict. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The existing guards only compared *names*: gen_stubs.py --check diffs the module and class member sets, so a stub could promise a parameter the compiled extension rejects and nothing noticed. That is not hypothetical. add_tags/remove_tags/tags_add/tags_remove documented `tag` but took `tag_or_tags`, and get_child_deep/get_children_deep documented `match_rules` but took `rules`. Calling them by keyword as the stubs describe type checked cleanly under both mypy and pyright, then raised TypeError at runtime. Rename the native parameters to the published names and lock the contract with a test. Run mypy.stubtest over both _hier_config_rust and the hier_config package in the lint gate so signature drift fails CI. The extension needs no exemptions; the package carries an audited allowlist covering pydantic, PyO3 enum, tp_new and sentinel-default idioms that stubtest cannot model. Unused entries are an error, so it cannot rot. This also lets the stale HConfigChildrenIter/GnmiRemediation exemption in verify_extension_surface go away, so the native stub's __all__ is now exactly the runtime surface. Return and parameter type annotations stay outside any gate: they are not introspectable from a compiled extension, so they remain the type checkers' responsibility alone. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Return annotations cannot be read back from a compiled extension, so the stub is the type checkers' *premise* rather than something they verify. Rewriting `vlan_ids -> frozenset[int]` as `dict[str, bytes]` changed the pyright strict error count by zero and stubtest passed; the one case that did move pyright (`vlans -> list[str]`) reported 38 errors in the test files that consume the value, never in the stub that lied. That coverage is incidental, and it blames the wrong file. Generating the stub from Rust does not close this. 39 of 131 exported self-methods (30%) return an opaque `PyObject`, so `vlans`, `stack_members` and `ipv4_default_gw` share one Rust signature and three Python types; derivation would emit `Any` for exactly the members most worth checking. `pyo3-introspection` erases the same set -- `type_object.rs:64` defaults `TYPE_HINT` to `_typeshed.Incomplete` -- and `pyo3-stub-gen`'s `override_type` only relocates the same hand-written strings into Rust: `type_repr` is stored as an unvalidated `String`, and their own test fixture ships a malformed `Callable[[str]]` unnoticed. It would also require moving off the pinned pyo3 0.24. That remains complementary for *parameter* types and is worth revisiting whenever PyO3 is upgraded for other reasons; the reasoning is recorded in the script's docstring. So check the stub against reality instead: exercise each declared member against a corpus of real configs and compare the observed value to its annotation, descending into container element types. Empty containers count as unobserved rather than failing, which keeps the check one-sided and free of false positives. Its first run found two stubs that contradicted their objects. `all_children_sorted()` was declared `Iterator[HConfigChild]` but returns an eager sequence, so the documented `next(...)` raised `TypeError`. `ConfigViewInterface.poe` was declared `bool` but is `Option<bool>` and returns `None` on EOS, NX-OS and XR. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Listing the members the type checker could not observe showed my own framing of them was wrong. They were not one category of fixture gap: six of the nineteen were holes in the harness. `HConfigChildren` lives in `hier_config.children`, a module the annotation resolver never imported, so `HConfigBase.children` failed to resolve. No `WorkflowRemediation` was ever constructed, leaving `remediation_config` and `rollback_config` — the members most worth verifying — entirely unchecked. And `RemediationTransform` is declared only in the stub, so `plugins` could not resolve either. Pair same-platform fixtures into workflows, harvest stub-only `TypeAlias` declarations into the resolver namespace, and teach the value check about `Callable`. One workflow is built with a no-op plugin so that arm is exercised rather than merely written. Coverage goes from 63 members to 69. The thirteen that remain are now honestly one category: no fixture produces a non-empty value, and an empty container cannot contradict an element type. Gating on a coverage count would fail whenever a fixture changed, teaching people to edit the threshold. Pin the identities instead, following `stubs/stubtest-allowlist.txt`: an unlisted gap fails so a new stub member cannot arrive unverified, and a stale entry fails so the list cannot outlive the gaps it records. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Remove obsolete ephemeral task list root-handle-identity-fix.md (specified self-deletion on completion). - Archive fully implemented rust-native-workflow-remediation.md to docs/plans/archived/ with status updated to Shipped. - Archive fully implemented rust-native-drivers-constructors-views.md to docs/plans/archived/ with status updated to Shipped. - Archive rust-rewrite-onto-next.md to docs/plans/archived/ following completed rebase. - Archive python-test-suite-trim.md to docs/plans/archived/ with status updated to Closed/Superseded by upstream/next migration and Python facade consolidation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Delete docs/plans directory containing completed and archived planning docs that are no longer needed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Upgrade quick-xml to 0.41 to resolve cargo-deny advisory RUSTSEC-2026-0195. - Update XML parser for quick-xml 0.41 Event::GeneralRef entity handling and attribute value normalization. - Fix rustdoc intra-doc link to private method in models.rs and remove redundant link target in view/platforms/mod.rs under -D warnings. - Create and activate virtual environments in GitHub Actions workflows before invoking maturin develop and test/lint tooling. - Ignore .venv directory in yamllint configuration. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Replace Option<Tree> and Option<Py<PyHConfig>> lazy caches in WorkflowRemediation with OnceLock, converting query methods and properties from &mut self to &self - Eliminate PyO3 BorrowMutError during concurrent or re-entrant property access from Python - Encapsulate loose parser accumulators into ParserState, ParserCursor, and IndentTracker in hier_config_core::parser - Introduce pure transactional tree constructors (parse_tree, parse_fast, etc.) returning complete trees without caller-provided mutable instances - Update Tree::from_str and Tree::from_str_with_callbacks to delegate to pure constructors - Update tests, benchmarks, stubs, and changelog Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…zing Document the distinction between direct children (`len(config.children)`) and recursive descendants (`len(config)`), highlighting O(1) sizing on root nodes and zero-allocation descendant counting in the Rust core. Detail lazy stack-based descendant iterators (`Tree::descendants`), encapsulation of remediation context structs, and `WorkflowRemediation` thread safety with `OnceLock` lazy caching. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…e stubs - Localize vendor-specific platform logic (sectional exits, negation swapping, preprocessors, post-load callbacks) behind a unified PlatformOps trait in platforms/<platform>/mod.rs. - Implement native Tree domain logic (merge, with_tags, add_ancestor_copy_of, from_json, from_xml) and WorkflowRemediation serialization in hier_config_core without Python runtime dependencies. - Fallback to source driver idempotency rules when target/change config is generic in Tree::idempotent_for, Tree::base_idempotent_for, and Tree::is_idempotent_command. - Provide root _hier_config_rust.pyi for maturin wheel stub packaging and assert sync with stubs/_hier_config_rust.pyi in lint scripts and unit tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
_hier_config_rust is recognized as first-party now that root stubs are packaged, so remove the blank line separating it from hier_config. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR lands the complete Rust rewrite of
hier_configfor the v4.0.0 major release (4.0.0-beta.4), rebased on top ofnext.Parsing, tree data structures, post-load fixups, remediation algorithms, structured formats (JSON, XML, NETCONF, gNMI), and configuration views are now implemented natively in Rust (
crates/hier_config_core) and exposed to Python via PyO3 (_hier_config_rust).hier_config_coreis also structured for direct, standalone Rust library consumption without Python or PyO3 dependencies.hier_confignow ships as compiled platform wheels built withmaturinfor CPython 3.10–3.14 across Linux, macOS, and Windows. Non-wheel platforms require a Rust 1.98+ toolchain to build from source.HConfigDriverBase:idempotent_for(),negate_with(),sectional_exit(), andswap_negation()(and their private helper machinery) have been removed. These behaviors are resolved natively by the Rust core from driver rule definitions (rules.negation,rules.idempotent_commands, etc.). Subclasses defining any of these four hooks now raiseTypeError.hier_config/platforms/view_base.pyand platform views) is now a native PyO3 facade backed by Rust.nac_max_dot1x_clientsandnac_max_mab_clientsreturnNoneinstead of raisingNotImplementedError(Cisco IOS, Aruba AOS-CX).module_numberreturnsNoneinstead of raisingAttributeError(Arista EOS, Cisco NX-OS, Cisco XR).bundle_member_interfacesreturns()instead of raising on a non-trunk interface (HP ProCurve).formats.py):from_json,to_json,from_xml,to_xml,hconfig_to_netconf_xml,hconfig_to_gnmi_json) are implemented in Rust. Duplicates in XML/JSON lists raiseDuplicateChildErrorinstead of being silently deduplicated or flattened to generic errors.Key Additions & Improvements
testdata/cases/) executed against both Python and Rust test harnesses.testdata/formats/expected.json).crates/hier_config_core/tests/properties.rsverifying structural invariants, parse totality, self-remediation emptiness, future consistency, rollback fidelity, and panic robustness across arbitrary inputs.scripts/gen_stubs.py --checkprevents undocumented PyO3 symbols and stub drift.mypy.stubtestcompares.pyisignatures against compiled extension runtime symbols with audited exemptions instubs/stubtest-allowlist.txt.scripts/check_stub_types.pyinspects live returned values across test fixtures against declared stub return types, backed bystubs/unobserved-allowlist.txtto eliminate silent type degradation under strictmypyandpyright.Self-Review Checklist
python scripts/build.py lint-and-testpasses locally (lint + 88% test coverage).CHANGELOG.mdhas an entry under## [Unreleased]referencing this issue/PR ((#NNN)).mkdocs build --strictpasses if docs were touched).AI-Assisted Contributions
This PR was developed and reviewed with AI coding assistance following the repository standards outlined in
AGENTS.mdand the developer documentation. All linting, strict typing, cargo tests, proptest suites, coverage floors, and documentation builds have been validated locally.