Skip to content

Commit fd9b755

Browse files
authored
refactor!: streamline comparison/hashing functions (#755)
we have different methods of object comparison here and there, some work on tuples, other on hashes, other on different structures. this PR streamlines this. these changes might cause breaking changes for downstream users. --------- Signed-off-by: Jan Kowalleck <[email protected]>
1 parent 2a87f50 commit fd9b755

17 files changed

+459
-354
lines changed

cyclonedx/_internal/compare.py

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -58,40 +58,25 @@ def __gt__(self, other: Any) -> bool:
5858
return False
5959

6060

61-
class ComparableDict:
61+
class ComparableDict(ComparableTuple):
6262
"""
6363
Allows comparison of dictionaries, allowing for missing/None values.
6464
"""
6565

66-
def __init__(self, dict_: Dict[Any, Any]) -> None:
67-
self._dict = dict_
68-
69-
def __lt__(self, other: Any) -> bool:
70-
if not isinstance(other, ComparableDict):
71-
return True
72-
keys = sorted(self._dict.keys() | other._dict.keys())
73-
return ComparableTuple(self._dict.get(k) for k in keys) \
74-
< ComparableTuple(other._dict.get(k) for k in keys)
75-
76-
def __gt__(self, other: Any) -> bool:
77-
if not isinstance(other, ComparableDict):
78-
return False
79-
keys = sorted(self._dict.keys() | other._dict.keys())
80-
return ComparableTuple(self._dict.get(k) for k in keys) \
81-
> ComparableTuple(other._dict.get(k) for k in keys)
66+
def __new__(cls, d: Dict[Any, Any]) -> 'ComparableDict':
67+
return super(ComparableDict, cls).__new__(cls, sorted(d.items()))
8268

8369

8470
class ComparablePackageURL(ComparableTuple):
8571
"""
8672
Allows comparison of PackageURL, allowing for qualifiers.
8773
"""
8874

89-
def __new__(cls, purl: 'PackageURL') -> 'ComparablePackageURL':
90-
return super().__new__(
91-
ComparablePackageURL, (
92-
purl.type,
93-
purl.namespace,
94-
purl.version,
95-
ComparableDict(purl.qualifiers) if isinstance(purl.qualifiers, dict) else purl.qualifiers,
96-
purl.subpath
97-
))
75+
def __new__(cls, p: 'PackageURL') -> 'ComparablePackageURL':
76+
return super(ComparablePackageURL, cls).__new__(cls, (
77+
p.type,
78+
p.namespace,
79+
p.version,
80+
ComparableDict(p.qualifiers) if isinstance(p.qualifiers, dict) else p.qualifiers,
81+
p.subpath
82+
))

cyclonedx/model/__init__.py

Lines changed: 69 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -133,22 +133,23 @@ def classification(self) -> str:
133133
def classification(self, classification: str) -> None:
134134
self._classification = classification
135135

136+
def __comparable_tuple(self) -> _ComparableTuple:
137+
return _ComparableTuple((
138+
self.flow, self.classification
139+
))
140+
136141
def __eq__(self, other: object) -> bool:
137142
if isinstance(other, DataClassification):
138-
return hash(other) == hash(self)
143+
return self.__comparable_tuple() == other.__comparable_tuple()
139144
return False
140145

141146
def __lt__(self, other: object) -> bool:
142147
if isinstance(other, DataClassification):
143-
return _ComparableTuple((
144-
self.flow, self.classification
145-
)) < _ComparableTuple((
146-
other.flow, other.classification
147-
))
148+
return self.__comparable_tuple() < other.__comparable_tuple()
148149
return NotImplemented
149150

150151
def __hash__(self) -> int:
151-
return hash((self.flow, self.classification))
152+
return hash(self.__comparable_tuple())
152153

153154
def __repr__(self) -> str:
154155
return f'<DataClassification flow={self.flow}>'
@@ -236,22 +237,23 @@ def content(self) -> str:
236237
def content(self, content: str) -> None:
237238
self._content = content
238239

240+
def __comparable_tuple(self) -> _ComparableTuple:
241+
return _ComparableTuple((
242+
self.content_type, self.encoding, self.content,
243+
))
244+
239245
def __eq__(self, other: object) -> bool:
240246
if isinstance(other, AttachedText):
241-
return hash(other) == hash(self)
247+
return self.__comparable_tuple() == other.__comparable_tuple()
242248
return False
243249

244250
def __lt__(self, other: Any) -> bool:
245251
if isinstance(other, AttachedText):
246-
return _ComparableTuple((
247-
self.content_type, self.content, self.encoding
248-
)) < _ComparableTuple((
249-
other.content_type, other.content, other.encoding
250-
))
252+
return self.__comparable_tuple() < other.__comparable_tuple()
251253
return NotImplemented
252254

253255
def __hash__(self) -> int:
254-
return hash((self.content, self.content_type, self.encoding))
256+
return hash(self.__comparable_tuple())
255257

256258
def __repr__(self) -> str:
257259
return f'<AttachedText content-type={self.content_type}, encoding={self.encoding}>'
@@ -515,22 +517,23 @@ def content(self) -> str:
515517
def content(self, content: str) -> None:
516518
self._content = content
517519

520+
def __comparable_tuple(self) -> _ComparableTuple:
521+
return _ComparableTuple((
522+
self.alg, self.content
523+
))
524+
518525
def __eq__(self, other: object) -> bool:
519526
if isinstance(other, HashType):
520-
return hash(other) == hash(self)
527+
return self.__comparable_tuple() == other.__comparable_tuple()
521528
return False
522529

523530
def __lt__(self, other: Any) -> bool:
524531
if isinstance(other, HashType):
525-
return _ComparableTuple((
526-
self.alg, self.content
527-
)) < _ComparableTuple((
528-
other.alg, other.content
529-
))
532+
return self.__comparable_tuple() < other.__comparable_tuple()
530533
return NotImplemented
531534

532535
def __hash__(self) -> int:
533-
return hash((self.alg, self.content))
536+
return hash(self.__comparable_tuple())
534537

535538
def __repr__(self) -> str:
536539
return f'<HashType {self.alg.name}:{self.content}>'
@@ -733,7 +736,7 @@ def __init__(self, uri: str) -> None:
733736

734737
def __eq__(self, other: Any) -> bool:
735738
if isinstance(other, XsUri):
736-
return hash(other) == hash(self)
739+
return self._uri == other._uri
737740
return False
738741

739742
def __lt__(self, other: Any) -> bool:
@@ -892,25 +895,24 @@ def hashes(self) -> 'SortedSet[HashType]':
892895
def hashes(self, hashes: Iterable[HashType]) -> None:
893896
self._hashes = SortedSet(hashes)
894897

898+
def __comparable_tuple(self) -> _ComparableTuple:
899+
return _ComparableTuple((
900+
self._type, self._url, self._comment,
901+
_ComparableTuple(self._hashes)
902+
))
903+
895904
def __eq__(self, other: object) -> bool:
896905
if isinstance(other, ExternalReference):
897-
return hash(other) == hash(self)
906+
return self.__comparable_tuple() == other.__comparable_tuple()
898907
return False
899908

900909
def __lt__(self, other: Any) -> bool:
901910
if isinstance(other, ExternalReference):
902-
return _ComparableTuple((
903-
self._type, self._url, self._comment
904-
)) < _ComparableTuple((
905-
other._type, other._url, other._comment
906-
))
911+
return self.__comparable_tuple() < other.__comparable_tuple()
907912
return NotImplemented
908913

909914
def __hash__(self) -> int:
910-
return hash((
911-
self._type, self._url, self._comment,
912-
tuple(sorted(self._hashes, key=hash))
913-
))
915+
return hash(self.__comparable_tuple())
914916

915917
def __repr__(self) -> str:
916918
return f'<ExternalReference {self.type.name}, {self.url}>'
@@ -969,22 +971,23 @@ def value(self) -> Optional[str]:
969971
def value(self, value: Optional[str]) -> None:
970972
self._value = value
971973

974+
def __comparable_tuple(self) -> _ComparableTuple:
975+
return _ComparableTuple((
976+
self.name, self.value
977+
))
978+
972979
def __eq__(self, other: object) -> bool:
973980
if isinstance(other, Property):
974-
return hash(other) == hash(self)
981+
return self.__comparable_tuple() == other.__comparable_tuple()
975982
return False
976983

977984
def __lt__(self, other: Any) -> bool:
978985
if isinstance(other, Property):
979-
return _ComparableTuple((
980-
self.name, self.value
981-
)) < _ComparableTuple((
982-
other.name, other.value
983-
))
986+
return self.__comparable_tuple() < other.__comparable_tuple()
984987
return NotImplemented
985988

986989
def __hash__(self) -> int:
987-
return hash((self.name, self.value))
990+
return hash(self.__comparable_tuple())
988991

989992
def __repr__(self) -> str:
990993
return f'<Property name={self.name}>'
@@ -1060,22 +1063,23 @@ def encoding(self) -> Optional[Encoding]:
10601063
def encoding(self, encoding: Optional[Encoding]) -> None:
10611064
self._encoding = encoding
10621065

1066+
def __comparable_tuple(self) -> _ComparableTuple:
1067+
return _ComparableTuple((
1068+
self.content, self.content_type, self.encoding
1069+
))
1070+
10631071
def __eq__(self, other: object) -> bool:
10641072
if isinstance(other, NoteText):
1065-
return hash(other) == hash(self)
1073+
return self.__comparable_tuple() == other.__comparable_tuple()
10661074
return False
10671075

10681076
def __lt__(self, other: Any) -> bool:
10691077
if isinstance(other, NoteText):
1070-
return _ComparableTuple((
1071-
self.content, self.content_type, self.encoding
1072-
)) < _ComparableTuple((
1073-
other.content, other.content_type, other.encoding
1074-
))
1078+
return self.__comparable_tuple() < other.__comparable_tuple()
10751079
return NotImplemented
10761080

10771081
def __hash__(self) -> int:
1078-
return hash((self.content, self.content_type, self.encoding))
1082+
return hash(self.__comparable_tuple())
10791083

10801084
def __repr__(self) -> str:
10811085
return f'<NoteText content_type={self.content_type}, encoding={self.encoding}>'
@@ -1144,22 +1148,23 @@ def locale(self, locale: Optional[str]) -> None:
11441148
" ISO-3166 (or higher) country code. according to ISO-639 format. Examples include: 'en', 'en-US'."
11451149
)
11461150

