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

1import difflib 

2from dataclasses import dataclass 

3from typing import TYPE_CHECKING, Any 

4 

5from ..abstraction import Statuses 

6from ..parameter_base import Compare 

7 

8if TYPE_CHECKING: 

9 from ..config import Config 

10 from ..parameter_base import LEGEND_RETURN_TYPE 

11 

12 

13@dataclass 

14class CompareListElement: 

15 config: "Config" 

16 value: Any 

17 status: Statuses 

18 

19 def render(self, tab_level: int = 0) -> str: 

20 return f"{self.status.value} {self.config.TAB * tab_level}{self.value}" 

21 

22 

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] = [] 

28 

29 def compare(self) -> Statuses: 

30 super().compare() 

31 

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(): 

42 

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) 

52 

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}") 

65 

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

72 

73 return self.status 

74 

75 def is_for_rendering(self) -> bool: 

76 return super().is_for_rendering() or len(self.changed_elements) > 0 

77 

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) 

80 

81 for i in self.elements: 

82 to_return += f"\n{i.render(tab_level + 1)}" 

83 return to_return 

84 

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 }