Coverage for jsonschema_diff/core/tools/context.py: 89%

56 statements  

« prev     ^ index     » next       coverage.py v7.10.5, created at 2025-08-25 07:00 +0000

1from __future__ import annotations 

2 

3from typing import ( 

4 TYPE_CHECKING, 

5 Dict, 

6 Iterable, 

7 List, 

8 Mapping, 

9 Sequence, 

10 Type, 

11 TypeAlias, 

12 Union, 

13) 

14 

15if TYPE_CHECKING: 

16 from jsonschema_diff.core.parameter_base import Compare 

17 

18# Key type accepted in rules: parameter name or Compare subclass 

19RULE_KEY: TypeAlias = Union[str, Type["Compare"]] 

20 

21CONTEXT_RULES_TYPE: TypeAlias = Mapping[RULE_KEY, Sequence[RULE_KEY]] 

22PAIR_CONTEXT_RULES_TYPE: TypeAlias = Sequence[Sequence[RULE_KEY]] 

23 

24 

25class RenderContextHandler: 

26 """Expand context comparators based on pair- and directed-dependency rules.""" 

27 

28 @staticmethod 

29 def resolve( 

30 *, 

31 pair_context_rules: PAIR_CONTEXT_RULES_TYPE, 

32 context_rules: CONTEXT_RULES_TYPE, 

33 for_render: Mapping[str, "Compare"], 

34 not_for_render: Mapping[str, "Compare"], 

35 ) -> Dict[str, "Compare"]: 

36 """ 

37 Build the final ordered context for rendering. 

38 

39 Parameters 

40 ---------- 

41 pair_context_rules : Sequence[Sequence[RULE_KEY]] 

42 Undirected groups: if one member is rendered, pull the rest (order preserved). 

43 context_rules : Mapping[RULE_KEY, Sequence[RULE_KEY]] 

44 Directed dependencies: ``source → [targets...]``. 

45 for_render : Mapping[str, Compare] 

46 Initial items, order defines primary screen order. 

47 not_for_render : Mapping[str, Compare] 

48 Optional items that may be added by the rules. 

49 

50 Returns 

51 ------- 

52 dict 

53 Ordered ``{name -> Compare}`` ready for UI. 

54 

55 Algorithm (high-level) 

56 ---------------------- 

57 * Walk through *for_render* keys. 

58 * While iterating, append new candidates to the tail of the scan list. 

59 * A candidate is added once when first matched by any rule. 

60 """ 

61 out: Dict[str, "Compare"] = dict(for_render) # preserves order 

62 pool_not: Dict[str, "Compare"] = dict(not_for_render) # preserves insertion order 

63 

64 seq: List[str] = list(out.keys()) # scan list 

65 in_out = set(seq) # O(1) membership checks 

66 

67 def _matches(rule: RULE_KEY, name: str, cmp_obj: "Compare") -> bool: 

68 """Return True if *rule* matches given *(name, object)* pair.""" 

69 if isinstance(rule, str): 

70 return rule == name 

71 # rule is a comparator class 

72 try: 

73 return isinstance(cmp_obj, rule) 

74 except TypeError: 

75 return False 

76 

77 def _expand(rule: RULE_KEY, pool: Mapping[str, "Compare"]) -> Iterable[str]: 

78 """ 

79 Yield keys from *pool* matching *rule* (order-stable). 

80 

81 * String rule → single key. 

82 * Class rule → all keys whose comparator ``isinstance`` the class. 

83 """ 

84 if isinstance(rule, str): 

85 if rule in pool: 

86 yield rule 

87 return 

88 

89 for n, obj in list(pool.items()): # snapshot to stay safe on ``del`` 

90 try: 

91 if isinstance(obj, rule): 

92 yield n 

93 except TypeError: 

94 continue 

95 

96 i = 0 

97 while i < len(seq): 

98 name = seq[i] 

99 cmp_obj = out[name] 

100 

101 # 1) Undirected groups 

102 for group in pair_context_rules: 

103 if any(_matches(entry, name, cmp_obj) for entry in group): 

104 for entry in group: 

105 for cand in _expand(entry, pool_not): 

106 if cand in in_out: 

107 continue 

108 out[cand] = pool_not[cand] 

109 seq.append(cand) 

110 in_out.add(cand) 

111 del pool_not[cand] 

112 

113 # 2) Directed dependencies 

114 for source, targets in context_rules.items(): 

115 if _matches(source, name, cmp_obj): 

116 for entry in targets: 

117 for cand in _expand(entry, pool_not): 

118 if cand in in_out: 

119 continue 

120 out[cand] = pool_not[cand] 

121 seq.append(cand) 

122 in_out.add(cand) 

123 del pool_not[cand] 

124 

125 i += 1 

126 

127 return out