1151+
def __comparable_tuple(self) -> _ComparableTuple:
1152+
return _ComparableTuple((
1153+
self.locale, self.text
1154+
))
1155+
11471156
def __eq__(self, other: object) -> bool:
11481157
if isinstance(other, Note):
1149-
return hash(other) == hash(self)
1158+
return self.__comparable_tuple() == other.__comparable_tuple()
11501159
return False
11511160

11521161
def __lt__(self, other: Any) -> bool:
11531162
if isinstance(other, Note):
1154-
return _ComparableTuple((
1155-
self.locale, self.text
1156-
)) < _ComparableTuple((
1157-
other.locale, other.text
1158-
))
1163+
return self.__comparable_tuple() < other.__comparable_tuple()
11591164
return NotImplemented
11601165

11611166
def __hash__(self) -> int:
1162-
return hash((self.text, self.locale))
1167+
return hash(self.__comparable_tuple())
11631168

11641169
def __repr__(self) -> str:
11651170
return f'<Note id={id(self)}, locale={self.locale}>'
@@ -1234,22 +1239,23 @@ def email(self) -> Optional[str]:
12341239
def email(self, email: Optional[str]) -> None:
12351240
self._email = email
12361241

1242+
def __comparable_tuple(self) -> _ComparableTuple:
1243+
return _ComparableTuple((
1244+
self.timestamp, self.name, self.email
1245+
))
1246+
12371247
def __eq__(self, other: object) -> bool:
12381248
if isinstance(other, IdentifiableAction):
1239-
return hash(other) == hash(self)
1249+
return self.__comparable_tuple() == other.__comparable_tuple()
12401250
return False
12411251

