diff --git a/.gitignore b/.gitignore index ac53476..19acbe1 100644 --- a/.gitignore +++ b/.gitignore @@ -109,3 +109,8 @@ venv.bak/ # vscode .vscode/ +.qodo + +# undodir +.undodir/ +*.undodir/ \ No newline at end of file diff --git a/hier_config/child.py b/hier_config/child.py index df3a90c..160fb67 100644 --- a/hier_config/child.py +++ b/hier_config/child.py @@ -1,7 +1,7 @@ from __future__ import annotations from itertools import chain -from logging import getLogger +from logging import Logger, getLogger from re import search from typing import TYPE_CHECKING, Any, Optional, Union @@ -16,13 +16,13 @@ from .root import HConfig -logger = getLogger(__name__) +logger: Logger = getLogger(name=__name__) class HConfigChild( # noqa: PLR0904 pylint: disable=too-many-instance-attributes HConfigBase, ): - __slots__ = ( + __slots__: tuple[str, ...] = ( "_tags", "_text", "comments", @@ -35,8 +35,15 @@ class HConfigChild( # noqa: PLR0904 pylint: disable=too-many-instance-attribut ) def __init__(self, parent: Union[HConfig, HConfigChild], text: str) -> None: + """Initialize the HconfigChild class. + + Args: + parent: Either an Hconfig object of a parent HConfigChild object. + text: The text config line of the HConfig or HconfigChild object. + + """ super().__init__() - self.parent = parent + self.parent: HConfig | HConfigChild = parent self._text: str = text.strip() self.real_indent_level: int # 0 is the default. Positive weights sink while negative weights rise. @@ -85,30 +92,66 @@ def __ne__(self, other: object) -> bool: @property def driver(self) -> HConfigDriverBase: + """Returns the driver of the HConfig object at the base of the tree. + + Returns: + HConfigDriverBase: The driver of the HConfig object at the base of the tree + + """ return self.root.driver @property def text(self) -> str: + """The text config of the HConfigChild object. + + Returns: + str: Config text for the HConfigChild. + + """ return self._text @text.setter def text(self, value: str) -> None: """Used for when self.text is changed after the object is instantiated to rebuild the children dictionary. + + Args: + value (str): Config text for the HConfigChild. + """ self._text = value.strip() self.parent.children.rebuild_mapping() @property def text_without_negation(self) -> str: + """The text config of the HConfigChild object without negation. + + Returns: + str: Config text for the HConfigChild without negation. + + """ return self.text.removeprefix(self.driver.negation_prefix) @property def root(self) -> HConfig: - """Returns the HConfig object at the base of the tree.""" + """Returns the HConfig object at the base of the tree. + + Returns: + HConfig: The HConfig object at the base of the tree. + + """ return self.parent.root def lines(self, *, sectional_exiting: bool = False) -> Iterable[str]: + """Returns the config lines of the HConfigChild object. + + Args: + sectional_exiting (bool, optional): Exit sectionally. + + Yields: + Iterator[Iterable[str]]: cisco_style_text string. + + """ yield self.cisco_style_text() for child in sorted(self.children): yield from child.lines(sectional_exiting=sectional_exiting) @@ -119,7 +162,7 @@ def lines(self, *, sectional_exiting: bool = False) -> Iterable[str]: @property def sectional_exit(self) -> Optional[str]: for rule in self.driver.rules.sectional_exiting: - if self.is_lineage_match(rule.match_rules): + if self.is_lineage_match(rules=rule.match_rules): if exit_text := rule.exit_text: return exit_text return None @@ -131,7 +174,7 @@ def sectional_exit(self) -> Optional[str]: def delete_sectional_exit(self) -> None: try: - potential_exit = self.children[-1] + potential_exit: HConfigChild = self.children[-1] except IndexError: return @@ -139,7 +182,12 @@ def delete_sectional_exit(self) -> None: potential_exit.delete() def depth(self) -> int: - """Returns the distance to the root HConfig object i.e. indent level.""" + """Returns the distance to the root HConfig object i.e. indent level. + + Returns: + int: Number of indents from the root HConfig object. + + """ return self.parent.depth() + 1 def move(self, new_parent: Union[HConfig, HConfigChild]) -> None: @@ -157,7 +205,7 @@ def move(self, new_parent: Union[HConfig, HConfigChild]) -> None: :param new_parent: HConfigChild object -> type list """ - new_parent.children.append(self) + new_parent.children.append(child=self) self.delete() def lineage(self) -> Iterator[HConfigChild]: @@ -175,13 +223,22 @@ def cisco_style_text( style: str = "without_comments", tag: Optional[str] = None, ) -> str: - """Return a Cisco style formated line i.e. indentation_level + text ! comments.""" + """Yields a Cisco style formated line. + + Args: + style: The style to use. Defaults to 'without_comments'. + tag: The tag to filter by. Defaults to None. + + Returns: + str: Indentation + text + comments if any. + + """ comments: list[str] = [] if style == "without_comments": pass elif style == "merged": # count the number of instances that have the tag - instance_count = 0 + instance_count: int = 0 instance_comments: set[str] = set() for instance in self.instances: if tag is None or tag in instance.tags: @@ -189,14 +246,14 @@ def cisco_style_text( instance_comments.update(instance.comments) # should the word 'instance' be plural? - word = "instance" if instance_count == 1 else "instances" + word: str = "instance" if instance_count == 1 else "instances" comments.append(f"{instance_count} {word}") comments.extend(instance_comments) elif style == "with_comments": comments.extend(self.comments) - comments_str = f" !{', '.join(sorted(comments))}" if comments else "" + comments_str: str = f" !{', '.join(sorted(comments))}" if comments else "" return f"{self.indentation}{self.text}{comments_str}" @property @@ -205,13 +262,13 @@ def indentation(self) -> str: def delete(self) -> None: """Delete the current object from its parent.""" - self.parent.children.delete(self) + self.parent.children.delete(child_or_text=self) def tags_add(self, tag: Union[str, Iterable[str]]) -> None: """Add a tag to self._tags on all leaf nodes.""" if self.is_branch: for child in self.children: - child.tags_add(tag) + child.tags_add(tag=tag) elif isinstance(tag, str): self._tags.add(tag) else: @@ -221,7 +278,7 @@ def tags_remove(self, tag: Union[str, Iterable[str]]) -> None: """Remove a tag from self._tags on all leaf nodes.""" if self.is_branch: for child in self.children: - child.tags_remove(tag) + child.tags_remove(tag=tag) elif isinstance(tag, str): self._tags.remove(tag) else: @@ -229,18 +286,18 @@ def tags_remove(self, tag: Union[str, Iterable[str]]) -> None: def negate(self) -> HConfigChild: """Negate self.text.""" - if negate_with := self.driver.negate_with(self): + if negate_with := self.driver.negate_with(config=self): self.text = negate_with return self - if self.use_default_for_negation(self): + if self.use_default_for_negation(config=self): return self._default() - return self.driver.swap_negation(self) + return self.driver.swap_negation(child=self) def use_default_for_negation(self, config: HConfigChild) -> bool: return any( - config.is_lineage_match(rule.match_rules) + config.is_lineage_match(rules=rule.match_rules) for rule in self.driver.rules.negation_default_when ) @@ -278,25 +335,30 @@ def is_idempotent_command(self, other_children: Iterable[HConfigChild]) -> bool: """Determine if self.text is an idempotent change.""" # Avoid list commands from matching as idempotent for rule in self.driver.rules.idempotent_commands_avoid: - if self.is_lineage_match(rule.match_rules): + if self.is_lineage_match(rules=rule.match_rules): return False # Idempotent command identification - return bool(self.driver.idempotent_for(self, other_children)) + return bool( + self.driver.idempotent_for( + config=self, + other_children=other_children, + ) + ) def use_sectional_overwrite_without_negation(self) -> bool: """Check self's text to see if negation should be handled by overwriting the section without first negating it. """ return any( - self.is_lineage_match(rule.match_rules) + self.is_lineage_match(rules=rule.match_rules) for rule in self.driver.rules.sectional_overwrite_no_negate ) def use_sectional_overwrite(self) -> bool: """Determines if self.text matches a sectional overwrite rule.""" return any( - self.is_lineage_match(rule.match_rules) + self.is_lineage_match(rules=rule.match_rules) for rule in self.driver.rules.sectional_overwrite ) @@ -310,18 +372,20 @@ def overwrite_with( """Deletes delta.child[self.text], adds a deep copy of target to delta.""" if self.children != target.children: if negate: - if negated := delta.children.get(self.text): + if negated := delta.children.get(key=self.text): negated.negate() else: negated = delta.add_child( - self.text, check_if_present=False + text=self.text, check_if_present=False ).negate() negated.comments.add("dropping section") else: - delta.children.delete(self.text) + delta.children.delete(child_or_text=self.text) if self.children: - new_item = delta.add_deep_copy_of(target) + new_item: HConfigChild = delta.add_deep_copy_of( + child_to_add=target, + ) new_item.comments.add("re-create section") def line_inclusion_test( @@ -356,21 +420,26 @@ def all_children_sorted_by_tags( ) -> Iterator[HConfigChild]: """Yield all children recursively that match include/exclude tags.""" if self.is_leaf: - if self.line_inclusion_test(include_tags, exclude_tags): + if self.line_inclusion_test( + include_tags=include_tags, + exclude_tags=exclude_tags, + ): yield self else: - self_iter = iter((self,)) + self_iter: Iterator[HConfigChild] = iter((self,)) for child in sorted(self.children): - included_children = child.all_children_sorted_by_tags( - include_tags, - exclude_tags, + included_children: Iterator[HConfigChild] = ( + child.all_children_sorted_by_tags( + include_tags=include_tags, + exclude_tags=exclude_tags, + ) ) if peek := next(included_children, None): yield from chain(self_iter, (peek,), included_children) def is_lineage_match(self, rules: tuple[MatchRule, ...]) -> bool: """A generic test against a lineage of HConfigChild objects.""" - lineage = tuple(self.lineage()) + lineage: tuple[HConfigChild, ...] = tuple(self.lineage()) return len(rules) == len(lineage) and all( child.is_match( @@ -414,7 +483,10 @@ def is_match( # noqa: PLR0911 return False # Regex filter - if isinstance(re_search, str) and not search(re_search, self.text): + if isinstance(re_search, str) and not search( + pattern=re_search, + string=self.text, + ): return False # The below filters are less commonly used @@ -436,9 +508,9 @@ def is_match( # noqa: PLR0911 def add_children_deep(self, lines: Iterable[str]) -> HConfigChild: """Add child instances of HConfigChild deeply.""" - base = self + base: HConfigChild = self for line in lines: - base = base.add_child(line) + base = base.add_child(text=line) return base def _default(self) -> HConfigChild: @@ -447,11 +519,11 @@ def _default(self) -> HConfigChild: return self def instantiate_child(self, text: str) -> HConfigChild: - return HConfigChild(self, text) + return HConfigChild(parent=self, text=text) def _is_duplicate_child_allowed(self) -> bool: """Determine if duplicate(identical text) children are allowed under the parent.""" return any( - self.is_lineage_match(rule.match_rules) + self.is_lineage_match(rules=rule.match_rules) for rule in self.driver.rules.parent_allows_duplicate_child ) diff --git a/hier_config/children.py b/hier_config/children.py index 3707b92..fcbc268 100644 --- a/hier_config/children.py +++ b/hier_config/children.py @@ -12,6 +12,7 @@ class HConfigChildren: def __init__(self) -> None: + """Initialize the HConfigChildren class.""" self._data: list[HConfigChild] = [] self._mapping: dict[str, HConfigChild] = {} @@ -81,6 +82,17 @@ def append( *, update_mapping: bool = True, ) -> HConfigChild: + """Add a child instance of HConfigChild. + + Args: + child (HConfigChild): The child to add. + update_mapping (bool, optional): Whether to update the text to child mapping. + Defaults to True. + + Returns: + HConfigChild: The child that was added. + + """ self._data.append(child) if update_mapping: self._mapping.setdefault(child.text, child) @@ -93,19 +105,29 @@ def clear(self) -> None: self._mapping.clear() def delete(self, child_or_text: Union[HConfigChild, str]) -> None: - """Delete a child from self._data and self._mapping.""" + """Delete a child from self._data and self._mapping. + + Args: + child_or_text (Union[HConfigChild, str]): The child or text to delete. + + """ if isinstance(child_or_text, str): if child_or_text in self._mapping: self._data[:] = [c for c in self._data if c.text != child_or_text] self.rebuild_mapping() else: - old_len = len(self._data) + old_len: int = len(self._data) self._data = [c for c in self._data if c is not child_or_text] if old_len != len(self._data): self.rebuild_mapping() def extend(self, children: Iterable[HConfigChild]) -> None: - """Add child instances of HConfigChild.""" + """Add child instances of HConfigChild and update _mapping. + + Args: + children (Iterable[HConfigChild]): The children to add. + + """ self._data.extend(children) for child in children: self._mapping.setdefault(child.text, child) @@ -113,13 +135,33 @@ def extend(self, children: Iterable[HConfigChild]) -> None: def get( self, key: str, default: Optional[_D] = None ) -> Union[HConfigChild, _D, None]: + """Get a child from self._mapping. + + Args: + key (str): The config text to get. + default (Optional[_D], optional): Object to return if the key doesn't exist. + Defaults to None. + + Returns: + Union[HConfigChild, _D, None]: HConfigChild if found, default otherwise. + + """ return self._mapping.get(key, default) def index(self, child: HConfigChild) -> int: + """Get the index of a child in self._data. + + Args: + child (HConfigChild): The child to get the index of. + + Returns: + int: The index of the child. + + """ return self._data.index(child) def rebuild_mapping(self) -> None: - """Rebuild self._mapping.""" + """Rebuild self._mapping from self._data.""" self._mapping.clear() for child in self._data: self._mapping.setdefault(child.text, child) diff --git a/hier_config/models.py b/hier_config/models.py index fe1fd22..e42ff69 100644 --- a/hier_config/models.py +++ b/hier_config/models.py @@ -20,6 +20,16 @@ class DumpLine(BaseModel): class MatchRule(BaseModel): + """A rule for matching configuration lines. + + Attrs: + equals: A string or set of strings to match exactly. + startswith: A string or tuple of strings to match lines that start with. + endswith: A string or tuple of strings to match lines that end with. + contains: A string or tuple of strings to match lines that contain. + re_search: A regex pattern to match lines that match the pattern. + """ + equals: Union[str, frozenset[str], None] = None startswith: Union[str, tuple[str, ...], None] = None endswith: Union[str, tuple[str, ...], None] = None @@ -33,61 +43,147 @@ class TagRule(BaseModel): class SectionalExitingRule(BaseModel): + """A rule for exiting a section. + + Attrs: + match_rules: A tuple of rules that must match to exit the section. + exit_text: The text returned when exiting the section. + """ + match_rules: tuple[MatchRule, ...] exit_text: str class SectionalOverwriteRule(BaseModel): + """Represents a rule for overwriting configuration lines. + + Attrs: + match_rules: A tuple of rules that must match to overwrite the section. + """ + match_rules: tuple[MatchRule, ...] class SectionalOverwriteNoNegateRule(BaseModel): + """Represents a rule for overwriting configuration lines. + + Attrs: + match_rules: A tuple of rules that must match to overwrite the section. + """ + match_rules: tuple[MatchRule, ...] class OrderingRule(BaseModel): + """Represents a rule for ordering configuration lines. + + Attrs: + match_rules: A tuple of rules that must match to be ordered. + weight: An integer determining the order (lower weights are processed earlier). + """ + match_rules: tuple[MatchRule, ...] weight: int class IndentAdjustRule(BaseModel): + """Represents a rule for adjusting indentation. + + Attrs: + start_expression: Regex or text marking the start of an adjustment. + end_expression: Regex or text marking the end of an adjustment. + """ + start_expression: str end_expression: str class ParentAllowsDuplicateChildRule(BaseModel): + """Represents a rule for allowing duplicate child configurations. + + Attrs: + match_rules: A tuple of rules that must match to allow duplicate children. + """ + match_rules: tuple[MatchRule, ...] class FullTextSubRule(BaseModel): + """Represents a full-text substitution rule. + + Attrs: + search: The text to search for. + replace: The text to replace the search text with. + """ + search: str replace: str class PerLineSubRule(BaseModel): + """Represents a per-line substitution rule. + + Attrs: + search: The text to search for. + replace: The text to replace the search text with. + """ + search: str replace: str class IdempotentCommandsRule(BaseModel): + """Represents a rule for idempotent commands. + + Attrs: + match_rules: A tuple of rules that must match to be idempotent. + """ + match_rules: tuple[MatchRule, ...] class IdempotentCommandsAvoidRule(BaseModel): + """Represents a rule for avoiding idempotent commands. + + Attrs: + match_rules: A tuple of rules that must match to avoid idempotent commands. + """ + match_rules: tuple[MatchRule, ...] class Instance(BaseModel): + """Represents a single configuration entity within a HConfig model. + + Attrs: + id: A unique identifier for the instance. + comments: A set of comments associated with the instance. + tags: A set of tags associated with the instance. + """ + id: PositiveInt comments: frozenset[str] tags: frozenset[str] class NegationDefaultWhenRule(BaseModel): + """Represents a rule for matching configuration lines. + + Attrs: + match_rules: A tuple of rules that must match to negate the default behavior. + """ + match_rules: tuple[MatchRule, ...] class NegationDefaultWithRule(BaseModel): + """Represents a rule for matching and negating configuration lines. + + Attrs: + match_rules: A tuple of rules that must match to negate the default behavior. + use: The default behavior to negate. + """ + match_rules: tuple[MatchRule, ...] use: str diff --git a/hier_config/platforms/__init__.py b/hier_config/platforms/__init__.py index e69de29..11605a4 100644 --- a/hier_config/platforms/__init__.py +++ b/hier_config/platforms/__init__.py @@ -0,0 +1 @@ +"""Initialize all platforms.""" diff --git a/hier_config/platforms/driver_base.py b/hier_config/platforms/driver_base.py index 12ba5a9..bc69a3f 100644 --- a/hier_config/platforms/driver_base.py +++ b/hier_config/platforms/driver_base.py @@ -24,6 +24,27 @@ class HConfigDriverRules(BaseModel): # pylint: disable=too-many-instance-attributes + """Configuration rules used by a driver to control parsing, matching, + ordering, rendering, and normalization of device configurations. + + Attributes: + full_text_sub: Substitutions applied across full config blocks. + idempotent_commands: Commands safe to appear multiple times. + idempotent_commands_avoid: Commands that should not be repeated. + indent_adjust: Manual indentation overrides for specific lines. + indentation: Number of spaces used for indentation. + negation_default_when: When to apply 'no' for default/removed lines. + negate_with: Custom negation rules for specific commands. + ordering: Defines sort order of config sections and lines. + parent_allows_duplicate_child: Allows child repetition under a parent. + per_line_sub: Line-based text substitutions. + post_load_callbacks: Functions run after config is loaded. + sectional_exiting: Rules for determining end of a config block. + sectional_overwrite: Replace full blocks instead of diffing lines. + sectional_overwrite_no_negate: Overwrite blocks without using 'no'. + + """ + full_text_sub: list[FullTextSubRule] = Field(default_factory=list) idempotent_commands: list[IdempotentCommandsRule] = Field(default_factory=list) idempotent_commands_avoid: list[IdempotentCommandsAvoidRule] = Field( @@ -52,28 +73,56 @@ class HConfigDriverBase(ABC): """ def __init__(self) -> None: - self.rules = self._instantiate_rules() + """Initialize the HConfigDriverBase class.""" + self.rules: HConfigDriverRules = self._instantiate_rules() def idempotent_for( self, config: HConfigChild, other_children: Iterable[HConfigChild], ) -> Optional[HConfigChild]: + """Determine if `config` is an idempotent change. + + Args: + config (HConfigChild): HConfigChild to check. + other_children (Iterable[HConfigChild]): Other HConfigChildren to check. + + Returns: + Optional[HConfigChild]: HConfigChild that matches `config` or None. + + """ for rule in self.rules.idempotent_commands: - if config.is_lineage_match(rule.match_rules): + if config.is_lineage_match(rules=rule.match_rules): for other_child in other_children: - if other_child.is_lineage_match(rule.match_rules): + if other_child.is_lineage_match(rules=rule.match_rules): return other_child return None def negate_with(self, config: HConfigChild) -> Optional[str]: + """Determine if `config` should be negated. + + Args: + config (HConfigChild): HConfigChild to check. + + Returns: + Optional[str]: String to use for negation or None. + + """ for with_rule in self.rules.negate_with: - if config.is_lineage_match(with_rule.match_rules): + if config.is_lineage_match(rules=with_rule.match_rules): return with_rule.use return None def swap_negation(self, child: HConfigChild) -> HConfigChild: - """Swap negation of a `child.text`.""" + """Swap negation of a `child.text`. + + Args: + child (HConfigChild): The child config object to check. + + Returns: + HConfigChild: The child config object with negation swapped. + + """ if child.text.startswith(self.negation_prefix): child.text = child.text_without_negation else: @@ -83,17 +132,43 @@ def swap_negation(self, child: HConfigChild) -> HConfigChild: @property def declaration_prefix(self) -> str: + """The prefix for declarations. + + Returns: + str: The declaration string. + + """ return "" @property def negation_prefix(self) -> str: + """The prefix for negation. + + Returns: + str: The negation string. + + """ return "no " @staticmethod def config_preprocessor(config_text: str) -> str: + """Preprocess the config text. + + Args: + config_text (str): The config text. + + Returns: + str: The preprocessed config text. + + """ return config_text @staticmethod @abstractmethod def _instantiate_rules() -> HConfigDriverRules: - pass + """Abstract method to instantiate rules. + + Returns: + HConfigDriverRules: The rules. + + """ diff --git a/hier_config/platforms/functions.py b/hier_config/platforms/functions.py index 3c1c620..30b70c7 100644 --- a/hier_config/platforms/functions.py +++ b/hier_config/platforms/functions.py @@ -1,14 +1,31 @@ def expand_range(number_range_str: str) -> tuple[int, ...]: - """Expand ranges like 2-5,8,22-45.""" + """Expand ranges like 2-5,8,22-45. + + Args: + number_range_str (str): The range to expand. + + Raises: + ValueError: The range of integers are invalid. + Should be of the form 2-5,8,22-45. + ValueError: The length of unique integers is not the same as the + length of all integers. + + Returns: + tuple[int, ...]: Tuple of integers. + + """ numbers: list[int] = [] - for number_range in number_range_str.split(","): - start_stop = number_range.split("-") + for number_range in number_range_str.split(sep=","): + start_stop: list[str] = number_range.split(sep="-") if len(start_stop) == 2: start = int(start_stop[0]) stop = int(start_stop[1]) numbers.extend(n for n in range(start, stop + 1)) - else: + if len(start_stop) == 1: numbers.append(int(start_stop[0])) + else: + message: str = f"Invalid range: {number_range}" + raise ValueError(message) if len(set(numbers)) != len(numbers): message = "len(set(numbers)) must be equal to len(numbers)." raise ValueError(message) @@ -24,12 +41,16 @@ def convert_to_set_commands(config_raw: str) -> str: config_raw (str): Configuration string """ - lines = config_raw.split("\n") + lines: list[str] = config_raw.split(sep="\n") + + # List of paths to the current command path: list[str] = [] + + # The list of actual configuration commands set_commands: list[str] = [] for line in lines: - stripped_line = line.strip() + stripped_line: str = line.strip() # Skip empty lines if not stripped_line: @@ -40,7 +61,7 @@ def convert_to_set_commands(config_raw: str) -> str: stripped_line = stripped_line.replace(";", "") # Count the number of spaces at the beginning to determine the level - level = line.find(stripped_line) // 4 + level: int = line.find(stripped_line) // 4 # Adjust the current path based on the level path = path[:level] @@ -53,7 +74,7 @@ def convert_to_set_commands(config_raw: str) -> str: set_commands.append(stripped_line) else: # It's a command line, construct the full command - command = f"set {' '.join(path)} {stripped_line}" + command: str = f"set {' '.join(path)} {stripped_line}" set_commands.append(command) return "\n".join(set_commands) diff --git a/hier_config/platforms/view_base.py b/hier_config/platforms/view_base.py index be53b20..532b5ff 100644 --- a/hier_config/platforms/view_base.py +++ b/hier_config/platforms/view_base.py @@ -15,32 +15,65 @@ class ConfigViewInterfaceBase: # noqa: PLR0904 + """Abstract base class for extracting structured interface data from an HConfigChild node.""" + def __init__(self, config: HConfigChild) -> None: - self.config = config + """Initialize the interface view. + + Args: + config (HConfigChild): The child config object to view. + + """ + self.config: HConfigChild = config @property @abstractmethod def bundle_id(self) -> Optional[str]: - """Determine the bundle ID.""" + """Determine the bundle (or LAG) ID. + + Returns: + Optional[str]: The bundle (or LAG) ID. + + """ @property @abstractmethod def bundle_member_interfaces(self) -> Iterable[str]: - """Determine the member interfaces of a bundle.""" + """Determine the member interfaces of a bundle. + + Returns: + Iterable[str]: The member interfaces of a bundle. + + """ @property @abstractmethod def bundle_name(self) -> Optional[str]: - """Determine the bundle name of a bundle member.""" + """Determine the bundle name of a bundle member. + + Returns: + Optional[str]: The bundle name of a bundle member or None. + + """ @property @abstractmethod def description(self) -> str: - """Determine the interface's description.""" + """Determine the interface's description. + + Returns: + str: The interface's description. + + """ @property def dot1q_mode(self) -> Optional[InterfaceDot1qMode]: - """Derive the configured 802.1Q mode.""" + """Derive the configured 802.1Q mode. + + Returns: + Optional[InterfaceDot1qMode]: The configured 802.1Q mode. + + """ if self.tagged_all: return InterfaceDot1qMode.TAGGED_ALL if self.tagged_vlans: @@ -52,17 +85,32 @@ def dot1q_mode(self) -> Optional[InterfaceDot1qMode]: @property @abstractmethod def duplex(self) -> InterfaceDuplex: - """Determine the configured Duplex of the interface.""" + """Determine the configured Duplex of the interface. + + Returns: + InterfaceDuplex: The configured duplex mode. + + """ @property @abstractmethod def enabled(self) -> bool: - """Determines if the interface is enabled.""" + """Determines if the interface is enabled. + + Returns: + bool: True if the interface is enabled, else False. + + """ @property @abstractmethod def has_nac(self) -> bool: - """Determine if the interface has NAC configured.""" + """Determine if the interface has NAC (Network Access Control) configured. + + Returns: + bool: True if the interface has NAC configured, else False. + + """ @property def ipv4_interface(self) -> Optional[IPv4Interface]: @@ -77,120 +125,244 @@ def ipv4_interfaces(self) -> Iterable[IPv4Interface]: @property @abstractmethod def is_bundle(self) -> bool: - """Determine if the interface is a bundle.""" + """Determine if the interface is a bundle. + + Returns: + bool: True if the interface is a bundle, else False. + + """ @property @abstractmethod def is_loopback(self) -> bool: - """Determine if the interface is a loopback.""" + """Determine if the interface is a loopback. + + Returns: + bool: True if the interface is a loopback, else False. + + """ @property def is_subinterface(self) -> bool: - """Determine if the interface is a subinterface.""" + """Determine if the interface is a subinterface. + + Returns: + bool: True if the interface is a subinterface, else False. + + """ return "." in self.name @property @abstractmethod def is_svi(self) -> bool: - """Determine if the interface is an SVI.""" + """Determine if the interface is an SVI. + + Returns: + bool: True if the interface is an SVI, else False. + + """ @property @abstractmethod def module_number(self) -> Optional[int]: - """Determine the module number of the interface.""" + """Determine the module number of the interface. + + Returns: + Optional[int]: The module number of the interface or None. + + """ @property @abstractmethod def nac_control_direction_in(self) -> bool: - """Determine if the interface has NAC 'control direction in' configured.""" + """Determine if the interface has NAC 'control direction in' configured. + + Returns: + bool: True if 'control direction in' is configured, else False. + + """ @property @abstractmethod def nac_host_mode(self) -> Optional[NACHostMode]: - """Determine the NAC host mode.""" + """Determine the NAC host mode. + + Returns: + Optional[NACHostMode]: The NAC host mode or None. + + """ @property @abstractmethod def nac_mab_first(self) -> bool: - """Determine if the interface has NAC configured for MAB first.""" + """Determine if the interface has NAC configured for MAB first. + + Returns: + bool: True is MAB authentication is configured first, else False. + + """ @property @abstractmethod def nac_max_dot1x_clients(self) -> int: - """Determine the max dot1x clients.""" + """Determine the max dot1x clients. + + Returns: + int: The max amount of dot1x clients. + + """ @property @abstractmethod def nac_max_mab_clients(self) -> int: - """Determine the max mab clients.""" + """Determine the max mab clients. + + Returns: + int: The max number of clients that can be authenticated using MAB. + + """ @property @abstractmethod def name(self) -> str: - """Determine the name of the interface.""" + """Determine the name of the interface. + + Returns: + str: The interface name. + + """ @property @abstractmethod def native_vlan(self) -> Optional[int]: - """Determine the native VLAN.""" + """Determine the native VLAN. + + Returns: + Optional[int]: Native VLAN ID or None if not set. + + """ @property @abstractmethod def number(self) -> str: - """Remove letters from the interface name, leaving just numbers and symbols.""" + """Remove letters from the interface name, leaving just numbers and symbols. + + Returns: + str: The interface number. + + """ @property @abstractmethod def parent_name(self) -> Optional[str]: - """Determine the parent bundle interface name.""" + """Determine the parent bundle interface name. + + Returns: + Optional[str]: The logical parent bundle interface name or None. + + """ @property @abstractmethod def poe(self) -> bool: - """Determine if PoE is enabled.""" + """Determine if PoE is enabled. + + Returns: + bool: True if PoE is enabled, else False. + + """ @property @abstractmethod def port_number(self) -> int: - """Determine the interface port number.""" + """Determine the interface port number. + + Returns: + int: The interface port number. + + """ @property @abstractmethod def speed(self) -> Optional[tuple[int, ...]]: - """Determine the statically allowed speeds the interface can operate at. In Mbps.""" + """Determine the statically allowed speeds the interface can operate at. In Mbps. + + Returns: + Optional[tuple[int, ...]]: The allowed speeds or None. + + """ @property @abstractmethod def subinterface_number(self) -> Optional[int]: - """Determine the sub-interface number.""" + """Determine the sub-interface number. + + Returns: + Optional[int]: The sub-interface number or None. + + """ @property @abstractmethod def tagged_all(self) -> bool: - """Determine if all the VLANs are tagged.""" + """Determine if all the VLANs are tagged. + + Returns: + bool: True if all the VLANs are tagged, else False. + + """ @property @abstractmethod def tagged_vlans(self) -> tuple[int, ...]: - """Determine the tagged VLANs.""" + """Determine the tagged VLANs. + + Returns: + tuple[int, ...]: Tuple of tagged VLAN IDs. + + """ @property @abstractmethod def vrf(self) -> str: - """Determine the VRF.""" + """Determine the VRF. + + Returns: + str: The VRF name. + + """ @property @abstractmethod def _bundle_prefix(self) -> str: - pass + """Determine the bundle prefix name. + + Returns: + str: The bundle prefix name. + + """ class HConfigViewBase(ABC): + """Abstract class to view HConfig config tree objects.""" + def __init__(self, config: HConfig) -> None: - self.config = config + """Initialize the HConfig view base class. + + Args: + config (HConfig): The HConfig object to view. + + """ + self.config: HConfig = config @property def bundle_interface_views(self) -> Iterable[ConfigViewInterfaceBase]: + """Determine the bundle interface views. + + Yields: + Iterable[ConfigViewInterfaceBase]: Iterable of bundle interface views. + + """ for interface_view in self.interface_views: if interface_view.is_bundle: yield interface_view @@ -203,19 +375,48 @@ def dot1q_mode_from_vlans( *, tagged_all: bool = False, ) -> Optional[InterfaceDot1qMode]: - pass + """Determine the 802.1Q mode from VLANs. + + Args: + untagged_vlan (Optional[int], optional): Untagged VLAN. Defaults to None. + tagged_vlans (tuple[int, ...], optional): Tagged VLANs. Defaults to (). + tagged_all (bool, optional): Tagged all VLANs. Defaults to False. + + Returns: + Optional[InterfaceDot1qMode]: The 802.1Q mode or None if not set. + + """ @property @abstractmethod def hostname(self) -> Optional[str]: - pass + """Determine the hostname. + + Returns: + Optional[str]: The hostname or None. + + """ @property @abstractmethod def interface_names_mentioned(self) -> frozenset[str]: - """Returns a set with all the interface names mentioned in the config.""" + """Returns a set with all the interface names mentioned in the config. + + Returns: + frozenset[str]: All the interface names mentioned in the config. + + """ def interface_view_by_name(self, name: str) -> Optional[ConfigViewInterfaceBase]: + """Determine the interface view by name. + + Args: + name (str): The interface name. + + Returns: + Optional[ConfigViewInterfaceBase]: The interface view or None. + + """ for interface_view in self.interface_views: if interface_view.name == name: return interface_view @@ -224,30 +425,63 @@ def interface_view_by_name(self, name: str) -> Optional[ConfigViewInterfaceBase] @property @abstractmethod def interface_views(self) -> Iterable[ConfigViewInterfaceBase]: - pass + """Determine the configured interfaces. + + Returns: + Iterable[ConfigViewInterfaceBase]: The configured interfaces. + + """ @property @abstractmethod def interfaces(self) -> Iterable[HConfigChild]: - pass + """Determine the configured interfaces. + + Returns: + Iterable[HConfigChild]: An iterbale of the configured interfaces' + HConfig objects. + + """ @property def interfaces_names(self) -> Iterable[str]: + """Determine the configured interface names. + + Yields: + Iterator[Iterable[str]]: The configured interface names. + + """ for interface_view in self.interface_views: yield interface_view.name @property @abstractmethod def ipv4_default_gw(self) -> Optional[IPv4Address]: - pass + """Determine the IPv4 default gateway IPv4Address. + + Returns: + Optional[IPv4Address]: The IPv4 default gateway object or None. + + """ @property @abstractmethod def location(self) -> str: - pass + """Determine the location of the device. + + Returns: + str: Location name of the device. + + """ @property def module_numbers(self) -> Iterable[int]: + """Determine the configured module numbers. + + Yields: + Iterator[Iterable[int]]: Yields unique module numbers. + + """ seen: set[int] = set() for interface_view in self.interface_views: if module_number := interface_view.module_number: @@ -259,14 +493,29 @@ def module_numbers(self) -> Iterable[int]: @property @abstractmethod def stack_members(self) -> Iterable[StackMember]: - """Determine the configured stack members.""" + """Determine the configured stack members. + + Returns: + Iterable[StackMember]: The configured stack members. + + """ @property def vlan_ids(self) -> frozenset[int]: - """Determine the VLAN IDs.""" + """Determine the VLAN IDs. + + Returns: + frozenset[int]: The VLAN IDs. + + """ return frozenset(vlan.id for vlan in self.vlans) @property @abstractmethod def vlans(self) -> Iterable[Vlan]: - """Determine the configured VLANs.""" + """Determine the configured VLANs. + + Returns: + Iterable[Vlan]: The configured VLANs. + + """ diff --git a/poetry.lock b/poetry.lock index 07b0f42..5ac9b89 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1309,4 +1309,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9.0,<4.0" -content-hash = "bb5271133ee0c5daa0512458b4890fd19b0c6bfee4f2a706573c74b740e001d3" +content-hash = "bb5271133ee0c5daa0512458b4890fd19b0c6bfee4f2a706573c74b740e001d3" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index b37f7d4..b972c5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -153,4 +153,4 @@ ignore = [ parametrize-values-type = "tuple" [tool.ruff.lint.per-file-ignores] -"**/tests/*" = ["PLC2701", "S101"] +"**/tests/*" = ["PLC2701", "S101"] \ No newline at end of file