Coverage for lib/datamodel/diffobject.py: 100%

44 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2025-07-28 07:24 +0000

1#!/usr/bin/env python3 

2# -*- coding: utf-8 -*- 

3 

4# Hermes : Change Data Capture (CDC) tool from any source(s) to any target 

5# Copyright (C) 2023, 2024 INSA Strasbourg 

6# 

7# This file is part of Hermes. 

8# 

9# Hermes is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# Hermes is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with Hermes. If not, see <https://www.gnu.org/licenses/>. 

21 

22 

23from typing import Any 

24 

25 

26class DiffObject: 

27 """Contain differences between two DataObject or two DataObjectList. 

28 Differences will be stored as keys in 3 properties sets: 

29 - added 

30 - modified 

31 - removed 

32 The sets should contain object properties names when objnew and objold are specified 

33 in constructor (e.g. when comparing two DataObject), or some objects otherwise 

34 (e.g. when comparing two DataObjectList). 

35 Objects added MUST not be tuple, list, set or frozenset. 

36 

37 When tested with 'if instance', DiffObject instance returns False if no 

38 difference was found, True otherwise. 

39 """ 

40 

41 def __init__(self, objnew: Any = None, objold: Any = None): 

42 """Create an empty new diff object""" 

43 self.objnew = objnew 

44 self.objold = objold 

45 self._added: set[Any] = set() 

46 self._modified: set[Any] = set() 

47 self._removed: set[Any] = set() 

48 

49 @property 

50 def added(self) -> list[Any] | set[Any]: 

51 """Returns a list of what is present in objnew and not in objold. 

52 If content can be sorted, returns a sorted list, returns a set otherwise""" 

53 try: 

54 return sorted(self._added) 

55 except TypeError: 

56 return self._added 

57 

58 @property 

59 def modified(self) -> list[Any] | set[Any]: 

60 """Returns a list of what exists in objnew and objold, but differs. 

61 If content can be sorted, returns a sorted list, returns a set otherwise""" 

62 try: 

63 return sorted(self._modified) 

64 except TypeError: 

65 return self._modified 

66 

67 @property 

68 def removed(self) -> list[Any] | set[Any]: 

69 """Returns a list of what is present in objold and not in objnew. 

70 If content can be sorted, returns a sorted list, returns a set otherwise""" 

71 try: 

72 return sorted(self._removed) 

73 except TypeError: 

74 return self._removed 

75 

76 @property 

77 def dict(self) -> dict[str, Any]: 

78 """Returns a diff dict always containing three keys: 'added', 'modified' and 

79 'removed'. 

80 The values differs depending on whether objnew has been set on constructor or 

81 not : 

82 - If objnew has been set: 

83 - 'added' and 'modified' will be a dict with attrname as key, and objnew's 

84 value of 'attrname' 

85 - 'removed' will be a dict with attrname as key, and None as value 

86 - If objnew hasn't been set, 'added', 'modified' and 'removed' will be a list of 

87 objects, sorted when possible 

88 """ 

89 if self.objnew is not None: 

90 return { 

91 "added": {attr: getattr(self.objnew, attr) for attr in self.added}, 

92 "modified": { 

93 attr: getattr(self.objnew, attr) for attr in self.modified 

94 }, 

95 "removed": {attr: None for attr in self.removed}, 

96 } 

97 

98 return { 

99 "added": self.added, 

100 "modified": self.modified, 

101 "removed": self.removed, 

102 } 

103 

104 def appendAdded(self, value: Any): 

105 """Mark specified value as added. Multiple values can be specified at once by 

106 encapsulating them in tuple, list, set, or frozenset""" 

107 self._append("_added", value) 

108 

109 def appendModified(self, value: Any): 

110 """Mark specified value as modified. Multiple values can be specified at once by 

111 encapsulating them in tuple, list, set, or frozenset""" 

112 self._append("_modified", value) 

113 

114 def appendRemoved(self, value: Any): 

115 """Mark specified value as removed. Multiple values can be specified at once by 

116 encapsulating them in tuple, list, set, or frozenset""" 

117 self._append("_removed", value) 

118 

119 def _append(self, attrname: str, value: Any): 

120 """Mark specified value as specified attrname. Multiple values can be specified 

121 at once by encapsulating them in tuple, list, set, or frozenset""" 

122 attr: set = getattr(self, attrname) 

123 if isinstance(value, (tuple, list, set, frozenset)): 

124 attr |= set(value) 

125 else: 

126 attr.add(value) 

127 

128 def __bool__(self) -> bool: 

129 """Allow to test current instance and return False if there's no difference, 

130 True otherwise""" 

131 return len(self._removed) + len(self._added) + len(self._modified) > 0