12421252
def __lt__(self, other: Any) -> bool:
12431253
if isinstance(other, IdentifiableAction):
1244-
return _ComparableTuple((
1245-
self.timestamp, self.name, self.email
1246-
)) < _ComparableTuple((
1247-
other.timestamp, other.name, other.email
1248-
))
1254+
return self.__comparable_tuple() < other.__comparable_tuple()
12491255
return NotImplemented
12501256

12511257
def __hash__(self) -> int:
1252-
return hash((self.timestamp, self.name, self.email))
1258+
return hash(self.__comparable_tuple())
12531259

12541260
def __repr__(self) -> str:
12551261
return f'<IdentifiableAction name={self.name}, email={self.email}>'
@@ -1287,16 +1293,16 @@ def text(self, text: str) -> None:
12871293

12881294
def __eq__(self, other: object) -> bool:
12891295
if isinstance(other, Copyright):
1290-
return hash(other) == hash(self)
1296+
return self._text == other._text
12911297
return False
12921298

12931299
def __lt__(self, other: Any) -> bool:
12941300
if isinstance(other, Copyright):
1295-
return self.text < other.text
1301+
return self._text < other._text
12961302
return NotImplemented
12971303

12981304
def __hash__(self) -> int:
1299-
return hash(self.text)
1305+
return hash(self._text)
13001306

