Skip to content

Commit 3777c90

Browse files
committed
feat: addressed __hash__ methods in a number of model classes and added tests #153
Signed-off-by: Paul Horton <[email protected]>
1 parent 20d7d4a commit 3777c90

File tree

7 files changed

+529
-86
lines changed

7 files changed

+529
-86
lines changed

cyclonedx/model/__init__.py

+43-2
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,14 @@ def get_algorithm(self) -> HashAlgorithm:
292292
def get_hash_value(self) -> str:
293293
return self._content
294294

295+
def __eq__(self, other: object) -> bool:
296+
if isinstance(other, HashType):
297+
return hash(other) == hash(self)
298+
return False
299+
300+
def __hash__(self) -> int:
301+
return hash((self._alg, self._content))
302+
295303
def __repr__(self) -> str:
296304
return f'<Hash {self._alg.value}:{self._content}>'
297305

@@ -351,7 +359,7 @@ def __hash__(self) -> int:
351359
return hash(self._uri)
352360

353361
def __repr__(self) -> str:
354-
return f'<XsUri uri={self._uri}>'
362+
return self._uri
355363

356364

357365
class ExternalReference:
@@ -416,8 +424,19 @@ def get_url(self) -> str:
416424
"""
417425
return self._url
418426

427+
def __eq__(self, other: object) -> bool:
428+
if isinstance(other, ExternalReference):
429+
return hash(other) == hash(self)
430+
return False
431+
432+
def __hash__(self) -> int:
433+
return hash((
434+
self._type, self._url, self._comment,
435+
tuple([hash(hash_) for hash_ in set(self._hashes)]) if self._hashes else None
436+
))
437+
419438
def __repr__(self) -> str:
420-
return f'<ExternalReference {self._type.name}, {self._url}> {self._hashes}'
439+
return f'<ExternalReference {self._type.name}, {self._url}>'
421440

422441

423442
class License:
@@ -503,6 +522,17 @@ def url(self) -> Optional[XsUri]:
503522
def url(self, url: Optional[XsUri]) -> None:
504523
self._url = url
505524

525+
def __eq__(self, other: object) -> bool:
526+
if isinstance(other, License):
527+
return hash(other) == hash(self)
528+
return False
529+
530+
def __hash__(self) -> int:
531+
return hash((self.id, self.name, self.text, self.url))
532+
533+
def __repr__(self) -> str:
534+
return f'<License id={self.id}, name={self.name}>'
535+
506536

507537
class LicenseChoice:
508538
"""
@@ -559,6 +589,17 @@ def expression(self) -> Optional[str]:
559589
def expression(self, expression: Optional[str]) -> None:
560590
self._expression = expression
561591

592+
def __eq__(self, other: object) -> bool:
593+
if isinstance(other, LicenseChoice):
594+
return hash(other) == hash(self)
595+
return False
596+
597+
def __hash__(self) -> int:
598+
return hash((self.license, self.expression))
599+
600+
def __repr__(self) -> str:
601+
return f'<LicenseChoice license={self.license}, expression={self.expression}>'
602+
562603

