Skip to content

Commit 32c0139

Browse files
authored
feat: Complete support for bom.components (#155)
* fix: implemented correct `__hash__` methods in models (#153) Signed-off-by: Paul Horton <[email protected]>
1 parent 0ce5de6 commit 32c0139

26 files changed

+3028
-113
lines changed

cyclonedx/model/__init__.py

Lines changed: 264 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414
#
1515
# SPDX-License-Identifier: Apache-2.0
1616
#
17-
1817
import hashlib
1918
import re
2019
import sys
2120
import warnings
21+
from datetime import datetime
2222
from enum import Enum
2323
from typing import List, Optional, Union
2424

@@ -119,6 +119,17 @@ def classification(self) -> str:
119119
def classification(self, classification: str) -> None:
120120
self._classification = classification
121121

122+
def __eq__(self, other: object) -> bool:
123+
if isinstance(other, DataClassification):
124+
return hash(other) == hash(self)
125+
return False
126+
127+
def __hash__(self) -> int:
128+
return hash((self.flow, self.classification))
129+
130+
def __repr__(self) -> str:
131+
return f'<DataClassification flow={self.flow}>'
132+
122133

123134
class Encoding(Enum):
124135
"""
@@ -191,6 +202,17 @@ def content(self) -> str:
191202
def content(self, content: str) -> None:
192203
self._content = content
193204

205+
def __eq__(self, other: object) -> bool:
206+
if isinstance(other, AttachedText):
207+
return hash(other) == hash(self)
208+
return False
209+
210+
def __hash__(self) -> int:
211+
return hash((self.content, self.content_type, self.encoding))
212+
213+
def __repr__(self) -> str:
214+
return f'<AttachedText content-type={self.content_type}, encoding={self.encoding}>'
215+
194216

195217
class HashAlgorithm(Enum):
196218
"""
@@ -270,8 +292,16 @@ def get_algorithm(self) -> HashAlgorithm:
270292
def get_hash_value(self) -> str:
271293
return self._content
272294

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+
273303
def __repr__(self) -> str:
274-
return f'<Hash {self._alg.value}:{self._content}>'
304+
return f'<HashType {self._alg.value}:{self._content}>'
275305

276306

277307
class ExternalReferenceType(Enum):
@@ -299,6 +329,17 @@ class ExternalReferenceType(Enum):
299329
VCS = 'vcs'
300330
WEBSITE = 'website'
301331

332+
# def __eq__(self, other: object) -> bool:
333+
# if isinstance(other, ExternalReferenceType):
334+
# return hash(other) == hash(self)
335+
# return False
336+
#
337+
# def __hash__(self) -> int:
338+
# return hash(self.value)
339+
#
340+
# def __repr__(self) -> str:
341+
# return f'<ExternalReferenceType name={self.name}, value={self.value}>'
342+
302343

303344
class XsUri:
304345
"""
@@ -322,9 +363,12 @@ def __init__(self, uri: str) -> None:
322363

323364
def __eq__(self, other: object) -> bool:
324365
if isinstance(other, XsUri):
325-
return str(self) == str(other)
366+
return hash(other) == hash(self)
326367
return False
327368

369+
def __hash__(self) -> int:
370+
return hash(self._uri)
371+
328372
def __repr__(self) -> str:
329373
return self._uri
330374

@@ -391,8 +435,19 @@ def get_url(self) -> str:
391435
"""
392436
return self._url
393437

438+
def __eq__(self, other: object) -> bool:
439+
if isinstance(other, ExternalReference):
440+
return hash(other) == hash(self)
441+
return False
442+
443+
def __hash__(self) -> int:
444+
return hash((
445+
self._type, self._url, self._comment,
446+
tuple([hash(hash_) for hash_ in set(sorted(self._hashes, key=hash))]) if self._hashes else None
447+
))
448+
394449
def __repr__(self) -> str:
395-
return f'<ExternalReference {self._type.name}, {self._url}> {self._hashes}'
450+
return f'<ExternalReference {self._type.name}, {self._url}>'
396451

397452

398453
class License:
@@ -478,6 +533,17 @@ def url(self) -> Optional[XsUri]:
478533
def url(self, url: Optional[XsUri]) -> None:
479534
self._url = url
480535

536+
def __eq__(self, other: object) -> bool:
537+
if isinstance(other, License):
538+
return hash(other) == hash(self)
539+
return False
540+
541+
def __hash__(self) -> int:
542+
return hash((self.id, self.name, self.text, self.url))
543+
544+
def __repr__(self) -> str:
545+
return f'<License id={self.id}, name={self.name}>'
546+
481547

482548
class LicenseChoice:
483549
"""
@@ -534,6 +600,17 @@ def expression(self) -> Optional[str]:
534600
def expression(self, expression: Optional[str]) -> None:
535601
self._expression = expression
536602

603+
def __eq__(self, other: object) -> bool:
604+
if isinstance(other, LicenseChoice):
605+
return hash(other) == hash(self)
606+
return False
607+
608+
def __hash__(self) -> int:
609+
return hash((self.license, self.expression))
610+
611+
def __repr__(self) -> str:
612+
return f'<LicenseChoice license={self.license}, expression={self.expression}>'
613+
537614

538615
class Property:
539616
"""
@@ -568,6 +645,17 @@ def get_value(self) -> str:
568645
"""
569646
return self._value
570647

648+
def __eq__(self, other: object) -> bool:
649+
if isinstance(other, Property):
650+
return hash(other) == hash(self)
651+
return False
652+
653+
def __hash__(self) -> int:
654+
return hash((self._name, self._value))
655+
656+
def __repr__(self) -> str:
657+
return f'<Property name={self._name}>'
658+
571659

572660
class NoteText:
573661
"""
@@ -630,6 +718,17 @@ def encoding(self) -> Optional[Encoding]:
630718
def encoding(self, encoding: Optional[Encoding]) -> None:
631719
self._encoding = encoding
632720

721+
def __eq__(self, other: object) -> bool:
722+
if isinstance(other, NoteText):
723+
return hash(other) == hash(self)
724+
return False
725+
726+
def __hash__(self) -> int:
727+
return hash((self.content, self.content_type, self.encoding))
728+
729+
def __repr__(self) -> str:
730+
return f'<NoteText content_type={self.content_type}, encoding={self.encoding}>'
731+
633732

634733
class Note:
635734
"""
@@ -686,6 +785,17 @@ def locale(self, locale: Optional[str]) -> None:
686785
f"ISO-3166 (or higher) country code. according to ISO-639 format. Examples include: 'en', 'en-US'."
687786
)
688787

788+
def __eq__(self, other: object) -> bool:
789+
if isinstance(other, Note):
790+
return hash(other) == hash(self)
791+
return False
792+
793+
def __hash__(self) -> int:
794+
return hash((hash(self.text), self.locale))
795+
796+
def __repr__(self) -> str:
797+
return f'<Note id={id(self)}, locale={self.locale}>'
798+
689799

690800
class OrganizationalContact:
691801
"""
@@ -735,6 +845,17 @@ def phone(self) -> Optional[str]:
735845
"""
736846
return self._phone
737847

848+
def __eq__(self, other: object) -> bool:
849+
if isinstance(other, OrganizationalContact):
850+
return hash(other) == hash(self)
851+
return False
852+
853+
def __hash__(self) -> int:
854+
return hash((self.name, self.phone, self.email))
855+
856+
def __repr__(self) -> str:
857+
return f'<OrganizationalContact name={self.name}>'
858+
738859

739860
class OrganizationalEntity:
740861
"""
@@ -785,6 +906,21 @@ def contacts(self) -> Optional[List[OrganizationalContact]]:
785906
"""
786907
return self._contact
787908

909+
def __eq__(self, other: object) -> bool:
910+
if isinstance(other, OrganizationalEntity):
911+
return hash(other) == hash(self)
912+
return False
913+
914+
def __hash__(self) -> int:
915+
return hash((
916+
self.name,
917+
tuple([hash(url) for url in set(sorted(self.urls, key=hash))]) if self.urls else None,
918+
tuple([hash(contact) for contact in set(sorted(self.contacts, key=hash))]) if self.contacts else None
919+
))
920+
921+
def __repr__(self) -> str:
922+
return f'<OrganizationalEntity name={self.name}>'
923+
788924

789925
class Tool:
790926
"""
@@ -876,8 +1012,131 @@ def get_version(self) -> Optional[str]:
8761012
"""
8771013
return self._version
8781014

1015+
def __eq__(self, other: object) -> bool:
1016+
if isinstance(other, Tool):
1017+
return hash(other) == hash(self)
1018+
return False
1019+
1020+
def __hash__(self) -> int:
1021+
return hash((
1022+
self._vendor, self._name, self._version,
1023+
tuple([hash(hash_) for hash_ in set(sorted(self._hashes, key=hash))]) if self._hashes else None,
1024+
tuple([hash(ref) for ref in
1025+
set(sorted(self._external_references, key=hash))]) if self._external_references else None
1026+
))
1027+
1028+
def __repr__(self) -> str:
1029+
return f'<Tool name={self._name}, version={self._version}, vendor={self._vendor}>'
1030+
1031+
1032+
class IdentifiableAction:
1033+
"""
1034+
This is out internal representation of the `identifiableActionType` complex type.
1035+
1036+
.. note::
1037+
See the CycloneDX specification: https://cyclonedx.org/docs/1.4/xml/#type_identifiableActionType
1038+
"""
1039+
1040+
def __init__(self, timestamp: Optional[datetime] = None, name: Optional[str] = None,
1041+
email: Optional[str] = None) -> None:
1042+
if not timestamp and not name and not email:
1043+
raise NoPropertiesProvidedException(
1044+
'At least one of `timestamp`, `name` or `email` must be provided for an `IdentifiableAction`.'
1045+
)
1046+
1047+
self.timestamp = timestamp
1048+
self.name = name
1049+
self.email = email
1050+
1051+
@property
1052+
def timestamp(self) -> Optional[datetime]:
1053+
"""
1054+
The timestamp in which the action occurred.
1055+
1056+
Returns:
1057+
`datetime` if set else `None`
1058+
"""
1059+
return self._timestamp
1060+
1061+
@timestamp.setter
1062+
def timestamp(self, timestamp: Optional[datetime]) -> None:
1063+
self._timestamp = timestamp
1064+
1065+
@property
1066+
def name(self) -> Optional[str]:
1067+
"""
1068+
The name of the individual who performed the action.
1069+
1070+
Returns:
1071+
`str` if set else `None`
1072+
"""
1073+
return self._name
1074+
1075+
@name.setter
1076+
def name(self, name: Optional[str]) -> None:
1077+
self._name = name
1078+
1079+
@property
1080+
def email(self) -> Optional[str]:
1081+
"""
1082+
The email address of the individual who performed the action.
1083+
1084+
Returns:
1085+
`str` if set else `None`
1086+
"""
1087+
return self._email
1088+
1089+
@email.setter
1090+
def email(self, email: Optional[str]) -> None:
1091+
self._email = email
1092+
1093+
def __eq__(self, other: object) -> bool:
1094+
if isinstance(other, IdentifiableAction):
1095+
return hash(other) == hash(self)
1096+
return False
1097+
1098+
def __hash__(self) -> int:
1099+
return hash((hash(self.timestamp), self.name, self.email))
1100+
1101+
def __repr__(self) -> str:
1102+
return f'<IdentifiableAction name={self.name}, email={self.email}>'
1103+
1104+
1105+
class Copyright:
1106+
"""
1107+
This is out internal representation of the `copyrightsType` complex type.
1108+
1109+
.. note::
1110+
See the CycloneDX specification: https://cyclonedx.org/docs/1.4/xml/#type_copyrightsType
1111+
"""
1112+
1113+
def __init__(self, text: str) -> None:
1114+
self.text = text
1115+
1116+
@property
1117+
def text(self) -> str:
1118+
"""
1119+
Copyright statement.
1120+
1121+
Returns:
1122+
`str` if set else `None`
1123+
"""
1124+
return self._text
1125+
1126+
@text.setter
1127+
def text(self, text: str) -> None:
1128+
self._text = text
1129+
1130+
def __eq__(self, other: object) -> bool:
1131+
if isinstance(other, Copyright):
1132+
return hash(other) == hash(self)
1133+
return False
1134+
1135+
def __hash__(self) -> int:
1136+
return hash(self.text)
1137+
8791138
def __repr__(self) -> str:
880-
return '<Tool {}:{}:{}>'.format(self._vendor, self._name, self._version)
1139+
return f'<Copyright text={self.text}>'
8811140

8821141

8831142
if sys.version_info >= (3, 8):

0 commit comments

Comments
 (0)