13011307
def __repr__(self) -> str:
13021308
return f'<Copyright text={self.text}>'

cyclonedx/model/bom.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import serializable
2626
from sortedcontainers import SortedSet
2727

28+
from .._internal.compare import ComparableTuple as _ComparableTuple
2829
from .._internal.time import get_now_utc as _get_now_utc
2930
from ..exception.model import LicenseExpressionAlongWithOthersException, UnknownComponentDependencyException
3031
from ..schema.schema import (
@@ -293,16 +294,20 @@ def properties(self) -> 'SortedSet[Property]':
293294
def properties(self, properties: Iterable[Property]) -> None:
294295
self._properties = SortedSet(properties)
295296

297+
def __comparable_tuple(self) -> _ComparableTuple:
298+
return _ComparableTuple((
299+
_ComparableTuple(self.authors), self.component, _ComparableTuple(self.licenses), self.manufacture,
300+
_ComparableTuple(self.properties),
301+
_ComparableTuple(self.lifecycles), self.supplier, self.timestamp, self.tools, self.manufacturer
302+
))
303+
296304
def __eq__(self, other: object) -> bool:
297305
if isinstance(other, BomMetaData):
298-
return hash(other) == hash(self)
306+
return self.__comparable_tuple() == other.__comparable_tuple()
299307
return False
300308

301309
def __hash__(self) -> int:
302-
return hash((
303-
tuple(self.authors), self.component, tuple(self.licenses), self.manufacture, tuple(self.properties),
304-
tuple(self.lifecycles), self.supplier, self.timestamp, self.tools, self.manufacturer
305-
))
310+
return hash(self.__comparable_tuple())
306311

307312
def __repr__(self) -> str:
308313
return f'<BomMetaData timestamp={self.timestamp}, component={self.component}>'
@@ -722,17 +727,22 @@ def validate(self) -> bool:
722727

723728
return True
724729

730+
def __comparable_tuple(self) -> _ComparableTuple:
731+
return _ComparableTuple((
732+
self.serial_number, self.version, self.metadata, _ComparableTuple(
733+
self.components), _ComparableTuple(self.services),
734+
_ComparableTuple(self.external_references), _ComparableTuple(
735+
self.dependencies), _ComparableTuple(self.properties),
736+
_ComparableTuple(self.vulnerabilities),
737+
))
738+
725739
def __eq__(self, other: object) -> bool:
726740
if isinstance(other, Bom):
727-
return hash(other) == hash(self)
741+
return self.__comparable_tuple() == other.__comparable_tuple()
728742
return False
729743

730744
def __hash__(self) -> int:
731-
return hash((
732-
self.serial_number, self.version, self.metadata, tuple(self.components), tuple(self.services),
733-
tuple(self.external_references), tuple(self.dependencies), tuple(self.properties),
734-
tuple(self.vulnerabilities),
735-
))
745+
return hash(self.__comparable_tuple())
736746

737747
def __repr__(self) -> str:
738748
return f'<Bom uuid={self.serial_number}, hash={hash(self)}>'

0 commit comments

Comments
 (0)