Coverage for netbox_interface_name_rules / family / claims.py: 100.00%
35 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-10-16 10:13 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-10-16 10:13 +0000
1# SPDX-License-Identifier: Apache-2.0
2# Copyright (C) 2025 Marcin Zieba <marcinpsk@gmail.com>
3"""Resolve two-sided uniqueness for complete template claim relations."""
5from collections import defaultdict
6from dataclasses import dataclass
9@dataclass(frozen=True, slots=True)
10class TemplateClaim:
11 """The units one template claims in a selection scope.
13 A unit is the interface names one form spells: one interface, or the members of one flat family.
14 """
16 claimant_id: object
17 template_name: str
18 units: tuple[tuple[str, ...], ...]
20 def __post_init__(self):
21 object.__setattr__(self, "units", tuple(tuple(dict.fromkeys(unit)) for unit in self.units))
24def resolve_template_claims(claims, *, module):
25 """Return accepted ``(claimant_id, unit)`` pairs in claimant order, and rendered collision messages.
27 Pass the complete relation. Repeated units count once. Claimant IDs must be unique.
28 A template claims one unit when exactly one of its units holds every name it claims. A template
29 that claims more, or a name that more than one template claims, disqualifies every claim involved.
30 The caller emits the messages through its own logger.
31 """
32 by_id, claimants = _index_claims(claims)
33 ambiguous = set()
34 messages = []
35 for claimant_id, (template_name, units, names) in by_id.items():
36 if units and len(_covering(units, names)) != 1:
37 ambiguous.add(claimant_id)
38 messages.append(
39 f"Interface template {template_name!r} of {module} could name any of {sorted(names)} "
40 "as its raw name or its renamed form; skipping them all rather than renaming a guess."
41 )
43 for name, ids in claimants.items():
44 if len(ids) > 1:
45 messages.append(
46 f"Interface {name!r} on {module} could be the raw or renamed name of any of the templates "
47 f"{sorted(by_id[claimant_id][0] for claimant_id in ids)}; "
48 "skipping it rather than renaming a guess."
49 )
50 ambiguous.update(ids)
52 accepted = tuple(
53 (claimant_id, _covering(units, names)[0])
54 for claimant_id, (_, units, names) in by_id.items()
55 if units and claimant_id not in ambiguous
56 )
57 return accepted, tuple(messages)
60def _covering(units, names):
61 """Return the units that hold every one of *names*."""
62 wanted = set(names)
63 return [unit for unit in units if wanted.issubset(unit)]
66def _index_claims(claims):
67 """Return each claimant's template name, distinct units and names, and the claimants of each name."""
68 by_id = {}
69 claimants = defaultdict(list)
70 for claim in claims:
71 if claim.claimant_id in by_id:
72 raise ValueError(f"Duplicate claimant_id: {claim.claimant_id}")
73 units = tuple(dict.fromkeys(claim.units))
74 names = tuple(dict.fromkeys(name for unit in units for name in unit))
75 by_id[claim.claimant_id] = (claim.template_name, units, names)
76 for name in names:
77 claimants[name].append(claim.claimant_id)
78 return by_id, claimants