Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Skipped by default; run with `poetry run pytest -m benchmark -v -s`.

- Added support for Huawei VRP with a new driver and test suite (#238).
- Custom exception hierarchy: `HierConfigError` base, `DriverNotFoundError`,
`InvalidConfigError`, `IncompatibleDriverError` (#219). `DuplicateChildError`
reparented under `HierConfigError`.
Comment on lines +18 to +20
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changelog entry describes a breaking behavior change (replacing ValueError with custom exceptions and reparenting DuplicateChildError). To match the existing changelog structure (e.g., other breaking renames listed under "Changed"), consider moving this bullet to the "### Changed" section or explicitly calling out that it’s breaking in the "Added" entry.

Copilot uses AI. Check for mistakes.

### Changed

Expand Down
12 changes: 12 additions & 0 deletions hier_config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,27 @@
get_hconfig_from_dump,
get_hconfig_view,
)
from .exceptions import (
DriverNotFoundError,
DuplicateChildError,
HierConfigError,
IncompatibleDriverError,
InvalidConfigError,
)
from .models import ChangeDetail, MatchRule, Platform, ReportSummary, TagRule
from .reporting import RemediationReporter
from .root import HConfig
from .workflows import WorkflowRemediation

__all__ = (
"ChangeDetail",
"DriverNotFoundError",
"DuplicateChildError",
"HConfig",
"HConfigChild",
"HierConfigError",
"IncompatibleDriverError",
"InvalidConfigError",
"MatchRule",
"Platform",
"RemediationReporter",
Expand Down
7 changes: 4 additions & 3 deletions hier_config/constructors.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from hier_config.platforms.driver_base import HConfigDriverBase

from .child import HConfigChild
from .exceptions import DriverNotFoundError, InvalidConfigError
from .models import Dump, Platform
from .platforms.arista_eos.driver import HConfigDriverAristaEOS
from .platforms.arista_eos.view import HConfigViewAristaEOS
Expand Down Expand Up @@ -49,7 +50,7 @@ def get_hconfig_driver(platform: Platform) -> HConfigDriverBase:

if driver_cls is None:
message = f"Unsupported platform: {platform}"
raise ValueError(message)
raise DriverNotFoundError(message)

return driver_cls()

Expand All @@ -72,7 +73,7 @@ def get_hconfig_view(config: HConfig) -> HConfigViewBase:
return HConfigViewHPProcurve(config)

message = f"Unsupported platform: {config.driver.__class__.__name__}"
raise ValueError(message)
raise DriverNotFoundError(message)


def get_hconfig(
Expand Down Expand Up @@ -307,4 +308,4 @@ def _load_from_string_lines(config: HConfig, config_text: str) -> None: # noqa:
end_indent_adjust.pop(0)
if in_banner:
message = "we are still in a banner for some reason"
raise ValueError(message)
raise InvalidConfigError(message)
18 changes: 17 additions & 1 deletion hier_config/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,18 @@
class DuplicateChildError(Exception):
class HierConfigError(Exception):
"""Base exception for all hier_config errors."""


class DuplicateChildError(HierConfigError):
"""Raised when attempting to add a duplicate child."""


class DriverNotFoundError(HierConfigError):
"""Raised when a platform driver cannot be found."""


class InvalidConfigError(HierConfigError):
"""Raised for malformed configuration text."""


class IncompatibleDriverError(HierConfigError):
"""Raised when configs with mismatched drivers are used together."""
3 changes: 2 additions & 1 deletion hier_config/workflows.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from collections.abc import Iterable
from logging import getLogger

from .exceptions import IncompatibleDriverError
from .models import TagRule
from .root import HConfig

Expand Down Expand Up @@ -59,7 +60,7 @@ def __init__(

if running_config.driver.__class__ is not generated_config.driver.__class__:
message = "The running and generated configs must use the same driver."
raise ValueError(message)
raise IncompatibleDriverError(message)
Comment on lines 61 to +63
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The WorkflowRemediation docstring still documents Raises: ValueError for driver mismatches, but the implementation now raises IncompatibleDriverError. Please update the docstring to match the new exception type so API docs stay accurate.

Copilot uses AI. Check for mistakes.

self._remediation_config: HConfig | None = None
self._rollback_config: HConfig | None = None
Expand Down
15 changes: 10 additions & 5 deletions tests/unit/test_constructors.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,25 @@
_load_from_string_lines, # pyright: ignore[reportPrivateUsage]
get_hconfig_fast_generic_load,
)
from hier_config.exceptions import DriverNotFoundError, InvalidConfigError
from hier_config.models import Platform
from hier_config.root import HConfig


def test_get_hconfig_driver_unsupported_platform() -> None:
"""Test ValueError when platform is not supported (lines 49-50)."""
with pytest.raises(ValueError, match="Unsupported platform: invalid_platform"):
"""Test DriverNotFoundError when platform is not supported (lines 49-50)."""
with pytest.raises(
DriverNotFoundError, match="Unsupported platform: invalid_platform"
):
get_hconfig_driver("invalid_platform") # type: ignore[arg-type]


def test_get_hconfig_view_unsupported_platform() -> None:
"""Test ValueError when platform is not supported (lines 72-73)."""
"""Test DriverNotFoundError when platform is not supported (lines 72-73)."""
driver = get_hconfig_driver(Platform.FORTINET_FORTIOS)
config = HConfig(driver=driver)
with pytest.raises(
ValueError, match="Unsupported platform: HConfigDriverFortinetFortiOS"
DriverNotFoundError, match="Unsupported platform: HConfigDriverFortinetFortiOS"
):
get_hconfig_view(config)

Expand Down Expand Up @@ -237,7 +240,9 @@ def test_load_from_string_lines_with_incomplete_banner() -> None:
This is line 1
This is line 2
"""
with pytest.raises(ValueError, match="we are still in a banner for some reason"):
with pytest.raises(
InvalidConfigError, match="we are still in a banner for some reason"
):
_load_from_string_lines(config, config_text)


Expand Down
52 changes: 52 additions & 0 deletions tests/unit/test_exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Tests for the custom exception hierarchy (#219)."""

import pytest

from hier_config import Platform, WorkflowRemediation, get_hconfig, get_hconfig_driver
from hier_config.exceptions import (
DriverNotFoundError,
DuplicateChildError,
HierConfigError,
IncompatibleDriverError,
InvalidConfigError,
)


def test_hier_config_error_is_base_exception() -> None:
"""All custom exceptions inherit from HierConfigError."""
assert issubclass(DuplicateChildError, HierConfigError)
assert issubclass(DriverNotFoundError, HierConfigError)
assert issubclass(InvalidConfigError, HierConfigError)
assert issubclass(IncompatibleDriverError, HierConfigError)


def test_hier_config_error_is_catchable_as_exception() -> None:
"""HierConfigError itself inherits from Exception."""
assert issubclass(HierConfigError, Exception)


def test_duplicate_child_error_on_duplicate_section() -> None:
config = get_hconfig(Platform.CISCO_IOS, "interface Loopback0")
with pytest.raises(DuplicateChildError, match="Found a duplicate section"):
config.add_child("interface Loopback0")


def test_driver_not_found_error_invalid_platform() -> None:
"""get_hconfig_driver raises DriverNotFoundError for unsupported platforms."""
with pytest.raises(DriverNotFoundError, match="Unsupported platform"):
get_hconfig_driver("bogus_platform") # type: ignore[arg-type]


def test_incompatible_driver_error_mismatched_drivers() -> None:
"""WorkflowRemediation raises IncompatibleDriverError for mismatched drivers."""
running = get_hconfig(Platform.CISCO_IOS)
generated = get_hconfig(Platform.ARISTA_EOS)
with pytest.raises(IncompatibleDriverError, match="same driver"):
WorkflowRemediation(running, generated)


def test_invalid_config_error_banner_parsing() -> None:
"""Malformed banner config raises InvalidConfigError."""
config_text = "banner motd ^C\nthis banner never ends"
with pytest.raises(InvalidConfigError, match="banner"):
get_hconfig(Platform.CISCO_IOS, config_text)
4 changes: 3 additions & 1 deletion tests/unit/test_workflows.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pytest

from hier_config import WorkflowRemediation, get_hconfig
from hier_config.exceptions import IncompatibleDriverError
from hier_config.models import Platform, TagRule


Expand Down Expand Up @@ -50,7 +51,8 @@ def test_remediation_config_driver_mismatch() -> None:
generated_config = get_hconfig(Platform.JUNIPER_JUNOS, "dummy_config")

Comment on lines 51 to 52
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There’s an outdated comment just above this block that says the test ensures a ValueError is raised for mismatched drivers, but the expectation is now IncompatibleDriverError. Please update/remove that comment to keep the test intent accurate.

Copilot uses AI. Check for mistakes.
with pytest.raises(
ValueError, match=r"The running and generated configs must use the same driver."
IncompatibleDriverError,
match=r"The running and generated configs must use the same driver.",
):
WorkflowRemediation(running_config, generated_config)

Expand Down
Loading