Coverage for jsonschema_diff/core/compare_base.py: 90%
51 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-15 18:01 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-15 18:01 +0000
1from typing import TYPE_CHECKING, Any, TypeAlias
3from .abstraction import Statuses, ToCompare
4from .tools.render import RenderTool
6if TYPE_CHECKING:
7 from .config import Config
9COMPARE_PATH_TYPE: TypeAlias = list[str | int]
10LEGEND_PROCESSOR_TYPE: TypeAlias = dict[str, Any]
11LEGEND_RETURN_TYPE: TypeAlias = dict[
12 str, str | LEGEND_PROCESSOR_TYPE | list[str | LEGEND_PROCESSOR_TYPE]
13]
16class Compare:
17 def __init__(
18 self,
19 config: "Config",
20 schema_path: COMPARE_PATH_TYPE,
21 json_path: COMPARE_PATH_TYPE,
22 to_compare: list[ToCompare],
23 ):
24 self.status = Statuses.UNKNOWN
26 self.config = config
27 self.schema_path = schema_path
28 self.json_path = json_path
30 if len(to_compare) <= 0:
31 raise ValueError("Cannot compare empty list")
32 self.to_compare = to_compare
34 @property
35 def my_config(self) -> dict:
36 return self.config.COMPARE_CONFIG.get(type(self), {})
38 def compare(self) -> Statuses:
39 if len(self.to_compare) > 1:
40 raise ValueError("Unsupported multiple compare for base logic")
42 self.status = self.to_compare[0].status
43 self.key = self.to_compare[0].key
44 self.value = self.to_compare[0].value
45 self.old_value = self.to_compare[0].old_value
46 self.new_value = self.to_compare[0].new_value
47 return self.status
49 def get_name(self) -> str:
50 return self.to_compare[0].key
52 def is_for_rendering(self) -> bool:
53 return self.status in [
54 Statuses.ADDED,
55 Statuses.DELETED,
56 Statuses.REPLACED,
57 Statuses.MODIFIED,
58 ]
60 def calc_diff(self) -> dict[str, int]:
61 """
62 Basic implementation: counts its own status as 1 element.
63 Complex comparators (e.g. CompareList) override this to return an aggregate.
64 """
65 return {self.status.name: 1}
67 def _render_start_line(
68 self,
69 tab_level: int = 0,
70 with_path: bool = True,
71 with_key: bool = True,
72 to_crop: tuple[int, int] = (0, 0),
73 ) -> str:
74 to_return = (
75 f"{RenderTool.make_prefix(self.status)} {RenderTool.make_tab(self.config, tab_level)}"
76 )
77 if with_path:
78 to_return += RenderTool.make_path(
79 self.schema_path[to_crop[0] :],
80 self.json_path[to_crop[1] :],
81 ignore=self.config.PATH_MAKER_IGNORE,
82 )
84 if with_key:
85 to_return += f".{self.get_name()}"
86 return to_return + ":"
88 def render(
89 self, tab_level: int = 0, with_path: bool = True, to_crop: tuple[int, int] = (0, 0)
90 ) -> str:
91 to_return = self._render_start_line(
92 tab_level=tab_level, with_path=with_path, to_crop=to_crop
93 )
95 if self.status in [Statuses.ADDED, Statuses.DELETED, Statuses.NO_DIFF]:
96 to_return += f" {self.value}"
97 elif self.status == Statuses.REPLACED:
98 to_return += f" {self.old_value} -> {self.new_value}"
99 else:
100 raise ValueError(f"Unsupported for render status: {self.status}")
102 return to_return
104 @staticmethod
105 def legend() -> LEGEND_RETURN_TYPE:
106 return {
107 "element": [
108 Statuses.ADDED.value,
109 Statuses.DELETED.value,
110 Statuses.REPLACED.value,
111 Statuses.MODIFIED.value,
112 Statuses.NO_DIFF.value,
113 Statuses.UNKNOWN.value,
114 ],
115 "description": [
116 Statuses.ADDED.name,
117 Statuses.DELETED.name,
118 Statuses.REPLACED.name,
119 Statuses.MODIFIED.name,
120 Statuses.NO_DIFF.name,
121 Statuses.UNKNOWN.name,
122 ],
123 "example": [
124 {"old_value": {}, "new_value": {"added_key": "value"}},
125 {"old_value": {"deleted_key": "value"}, "new_value": {}},
126 {
127 "old_value": {"replaced_key": "old-value"},
128 "new_value": {"replaced_key": "new-value"},
129 },
130 {
131 "old_value": {"modified_key": []},
132 "new_value": {"modified_key": ["value"]},
133 },
134 {
135 "old_value": {"no_diff_key": "value"},
136 "new_value": {"no_diff_key": "value"},
137 },
138 ],
139 }