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
« prev ^ index » next coverage.py v7.10.5, created at 2025-08-25 07:00 +0000
1from __future__ import annotations
3from typing import (
4 TYPE_CHECKING,
5 Dict,
6 Iterable,
7 List,
8 Mapping,
9 Sequence,
10 Type,
11 TypeAlias,
12 Union,
13)
15if TYPE_CHECKING:
16 from jsonschema_diff.core.parameter_base import Compare
18# Key type accepted in rules: parameter name or Compare subclass
19RULE_KEY: TypeAlias = Union[str, Type["Compare"]]
21CONTEXT_RULES_TYPE: TypeAlias = Mapping[RULE_KEY, Sequence[RULE_KEY]]
22PAIR_CONTEXT_RULES_TYPE: TypeAlias = Sequence[Sequence[RULE_KEY]]
25class RenderContextHandler:
26 """Expand context comparators based on pair- and directed-dependency rules."""
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.
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.
50 Returns
51 -------
52 dict
53 Ordered ``{name -> Compare}`` ready for UI.
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
64 seq: List[str] = list(out.keys()) # scan list
65 in_out = set(seq) # O(1) membership checks
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
77 def _expand(rule: RULE_KEY, pool: Mapping[str, "Compare"]) -> Iterable[str]:
78 """
79 Yield keys from *pool* matching *rule* (order-stable).
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
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
96 i = 0
97 while i < len(seq):
98 name = seq[i]
99 cmp_obj = out[name]
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]
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]
125 i += 1
127 return out