Skip to content

Commit c43f6d8

Browse files
authored
Merge pull request #235 from RodneyRichardson/use-sorted-set
feat: use `SortedSet` in model to improve reproducibility - this will provide predictable ordering of various items in generated CycloneDX documents - thanks to @RodneyRichardson
2 parents 32af991 + 1b8ac25 commit c43f6d8

26 files changed

+1327
-258
lines changed

.mypy.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
[mypy]
22

33
files = cyclonedx/
4+
mypy_path = $MYPY_CONFIG_FILE_DIR/typings
45

56
show_error_codes = True
67
pretty = True

cyclonedx/model/__init__.py

Lines changed: 131 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
import warnings
2222
from datetime import datetime
2323
from enum import Enum
24-
from typing import Iterable, Optional, Set
24+
from typing import Any, Iterable, Optional, Tuple, TypeVar
25+
26+
from sortedcontainers import SortedSet
2527

2628
from ..exception.model import (
2729
InvalidLocaleTypeException,
@@ -57,7 +59,44 @@ def sha1sum(filename: str) -> str:
5759
return h.hexdigest()
5860

5961

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

134173

135-
class Encoding(Enum):
174+
class Encoding(str, Enum):
136175
"""
137176
This is our internal representation of the encoding simple type within the CycloneDX standard.
138177
@@ -208,14 +247,20 @@ def __eq__(self, other: object) -> bool:
208247
return hash(other) == hash(self)
209248
return False
210249

250+
def __lt__(self, other: Any) -> bool:
251+
if isinstance(other, AttachedText):
252+
return ComparableTuple((self.content_type, self.content, self.encoding)) < \
253+
ComparableTuple((other.content_type, other.content, other.encoding))
254+
return NotImplemented
255+
211256
def __hash__(self) -> int:
212257
return hash((self.content, self.content_type, self.encoding))
213258

214259
def __repr__(self) -> str:
215260
return f'<AttachedText content-type={self.content_type}, encoding={self.encoding}>'
216261

217262

218-
class HashAlgorithm(Enum):
263+
class HashAlgorithm(str, Enum):
219264
"""
220265
This is our internal representation of the hashAlg simple type within the CycloneDX standard.
221266
@@ -320,14 +365,19 @@ def __eq__(self, other: object) -> bool:
320365
return hash(other) == hash(self)
321366
return False
322367

368+
def __lt__(self, other: Any) -> bool:
369+
if isinstance(other, HashType):
370+
return ComparableTuple((self.alg, self.content)) < ComparableTuple((other.alg, other.content))
371+
return NotImplemented
372+
323373
def __hash__(self) -> int:
324374
return hash((self.alg, self.content))
325375

326376
def __repr__(self) -> str:
327377
return f'<HashType {self.alg.name}:{self.content}>'
328378

329379

330-
class ExternalReferenceType(Enum):
380+
class ExternalReferenceType(str, Enum):
331381
"""
332382
Enum object that defines the permissible 'types' for an External Reference according to the CycloneDX schema.
333383
@@ -378,6 +428,11 @@ def __eq__(self, other: object) -> bool:
378428
return hash(other) == hash(self)
379429
return False
380430

431+
def __lt__(self, other: Any) -> bool:
432+
if isinstance(other, XsUri):
433+
return self._uri < other._uri
434+
return NotImplemented
435+
381436
def __hash__(self) -> int:
382437
return hash(self._uri)
383438

@@ -402,7 +457,7 @@ def __init__(self, *, reference_type: ExternalReferenceType, url: XsUri, comment
402457
self.url = url
403458
self.comment = comment
404459
self.type = reference_type
405-
self.hashes = set(hashes or [])
460+
self.hashes = SortedSet(hashes or [])
406461

407462
@property
408463
def url(self) -> XsUri:
@@ -450,7 +505,7 @@ def type(self, type_: ExternalReferenceType) -> None:
450505
self._type = type_
451506

452507
@property
453-
def hashes(self) -> Set[HashType]:
508+
def hashes(self) -> "SortedSet[HashType]":
454509
"""
455510
The hashes of the external reference (if applicable).
456511
@@ -461,13 +516,19 @@ def hashes(self) -> Set[HashType]:
461516

462517
@hashes.setter
463518
def hashes(self, hashes: Iterable[HashType]) -> None:
464-
self._hashes = set(hashes)
519+
self._hashes = SortedSet(hashes)
465520

466521
def __eq__(self, other: object) -> bool:
467522
if isinstance(other, ExternalReference):
468523
return hash(other) == hash(self)
469524
return False
470525

526+
def __lt__(self, other: Any) -> bool:
527+
if isinstance(other, ExternalReference):
528+
return ComparableTuple((self._type, self._url, self._comment)) < \
529+
ComparableTuple((other._type, other._url, other._comment))
530+
return NotImplemented
531+
471532
def __hash__(self) -> int:
472533
return hash((
473534
self._type, self._url, self._comment,
@@ -566,6 +627,11 @@ def __eq__(self, other: object) -> bool:
566627
return hash(other) == hash(self)
567628
return False
568629

630+
def __lt__(self, other: Any) -> bool:
631+
if isinstance(other, License):
632+
return ComparableTuple((self.id, self.name)) < ComparableTuple((other.id, other.name))
633+
return NotImplemented
634+
569635
def __hash__(self) -> int:
570636
return hash((self.id, self.name, self.text, self.url))
571637

@@ -633,6 +699,11 @@ def __eq__(self, other: object) -> bool:
633699
return hash(other) == hash(self)
634700
return False
635701

702+
def __lt__(self, other: Any) -> bool:
703+
if isinstance(other, LicenseChoice):
704+
return ComparableTuple((self.license, self.expression)) < ComparableTuple((other.license, other.expression))
705+
return NotImplemented
706+
636707
def __hash__(self) -> int:
637708
return hash((self.license, self.expression))
638709

@@ -690,6 +761,11 @@ def __eq__(self, other: object) -> bool:
690761
return hash(other) == hash(self)
691762
return False
692763

764+
def __lt__(self, other: Any) -> bool:
765+
if isinstance(other, Property):
766+
return ComparableTuple((self.name, self.value)) < ComparableTuple((other.name, other.value))
767+
return NotImplemented
768+
693769
def __hash__(self) -> int:
694770
return hash((self.name, self.value))
695771

@@ -763,6 +839,12 @@ def __eq__(self, other: object) -> bool:
763839
return hash(other) == hash(self)
764840
return False
765841

842+
def __lt__(self, other: Any) -> bool:
843+
if isinstance(other, NoteText):
844+
return ComparableTuple((self.content, self.content_type, self.encoding)) < \
845+
ComparableTuple((other.content, other.content_type, other.encoding))
846+
return NotImplemented
847+
766848
def __hash__(self) -> int:
767849
return hash((self.content, self.content_type, self.encoding))
768850

@@ -830,6 +912,11 @@ def __eq__(self, other: object) -> bool:
830912
return hash(other) == hash(self)
831913
return False
832914

915+
def __lt__(self, other: Any) -> bool:
916+
if isinstance(other, Note):
917+
return ComparableTuple((self.locale, self.text)) < ComparableTuple((other.locale, other.text))
918+
return NotImplemented
919+
833920
def __hash__(self) -> int:
834921
return hash((self.text, self.locale))
835922

@@ -902,11 +989,17 @@ def __eq__(self, other: object) -> bool:
902989
return hash(other) == hash(self)
903990
return False
904991

992+
def __lt__(self, other: Any) -> bool:
993+
if isinstance(other, OrganizationalContact):
994+
return ComparableTuple((self.name, self.email, self.phone)) < \
995+
ComparableTuple((other.name, other.email, other.phone))
996+
return NotImplemented
997+
905998
def __hash__(self) -> int:
906999
return hash((self.name, self.phone, self.email))
9071000

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

9111004

9121005
class OrganizationalEntity:
@@ -925,8 +1018,8 @@ def __init__(self, *, name: Optional[str] = None, urls: Optional[Iterable[XsUri]
9251018
'One of name, urls or contacts must be supplied for an OrganizationalEntity - none supplied.'
9261019
)
9271020
self.name = name
928-
self.url = set(urls or [])
929-
self.contact = set(contacts or [])
1021+
self.url = SortedSet(urls or [])
1022+
self.contact = SortedSet(contacts or [])
9301023

9311024
@property
9321025
def name(self) -> Optional[str]:
@@ -943,7 +1036,7 @@ def name(self, name: Optional[str]) -> None:
9431036
self._name = name
9441037

9451038
@property
946-
def url(self) -> Set[XsUri]:
1039+
def url(self) -> "SortedSet[XsUri]":
9471040
"""
9481041
Get a list of URLs of the organization. Multiple URLs are allowed.
9491042
@@ -954,10 +1047,10 @@ def url(self) -> Set[XsUri]:
9541047

9551048
@url.setter
9561049
def url(self, urls: Iterable[XsUri]) -> None:
957-
self._url = set(urls)
1050+
self._url = SortedSet(urls)
9581051

9591052
@property
960-
def contact(self) -> Set[OrganizationalContact]:
1053+
def contact(self) -> "SortedSet[OrganizationalContact]":
9611054
"""
9621055
Get a list of contact person at the organization. Multiple contacts are allowed.
9631056
@@ -968,7 +1061,7 @@ def contact(self) -> Set[OrganizationalContact]:
9681061

9691062
@contact.setter
9701063
def contact(self, contacts: Iterable[OrganizationalContact]) -> None:
971-
self._contact = set(contacts)
1064+
self._contact = SortedSet(contacts)
9721065

9731066
def __eq__(self, other: object) -> bool:
9741067
if isinstance(other, OrganizationalEntity):
@@ -998,8 +1091,8 @@ def __init__(self, *, vendor: Optional[str] = None, name: Optional[str] = None,
9981091
self.vendor = vendor
9991092
self.name = name
10001093
self.version = version
1001-
self.hashes = set(hashes or [])
1002-
self.external_references = set(external_references or [])
1094+
self.hashes = SortedSet(hashes or [])
1095+
self.external_references = SortedSet(external_references or [])
10031096

10041097
@property
10051098
def vendor(self) -> Optional[str]:
@@ -1044,7 +1137,7 @@ def version(self, version: Optional[str]) -> None:
10441137
self._version = version
10451138

10461139
@property
1047-
def hashes(self) -> Set[HashType]:
1140+
def hashes(self) -> "SortedSet[HashType]":
10481141
"""
10491142
The hashes of the tool (if applicable).
10501143
@@ -1055,10 +1148,10 @@ def hashes(self) -> Set[HashType]:
10551148

10561149
@hashes.setter
10571150
def hashes(self, hashes: Iterable[HashType]) -> None:
1058-
self._hashes = set(hashes)
1151+
self._hashes = SortedSet(hashes)
10591152

10601153
@property
1061-
def external_references(self) -> Set[ExternalReference]:
1154+
def external_references(self) -> "SortedSet[ExternalReference]":
10621155
"""
10631156
External References provide a way to document systems, sites, and information that may be relevant but which
10641157
are not included with the BOM.
@@ -1070,13 +1163,19 @@ def external_references(self) -> Set[ExternalReference]:
10701163

10711164
@external_references.setter
10721165
def external_references(self, external_references: Iterable[ExternalReference]) -> None:
1073-
self._external_references = set(external_references)
1166+
self._external_references = SortedSet(external_references)
10741167

10751168
def __eq__(self, other: object) -> bool:
10761169
if isinstance(other, Tool):
10771170
return hash(other) == hash(self)
10781171
return False
10791172

1173+
def __lt__(self, other: Any) -> bool:
1174+
if isinstance(other, Tool):
1175+
return ComparableTuple((self.vendor, self.name, self.version)) < \
1176+
ComparableTuple((other.vendor, other.name, other.version))
1177+
return NotImplemented
1178+
10801179
def __hash__(self) -> int:
10811180
return hash((self.vendor, self.name, self.version, tuple(self.hashes), tuple(self.external_references)))
10821181

@@ -1150,6 +1249,12 @@ def __eq__(self, other: object) -> bool:
11501249
return hash(other) == hash(self)
11511250
return False
11521251

1252+
def __lt__(self, other: Any) -> bool:
1253+
if isinstance(other, IdentifiableAction):
1254+
return ComparableTuple((self.timestamp, self.name, self.email)) < \
1255+
ComparableTuple((other.timestamp, other.name, other.email))
1256+
return NotImplemented
1257+
11531258
def __hash__(self) -> int:
11541259
return hash((self.timestamp, self.name, self.email))
11551260

@@ -1187,6 +1292,11 @@ def __eq__(self, other: object) -> bool:
11871292
return hash(other) == hash(self)
11881293
return False
11891294

1295+
def __lt__(self, other: Any) -> bool:
1296+
if isinstance(other, Copyright):
1297+
return self.text < other.text
1298+
return NotImplemented
1299+
11901300
def __hash__(self) -> int:
11911301
return hash(self.text)
11921302

0 commit comments

Comments
 (0)