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
« 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 -*-
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/>.
23from typing import Any
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.
37 When tested with 'if instance', DiffObject instance returns False if no
38 difference was found, True otherwise.
39 """
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()
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
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
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
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 }
98 return {
99 "added": self.added,
100 "modified": self.modified,
101 "removed": self.removed,
102 }
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)
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)
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)
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)
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