Coverage for jsonschema_diff/core/custom_compare/list.py: 87%
63 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
1import difflib
2from dataclasses import dataclass
3from typing import TYPE_CHECKING, Any
5from ..abstraction import Statuses
6from ..parameter_base import Compare
8if TYPE_CHECKING:
9 from ..config import Config
10 from ..parameter_base import LEGEND_RETURN_TYPE
13@dataclass
14class CompareListElement:
15 config: "Config"
16 value: Any
17 status: Statuses
19 def render(self, tab_level: int = 0) -> str:
20 return f"{self.status.value} {self.config.TAB * tab_level}{self.value}"
23class CompareList(Compare):
24 def __init__(self, *args: Any, **kwargs: Any) -> None:
25 super().__init__(*args, **kwargs)
26 self.elements: list[CompareListElement] = []
27 self.changed_elements: list[CompareListElement] = []
29 def compare(self) -> Statuses:
30 super().compare()
32 if self.status == Statuses.NO_DIFF:
33 return self.status
34 elif self.status in [Statuses.ADDED, Statuses.DELETED]: # add
35 for v in self.value:
36 element = CompareListElement(self.config, v, self.status)
37 self.elements.append(element)
38 self.changed_elements.append(element)
39 elif self.status == Statuses.REPLACED: # replace or no-diff
40 sm = difflib.SequenceMatcher(a=self.old_value, b=self.new_value, autojunk=False)
41 for tag, i1, i2, j1, j2 in sm.get_opcodes():
43 def add_element(
44 source: list[Any], status: Statuses, from_index: int, to_index: int
45 ) -> None:
46 is_change = status != Statuses.NO_DIFF
47 for v in source[from_index:to_index]:
48 element = CompareListElement(self.config, v, status)
49 self.elements.append(element)
50 if is_change:
51 self.changed_elements.append(element)
53 match tag:
54 case "equal":
55 add_element(self.old_value, Statuses.NO_DIFF, i1, i2)
56 case "delete":
57 add_element(self.old_value, Statuses.DELETED, i1, i2)
58 case "insert":
59 add_element(self.new_value, Statuses.ADDED, j1, j2)
60 case "replace":
61 add_element(self.old_value, Statuses.DELETED, i1, i2)
62 add_element(self.new_value, Statuses.ADDED, j1, j2)
63 case _:
64 raise ValueError(f"Unknown tag: {tag}")
66 if len(self.changed_elements) > 0:
67 self.status = Statuses.MODIFIED
68 else:
69 self.status = Statuses.NO_DIFF
70 else:
71 raise ValueError("Unsupported keys combination")
73 return self.status
75 def is_for_rendering(self) -> bool:
76 return super().is_for_rendering() or len(self.changed_elements) > 0
78 def render(self, tab_level: int = 0, with_path: bool = True) -> str:
79 to_return = self._render_start_line(tab_level=tab_level, with_path=with_path)
81 for i in self.elements:
82 to_return += f"\n{i.render(tab_level + 1)}"
83 return to_return
85 @staticmethod
86 def legend() -> "LEGEND_RETURN_TYPE":
87 return {
88 "element": "Arrays\nLists",
89 "description": (
90 "Arrays are always displayed fully, with statuses of all elements "
91 "separately (left to them).\nIn example:\n"
92 '["Masha", "Misha", "Vasya"] replace to ["Masha", "Olya", "Misha"]'
93 ),
94 "example": {
95 "old_value": {"some_list": ["Masha", "Misha", "Vasya"]},
96 "new_value": {"some_list": ["Masha", "Olya", "Misha"]},
97 },
98 }