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

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.""" 

4 

5from collections import defaultdict 

6from dataclasses import dataclass 

7 

8 

9@dataclass(frozen=True, slots=True) 

10class TemplateClaim: 

11 """The units one template claims in a selection scope. 

12 

13 A unit is the interface names one form spells: one interface, or the members of one flat family. 

14 """ 

15 

16 claimant_id: object 

17 template_name: str 

18 units: tuple[tuple[str, ...], ...] 

19 

20 def __post_init__(self): 

21 object.__setattr__(self, "units", tuple(tuple(dict.fromkeys(unit)) for unit in self.units)) 

22 

23 

24def resolve_template_claims(claims, *, module): 

25 """Return accepted ``(claimant_id, unit)`` pairs in claimant order, and rendered collision messages. 

26 

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 ) 

42 

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) 

51 

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) 

58 

59 

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)] 

64 

65 

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