563604
class Property:
564605
"""

cyclonedx/model/component.py

+25-7
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626
# See https://github.com/package-url/packageurl-python/issues/65
2727
from packageurl import PackageURL # type: ignore
2828

29-
from . import AttachedText, ExternalReference, HashAlgorithm, HashType, LicenseChoice, OrganizationalEntity, Property, \
30-
sha1sum, XsUri, IdentifiableAction, Copyright
29+
from . import AttachedText, Copyright, ExternalReference, HashAlgorithm, HashType, IdentifiableAction, LicenseChoice, \
30+
OrganizationalEntity, Property, sha1sum, XsUri
3131
from .issue import IssueType
3232
from .release_note import ReleaseNotes
3333
from .vulnerability import Vulnerability
@@ -151,6 +151,11 @@ class ComponentEvidence:
151151

152152
def __init__(self, licenses: Optional[List[LicenseChoice]] = None,
153153
copyright_: Optional[List[Copyright]] = None) -> None:
154+
if not licenses and not copyright_:
155+
raise NoPropertiesProvidedException(
156+
'At least one of `licenses` or `copyright_` must be supplied for a `ComponentEvidence`.'
157+
)
158+
154159
self.licenses = licenses
155160
self.copyright = copyright_
156161

@@ -372,7 +377,10 @@ def __eq__(self, other: object) -> bool:
372377
return False
373378

374379
def __hash__(self) -> int:
375-
return hash((hash(self.type), hash(self.diff), str(self.resolves)))
380+
return hash((
381+
hash(self.type), hash(self.diff),
382+
tuple([hash(issue) for issue in set(self.resolves)]) if self.resolves else None
383+
))
376384

377385
def __repr__(self) -> str:
378386
return f'<Patch type={self.type}, id={id(self)}>'
@@ -559,7 +567,11 @@ def __eq__(self, other: object) -> bool:
559567

560568
def __hash__(self) -> int:
561569
return hash((
562-
str(self.ancestors), str(self.descendants), str(self.variants), str(self.commits), str(self.patches),
570+
tuple([hash(ancestor) for ancestor in set(self.ancestors)]) if self.ancestors else None,
571+
tuple([hash(descendant) for descendant in set(self.descendants)]) if self.descendants else None,
572+
tuple([hash(variant) for variant in set(self.variants)]) if self.variants else None,
573+
tuple([hash(commit) for commit in set(self.commits)]) if self.commits else None,
574+
tuple([hash(patch) for patch in set(self.patches)]) if self.patches else None,
563575
self.notes
564576
))
565577

@@ -1220,9 +1232,15 @@ def __eq__(self, other: object) -> bool:
12201232

12211233
def __hash__(self) -> int:
12221234
return hash((
1223-
self.author, self.copyright, self.description, str(self.external_references), self.group,
1224-
str(self.hashes), str(self.licenses), self.mime_type, self.name, str(self.properties), self.publisher,
1225-
self.purl, self.release_notes, self.scope, self.supplier, self.type, self.version, self.cpe
1235+
self.type, self.mime_type, self.supplier, self.author, self.publisher, self.group, self.name,
1236+
self.version, self.description, self.scope,
1237+
tuple([hash(hash_) for hash_ in set(self.hashes)]) if self.hashes else None,
1238+
tuple([hash(license_) for license_ in set(self.licenses)]) if self.licenses else None,
1239+
self.copyright, self.cpe, self.purl, self.swid, self.pedigree,
1240+
tuple([hash(ref) for ref in set(self.external_references)]) if self.external_references else None,
1241+
tuple([hash(prop) for prop in set(self.properties)]) if self.properties else None,
1242+
tuple([hash(component) for component in set(self.components)]) if self.components else None,
1243+
self.evidence, self.release_notes
12261244
))
12271245

12281246
def __repr__(self) -> str:

cyclonedx/model/issue.py

+25
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,17 @@ def url(self) -> Optional[XsUri]:
7878
def url(self, url: Optional[XsUri]) -> None:
7979
self._url = url
8080

81+
def __eq__(self, other: object) -> bool:
82+
if isinstance(other, IssueTypeSource):
83+
return hash(other) == hash(self)
84+
return False
85+
86+
def __hash__(self) -> int:
87+
return hash((self.name, self.url))
88+
89+
def __repr__(self) -> str:
90+
return f'<IssueTypeSource name={self._name}, url={self.url}>'
91+
8192

8293
class IssueType:
8394
"""
@@ -263,3 +274,17 @@ def set_source_url(self, source_url: XsUri) -> None:
263274
self._source.url = source_url
264275
else:
265276
self._source = IssueTypeSource(url=source_url)
277+
278+
def __eq__(self, other: object) -> bool:
279+
if isinstance(other, IssueType):
280+
return hash(other) == hash(self)
281+
return False
282+
283+
def __hash__(self) -> int:
284+
return hash((
285+
self._type, self._id, self._name, self._description, self._source,
286+
tuple([hash(ref) for ref in set(self._references)]) if self._references else None
287+
))
288+
289+
def __repr__(self) -> str:
290+
return f'<IssueType type={self._type}, id={self._id}, name={self._name}>'

0 commit comments

Comments
 (0)