Skip to content

Commit 368f522

Browse files
Use SortedSet in model to improve reproducibility
Added `__lt__()` to all model classes used in SortedSet, with tests Explicitly declared Enums as (str, Enum) to allow sorting Added dependency to sortedcollections package Signed-off-by: Rodney Richardson <[email protected]>
1 parent 91e1297 commit 368f522

23 files changed

+1060
-175
lines changed

cyclonedx/model/__init__.py

Lines changed: 111 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
from enum import Enum
2424
from typing import Iterable, Optional, Set
2525

26+
from sortedcontainers import SortedSet
27+
2628
from ..exception.model import (
2729
InvalidLocaleTypeException,
2830
InvalidUriException,
@@ -57,7 +59,41 @@ def sha1sum(filename: str) -> str:
5759
return h.hexdigest()
5860

5961

60-
class DataFlow(Enum):
62+
class ComparableTuple(tuple):
63+
"""
64+
Allows comparison of tuples, allowing for None values.
65+
"""
66+
67+
def __lt__(self, other: object) -> bool:
68+
for s, o in zip(self, other):
69+
if s == o:
70+
continue
71+
if s is None:
72+
return False
73+
if o is None:
74+
return True
75+
if s < o:
76+
return True
77+
if s > o:
78+
return False
79+
return False
80+
81+
def __gt__(self, other: object) -> bool:
82+
for s, o in zip(self, other):
83+
if s == o:
84+
continue
85+
if s is None:
86+
return True
87+
if o is None:
88+
return False
89+
if s < o:
90+
return False
91+
if s > o:
92+
return True
93+
return False
94+
95+
96+
class DataFlow(str, Enum):
6197
"""
6298
This is our internal representation of the dataFlowType simple type within the CycloneDX standard.
6399
@@ -132,7 +168,7 @@ def __repr__(self) -> str:
132168
return f'<DataClassification flow={self.flow}>'
133169

134170

135-
class Encoding(Enum):
171+
class Encoding(str, Enum):
136172
"""
137173
This is our internal representation of the encoding simple type within the CycloneDX standard.
138174
@@ -215,7 +251,7 @@ def __repr__(self) -> str:
215251
return f'<AttachedText content-type={self.content_type}, encoding={self.encoding}>'
216252

217253

218-
class HashAlgorithm(Enum):
254+
class HashAlgorithm(str, Enum):
219255
"""
220256
This is our internal representation of the hashAlg simple type within the CycloneDX standard.
221257
@@ -320,14 +356,19 @@ def __eq__(self, other: object) -> bool:
320356
return hash(other) == hash(self)
321357
return False
322358

359+
def __lt__(self, other: object) -> bool:
360+
if isinstance(other, HashType):
361+
return ComparableTuple((self.alg, self.content)) < ComparableTuple((other.alg, other.content))
362+
return NotImplemented
363+
323364
def __hash__(self) -> int:
324365
return hash((self.alg, self.content))
325366

326367
def __repr__(self) -> str:
327368
return f'<HashType {self.alg.name}:{self.content}>'
328369

329370

330-
class ExternalReferenceType(Enum):
371+
class ExternalReferenceType(str, Enum):
331372
"""
332373
Enum object that defines the permissible 'types' for an External Reference according to the CycloneDX schema.
333374
@@ -378,6 +419,11 @@ def __eq__(self, other: object) -> bool:
378419
return hash(other) == hash(self)
379420
return False
380421

422+
def __lt__(self, other: object) -> bool:
423+
if isinstance(other, XsUri):
424+
return self._uri < other._uri
425+
return NotImplemented
426+
381427
def __hash__(self) -> int:
382428
return hash(self._uri)
383429

@@ -402,7 +448,7 @@ def __init__(self, *, reference_type: ExternalReferenceType, url: XsUri, comment
402448
self.url = url
403449
self.comment = comment
404450
self.type = reference_type
405-
self.hashes = set(hashes or [])
451+
self.hashes = SortedSet(hashes or [])
406452

407453
@property
408454
def url(self) -> XsUri:
@@ -461,13 +507,18 @@ def hashes(self) -> Set[HashType]:
461507

462508
@hashes.setter
463509
def hashes(self, hashes: Iterable[HashType]) -> None:
464-
self._hashes = set(hashes)
510+
self._hashes = SortedSet(hashes)
465511

466512
def __eq__(self, other: object) -> bool:
467513
if isinstance(other, ExternalReference):
468514
return hash(other) == hash(self)
469515
return False
470516

517+
def __lt__(self, other: object) -> bool:
518+
if isinstance(other, ExternalReference):
519+
return ComparableTuple((self._type, self._url, self._comment)) < ComparableTuple((other._type, other._url, other._comment))
520+
return NotImplemented
521+
471522
def __hash__(self) -> int:
472523
return hash((
473524
self._type, self._url, self._comment,
@@ -566,6 +617,11 @@ def __eq__(self, other: object) -> bool:
566617
return hash(other) == hash(self)
567618
return False
568619

620+
def __lt__(self, other: object) -> bool:
621+
if isinstance(other, License):
622+
return ComparableTuple((self.id, self.name)) < ComparableTuple((other.id, other.name))
623+
return NotImplemented
624+
569625
def __hash__(self) -> int:
570626
return hash((self.id, self.name, self.text, self.url))
571627

@@ -633,6 +689,11 @@ def __eq__(self, other: object) -> bool:
633689
return hash(other) == hash(self)
634690
return False
635691

692+
def __lt__(self, other: object) -> bool:
693+
if isinstance(other, LicenseChoice):
694+
return ComparableTuple((self.license, self.expression)) < ComparableTuple((other.license, other.expression))
695+
return NotImplemented
696+
636697
def __hash__(self) -> int:
637698
return hash((self.license, self.expression))
638699

@@ -690,6 +751,11 @@ def __eq__(self, other: object) -> bool:
690751
return hash(other) == hash(self)
691752
return False
692753

754+
def __lt__(self, other: object) -> bool:
755+
if isinstance(other, Property):
756+
return ComparableTuple((self.name, self.value)) < ComparableTuple((other.name, other.value))
757+
return NotImplemented
758+
693759
def __hash__(self) -> int:
694760
return hash((self.name, self.value))
695761

@@ -763,6 +829,11 @@ def __eq__(self, other: object) -> bool:
763829
return hash(other) == hash(self)
764830
return False
765831

832+
def __lt__(self, other: object) -> bool:
833+
if isinstance(other, NoteText):
834+
return ComparableTuple((self.content, self.content_type, self.encoding)) < ComparableTuple((other.content, other.content_type, other.encoding))
835+
return NotImplemented
836+
766837
def __hash__(self) -> int:
767838
return hash((self.content, self.content_type, self.encoding))
768839

@@ -830,6 +901,11 @@ def __eq__(self, other: object) -> bool:
830901
return hash(other) == hash(self)
831902
return False
832903

904+
def __lt__(self, other: object) -> bool:
905+
if isinstance(other, Note):
906+
return ComparableTuple((self.locale, self.text)) < ComparableTuple((other.locale, other.text))
907+
return NotImplemented
908+
833909
def __hash__(self) -> int:
834910
return hash((self.text, self.locale))
835911

@@ -902,11 +978,16 @@ def __eq__(self, other: object) -> bool:
902978
return hash(other) == hash(self)
903979
return False
904980

981+
def __lt__(self, other: object) -> bool:
982+
if isinstance(other, OrganizationalContact):
983+
return ComparableTuple((self.name, self.email, self.phone)) < ComparableTuple((other.name, other.email, other.phone))
984+
return NotImplemented
985+
905986
def __hash__(self) -> int:
906987
return hash((self.name, self.phone, self.email))
907988

908989
def __repr__(self) -> str:
909-
return f'<OrganizationalContact name={self.name}>'
990+
return f'<OrganizationalContact name={self.name}, email={self.email}, phone={self.phone}>'
910991

911992

912993
class OrganizationalEntity:
@@ -925,8 +1006,8 @@ def __init__(self, *, name: Optional[str] = None, urls: Optional[Iterable[XsUri]
9251006
'One of name, urls or contacts must be supplied for an OrganizationalEntity - none supplied.'
9261007
)
9271008
self.name = name
928-
self.url = set(urls or [])
929-
self.contact = set(contacts or [])
1009+
self.url = SortedSet(urls or [])
1010+
self.contact = SortedSet(contacts or [])
9301011

9311012
@property
9321013
def name(self) -> Optional[str]:
@@ -954,7 +1035,7 @@ def url(self) -> Set[XsUri]:
9541035

9551036
@url.setter
9561037
def url(self, urls: Iterable[XsUri]) -> None:
957-
self._url = set(urls)
1038+
self._url = SortedSet(urls)
9581039

9591040
@property
9601041
def contact(self) -> Set[OrganizationalContact]:
@@ -968,7 +1049,7 @@ def contact(self) -> Set[OrganizationalContact]:
9681049

9691050
@contact.setter
9701051
def contact(self, contacts: Iterable[OrganizationalContact]) -> None:
971-
self._contact = set(contacts)
1052+
self._contact = SortedSet(contacts)
9721053

9731054
def __eq__(self, other: object) -> bool:
9741055
if isinstance(other, OrganizationalEntity):
@@ -998,8 +1079,8 @@ def __init__(self, *, vendor: Optional[str] = None, name: Optional[str] = None,
9981079
self.vendor = vendor
9991080
self.name = name
10001081
self.version = version
1001-
self.hashes = set(hashes or [])
1002-
self.external_references = set(external_references or [])
1082+
self.hashes = SortedSet(hashes or [])
1083+
self.external_references = SortedSet(external_references or [])
10031084

10041085
@property
10051086
def vendor(self) -> Optional[str]:
@@ -1055,7 +1136,7 @@ def hashes(self) -> Set[HashType]:
10551136

10561137
@hashes.setter
10571138
def hashes(self, hashes: Iterable[HashType]) -> None:
1058-
self._hashes = set(hashes)
1139+
self._hashes = SortedSet(hashes)
10591140

10601141
@property
10611142
def external_references(self) -> Set[ExternalReference]:
@@ -1070,13 +1151,18 @@ def external_references(self) -> Set[ExternalReference]:
10701151

10711152
@external_references.setter
10721153
def external_references(self, external_references: Iterable[ExternalReference]) -> None:
1073-
self._external_references = set(external_references)
1154+
self._external_references = SortedSet(external_references)
10741155

10751156
def __eq__(self, other: object) -> bool:
10761157
if isinstance(other, Tool):
10771158
return hash(other) == hash(self)
10781159
return False
10791160

1161+
def __lt__(self, other: object) -> bool:
1162+
if isinstance(other, Tool):
1163+
return ComparableTuple((self.vendor, self.name, self.version)) < ComparableTuple((other.vendor, other.name, other.version))
1164+
return NotImplemented
1165+
10801166
def __hash__(self) -> int:
10811167
return hash((self.vendor, self.name, self.version, tuple(self.hashes), tuple(self.external_references)))
10821168

@@ -1150,6 +1236,11 @@ def __eq__(self, other: object) -> bool:
11501236
return hash(other) == hash(self)
11511237
return False
11521238

1239+
def __lt__(self, other: object) -> bool:
1240+
if isinstance(other, IdentifiableAction):
1241+
return ComparableTuple((self.timestamp, self.name, self.email)) < ComparableTuple((other.timestamp, other.name, other.email))
1242+
return NotImplemented
1243+
11531244
def __hash__(self) -> int:
11541245
return hash((self.timestamp, self.name, self.email))
11551246

@@ -1187,6 +1278,11 @@ def __eq__(self, other: object) -> bool:
11871278
return hash(other) == hash(self)
11881279
return False
11891280

1281+
def __lt__(self, other: object) -> bool:
1282+
if isinstance(other, Copyright):
1283+
return self.text < other.text
1284+
return NotImplemented
1285+
11901286
def __hash__(self) -> int:
11911287
return hash(self.text)
11921288

cyclonedx/model/bom.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def __init__(self, *, tools: Optional[Iterable[Tool]] = None,
5151
self.licenses = set(licenses or [])
5252
self.properties = set(properties or [])
5353

54-
if not self.tools:
54+
if not tools:
5555
self.tools.add(ThisTool)
5656

5757
@property

cyclonedx/model/bom_ref.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ def __eq__(self, other: object) -> bool:
4747
return hash(other) == hash(self)
4848
return False
4949

50+
def __lt__(self, other: object) -> bool:
51+
if isinstance(other, BomRef):
52+
return self.value < other.value
53+
return NotImplemented
54+
5055
def __hash__(self) -> int:
5156
return hash(self.value)
5257

0 commit comments

Comments
 (0)