diff --git a/cyclonedx/__init__.py b/cyclonedx/__init__.py index b56627fc..de60e8e9 100644 --- a/cyclonedx/__init__.py +++ b/cyclonedx/__init__.py @@ -22,4 +22,4 @@ # !! version is managed by semantic_release # do not use typing here, or else `semantic_release` might have issues finding the variable -__version__ = "8.9.0" # noqa:Q000 +__version__ = "9.0.1-rc.1" # noqa:Q000 diff --git a/cyclonedx/_internal/compare.py b/cyclonedx/_internal/compare.py index 226fa615..bd64e692 100644 --- a/cyclonedx/_internal/compare.py +++ b/cyclonedx/_internal/compare.py @@ -42,7 +42,7 @@ def __lt__(self, other: Any) -> bool: return False if o is None: return True - return True if s < o else False + return bool(s < o) return False def __gt__(self, other: Any) -> bool: @@ -54,31 +54,17 @@ def __gt__(self, other: Any) -> bool: return True if o is None: return False - return True if s > o else False + return bool(s > o) return False -class ComparableDict: +class ComparableDict(ComparableTuple): """ Allows comparison of dictionaries, allowing for missing/None values. """ - def __init__(self, dict_: Dict[Any, Any]) -> None: - self._dict = dict_ - - def __lt__(self, other: Any) -> bool: - if not isinstance(other, ComparableDict): - return True - keys = sorted(self._dict.keys() | other._dict.keys()) - return ComparableTuple(self._dict.get(k) for k in keys) \ - < ComparableTuple(other._dict.get(k) for k in keys) - - def __gt__(self, other: Any) -> bool: - if not isinstance(other, ComparableDict): - return False - keys = sorted(self._dict.keys() | other._dict.keys()) - return ComparableTuple(self._dict.get(k) for k in keys) \ - > ComparableTuple(other._dict.get(k) for k in keys) + def __new__(cls, d: Dict[Any, Any]) -> 'ComparableDict': + return super(ComparableDict, cls).__new__(cls, sorted(d.items())) class ComparablePackageURL(ComparableTuple): @@ -86,12 +72,11 @@ class ComparablePackageURL(ComparableTuple): Allows comparison of PackageURL, allowing for qualifiers. """ - def __new__(cls, purl: 'PackageURL') -> 'ComparablePackageURL': - return super().__new__( - ComparablePackageURL, ( - purl.type, - purl.namespace, - purl.version, - ComparableDict(purl.qualifiers) if isinstance(purl.qualifiers, dict) else purl.qualifiers, - purl.subpath - )) + def __new__(cls, p: 'PackageURL') -> 'ComparablePackageURL': + return super(ComparablePackageURL, cls).__new__(cls, ( + p.type, + p.namespace, + p.version, + ComparableDict(p.qualifiers) if isinstance(p.qualifiers, dict) else p.qualifiers, + p.subpath + )) diff --git a/cyclonedx/factory/license.py b/cyclonedx/factory/license.py index f96cb697..40d4484d 100644 --- a/cyclonedx/factory/license.py +++ b/cyclonedx/factory/license.py @@ -19,7 +19,7 @@ from ..exception.factory import InvalidLicenseExpressionException, InvalidSpdxLicenseException from ..model.license import DisjunctiveLicense, LicenseExpression -from ..spdx import fixup_id as spdx_fixup, is_compound_expression as is_spdx_compound_expression +from ..spdx import fixup_id as spdx_fixup, is_expression as is_spdx_expression if TYPE_CHECKING: # pragma: no cover from ..model import AttachedText, XsUri @@ -57,11 +57,11 @@ def make_with_expression(self, expression: str, *, ) -> LicenseExpression: """Make a :class:`cyclonedx.model.license.LicenseExpression` with a compound expression. - Utilizes :func:`cyclonedx.spdx.is_compound_expression`. + Utilizes :func:`cyclonedx.spdx.is_expression`. :raises InvalidLicenseExpressionException: if param `value` is not known/supported license expression """ - if is_spdx_compound_expression(expression): + if is_spdx_expression(expression): return LicenseExpression(expression, acknowledgement=acknowledgement) raise InvalidLicenseExpressionException(expression) diff --git a/cyclonedx/model/__init__.py b/cyclonedx/model/__init__.py index 3a041b20..b2e40187 100644 --- a/cyclonedx/model/__init__.py +++ b/cyclonedx/model/__init__.py @@ -33,7 +33,7 @@ from warnings import warn from xml.etree.ElementTree import Element as XmlElement # nosec B405 -import serializable +import py_serializable as serializable from sortedcontainers import SortedSet from .._internal.compare import ComparableTuple as _ComparableTuple @@ -128,22 +128,23 @@ def classification(self) -> str: def classification(self, classification: str) -> None: self._classification = classification + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.flow, self.classification + )) + def __eq__(self, other: object) -> bool: if isinstance(other, DataClassification): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: object) -> bool: if isinstance(other, DataClassification): - return _ComparableTuple(( - self.flow, self.classification - )) < _ComparableTuple(( - other.flow, other.classification - )) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __hash__(self) -> int: - return hash((self.flow, self.classification)) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<DataClassification flow={self.flow}>' @@ -231,22 +232,23 @@ def content(self) -> str: def content(self, content: str) -> None: self._content = content + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.content_type, self.encoding, self.content, + )) + def __eq__(self, other: object) -> bool: if isinstance(other, AttachedText): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, AttachedText): - return _ComparableTuple(( - self.content_type, self.content, self.encoding - )) < _ComparableTuple(( - other.content_type, other.content, other.encoding - )) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __hash__(self) -> int: - return hash((self.content, self.content_type, self.encoding)) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<AttachedText content-type={self.content_type}, encoding={self.encoding}>' @@ -510,22 +512,23 @@ def content(self) -> str: def content(self, content: str) -> None: self._content = content + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.alg, self.content + )) + def __eq__(self, other: object) -> bool: if isinstance(other, HashType): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, HashType): - return _ComparableTuple(( - self.alg, self.content - )) < _ComparableTuple(( - other.alg, other.content - )) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __hash__(self) -> int: - return hash((self.alg, self.content)) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<HashType {self.alg.name}:{self.content}>' @@ -728,7 +731,7 @@ def __init__(self, uri: str) -> None: def __eq__(self, other: Any) -> bool: if isinstance(other, XsUri): - return hash(other) == hash(self) + return self._uri == other._uri return False def __lt__(self, other: Any) -> bool: @@ -887,25 +890,24 @@ def hashes(self) -> 'SortedSet[HashType]': def hashes(self, hashes: Iterable[HashType]) -> None: self._hashes = SortedSet(hashes) + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self._type, self._url, self._comment, + _ComparableTuple(self._hashes) + )) + def __eq__(self, other: object) -> bool: if isinstance(other, ExternalReference): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, ExternalReference): - return _ComparableTuple(( - self._type, self._url, self._comment - )) < _ComparableTuple(( - other._type, other._url, other._comment - )) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __hash__(self) -> int: - return hash(( - self._type, self._url, self._comment, - tuple(sorted(self._hashes, key=hash)) - )) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<ExternalReference {self.type.name}, {self.url}>' @@ -964,22 +966,23 @@ def value(self) -> Optional[str]: def value(self, value: Optional[str]) -> None: self._value = value + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.name, self.value + )) + def __eq__(self, other: object) -> bool: if isinstance(other, Property): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, Property): - return _ComparableTuple(( - self.name, self.value - )) < _ComparableTuple(( - other.name, other.value - )) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __hash__(self) -> int: - return hash((self.name, self.value)) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<Property name={self.name}>' @@ -1055,22 +1058,23 @@ def encoding(self) -> Optional[Encoding]: def encoding(self, encoding: Optional[Encoding]) -> None: self._encoding = encoding + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.content, self.content_type, self.encoding + )) + def __eq__(self, other: object) -> bool: if isinstance(other, NoteText): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, NoteText): - return _ComparableTuple(( - self.content, self.content_type, self.encoding - )) < _ComparableTuple(( - other.content, other.content_type, other.encoding - )) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __hash__(self) -> int: - return hash((self.content, self.content_type, self.encoding)) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<NoteText content_type={self.content_type}, encoding={self.encoding}>' @@ -1139,22 +1143,23 @@ def locale(self, locale: Optional[str]) -> None: " ISO-3166 (or higher) country code. according to ISO-639 format. Examples include: 'en', 'en-US'." ) + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.locale, self.text + )) + def __eq__(self, other: object) -> bool: if isinstance(other, Note): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, Note): - return _ComparableTuple(( - self.locale, self.text - )) < _ComparableTuple(( - other.locale, other.text - )) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __hash__(self) -> int: - return hash((self.text, self.locale)) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<Note id={id(self)}, locale={self.locale}>' @@ -1224,22 +1229,23 @@ def email(self) -> Optional[str]: def email(self, email: Optional[str]) -> None: self._email = email + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.timestamp, self.name, self.email + )) + def __eq__(self, other: object) -> bool: if isinstance(other, IdentifiableAction): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, IdentifiableAction): - return _ComparableTuple(( - self.timestamp, self.name, self.email - )) < _ComparableTuple(( - other.timestamp, other.name, other.email - )) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __hash__(self) -> int: - return hash((self.timestamp, self.name, self.email)) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<IdentifiableAction name={self.name}, email={self.email}>' @@ -1277,16 +1283,16 @@ def text(self, text: str) -> None: def __eq__(self, other: object) -> bool: if isinstance(other, Copyright): - return hash(other) == hash(self) + return self._text == other._text return False def __lt__(self, other: Any) -> bool: if isinstance(other, Copyright): - return self.text < other.text + return self._text < other._text return NotImplemented def __hash__(self) -> int: - return hash(self.text) + return hash(self._text) def __repr__(self) -> str: return f'<Copyright text={self.text}>' diff --git a/cyclonedx/model/bom.py b/cyclonedx/model/bom.py index 8a8d7405..130074ee 100644 --- a/cyclonedx/model/bom.py +++ b/cyclonedx/model/bom.py @@ -22,9 +22,10 @@ from uuid import UUID, uuid4 from warnings import warn -import serializable +import py_serializable as serializable from sortedcontainers import SortedSet +from .._internal.compare import ComparableTuple as _ComparableTuple from .._internal.time import get_now_utc as _get_now_utc from ..exception.model import LicenseExpressionAlongWithOthersException, UnknownComponentDependencyException from ..schema.schema import ( @@ -293,16 +294,20 @@ def properties(self) -> 'SortedSet[Property]': def properties(self, properties: Iterable[Property]) -> None: self._properties = SortedSet(properties) + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + _ComparableTuple(self.authors), self.component, _ComparableTuple(self.licenses), self.manufacture, + _ComparableTuple(self.properties), + _ComparableTuple(self.lifecycles), self.supplier, self.timestamp, self.tools, self.manufacturer + )) + def __eq__(self, other: object) -> bool: if isinstance(other, BomMetaData): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __hash__(self) -> int: - return hash(( - tuple(self.authors), self.component, tuple(self.licenses), self.manufacture, tuple(self.properties), - tuple(self.lifecycles), self.supplier, self.timestamp, self.tools, self.manufacturer - )) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<BomMetaData timestamp={self.timestamp}, component={self.component}>' @@ -722,17 +727,22 @@ def validate(self) -> bool: return True + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.serial_number, self.version, self.metadata, _ComparableTuple( + self.components), _ComparableTuple(self.services), + _ComparableTuple(self.external_references), _ComparableTuple( + self.dependencies), _ComparableTuple(self.properties), + _ComparableTuple(self.vulnerabilities), + )) + def __eq__(self, other: object) -> bool: if isinstance(other, Bom): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __hash__(self) -> int: - return hash(( - self.serial_number, self.version, self.metadata, tuple(self.components), tuple(self.services), - tuple(self.external_references), tuple(self.dependencies), tuple(self.properties), - tuple(self.vulnerabilities), - )) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<Bom uuid={self.serial_number}, hash={hash(self)}>' diff --git a/cyclonedx/model/bom_ref.py b/cyclonedx/model/bom_ref.py index 85bcf501..cc4571a7 100644 --- a/cyclonedx/model/bom_ref.py +++ b/cyclonedx/model/bom_ref.py @@ -18,7 +18,7 @@ from typing import TYPE_CHECKING, Any, Optional -import serializable +import py_serializable as serializable from ..exception.serialization import CycloneDxDeserializationException, SerializationOfUnexpectedValueException diff --git a/cyclonedx/model/component.py b/cyclonedx/model/component.py index df072597..28de1ea0 100644 --- a/cyclonedx/model/component.py +++ b/cyclonedx/model/component.py @@ -22,7 +22,7 @@ from warnings import warn # See https://github.com/package-url/packageurl-python/issues/65 -import serializable +import py_serializable as serializable from packageurl import PackageURL from sortedcontainers import SortedSet @@ -166,22 +166,25 @@ def message(self) -> Optional[str]: def message(self, message: Optional[str]) -> None: self._message = message + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.uid, self.url, + self.author, self.committer, + self.message + )) + def __eq__(self, other: object) -> bool: if isinstance(other, Commit): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, Commit): - return _ComparableTuple(( - self.uid, self.url, self.author, self.committer, self.message - )) < _ComparableTuple(( - other.uid, other.url, other.author, other.committer, other.message - )) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __hash__(self) -> int: - return hash((self.uid, self.url, self.author, self.committer, self.message)) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<Commit uid={self.uid}, url={self.url}, message={self.message}>' @@ -271,13 +274,19 @@ def copyright(self) -> 'SortedSet[Copyright]': def copyright(self, copyright: Iterable[Copyright]) -> None: self._copyright = SortedSet(copyright) + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + _ComparableTuple(self.licenses), + _ComparableTuple(self.copyright), + )) + def __eq__(self, other: object) -> bool: if isinstance(other, ComponentEvidence): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __hash__(self) -> int: - return hash((tuple(self.licenses), tuple(self.copyright))) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<ComponentEvidence id={id(self)}>' @@ -463,22 +472,24 @@ def url(self) -> Optional[XsUri]: def url(self, url: Optional[XsUri]) -> None: self._url = url + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.url, + self.text, + )) + def __eq__(self, other: object) -> bool: if isinstance(other, Diff): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, Diff): - return _ComparableTuple(( - self.url, self.text - )) < _ComparableTuple(( - other.url, other.text - )) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __hash__(self) -> int: - return hash((self.text, self.url)) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<Diff url={self.url}>' @@ -565,22 +576,24 @@ def resolves(self) -> 'SortedSet[IssueType]': def resolves(self, resolves: Iterable[IssueType]) -> None: self._resolves = SortedSet(resolves) + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.type, self.diff, + _ComparableTuple(self.resolves) + )) + def __eq__(self, other: object) -> bool: if isinstance(other, Patch): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, Patch): - return _ComparableTuple(( - self.type, self.diff, _ComparableTuple(self.resolves) - )) < _ComparableTuple(( - other.type, other.diff, _ComparableTuple(other.resolves) - )) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __hash__(self) -> int: - return hash((self.type, self.diff, tuple(self.resolves))) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<Patch type={self.type}, id={id(self)}>' @@ -727,16 +740,23 @@ def notes(self) -> Optional[str]: def notes(self, notes: Optional[str]) -> None: self._notes = notes + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + _ComparableTuple(self.ancestors), + _ComparableTuple(self.descendants), + _ComparableTuple(self.variants), + _ComparableTuple(self.commits), + _ComparableTuple(self.patches), + self.notes + )) + def __eq__(self, other: object) -> bool: if isinstance(other, Pedigree): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __hash__(self) -> int: - return hash(( - tuple(self.ancestors), tuple(self.descendants), tuple(self.variants), tuple(self.commits), - tuple(self.patches), self.notes - )) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<Pedigree id={id(self)}, hash={hash(self)}>' @@ -872,13 +892,23 @@ def url(self) -> Optional[XsUri]: def url(self, url: Optional[XsUri]) -> None: self._url = url + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.tag_id, + self.name, self.version, + self.tag_version, + self.patch, + self.url, + self.text, + )) + def __eq__(self, other: object) -> bool: if isinstance(other, Swid): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __hash__(self) -> int: - return hash((self.tag_id, self.name, self.version, self.tag_version, self.patch, self.text, self.url)) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<Swid tagId={self.tag_id}, name={self.name}, version={self.version}>' @@ -925,7 +955,7 @@ def deserialize(cls, o: Any) -> 'OmniborId': def __eq__(self, other: Any) -> bool: if isinstance(other, OmniborId): - return hash(other) == hash(self) + return self._id == other._id return False def __lt__(self, other: Any) -> bool: @@ -984,7 +1014,7 @@ def deserialize(cls, o: Any) -> 'Swhid': def __eq__(self, other: Any) -> bool: if isinstance(other, Swhid): - return hash(other) == hash(self) + return self._id == other._id return False def __lt__(self, other: Any) -> bool: @@ -1720,51 +1750,35 @@ def get_pypi_url(self) -> str: else: return f'https://pypi.org/project/{self.name}' + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.type, self.group, self.name, self.version, + self.bom_ref.value, + None if self.purl is None else _ComparablePackageURL(self.purl), + self.swid, self.cpe, _ComparableTuple(self.swhids), + self.supplier, self.author, self.publisher, + self.description, + self.mime_type, self.scope, _ComparableTuple(self.hashes), + _ComparableTuple(self.licenses), self.copyright, + self.pedigree, + _ComparableTuple(self.external_references), _ComparableTuple(self.properties), + _ComparableTuple(self.components), self.evidence, self.release_notes, self.modified, + _ComparableTuple(self.authors), _ComparableTuple(self.omnibor_ids), self.manufacturer, + self.crypto_properties, _ComparableTuple(self.tags), + )) + def __eq__(self, other: object) -> bool: if isinstance(other, Component): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, Component): - return _ComparableTuple(( - self.type, self.group, self.name, self.version, - self.mime_type, self.supplier, self.author, self.publisher, - self.description, self.scope, _ComparableTuple(self.hashes), - _ComparableTuple(self.licenses), self.copyright, self.cpe, - None if self.purl is None else _ComparablePackageURL(self.purl), - self.swid, self.pedigree, - _ComparableTuple(self.external_references), _ComparableTuple(self.properties), - _ComparableTuple(self.components), self.evidence, self.release_notes, self.modified, - _ComparableTuple(self.authors), _ComparableTuple(self.omnibor_ids), self.manufacturer, - _ComparableTuple(self.swhids), self.crypto_properties, _ComparableTuple(self.tags) - )) < _ComparableTuple(( - other.type, other.group, other.name, other.version, - other.mime_type, other.supplier, other.author, other.publisher, - other.description, other.scope, _ComparableTuple(other.hashes), - _ComparableTuple(other.licenses), other.copyright, other.cpe, - None if other.purl is None else _ComparablePackageURL(other.purl), - other.swid, other.pedigree, - _ComparableTuple(other.external_references), _ComparableTuple(other.properties), - _ComparableTuple(other.components), other.evidence, other.release_notes, other.modified, - _ComparableTuple(other.authors), _ComparableTuple(other.omnibor_ids), other.manufacturer, - _ComparableTuple(other.swhids), other.crypto_properties, _ComparableTuple(other.tags) - )) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __hash__(self) -> int: - return hash(( - self.type, self.group, self.name, self.version, - self.mime_type, self.supplier, self.author, self.publisher, - self.description, self.scope, tuple(self.hashes), - tuple(self.licenses), self.copyright, self.cpe, - self.purl, - self.swid, self.pedigree, - tuple(self.external_references), tuple(self.properties), - tuple(self.components), self.evidence, self.release_notes, self.modified, - tuple(self.authors), tuple(self.omnibor_ids), self.manufacturer, - tuple(self.swhids), self.crypto_properties, tuple(self.tags) - )) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<Component bom-ref={self.bom_ref!r}, group={self.group}, name={self.name}, ' \ diff --git a/cyclonedx/model/contact.py b/cyclonedx/model/contact.py index 299e8c06..cea865e7 100644 --- a/cyclonedx/model/contact.py +++ b/cyclonedx/model/contact.py @@ -18,7 +18,7 @@ from typing import Any, Iterable, Optional, Union -import serializable +import py_serializable as serializable from sortedcontainers import SortedSet from .._internal.bom_ref import bom_ref_from_str as _bom_ref_from_str @@ -161,25 +161,26 @@ def street_address(self) -> Optional[str]: def street_address(self, street_address: Optional[str]) -> None: self._street_address = street_address + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.country, self.region, self.locality, self.postal_code, + self.post_office_box_number, + self.street_address, + None if self.bom_ref is None else self.bom_ref.value, + )) + def __eq__(self, other: object) -> bool: if isinstance(other, PostalAddress): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, PostalAddress): - return _ComparableTuple(( - self.bom_ref, self.country, self.region, self.locality, self.post_office_box_number, self.postal_code, - self.street_address - )) < _ComparableTuple(( - other.bom_ref, other.country, other.region, other.locality, other.post_office_box_number, - other.postal_code, other.street_address - )) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __hash__(self) -> int: - return hash((self.bom_ref, self.country, self.region, self.locality, self.post_office_box_number, - self.postal_code, self.street_address)) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<PostalAddress bom-ref={self.bom_ref}, street_address={self.street_address}, country={self.country}>' @@ -253,22 +254,23 @@ def phone(self) -> Optional[str]: def phone(self, phone: Optional[str]) -> None: self._phone = phone + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.name, self.email, self.phone + )) + def __eq__(self, other: object) -> bool: if isinstance(other, OrganizationalContact): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, OrganizationalContact): - return _ComparableTuple(( - self.name, self.email, self.phone - )) < _ComparableTuple(( - other.name, other.email, other.phone - )) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __hash__(self) -> int: - return hash((self.name, self.phone, self.email)) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<OrganizationalContact name={self.name}, email={self.email}, phone={self.phone}>' @@ -362,18 +364,23 @@ def contacts(self) -> 'SortedSet[OrganizationalContact]': def contacts(self, contacts: Iterable[OrganizationalContact]) -> None: self._contacts = SortedSet(contacts) + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.name, _ComparableTuple(self.urls), _ComparableTuple(self.contacts) + )) + def __eq__(self, other: object) -> bool: if isinstance(other, OrganizationalEntity): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, OrganizationalEntity): - return hash(self) < hash(other) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __hash__(self) -> int: - return hash((self.name, tuple(self.urls), tuple(self.contacts))) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<OrganizationalEntity name={self.name}>' diff --git a/cyclonedx/model/crypto.py b/cyclonedx/model/crypto.py index b1875f53..765e840b 100644 --- a/cyclonedx/model/crypto.py +++ b/cyclonedx/model/crypto.py @@ -29,7 +29,7 @@ from enum import Enum from typing import Any, Iterable, Optional -import serializable +import py_serializable as serializable from sortedcontainers import SortedSet from .._internal.compare import ComparableTuple as _ComparableTuple @@ -494,15 +494,20 @@ def nist_quantum_security_level(self, nist_quantum_security_level: Optional[int] ) self._nist_quantum_security_level = nist_quantum_security_level + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.primitive, self._parameter_set_identifier, self.curve, self.execution_environment, + self.implementation_platform, _ComparableTuple(self.certification_levels), self.mode, self.padding, + _ComparableTuple(self.crypto_functions), self.classical_security_level, self.nist_quantum_security_level, + )) + def __eq__(self, other: object) -> bool: if isinstance(other, AlgorithmProperties): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __hash__(self) -> int: - return hash((self.primitive, self._parameter_set_identifier, self.curve, self.execution_environment, - self.implementation_platform, tuple(self.certification_levels), self.mode, self.padding, - tuple(self.crypto_functions), self.classical_security_level, self.nist_quantum_security_level,)) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<AlgorithmProperties primitive={self.primitive}, execution_environment={self.execution_environment}>' @@ -666,14 +671,19 @@ def certificate_extension(self) -> Optional[str]: def certificate_extension(self, certificate_extension: Optional[str]) -> None: self._certificate_extension = certificate_extension + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.subject_name, self.issuer_name, self.not_valid_before, self.not_valid_after, + self.certificate_format, self.certificate_extension + )) + def __eq__(self, other: object) -> bool: if isinstance(other, CertificateProperties): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __hash__(self) -> int: - return hash((self.subject_name, self.issuer_name, self.not_valid_before, self.not_valid_after, - self.certificate_format, self.certificate_extension)) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<CertificateProperties subject_name={self.subject_name}, certificate_format={self.certificate_format}>' @@ -789,13 +799,18 @@ def algorithm_ref(self) -> Optional[BomRef]: def algorithm_ref(self, algorithm_ref: Optional[BomRef]) -> None: self._algorithm_ref = algorithm_ref + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.mechanism, self.algorithm_ref + )) + def __eq__(self, other: object) -> bool: if isinstance(other, RelatedCryptoMaterialSecuredBy): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __hash__(self) -> int: - return hash((self.mechanism, self.algorithm_ref)) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<RelatedCryptoMaterialSecuredBy mechanism={self.mechanism}, algorithm_ref={self.algorithm_ref}>' @@ -1028,14 +1043,19 @@ def secured_by(self) -> Optional[RelatedCryptoMaterialSecuredBy]: def secured_by(self, secured_by: Optional[RelatedCryptoMaterialSecuredBy]) -> None: self._secured_by = secured_by + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.type, self.id, self.state, self.algorithm_ref, self.creation_date, self.activation_date, + self.update_date, self.expiration_date, self.value, self.size, self.format, self.secured_by + )) + def __eq__(self, other: object) -> bool: if isinstance(other, RelatedCryptoMaterialProperties): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __hash__(self) -> int: - return hash((self.type, self.id, self.state, self.algorithm_ref, self.creation_date, self.activation_date, - self.update_date, self.expiration_date, self.value, self.size, self.format, self.secured_by)) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<RelatedCryptoMaterialProperties type={self.type}, id={self.id} state={self.state}>' @@ -1136,22 +1156,23 @@ def identifiers(self) -> 'SortedSet[str]': def identifiers(self, identifiers: Iterable[str]) -> None: self._identifiers = SortedSet(identifiers) + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.name, _ComparableTuple(self.algorithms), _ComparableTuple(self.identifiers) + )) + def __eq__(self, other: object) -> bool: if isinstance(other, ProtocolPropertiesCipherSuite): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, ProtocolPropertiesCipherSuite): - return _ComparableTuple(( - self.name, _ComparableTuple(self.algorithms), _ComparableTuple(self.identifiers) - )) < _ComparableTuple(( - other.name, _ComparableTuple(other.algorithms), _ComparableTuple(other.identifiers) - )) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __hash__(self) -> int: - return hash((self.name, tuple(self.algorithms), tuple(self.identifiers))) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<ProtocolPropertiesCipherSuite name={self.name}>' @@ -1277,13 +1298,23 @@ def auth(self) -> 'SortedSet[BomRef]': def auth(self, auth: Iterable[BomRef]) -> None: self._auth = SortedSet(auth) + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + _ComparableTuple(self.encr), + _ComparableTuple(self.prf), + _ComparableTuple(self.integ), + _ComparableTuple(self.ke), + self.esn, + _ComparableTuple(self.auth) + )) + def __eq__(self, other: object) -> bool: if isinstance(other, Ikev2TransformTypes): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __hash__(self) -> int: - return hash((tuple(self.encr), tuple(self.prf), tuple(self.integ), tuple(self.ke), self.esn, tuple(self.auth))) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<Ikev2TransformTypes esn={self.esn}>' @@ -1394,21 +1425,22 @@ def crypto_refs(self) -> 'SortedSet[BomRef]': def crypto_refs(self, crypto_refs: Iterable[BomRef]) -> None: self._crypto_refs = SortedSet(crypto_refs) + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.type, + self.version, + _ComparableTuple(self.cipher_suites), + self.ikev2_transform_types, + _ComparableTuple(self.crypto_refs) + )) + def __eq__(self, other: object) -> bool: if isinstance(other, ProtocolProperties): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __hash__(self) -> int: - return hash( - ( - self.type, - self.version, - tuple(self.cipher_suites), - self.ikev2_transform_types, - tuple(self.crypto_refs) - ) - ) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<ProtocolProperties type={self.type}, version={self.version}>' @@ -1539,33 +1571,28 @@ def oid(self) -> Optional[str]: def oid(self, oid: Optional[str]) -> None: self._oid = oid + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.asset_type, + self.algorithm_properties, + self.certificate_properties, + self.related_crypto_material_properties, + self.protocol_properties, + self.oid, + )) + def __eq__(self, other: object) -> bool: if isinstance(other, CryptoProperties): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, CryptoProperties): - return _ComparableTuple(( - self.asset_type, - self.algorithm_properties, - self.certificate_properties, - self.related_crypto_material_properties, - self.protocol_properties, - self.oid, - )) < _ComparableTuple(( - other.asset_type, - other.algorithm_properties, - other.certificate_properties, - other.related_crypto_material_properties, - other.protocol_properties, - other.oid, - )) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __hash__(self) -> int: - return hash((self.asset_type, self.algorithm_properties, self.certificate_properties, - self.related_crypto_material_properties, self.protocol_properties, self.oid)) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<CryptoProperties asset_type={self.asset_type}, oid={self.oid}>' diff --git a/cyclonedx/model/definition.py b/cyclonedx/model/definition.py index 90872e32..675e5476 100644 --- a/cyclonedx/model/definition.py +++ b/cyclonedx/model/definition.py @@ -18,7 +18,7 @@ import re from typing import TYPE_CHECKING, Any, Iterable, Optional, Union -import serializable +import py_serializable as serializable from sortedcontainers import SortedSet from .._internal.bom_ref import bom_ref_from_str as _bom_ref_from_str @@ -256,7 +256,7 @@ def external_references(self, external_references: Iterable[ExternalReference]) def __comparable_tuple(self) -> _ComparableTuple: # all properties are optional - so need to compare all, in hope that one is unique return _ComparableTuple(( - self.bom_ref, self.identifier, + self.identifier, self.bom_ref.value, self.title, self.text, _ComparableTuple(self.descriptions), _ComparableTuple(self.open_cre), self.parent, _ComparableTuple(self.properties), @@ -373,7 +373,9 @@ def requirements(self, requirements: Iterable[Union[str, BomRef]]) -> None: def __comparable_tuple(self) -> _ComparableTuple: # all properties are optional - so need to compare all, in hope that one is unique return _ComparableTuple(( - self.bom_ref, self.identifier, self.title, self.description, _ComparableTuple(self.requirements) + self.identifier, self.bom_ref.value, + self.title, self.description, + _ComparableTuple(self.requirements) )) def __lt__(self, other: Any) -> bool: @@ -545,8 +547,9 @@ def external_references(self, external_references: Iterable[ExternalReference]) def __comparable_tuple(self) -> _ComparableTuple: # all properties are optional - so need to apply all, in hope that one is unique return _ComparableTuple(( - self.bom_ref, - self.name, self.version, self.description, self.owner, + self.name, self.version, + self.bom_ref.value, + self.description, self.owner, _ComparableTuple(self.requirements), _ComparableTuple(self.levels), _ComparableTuple(self.external_references) )) @@ -608,13 +611,13 @@ def __eq__(self, other: object) -> bool: return self.__comparable_tuple() == other.__comparable_tuple() return False - def __hash__(self) -> int: - return hash(self.__comparable_tuple()) - def __lt__(self, other: Any) -> bool: if isinstance(other, Definitions): return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented + def __hash__(self) -> int: + return hash(self.__comparable_tuple()) + def __repr__(self) -> str: return f'<Definitions standards={self.standards!r} >' diff --git a/cyclonedx/model/dependency.py b/cyclonedx/model/dependency.py index 0b054c37..8241fdfc 100644 --- a/cyclonedx/model/dependency.py +++ b/cyclonedx/model/dependency.py @@ -19,7 +19,7 @@ from abc import ABC, abstractmethod from typing import Any, Iterable, List, Optional, Set -import serializable +import py_serializable as serializable from sortedcontainers import SortedSet from .._internal.compare import ComparableTuple as _ComparableTuple @@ -83,22 +83,23 @@ def dependencies(self, dependencies: Iterable['Dependency']) -> None: def dependencies_as_bom_refs(self) -> Set[BomRef]: return set(map(lambda d: d.ref, self.dependencies)) + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.ref, _ComparableTuple(self.dependencies) + )) + def __eq__(self, other: object) -> bool: if isinstance(other, Dependency): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, Dependency): - return _ComparableTuple(( - self.ref, _ComparableTuple(self.dependencies) - )) < _ComparableTuple(( - other.ref, _ComparableTuple(other.dependencies) - )) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __hash__(self) -> int: - return hash((self.ref, tuple(self.dependencies))) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<Dependency ref={self.ref!r}, targets={len(self.dependencies)}>' diff --git a/cyclonedx/model/impact_analysis.py b/cyclonedx/model/impact_analysis.py index 6722efaf..a289daf2 100644 --- a/cyclonedx/model/impact_analysis.py +++ b/cyclonedx/model/impact_analysis.py @@ -28,7 +28,7 @@ from enum import Enum -import serializable +import py_serializable as serializable @serializable.serializable_enum diff --git a/cyclonedx/model/issue.py b/cyclonedx/model/issue.py index b0886d63..4b1f1aa2 100644 --- a/cyclonedx/model/issue.py +++ b/cyclonedx/model/issue.py @@ -18,7 +18,7 @@ from enum import Enum from typing import Any, Iterable, Optional -import serializable +import py_serializable as serializable from sortedcontainers import SortedSet from .._internal.compare import ComparableTuple as _ComparableTuple @@ -85,22 +85,23 @@ def url(self) -> Optional[XsUri]: def url(self, url: Optional[XsUri]) -> None: self._url = url + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.name, self.url + )) + def __eq__(self, other: object) -> bool: if isinstance(other, IssueTypeSource): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, IssueTypeSource): - return _ComparableTuple(( - self.name, self.url - )) < _ComparableTuple(( - other.name, other.url - )) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __hash__(self) -> int: - return hash((self.name, self.url)) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<IssueTypeSource name={self._name}, url={self.url}>' @@ -226,24 +227,24 @@ def references(self) -> 'SortedSet[XsUri]': def references(self, references: Iterable[XsUri]) -> None: self._references = SortedSet(references) + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.type, self.id, self.name, self.description, self.source, + _ComparableTuple(self.references) + )) + def __eq__(self, other: object) -> bool: if isinstance(other, IssueType): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, IssueType): - return _ComparableTuple(( - self.type, self.id, self.name, self.description, self.source - )) < _ComparableTuple(( - other.type, other.id, other.name, other.description, other.source - )) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __hash__(self) -> int: - return hash(( - self.type, self.id, self.name, self.description, self.source, tuple(self.references) - )) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<IssueType type={self.type}, id={self.id}, name={self.name}>' diff --git a/cyclonedx/model/license.py b/cyclonedx/model/license.py index 7c6a40e0..b4348993 100644 --- a/cyclonedx/model/license.py +++ b/cyclonedx/model/license.py @@ -26,7 +26,7 @@ from warnings import warn from xml.etree.ElementTree import Element # nosec B405 -import serializable +import py_serializable as serializable from sortedcontainers import SortedSet from .._internal.compare import ComparableTuple as _ComparableTuple @@ -218,24 +218,28 @@ def acknowledgement(self) -> Optional[LicenseAcknowledgement]: def acknowledgement(self, acknowledgement: Optional[LicenseAcknowledgement]) -> None: self._acknowledgement = acknowledgement + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self._acknowledgement, + self._id, self._name, + self._url, + self._text, + )) + def __eq__(self, other: object) -> bool: if isinstance(other, DisjunctiveLicense): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, DisjunctiveLicense): - return _ComparableTuple(( - self._id, self._name - )) < _ComparableTuple(( - other._id, other._name - )) + return self.__comparable_tuple() < other.__comparable_tuple() if isinstance(other, LicenseExpression): return False # self after any LicenseExpression return NotImplemented def __hash__(self) -> int: - return hash((self._id, self._name, self._text, self._url, self._acknowledgement)) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<License id={self._id!r}, name={self._name!r}>' @@ -311,17 +315,23 @@ def acknowledgement(self) -> Optional[LicenseAcknowledgement]: def acknowledgement(self, acknowledgement: Optional[LicenseAcknowledgement]) -> None: self._acknowledgement = acknowledgement + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self._acknowledgement, + self._value, + )) + def __hash__(self) -> int: - return hash((self._value, self._acknowledgement)) + return hash(self.__comparable_tuple()) def __eq__(self, other: object) -> bool: if isinstance(other, LicenseExpression): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, LicenseExpression): - return self._value < other._value + return self.__comparable_tuple() < other.__comparable_tuple() if isinstance(other, DisjunctiveLicense): return True # self before any DisjunctiveLicense return NotImplemented diff --git a/cyclonedx/model/lifecycle.py b/cyclonedx/model/lifecycle.py index 7975e339..db688bb8 100644 --- a/cyclonedx/model/lifecycle.py +++ b/cyclonedx/model/lifecycle.py @@ -30,15 +30,15 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union from xml.etree.ElementTree import Element # nosec B405 -import serializable -from serializable.helpers import BaseHelper +import py_serializable as serializable +from py_serializable.helpers import BaseHelper from sortedcontainers import SortedSet from .._internal.compare import ComparableTuple as _ComparableTuple from ..exception.serialization import CycloneDxDeserializationException if TYPE_CHECKING: # pragma: no cover - from serializable import ViewType + from py_serializable import ViewType @serializable.serializable_enum @@ -83,7 +83,7 @@ def __hash__(self) -> int: def __eq__(self, other: object) -> bool: if isinstance(other, PredefinedLifecycle): - return hash(other) == hash(self) + return self._phase == other._phase return False def __lt__(self, other: Any) -> bool: @@ -142,19 +142,22 @@ def description(self) -> Optional[str]: def description(self, description: Optional[str]) -> None: self._description = description + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self._name, self._description + )) + def __hash__(self) -> int: - return hash((self._name, self._description)) + return hash(self.__comparable_tuple()) def __eq__(self, other: object) -> bool: if isinstance(other, NamedLifecycle): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, NamedLifecycle): - return _ComparableTuple((self._name, self._description)) < _ComparableTuple( - (other._name, other._description) - ) + return self.__comparable_tuple() < other.__comparable_tuple() if isinstance(other, PredefinedLifecycle): return False # put NamedLifecycle after any PredefinedLifecycle return NotImplemented diff --git a/cyclonedx/model/release_note.py b/cyclonedx/model/release_note.py index c6591735..4509bb2b 100644 --- a/cyclonedx/model/release_note.py +++ b/cyclonedx/model/release_note.py @@ -18,9 +18,10 @@ from datetime import datetime from typing import Iterable, Optional -import serializable +import py_serializable as serializable from sortedcontainers import SortedSet +from .._internal.compare import ComparableTuple as _ComparableTuple from ..model import Note, Property, XsUri from ..model.issue import IssueType @@ -233,16 +234,23 @@ def properties(self) -> 'SortedSet[Property]': def properties(self, properties: Iterable[Property]) -> None: self._properties = SortedSet(properties) + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.type, self.title, self.featured_image, self.social_image, self.description, self.timestamp, + _ComparableTuple(self.aliases), + _ComparableTuple(self.tags), + _ComparableTuple(self.resolves), + _ComparableTuple(self.notes), + _ComparableTuple(self.properties) + )) + def __eq__(self, other: object) -> bool: if isinstance(other, ReleaseNotes): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __hash__(self) -> int: - return hash(( - self.type, self.title, self.featured_image, self.social_image, self.description, self.timestamp, - tuple(self.aliases), tuple(self.tags), tuple(self.resolves), tuple(self.notes), tuple(self.properties) - )) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<ReleaseNotes type={self.type}, title={self.title}>' diff --git a/cyclonedx/model/service.py b/cyclonedx/model/service.py index f96ef1da..91541f6b 100644 --- a/cyclonedx/model/service.py +++ b/cyclonedx/model/service.py @@ -26,7 +26,7 @@ from typing import Any, Iterable, Optional, Union -import serializable +import py_serializable as serializable from sortedcontainers import SortedSet from .._internal.bom_ref import bom_ref_from_str as _bom_ref_from_str @@ -352,26 +352,29 @@ def release_notes(self) -> Optional[ReleaseNotes]: def release_notes(self, release_notes: Optional[ReleaseNotes]) -> None: self._release_notes = release_notes + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.group, self.name, self.version, + self.bom_ref.value, + self.provider, self.description, + self.authenticated, _ComparableTuple(self.data), _ComparableTuple(self.endpoints), + _ComparableTuple(self.external_references), _ComparableTuple(self.licenses), + _ComparableTuple(self.properties), self.release_notes, _ComparableTuple(self.services), + self.x_trust_boundary + )) + def __eq__(self, other: object) -> bool: if isinstance(other, Service): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, Service): - return _ComparableTuple(( - self.group, self.name, self.version - )) < _ComparableTuple(( - other.group, other.name, other.version - )) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __hash__(self) -> int: - return hash(( - self.authenticated, tuple(self.data), self.description, tuple(self.endpoints), - tuple(self.external_references), self.group, tuple(self.licenses), self.name, tuple(self.properties), - self.provider, self.release_notes, tuple(self.services), self.version, self.x_trust_boundary - )) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<Service bom-ref={self.bom_ref}, group={self.group}, name={self.name}, version={self.version}>' diff --git a/cyclonedx/model/tool.py b/cyclonedx/model/tool.py index ff923bea..38b1e065 100644 --- a/cyclonedx/model/tool.py +++ b/cyclonedx/model/tool.py @@ -21,8 +21,8 @@ from warnings import warn from xml.etree.ElementTree import Element # nosec B405 -import serializable -from serializable.helpers import BaseHelper +import py_serializable as serializable +from py_serializable.helpers import BaseHelper from sortedcontainers import SortedSet from .._internal.compare import ComparableTuple as _ComparableTuple @@ -33,7 +33,7 @@ from .service import Service if TYPE_CHECKING: # pragma: no cover - from serializable import ObjectMetadataLibrary, ViewType + from py_serializable import ObjectMetadataLibrary, ViewType @serializable.serializable_class @@ -148,22 +148,24 @@ def external_references(self) -> 'SortedSet[ExternalReference]': def external_references(self, external_references: Iterable[ExternalReference]) -> None: self._external_references = SortedSet(external_references) + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.vendor, self.name, self.version, + _ComparableTuple(self.hashes), _ComparableTuple(self.external_references) + )) + def __eq__(self, other: object) -> bool: if isinstance(other, Tool): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, Tool): - return _ComparableTuple(( - self.vendor, self.name, self.version - )) < _ComparableTuple(( - other.vendor, other.name, other.version - )) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __hash__(self) -> int: - return hash((self.vendor, self.name, self.version, tuple(self.hashes), tuple(self.external_references))) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<Tool name={self.name}, version={self.version}, vendor={self.vendor}>' @@ -250,16 +252,20 @@ def __bool__(self) -> bool: or len(self._components) > 0 \ or len(self._services) > 0 - def __eq__(self, other: object) -> bool: - if not isinstance(other, ToolRepository): - return False + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + _ComparableTuple(self._tools), + _ComparableTuple(self._components), + _ComparableTuple(self._services) + )) - return self._tools == other._tools \ - and self._components == other._components \ - and self._services == other._services + def __eq__(self, other: object) -> bool: + if isinstance(other, ToolRepository): + return self.__comparable_tuple() == other.__comparable_tuple() + return False def __hash__(self) -> int: - return hash((tuple(self._tools), tuple(self._components), tuple(self._services))) + return hash(self.__comparable_tuple()) class _ToolRepositoryHelper(BaseHelper): diff --git a/cyclonedx/model/vulnerability.py b/cyclonedx/model/vulnerability.py index 2e110073..9ad3d52f 100644 --- a/cyclonedx/model/vulnerability.py +++ b/cyclonedx/model/vulnerability.py @@ -35,7 +35,7 @@ from enum import Enum from typing import Any, Dict, FrozenSet, Iterable, Optional, Tuple, Type, Union -import serializable +import py_serializable as serializable from sortedcontainers import SortedSet from .._internal.bom_ref import bom_ref_from_str as _bom_ref_from_str @@ -125,22 +125,23 @@ def status(self) -> Optional[ImpactAnalysisAffectedStatus]: def status(self, status: Optional[ImpactAnalysisAffectedStatus]) -> None: self._status = status + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.version, self.range, self.status + )) + def __eq__(self, other: object) -> bool: if isinstance(other, BomTargetVersionRange): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, BomTargetVersionRange): - return _ComparableTuple(( - self.version, self.range, self.status - )) < _ComparableTuple(( - other.version, other.range, other.status - )) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __hash__(self) -> int: - return hash((self.version, self.range, self.status)) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<BomTargetVersionRange version={self.version}, version_range={self.range}, status={self.status}>' @@ -196,18 +197,24 @@ def versions(self) -> 'SortedSet[BomTargetVersionRange]': def versions(self, versions: Iterable[BomTargetVersionRange]) -> None: self._versions = SortedSet(versions) + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.ref, + _ComparableTuple(self.versions) + )) + def __eq__(self, other: object) -> bool: if isinstance(other, BomTarget): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, BomTarget): - return self.ref < other.ref + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __hash__(self) -> int: - return hash((self.ref, tuple(self.versions))) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<BomTarget ref={self.ref}>' @@ -320,13 +327,18 @@ def detail(self, detail: Optional[str]) -> None: # def last_updated(self, ...) -> None: # ... # TODO since CDX 1.5 + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.state, self.justification, tuple(self.responses), self.detail + )) + def __eq__(self, other: object) -> bool: if isinstance(other, VulnerabilityAnalysis): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __hash__(self) -> int: - return hash((self.state, self.justification, tuple(self.responses), self.detail)) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<VulnerabilityAnalysis state={self.state}, justification={self.justification}>' @@ -374,22 +386,23 @@ def url(self) -> XsUri: def url(self, url: XsUri) -> None: self._url = url + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.title, self.url + )) + def __eq__(self, other: object) -> bool: if isinstance(other, VulnerabilityAdvisory): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, VulnerabilityAdvisory): - return _ComparableTuple(( - self.title, self.url - )) < _ComparableTuple(( - other.title, other.url - )) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __hash__(self) -> int: - return hash((self.title, self.url)) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<VulnerabilityAdvisory url={self.url}, title={self.title}>' @@ -439,22 +452,23 @@ def url(self) -> Optional[XsUri]: def url(self, url: Optional[XsUri]) -> None: self._url = url + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.name, self.url + )) + def __eq__(self, other: object) -> bool: if isinstance(other, VulnerabilitySource): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, VulnerabilitySource): - return _ComparableTuple(( - self.name, self.url - )) < _ComparableTuple(( - other.name, other.url - )) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __hash__(self) -> int: - return hash((self.name, self.url)) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<VulnerabilityAdvisory name={self.name}, url={self.url}>' @@ -472,61 +486,74 @@ class VulnerabilityReference: .. note:: See the CycloneDX schema: https://cyclonedx.org/docs/1.6/xml/#type_vulnerabilityType + + .. note:: + Properties ``id`` and ``source`` are mandatory. + + History: + * In v1.4 JSON scheme, both properties were mandatory + https://github.com/CycloneDX/specification/blob/d570ffb8956d796585b9574e57598c42ee9de770/schema/bom-1.4.schema.json#L1455-L1474 + * In v1.4 XML schema, both properties were optional + https://github.com/CycloneDX/specification/blob/d570ffb8956d796585b9574e57598c42ee9de770/schema/bom-1.4.xsd#L1788-L1797 + * In v1.5 XML schema, both were mandatory + https://github.com/CycloneDX/specification/blob/d570ffb8956d796585b9574e57598c42ee9de770/schema/bom-1.5.xsd#L3364-L3374 + + Decision: + Since CycloneDXCoreWorkingGroup chose JSON schema as the dominant schema, the one that serves as first spec + implementation, and since XML schema was "fixed" to work same as JSON schema, we'd consider it canon/spec that + both properties were always mandatory. """ def __init__( self, *, - id: Optional[str] = None, - source: Optional[VulnerabilitySource] = None, + id: str, + source: VulnerabilitySource, ) -> None: - if not id and not source: - raise NoPropertiesProvidedException( - 'Either id or source must be provided for a VulnerabilityReference - neither provided' - ) self.id = id self.source = source @property @serializable.xml_sequence(1) @serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING) - def id(self) -> Optional[str]: + def id(self) -> str: """ The identifier that uniquely identifies the vulnerability in the associated Source. For example: CVE-2021-39182. """ return self._id @id.setter - def id(self, id: Optional[str]) -> None: + def id(self, id: str) -> None: self._id = id @property @serializable.xml_sequence(2) - def source(self) -> Optional[VulnerabilitySource]: + def source(self) -> VulnerabilitySource: """ The source that published the vulnerability. """ return self._source @source.setter - def source(self, source: Optional[VulnerabilitySource]) -> None: + def source(self, source: VulnerabilitySource) -> None: self._source = source + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.id, self.source + )) + def __eq__(self, other: object) -> bool: if isinstance(other, VulnerabilityReference): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, VulnerabilityReference): - return _ComparableTuple(( - self.id, self.source - )) < _ComparableTuple(( - other.id, other.source - )) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __hash__(self) -> int: - return hash((self.id, self.source)) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<VulnerabilityReference id={self.id}, source={self.source}>' @@ -817,25 +844,30 @@ def justification(self) -> Optional[str]: def justification(self, justification: Optional[str]) -> None: self._justification = justification + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.severity, self.score or 0, + self.source, self.method, self.vector, + self.justification + )) + def __eq__(self, other: object) -> bool: if isinstance(other, VulnerabilityRating): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, VulnerabilityRating): - return _ComparableTuple(( - self.severity, self.score, self.score or 0, self.method, self.vector, self.justification - )) < _ComparableTuple(( - other.severity, other.score, other.score or 0, other.method, other.vector, other.justification - )) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __hash__(self) -> int: - return hash((self.source, self.score or 0, self.severity, self.method, self.vector, self.justification)) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: - return f'<VulnerabilityRating score={self.score}, source={self.source}>' + return f'<VulnerabilityRating severity={self.severity} score={self.score}, ' \ + f'source={self.source} method={self.method} vector={self.vector}' \ + f'justification={self.justification}>' @serializable.serializable_class @@ -890,18 +922,24 @@ def individuals(self) -> 'SortedSet[OrganizationalContact]': def individuals(self, individuals: Iterable[OrganizationalContact]) -> None: self._individuals = SortedSet(individuals) + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + _ComparableTuple(self.organizations), + _ComparableTuple(self.individuals) + )) + def __eq__(self, other: object) -> bool: if isinstance(other, VulnerabilityCredits): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, VulnerabilityCredits): - return hash(self) < hash(other) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __hash__(self) -> int: - return hash((tuple(self.organizations), tuple(self.individuals))) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<VulnerabilityCredits id={id(self)}>' @@ -1289,26 +1327,30 @@ def properties(self) -> 'SortedSet[Property]': def properties(self, properties: Iterable[Property]) -> None: self._properties = SortedSet(properties) + def __comparable_tuple(self) -> _ComparableTuple: + return _ComparableTuple(( + self.id, self.bom_ref.value, + self.source, _ComparableTuple(self.references), + _ComparableTuple(self.ratings), _ComparableTuple(self.cwes), self.description, + self.detail, self.recommendation, self.workaround, _ComparableTuple(self.advisories), + self.created, self.published, self.updated, + self.credits, self.tools, self.analysis, + _ComparableTuple(self.affects), + _ComparableTuple(self.properties) + )) + def __eq__(self, other: object) -> bool: if isinstance(other, Vulnerability): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __lt__(self, other: Any) -> bool: if isinstance(other, Vulnerability): - return _ComparableTuple(( - self.id, self.description, self.detail, self.source, self.created, self.published - )) < _ComparableTuple(( - other.id, other.description, other.detail, other.source, other.created, other.published - )) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __hash__(self) -> int: - return hash(( - self.id, self.source, tuple(self.references), tuple(self.ratings), tuple(self.cwes), self.description, - self.detail, self.recommendation, self.workaround, tuple(self.advisories), self.created, self.published, - self.updated, self.credits, self.tools, self.analysis, tuple(self.affects), tuple(self.properties) - )) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f'<Vulnerability bom-ref={self.bom_ref.value}, id={self.id}>' diff --git a/cyclonedx/schema/schema.py b/cyclonedx/schema/schema.py index 408e8a6a..a51686e9 100644 --- a/cyclonedx/schema/schema.py +++ b/cyclonedx/schema/schema.py @@ -18,7 +18,7 @@ from abc import ABC, abstractmethod from typing import Dict, Literal, Type -from serializable import ViewType +from py_serializable import ViewType from . import SchemaVersion diff --git a/cyclonedx/serialization/__init__.py b/cyclonedx/serialization/__init__.py index 34489b28..aeab0364 100644 --- a/cyclonedx/serialization/__init__.py +++ b/cyclonedx/serialization/__init__.py @@ -25,7 +25,7 @@ # See https://github.com/package-url/packageurl-python/issues/65 from packageurl import PackageURL -from serializable.helpers import BaseHelper +from py_serializable.helpers import BaseHelper from ..exception.serialization import CycloneDxDeserializationException, SerializationOfUnexpectedValueException from ..model.bom_ref import BomRef diff --git a/cyclonedx/spdx.py b/cyclonedx/spdx.py index 8f7e30b1..9781af54 100644 --- a/cyclonedx/spdx.py +++ b/cyclonedx/spdx.py @@ -18,7 +18,7 @@ __all__ = [ 'is_supported_id', 'fixup_id', - 'is_compound_expression' + 'is_expression' ] from json import load as json_load @@ -47,26 +47,26 @@ def is_supported_id(value: str) -> bool: - """Validate a SPDX-ID according to current spec.""" + """Validate SPDX-ID according to current spec.""" return value in __IDS def fixup_id(value: str) -> Optional[str]: - """Fixup a SPDX-ID. + """Fixup SPDX-ID. :returns: repaired value string, or `None` if fixup was unable to help. """ return __IDS_LOWER_MAP.get(value.lower()) -def is_compound_expression(value: str) -> bool: - """Validate compound expression. +def is_expression(value: str) -> bool: + """Validate SPDX license expression. .. note:: Utilizes `license-expression library`_ to validate SPDX compound expression according to `SPDX license expression spec`_. - .. _SPDX license expression spec: https://spdx.github.io/spdx-spec/v2.3/SPDX-license-expressions/ + .. _SPDX license expression spec: https://spdx.github.io/spdx-spec/v3.0.1/annexes/spdx-license-expressions/ .. _license-expression library: https://github.com/nexB/license-expression """ try: diff --git a/docs/conf.py b/docs/conf.py index 86094a5d..e8c331f1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -23,7 +23,7 @@ # The full version, including alpha/beta/rc tags # !! version is managed by semantic_release -release = '8.9.0' +release = '9.0.1-rc.1' # -- General configuration --------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index e8742131..2bcac94e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "cyclonedx-python-lib" # !! version is managed by semantic_release -version = "8.9.0" +version = "9.0.1-rc.1" description = "Python library for CycloneDX" authors = [ "Paul Horton <phorton@sonatype.com>", @@ -70,7 +70,7 @@ keywords = [ [tool.poetry.dependencies] python = "^3.8" packageurl-python = ">=0.11, <2" -py-serializable = "^1.1.1" +py-serializable = "^2.0.0" sortedcontainers = "^2.4.0" license-expression = "^30" jsonschema = { version = "^4.18", extras=['format'], optional=true } diff --git a/tests/_data/models.py b/tests/_data/models.py index 60a823c8..a312178a 100644 --- a/tests/_data/models.py +++ b/tests/_data/models.py @@ -1402,6 +1402,37 @@ def get_bom_with_definitions_and_detailed_standards() -> Bom: ])) +def get_bom_for_issue540_duplicate_components() -> Bom: + # tests https://github.com/CycloneDX/cyclonedx-python-lib/issues/540 + bom = _make_bom() + bom.metadata.component = root_component = Component( + name='myApp', + type=ComponentType.APPLICATION, + bom_ref='myApp' + ) + component1 = Component( + type=ComponentType.LIBRARY, + name='some-component', + bom_ref='some-component' + ) + bom.components.add(component1) + bom.register_dependency(root_component, [component1]) + component2 = Component( + type=ComponentType.LIBRARY, + name='some-library', + bom_ref='some-library1' + ) + bom.components.add(component2) + bom.register_dependency(component1, [component2]) + component3 = Component( + type=ComponentType.LIBRARY, + name='some-library', + bom_ref='some-library2' + ) + bom.components.add(component3) + bom.register_dependency(component1, [component3]) + return bom + # --- diff --git a/tests/_data/own/json/1.5/issue677.json b/tests/_data/own/json/1.5/issue677.json new file mode 100644 index 00000000..090d97f8 --- /dev/null +++ b/tests/_data/own/json/1.5/issue677.json @@ -0,0 +1,49 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:66fa5692-2e9d-45c5-830a-ec8ccaf7dcc9", + "version": 1, + "metadata": { + "component": { + "description": "see issue #677 - https://github.com/CycloneDX/cyclonedx-python-lib/issues/677", + "type": "application", + "name": "test" + } + }, + "components": [ + { + "type": "operating-system", + "bom-ref": "test12", + "name": "alpine" + }, + { + "type": "container", + "bom-ref": "test11", + "name": "alpine" + }, + { + "type": "operating-system", + "bom-ref": "test22", + "name": "alpine" + }, + { + "type": "container", + "bom-ref": "test21", + "name": "alpine" + } + ], + "dependencies": [ + { + "ref": "test11", + "dependsOn": [ + "test12" + ] + }, + { + "ref": "test21", + "dependsOn": [ + "test22" + ] + } + ] +} diff --git a/tests/_data/own/json/1.5/issue753.json b/tests/_data/own/json/1.5/issue753.json new file mode 100644 index 00000000..15123220 --- /dev/null +++ b/tests/_data/own/json/1.5/issue753.json @@ -0,0 +1,37 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "metadata": { + "component": { + "type": "application", + "name": "example", + "version": "1.2.3", + "bom-ref": "topref" + } + }, + "components": [ + { + "type": "library", + "name": "styled-engine", + "version": "5.16.6", + "bom-ref": "@mui/styled-engine@npm:5.16.6 [296f2]" + }, + { + "type": "library", + "name": "styled-engine", + "version": "5.16.6", + "bom-ref": "@mui/styled-engine@npm:5.16.6 [3135b]" + } + ], + "dependencies": [ + { + "ref": "topref", + "dependsOn": [ + "@mui/styled-engine@npm:5.16.6 [296f2]", + "@mui/styled-engine@npm:5.16.6 [3135b]" + ] + } + ] +} diff --git a/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.0.xml.bin b/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.0.xml.bin new file mode 100644 index 00000000..dd1947eb --- /dev/null +++ b/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.0.xml.bin @@ -0,0 +1,20 @@ +<?xml version="1.0" ?> +<bom xmlns="http://cyclonedx.org/schema/bom/1.0" version="1"> + <components> + <component type="library"> + <name>some-component</name> + <version/> + <modified>false</modified> + </component> + <component type="library"> + <name>some-library</name> + <version/> + <modified>false</modified> + </component> + <component type="library"> + <name>some-library</name> + <version/> + <modified>false</modified> + </component> + </components> +</bom> diff --git a/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.1.xml.bin b/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.1.xml.bin new file mode 100644 index 00000000..6d4d9bdb --- /dev/null +++ b/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.1.xml.bin @@ -0,0 +1,17 @@ +<?xml version="1.0" ?> +<bom xmlns="http://cyclonedx.org/schema/bom/1.1" serialNumber="urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac" version="1"> + <components> + <component type="library" bom-ref="some-component"> + <name>some-component</name> + <version/> + </component> + <component type="library" bom-ref="some-library1"> + <name>some-library</name> + <version/> + </component> + <component type="library" bom-ref="some-library2"> + <name>some-library</name> + <version/> + </component> + </components> +</bom> diff --git a/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.2.json.bin b/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.2.json.bin new file mode 100644 index 00000000..fd8f32b5 --- /dev/null +++ b/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.2.json.bin @@ -0,0 +1,57 @@ +{ + "components": [ + { + "bom-ref": "some-component", + "name": "some-component", + "type": "library", + "version": "" + }, + { + "bom-ref": "some-library1", + "name": "some-library", + "type": "library", + "version": "" + }, + { + "bom-ref": "some-library2", + "name": "some-library", + "type": "library", + "version": "" + } + ], + "dependencies": [ + { + "dependsOn": [ + "some-component" + ], + "ref": "myApp" + }, + { + "dependsOn": [ + "some-library1", + "some-library2" + ], + "ref": "some-component" + }, + { + "ref": "some-library1" + }, + { + "ref": "some-library2" + } + ], + "metadata": { + "component": { + "bom-ref": "myApp", + "name": "myApp", + "type": "application", + "version": "" + }, + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.2b.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.2" +} \ No newline at end of file diff --git a/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.2.xml.bin b/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.2.xml.bin new file mode 100644 index 00000000..625bc1dc --- /dev/null +++ b/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.2.xml.bin @@ -0,0 +1,35 @@ +<?xml version="1.0" ?> +<bom xmlns="http://cyclonedx.org/schema/bom/1.2" serialNumber="urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac" version="1"> + <metadata> + <timestamp>2023-01-07T13:44:32.312678+00:00</timestamp> + <component type="application" bom-ref="myApp"> + <name>myApp</name> + <version/> + </component> + </metadata> + <components> + <component type="library" bom-ref="some-component"> + <name>some-component</name> + <version/> + </component> + <component type="library" bom-ref="some-library1"> + <name>some-library</name> + <version/> + </component> + <component type="library" bom-ref="some-library2"> + <name>some-library</name> + <version/> + </component> + </components> + <dependencies> + <dependency ref="myApp"> + <dependency ref="some-component"/> + </dependency> + <dependency ref="some-component"> + <dependency ref="some-library1"/> + <dependency ref="some-library2"/> + </dependency> + <dependency ref="some-library1"/> + <dependency ref="some-library2"/> + </dependencies> +</bom> diff --git a/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.3.json.bin b/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.3.json.bin new file mode 100644 index 00000000..601c5974 --- /dev/null +++ b/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.3.json.bin @@ -0,0 +1,57 @@ +{ + "components": [ + { + "bom-ref": "some-component", + "name": "some-component", + "type": "library", + "version": "" + }, + { + "bom-ref": "some-library1", + "name": "some-library", + "type": "library", + "version": "" + }, + { + "bom-ref": "some-library2", + "name": "some-library", + "type": "library", + "version": "" + } + ], + "dependencies": [ + { + "dependsOn": [ + "some-component" + ], + "ref": "myApp" + }, + { + "dependsOn": [ + "some-library1", + "some-library2" + ], + "ref": "some-component" + }, + { + "ref": "some-library1" + }, + { + "ref": "some-library2" + } + ], + "metadata": { + "component": { + "bom-ref": "myApp", + "name": "myApp", + "type": "application", + "version": "" + }, + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.3a.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.3" +} \ No newline at end of file diff --git a/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.3.xml.bin b/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.3.xml.bin new file mode 100644 index 00000000..9ca84692 --- /dev/null +++ b/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.3.xml.bin @@ -0,0 +1,35 @@ +<?xml version="1.0" ?> +<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac" version="1"> + <metadata> + <timestamp>2023-01-07T13:44:32.312678+00:00</timestamp> + <component type="application" bom-ref="myApp"> + <name>myApp</name> + <version/> + </component> + </metadata> + <components> + <component type="library" bom-ref="some-component"> + <name>some-component</name> + <version/> + </component> + <component type="library" bom-ref="some-library1"> + <name>some-library</name> + <version/> + </component> + <component type="library" bom-ref="some-library2"> + <name>some-library</name> + <version/> + </component> + </components> + <dependencies> + <dependency ref="myApp"> + <dependency ref="some-component"/> + </dependency> + <dependency ref="some-component"> + <dependency ref="some-library1"/> + <dependency ref="some-library2"/> + </dependency> + <dependency ref="some-library1"/> + <dependency ref="some-library2"/> + </dependencies> +</bom> diff --git a/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.4.json.bin b/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.4.json.bin new file mode 100644 index 00000000..1da35538 --- /dev/null +++ b/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.4.json.bin @@ -0,0 +1,53 @@ +{ + "components": [ + { + "bom-ref": "some-component", + "name": "some-component", + "type": "library" + }, + { + "bom-ref": "some-library1", + "name": "some-library", + "type": "library" + }, + { + "bom-ref": "some-library2", + "name": "some-library", + "type": "library" + } + ], + "dependencies": [ + { + "dependsOn": [ + "some-component" + ], + "ref": "myApp" + }, + { + "dependsOn": [ + "some-library1", + "some-library2" + ], + "ref": "some-component" + }, + { + "ref": "some-library1" + }, + { + "ref": "some-library2" + } + ], + "metadata": { + "component": { + "bom-ref": "myApp", + "name": "myApp", + "type": "application" + }, + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.4" +} \ No newline at end of file diff --git a/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.4.xml.bin b/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.4.xml.bin new file mode 100644 index 00000000..22db0779 --- /dev/null +++ b/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.4.xml.bin @@ -0,0 +1,31 @@ +<?xml version="1.0" ?> +<bom xmlns="http://cyclonedx.org/schema/bom/1.4" serialNumber="urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac" version="1"> + <metadata> + <timestamp>2023-01-07T13:44:32.312678+00:00</timestamp> + <component type="application" bom-ref="myApp"> + <name>myApp</name> + </component> + </metadata> + <components> + <component type="library" bom-ref="some-component"> + <name>some-component</name> + </component> + <component type="library" bom-ref="some-library1"> + <name>some-library</name> + </component> + <component type="library" bom-ref="some-library2"> + <name>some-library</name> + </component> + </components> + <dependencies> + <dependency ref="myApp"> + <dependency ref="some-component"/> + </dependency> + <dependency ref="some-component"> + <dependency ref="some-library1"/> + <dependency ref="some-library2"/> + </dependency> + <dependency ref="some-library1"/> + <dependency ref="some-library2"/> + </dependencies> +</bom> diff --git a/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.5.json.bin b/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.5.json.bin new file mode 100644 index 00000000..64c0371b --- /dev/null +++ b/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.5.json.bin @@ -0,0 +1,63 @@ +{ + "components": [ + { + "bom-ref": "some-component", + "name": "some-component", + "type": "library" + }, + { + "bom-ref": "some-library1", + "name": "some-library", + "type": "library" + }, + { + "bom-ref": "some-library2", + "name": "some-library", + "type": "library" + } + ], + "dependencies": [ + { + "dependsOn": [ + "some-component" + ], + "ref": "myApp" + }, + { + "dependsOn": [ + "some-library1", + "some-library2" + ], + "ref": "some-component" + }, + { + "ref": "some-library1" + }, + { + "ref": "some-library2" + } + ], + "metadata": { + "component": { + "bom-ref": "myApp", + "name": "myApp", + "type": "application" + }, + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ], + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.5" +} \ No newline at end of file diff --git a/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.5.xml.bin b/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.5.xml.bin new file mode 100644 index 00000000..9146c15a --- /dev/null +++ b/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.5.xml.bin @@ -0,0 +1,35 @@ +<?xml version="1.0" ?> +<bom xmlns="http://cyclonedx.org/schema/bom/1.5" serialNumber="urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac" version="1"> + <metadata> + <timestamp>2023-01-07T13:44:32.312678+00:00</timestamp> + <component type="application" bom-ref="myApp"> + <name>myApp</name> + </component> + </metadata> + <components> + <component type="library" bom-ref="some-component"> + <name>some-component</name> + </component> + <component type="library" bom-ref="some-library1"> + <name>some-library</name> + </component> + <component type="library" bom-ref="some-library2"> + <name>some-library</name> + </component> + </components> + <dependencies> + <dependency ref="myApp"> + <dependency ref="some-component"/> + </dependency> + <dependency ref="some-component"> + <dependency ref="some-library1"/> + <dependency ref="some-library2"/> + </dependency> + <dependency ref="some-library1"/> + <dependency ref="some-library2"/> + </dependencies> + <properties> + <property name="key1">val1</property> + <property name="key2">val2</property> + </properties> +</bom> diff --git a/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.6.json.bin b/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.6.json.bin new file mode 100644 index 00000000..0ce6b2c9 --- /dev/null +++ b/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.6.json.bin @@ -0,0 +1,63 @@ +{ + "components": [ + { + "bom-ref": "some-component", + "name": "some-component", + "type": "library" + }, + { + "bom-ref": "some-library1", + "name": "some-library", + "type": "library" + }, + { + "bom-ref": "some-library2", + "name": "some-library", + "type": "library" + } + ], + "dependencies": [ + { + "dependsOn": [ + "some-component" + ], + "ref": "myApp" + }, + { + "dependsOn": [ + "some-library1", + "some-library2" + ], + "ref": "some-component" + }, + { + "ref": "some-library1" + }, + { + "ref": "some-library2" + } + ], + "metadata": { + "component": { + "bom-ref": "myApp", + "name": "myApp", + "type": "application" + }, + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ], + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6" +} \ No newline at end of file diff --git a/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.6.xml.bin b/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.6.xml.bin new file mode 100644 index 00000000..c64cf536 --- /dev/null +++ b/tests/_data/snapshots/get_bom_for_issue540_duplicate_components-1.6.xml.bin @@ -0,0 +1,35 @@ +<?xml version="1.0" ?> +<bom xmlns="http://cyclonedx.org/schema/bom/1.6" serialNumber="urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac" version="1"> + <metadata> + <timestamp>2023-01-07T13:44:32.312678+00:00</timestamp> + <component type="application" bom-ref="myApp"> + <name>myApp</name> + </component> + </metadata> + <components> + <component type="library" bom-ref="some-component"> + <name>some-component</name> + </component> + <component type="library" bom-ref="some-library1"> + <name>some-library</name> + </component> + <component type="library" bom-ref="some-library2"> + <name>some-library</name> + </component> + </components> + <dependencies> + <dependency ref="myApp"> + <dependency ref="some-component"/> + </dependency> + <dependency ref="some-component"> + <dependency ref="some-library1"/> + <dependency ref="some-library2"/> + </dependency> + <dependency ref="some-library1"/> + <dependency ref="some-library2"/> + </dependencies> + <properties> + <property name="key1">val1</property> + <property name="key2">val2</property> + </properties> +</bom> diff --git a/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.0.xml.bin b/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.0.xml.bin index e70bbd27..5262d1d2 100644 --- a/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.0.xml.bin +++ b/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.0.xml.bin @@ -4,13 +4,13 @@ <component type="library"> <name>dummy</name> <version>2.3.5</version> - <purl>pkg:pypi/pathlib2@2.3.5?vcs_url=git%2Bhttps://github.com/jazzband/pathlib2.git%405a6a88db3cc1d08dbc86fbe15edfb69fb5f5a3d6</purl> + <purl>pkg:pypi/pathlib2@2.3.5</purl> <modified>false</modified> </component> <component type="library"> <name>dummy</name> <version>2.3.5</version> - <purl>pkg:pypi/pathlib2@2.3.5</purl> + <purl>pkg:pypi/pathlib2@2.3.5?vcs_url=git%2Bhttps://github.com/jazzband/pathlib2.git%405a6a88db3cc1d08dbc86fbe15edfb69fb5f5a3d6</purl> <modified>false</modified> </component> </components> diff --git a/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.1.xml.bin b/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.1.xml.bin index baf7bca9..7e9f29bb 100644 --- a/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.1.xml.bin +++ b/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.1.xml.bin @@ -1,15 +1,15 @@ <?xml version="1.0" ?> <bom xmlns="http://cyclonedx.org/schema/bom/1.1" serialNumber="urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac" version="1"> <components> - <component type="library" bom-ref="dummy-b"> + <component type="library" bom-ref="dummy-a"> <name>dummy</name> <version>2.3.5</version> - <purl>pkg:pypi/pathlib2@2.3.5?vcs_url=git%2Bhttps://github.com/jazzband/pathlib2.git%405a6a88db3cc1d08dbc86fbe15edfb69fb5f5a3d6</purl> + <purl>pkg:pypi/pathlib2@2.3.5</purl> </component> - <component type="library" bom-ref="dummy-a"> + <component type="library" bom-ref="dummy-b"> <name>dummy</name> <version>2.3.5</version> - <purl>pkg:pypi/pathlib2@2.3.5</purl> + <purl>pkg:pypi/pathlib2@2.3.5?vcs_url=git%2Bhttps://github.com/jazzband/pathlib2.git%405a6a88db3cc1d08dbc86fbe15edfb69fb5f5a3d6</purl> </component> </components> </bom> diff --git a/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.2.json.bin b/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.2.json.bin index 651e8e36..cba8ccc0 100644 --- a/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.2.json.bin +++ b/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.2.json.bin @@ -1,16 +1,16 @@ { "components": [ { - "bom-ref": "dummy-b", + "bom-ref": "dummy-a", "name": "dummy", - "purl": "pkg:pypi/pathlib2@2.3.5?vcs_url=git%2Bhttps://github.com/jazzband/pathlib2.git%405a6a88db3cc1d08dbc86fbe15edfb69fb5f5a3d6", + "purl": "pkg:pypi/pathlib2@2.3.5", "type": "library", "version": "2.3.5" }, { - "bom-ref": "dummy-a", + "bom-ref": "dummy-b", "name": "dummy", - "purl": "pkg:pypi/pathlib2@2.3.5", + "purl": "pkg:pypi/pathlib2@2.3.5?vcs_url=git%2Bhttps://github.com/jazzband/pathlib2.git%405a6a88db3cc1d08dbc86fbe15edfb69fb5f5a3d6", "type": "library", "version": "2.3.5" } diff --git a/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.2.xml.bin b/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.2.xml.bin index cf695a4d..8e52087f 100644 --- a/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.2.xml.bin +++ b/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.2.xml.bin @@ -4,15 +4,15 @@ <timestamp>2023-01-07T13:44:32.312678+00:00</timestamp> </metadata> <components> - <component type="library" bom-ref="dummy-b"> + <component type="library" bom-ref="dummy-a"> <name>dummy</name> <version>2.3.5</version> - <purl>pkg:pypi/pathlib2@2.3.5?vcs_url=git%2Bhttps://github.com/jazzband/pathlib2.git%405a6a88db3cc1d08dbc86fbe15edfb69fb5f5a3d6</purl> + <purl>pkg:pypi/pathlib2@2.3.5</purl> </component> - <component type="library" bom-ref="dummy-a"> + <component type="library" bom-ref="dummy-b"> <name>dummy</name> <version>2.3.5</version> - <purl>pkg:pypi/pathlib2@2.3.5</purl> + <purl>pkg:pypi/pathlib2@2.3.5?vcs_url=git%2Bhttps://github.com/jazzband/pathlib2.git%405a6a88db3cc1d08dbc86fbe15edfb69fb5f5a3d6</purl> </component> </components> <dependencies> diff --git a/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.3.json.bin b/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.3.json.bin index 6ebec9dd..4f4e5528 100644 --- a/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.3.json.bin +++ b/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.3.json.bin @@ -1,16 +1,16 @@ { "components": [ { - "bom-ref": "dummy-b", + "bom-ref": "dummy-a", "name": "dummy", - "purl": "pkg:pypi/pathlib2@2.3.5?vcs_url=git%2Bhttps://github.com/jazzband/pathlib2.git%405a6a88db3cc1d08dbc86fbe15edfb69fb5f5a3d6", + "purl": "pkg:pypi/pathlib2@2.3.5", "type": "library", "version": "2.3.5" }, { - "bom-ref": "dummy-a", + "bom-ref": "dummy-b", "name": "dummy", - "purl": "pkg:pypi/pathlib2@2.3.5", + "purl": "pkg:pypi/pathlib2@2.3.5?vcs_url=git%2Bhttps://github.com/jazzband/pathlib2.git%405a6a88db3cc1d08dbc86fbe15edfb69fb5f5a3d6", "type": "library", "version": "2.3.5" } diff --git a/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.3.xml.bin b/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.3.xml.bin index 9b5b5f7a..56808073 100644 --- a/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.3.xml.bin +++ b/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.3.xml.bin @@ -4,15 +4,15 @@ <timestamp>2023-01-07T13:44:32.312678+00:00</timestamp> </metadata> <components> - <component type="library" bom-ref="dummy-b"> + <component type="library" bom-ref="dummy-a"> <name>dummy</name> <version>2.3.5</version> - <purl>pkg:pypi/pathlib2@2.3.5?vcs_url=git%2Bhttps://github.com/jazzband/pathlib2.git%405a6a88db3cc1d08dbc86fbe15edfb69fb5f5a3d6</purl> + <purl>pkg:pypi/pathlib2@2.3.5</purl> </component> - <component type="library" bom-ref="dummy-a"> + <component type="library" bom-ref="dummy-b"> <name>dummy</name> <version>2.3.5</version> - <purl>pkg:pypi/pathlib2@2.3.5</purl> + <purl>pkg:pypi/pathlib2@2.3.5?vcs_url=git%2Bhttps://github.com/jazzband/pathlib2.git%405a6a88db3cc1d08dbc86fbe15edfb69fb5f5a3d6</purl> </component> </components> <dependencies> diff --git a/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.4.json.bin b/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.4.json.bin index f1eeb9dc..5745f1cf 100644 --- a/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.4.json.bin +++ b/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.4.json.bin @@ -1,16 +1,16 @@ { "components": [ { - "bom-ref": "dummy-b", + "bom-ref": "dummy-a", "name": "dummy", - "purl": "pkg:pypi/pathlib2@2.3.5?vcs_url=git%2Bhttps://github.com/jazzband/pathlib2.git%405a6a88db3cc1d08dbc86fbe15edfb69fb5f5a3d6", + "purl": "pkg:pypi/pathlib2@2.3.5", "type": "library", "version": "2.3.5" }, { - "bom-ref": "dummy-a", + "bom-ref": "dummy-b", "name": "dummy", - "purl": "pkg:pypi/pathlib2@2.3.5", + "purl": "pkg:pypi/pathlib2@2.3.5?vcs_url=git%2Bhttps://github.com/jazzband/pathlib2.git%405a6a88db3cc1d08dbc86fbe15edfb69fb5f5a3d6", "type": "library", "version": "2.3.5" } diff --git a/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.4.xml.bin b/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.4.xml.bin index cb9ea370..81a3cb22 100644 --- a/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.4.xml.bin +++ b/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.4.xml.bin @@ -4,15 +4,15 @@ <timestamp>2023-01-07T13:44:32.312678+00:00</timestamp> </metadata> <components> - <component type="library" bom-ref="dummy-b"> + <component type="library" bom-ref="dummy-a"> <name>dummy</name> <version>2.3.5</version> - <purl>pkg:pypi/pathlib2@2.3.5?vcs_url=git%2Bhttps://github.com/jazzband/pathlib2.git%405a6a88db3cc1d08dbc86fbe15edfb69fb5f5a3d6</purl> + <purl>pkg:pypi/pathlib2@2.3.5</purl> </component> - <component type="library" bom-ref="dummy-a"> + <component type="library" bom-ref="dummy-b"> <name>dummy</name> <version>2.3.5</version> - <purl>pkg:pypi/pathlib2@2.3.5</purl> + <purl>pkg:pypi/pathlib2@2.3.5?vcs_url=git%2Bhttps://github.com/jazzband/pathlib2.git%405a6a88db3cc1d08dbc86fbe15edfb69fb5f5a3d6</purl> </component> </components> <dependencies> diff --git a/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.5.json.bin b/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.5.json.bin index 206aaec4..a88cf7ae 100644 --- a/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.5.json.bin +++ b/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.5.json.bin @@ -1,16 +1,16 @@ { "components": [ { - "bom-ref": "dummy-b", + "bom-ref": "dummy-a", "name": "dummy", - "purl": "pkg:pypi/pathlib2@2.3.5?vcs_url=git%2Bhttps://github.com/jazzband/pathlib2.git%405a6a88db3cc1d08dbc86fbe15edfb69fb5f5a3d6", + "purl": "pkg:pypi/pathlib2@2.3.5", "type": "library", "version": "2.3.5" }, { - "bom-ref": "dummy-a", + "bom-ref": "dummy-b", "name": "dummy", - "purl": "pkg:pypi/pathlib2@2.3.5", + "purl": "pkg:pypi/pathlib2@2.3.5?vcs_url=git%2Bhttps://github.com/jazzband/pathlib2.git%405a6a88db3cc1d08dbc86fbe15edfb69fb5f5a3d6", "type": "library", "version": "2.3.5" } diff --git a/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.5.xml.bin b/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.5.xml.bin index 2944adfc..36630746 100644 --- a/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.5.xml.bin +++ b/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.5.xml.bin @@ -4,15 +4,15 @@ <timestamp>2023-01-07T13:44:32.312678+00:00</timestamp> </metadata> <components> - <component type="library" bom-ref="dummy-b"> + <component type="library" bom-ref="dummy-a"> <name>dummy</name> <version>2.3.5</version> - <purl>pkg:pypi/pathlib2@2.3.5?vcs_url=git%2Bhttps://github.com/jazzband/pathlib2.git%405a6a88db3cc1d08dbc86fbe15edfb69fb5f5a3d6</purl> + <purl>pkg:pypi/pathlib2@2.3.5</purl> </component> - <component type="library" bom-ref="dummy-a"> + <component type="library" bom-ref="dummy-b"> <name>dummy</name> <version>2.3.5</version> - <purl>pkg:pypi/pathlib2@2.3.5</purl> + <purl>pkg:pypi/pathlib2@2.3.5?vcs_url=git%2Bhttps://github.com/jazzband/pathlib2.git%405a6a88db3cc1d08dbc86fbe15edfb69fb5f5a3d6</purl> </component> </components> <dependencies> diff --git a/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.6.json.bin b/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.6.json.bin index 77097c87..1251020b 100644 --- a/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.6.json.bin +++ b/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.6.json.bin @@ -1,16 +1,16 @@ { "components": [ { - "bom-ref": "dummy-b", + "bom-ref": "dummy-a", "name": "dummy", - "purl": "pkg:pypi/pathlib2@2.3.5?vcs_url=git%2Bhttps://github.com/jazzband/pathlib2.git%405a6a88db3cc1d08dbc86fbe15edfb69fb5f5a3d6", + "purl": "pkg:pypi/pathlib2@2.3.5", "type": "library", "version": "2.3.5" }, { - "bom-ref": "dummy-a", + "bom-ref": "dummy-b", "name": "dummy", - "purl": "pkg:pypi/pathlib2@2.3.5", + "purl": "pkg:pypi/pathlib2@2.3.5?vcs_url=git%2Bhttps://github.com/jazzband/pathlib2.git%405a6a88db3cc1d08dbc86fbe15edfb69fb5f5a3d6", "type": "library", "version": "2.3.5" } diff --git a/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.6.xml.bin b/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.6.xml.bin index 92263f13..18390367 100644 --- a/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.6.xml.bin +++ b/tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.6.xml.bin @@ -4,15 +4,15 @@ <timestamp>2023-01-07T13:44:32.312678+00:00</timestamp> </metadata> <components> - <component type="library" bom-ref="dummy-b"> + <component type="library" bom-ref="dummy-a"> <name>dummy</name> <version>2.3.5</version> - <purl>pkg:pypi/pathlib2@2.3.5?vcs_url=git%2Bhttps://github.com/jazzband/pathlib2.git%405a6a88db3cc1d08dbc86fbe15edfb69fb5f5a3d6</purl> + <purl>pkg:pypi/pathlib2@2.3.5</purl> </component> - <component type="library" bom-ref="dummy-a"> + <component type="library" bom-ref="dummy-b"> <name>dummy</name> <version>2.3.5</version> - <purl>pkg:pypi/pathlib2@2.3.5</purl> + <purl>pkg:pypi/pathlib2@2.3.5?vcs_url=git%2Bhttps://github.com/jazzband/pathlib2.git%405a6a88db3cc1d08dbc86fbe15edfb69fb5f5a3d6</purl> </component> </components> <dependencies> diff --git a/tests/test_factory_license.py b/tests/test_factory_license.py index f7fd7b99..051a88b3 100644 --- a/tests/test_factory_license.py +++ b/tests/test_factory_license.py @@ -33,7 +33,7 @@ def test_make_from_string_with_id(self) -> None: expected = DisjunctiveLicense(id='bar', text=text, url=url, acknowledgement=acknowledgement) with unittest.mock.patch('cyclonedx.factory.license.spdx_fixup', return_value='bar'), \ - unittest.mock.patch('cyclonedx.factory.license.is_spdx_compound_expression', return_value=True): + unittest.mock.patch('cyclonedx.factory.license.is_spdx_expression', return_value=True): actual = LicenseFactory().make_from_string('foo', license_text=text, license_url=url, @@ -48,7 +48,7 @@ def test_make_from_string_with_name(self) -> None: expected = DisjunctiveLicense(name='foo', text=text, url=url, acknowledgement=acknowledgement) with unittest.mock.patch('cyclonedx.factory.license.spdx_fixup', return_value=None), \ - unittest.mock.patch('cyclonedx.factory.license.is_spdx_compound_expression', return_value=False): + unittest.mock.patch('cyclonedx.factory.license.is_spdx_expression', return_value=False): actual = LicenseFactory().make_from_string('foo', license_text=text, license_url=url, @@ -61,7 +61,7 @@ def test_make_from_string_with_expression(self) -> None: expected = LicenseExpression('foo', acknowledgement=acknowledgement) with unittest.mock.patch('cyclonedx.factory.license.spdx_fixup', return_value=None), \ - unittest.mock.patch('cyclonedx.factory.license.is_spdx_compound_expression', return_value=True): + unittest.mock.patch('cyclonedx.factory.license.is_spdx_expression', return_value=True): actual = LicenseFactory().make_from_string('foo', license_acknowledgement=acknowledgement) @@ -94,11 +94,11 @@ def test_make_with_name(self) -> None: def test_make_with_expression(self) -> None: acknowledgement = unittest.mock.NonCallableMock(spec=LicenseAcknowledgement) expected = LicenseExpression('foo', acknowledgement=acknowledgement) - with unittest.mock.patch('cyclonedx.factory.license.is_spdx_compound_expression', return_value=True): + with unittest.mock.patch('cyclonedx.factory.license.is_spdx_expression', return_value=True): actual = LicenseFactory().make_with_expression(expression='foo', acknowledgement=acknowledgement) self.assertEqual(expected, actual) def test_make_with_expression_raises(self) -> None: with self.assertRaises(InvalidLicenseExpressionException, msg='foo'): - with unittest.mock.patch('cyclonedx.factory.license.is_spdx_compound_expression', return_value=False): + with unittest.mock.patch('cyclonedx.factory.license.is_spdx_expression', return_value=False): LicenseFactory().make_with_expression('foo') diff --git a/tests/test_model_component.py b/tests/test_model_component.py index 54d3cfe6..3f893907 100644 --- a/tests/test_model_component.py +++ b/tests/test_model_component.py @@ -142,17 +142,17 @@ def test_multiple_basic_components(self) -> None: self.assertNotEqual(c1, c2) def test_external_references(self) -> None: - c = Component(name='test-component') - c.external_references.add(ExternalReference( + c1 = Component(name='test-component') + c1.external_references.add(ExternalReference( type=ExternalReferenceType.OTHER, url=XsUri('https://cyclonedx.org'), comment='No comment' )) - self.assertEqual(c.name, 'test-component') - self.assertIsNone(c.version) - self.assertEqual(c.type, ComponentType.LIBRARY) - self.assertEqual(len(c.external_references), 1) - self.assertEqual(len(c.hashes), 0) + self.assertEqual(c1.name, 'test-component') + self.assertIsNone(c1.version) + self.assertEqual(c1.type, ComponentType.LIBRARY) + self.assertEqual(len(c1.external_references), 1) + self.assertEqual(len(c1.hashes), 0) c2 = Component(name='test2-component') self.assertEqual(c2.name, 'test2-component') @@ -170,39 +170,35 @@ def test_empty_component_with_version(self) -> None: self.assertEqual(len(c.hashes), 0) def test_component_equal_1(self) -> None: - c = Component(name='test-component') - c.external_references.add(ExternalReference( + c1 = Component(name='test-component') + c1.external_references.add(ExternalReference( type=ExternalReferenceType.OTHER, url=XsUri('https://cyclonedx.org'), comment='No comment' )) - c2 = Component(name='test-component') c2.external_references.add(ExternalReference( type=ExternalReferenceType.OTHER, url=XsUri('https://cyclonedx.org'), comment='No comment' )) - - self.assertEqual(c, c2) + self.assertEqual(c1, c2) def test_component_equal_2(self) -> None: - props: List[Property] = [ + props: List[Property] = ( Property(name='prop1', value='val1'), - Property(name='prop2', value='val2') - ] - - c = Component( + Property(name='prop2', value='val2'), + ) + c1 = Component( name='test-component', version='1.2.3', properties=props ) c2 = Component( name='test-component', version='1.2.3', properties=props ) - - self.assertEqual(c, c2) + self.assertEqual(c1, c2) def test_component_equal_3(self) -> None: - c = Component( + c1 = Component( name='test-component', version='1.2.3', properties=[ Property(name='prop1', value='val1'), Property(name='prop2', value='val2') @@ -214,8 +210,16 @@ def test_component_equal_3(self) -> None: Property(name='prop4', value='val4') ] ) + self.assertNotEqual(c1, c2) - self.assertNotEqual(c, c2) + def test_component_equal_4(self) -> None: + c1 = Component( + name='test-component', version='1.2.3', bom_ref='ref1' + ) + c2 = Component( + name='test-component', version='1.2.3', bom_ref='ref2' + ) + self.assertNotEqual(c1, c2) def test_same_1(self) -> None: c1 = get_component_setuptools_simple() @@ -347,8 +351,8 @@ def test_sort(self) -> None: class TestModelAttachedText(TestCase): def test_sort(self) -> None: - # expected sort order: (content_type, content, encoding) - expected_order = [0, 4, 2, 1, 3] + # expected sort order: (content_type, encoding, content) + expected_order = [0, 2, 4, 1, 3] text = [ AttachedText(content='a', content_type='a', encoding=Encoding.BASE_64), AttachedText(content='a', content_type='b', encoding=Encoding.BASE_64), @@ -438,17 +442,17 @@ def test_no_params(self) -> None: def test_same_1(self) -> None: p1 = get_pedigree_1() p2 = get_pedigree_1() - self.assertNotEqual(id(p1), id(p2)) - self.assertEqual(hash(p1), hash(p2)) - self.assertTrue(p1 == p2) + self.assertNotEqual(id(p1), id(p2), 'id') + self.assertEqual(hash(p1), hash(p2), 'hash') + self.assertTrue(p1 == p2, 'equal') def test_not_same_1(self) -> None: p1 = get_pedigree_1() p2 = get_pedigree_1() p2.notes = 'Some other notes here' - self.assertNotEqual(id(p1), id(p2)) - self.assertNotEqual(hash(p1), hash(p2)) - self.assertFalse(p1 == p2) + self.assertNotEqual(id(p1), id(p2), 'id') + self.assertNotEqual(hash(p1), hash(p2), 'hash') + self.assertFalse(p1 == p2, 'equal') class TestModelSwid(TestCase): @@ -456,20 +460,20 @@ class TestModelSwid(TestCase): def test_same_1(self) -> None: sw_1 = get_swid_1() sw_2 = get_swid_1() - self.assertNotEqual(id(sw_1), id(sw_2)) - self.assertEqual(hash(sw_1), hash(sw_2)) - self.assertTrue(sw_1 == sw_2) + self.assertNotEqual(id(sw_1), id(sw_2), 'id') + self.assertEqual(hash(sw_1), hash(sw_2), 'hash') + self.assertTrue(sw_1 == sw_2, 'equal') def test_same_2(self) -> None: sw_1 = get_swid_2() sw_2 = get_swid_2() - self.assertNotEqual(id(sw_1), id(sw_2)) - self.assertEqual(hash(sw_1), hash(sw_2)) - self.assertTrue(sw_1 == sw_2) + self.assertNotEqual(id(sw_1), id(sw_2), 'id') + self.assertEqual(hash(sw_1), hash(sw_2), 'hash') + self.assertTrue(sw_1 == sw_2, 'equal') def test_not_same(self) -> None: sw_1 = get_swid_1() sw_2 = get_swid_2() - self.assertNotEqual(id(sw_1), id(sw_2)) - self.assertNotEqual(hash(sw_1), hash(sw_2)) - self.assertFalse(sw_1 == sw_2) + self.assertNotEqual(id(sw_1), id(sw_2), 'id') + self.assertNotEqual(hash(sw_1), hash(sw_2), 'hash') + self.assertFalse(sw_1 == sw_2, 'equal') diff --git a/tests/test_model_vulnerability.py b/tests/test_model_vulnerability.py index e18f6003..830cbba2 100644 --- a/tests/test_model_vulnerability.py +++ b/tests/test_model_vulnerability.py @@ -188,7 +188,7 @@ def test_sort(self) -> None: datetime2 = datetime1 + timedelta(seconds=5) # expected sort order: (id, description, detail, source, created, published) - expected_order = [0, 6, 1, 7, 2, 8, 3, 9, 4, 10, 5, 11] + expected_order = [0, 1, 10, 2, 3, 4, 5, 6, 7, 8, 9, 11] vulnerabilities = [ Vulnerability(bom_ref='0', id='a', description='a', detail='a', source=source1, created=datetime1, published=datetime1), @@ -258,16 +258,12 @@ def test_sort(self) -> None: source_b = VulnerabilitySource(name='b') # expected sort order: ([id], [source]) - expected_order = [0, 1, 4, 2, 3, 5, 6, 7] + expected_order = [2, 3, 1, 0] refs = [ + VulnerabilityReference(id='b', source=source_b), + VulnerabilityReference(id='b', source=source_a), VulnerabilityReference(id='a', source=source_a), VulnerabilityReference(id='a', source=source_b), - VulnerabilityReference(id='b', source=source_a), - VulnerabilityReference(id='b', source=source_b), - VulnerabilityReference(id='a'), - VulnerabilityReference(id='b'), - VulnerabilityReference(source=source_a), - VulnerabilityReference(source=source_b), ] sorted_refs = sorted(refs) expected_refs = reorder(refs, expected_order) @@ -281,7 +277,7 @@ def test_sort(self) -> None: method_a = VulnerabilityScoreSource.CVSS_V3_1 # expected sort order: ([severity], [score], [source], [method], [vector], [justification]) - expected_order = [0, 1, 2, 3, 4, 5, 6, 7] + expected_order = [5, 0, 1, 2, 3, 4, 6, 7] refs = [ VulnerabilityRating(severity=VulnerabilitySeverity.HIGH, score=Decimal(10), source=source_a, method=method_a, vector='a', justification='a'), @@ -298,6 +294,7 @@ def test_sort(self) -> None: ] sorted_refs = sorted(refs) expected_refs = reorder(refs, expected_order) + self.maxDiff = None # gimme all diff on error self.assertListEqual(sorted_refs, expected_refs) diff --git a/tests/test_real_world_examples.py b/tests/test_real_world_examples.py index 1df29fca..0fe3e480 100644 --- a/tests/test_real_world_examples.py +++ b/tests/test_real_world_examples.py @@ -17,6 +17,7 @@ import unittest from datetime import datetime +from json import loads as json_loads from os.path import join from typing import Any from unittest.mock import patch @@ -36,3 +37,19 @@ def test_webgoat_6_1(self, *_: Any, **__: Any) -> None: def test_regression_issue_630(self, *_: Any, **__: Any) -> None: with open(join(OWN_DATA_DIRECTORY, 'xml', '1.6', 'regression_issue630.xml')) as input_xml: Bom.from_xml(input_xml) + + def test_regression_issue677(self, *_: Any, **__: Any) -> None: + # tests https://github.com/CycloneDX/cyclonedx-python-lib/issues/677 + with open(join(OWN_DATA_DIRECTORY, 'json', '1.5', 'issue677.json')) as input_json: + json = json_loads(input_json.read()) + bom = Bom.from_json(json) + self.assertEqual(4, len(bom.components)) + bom.validate() + + def test_regression_issue753(self, *_: Any, **__: Any) -> None: + # tests https://github.com/CycloneDX/cyclonedx-python-lib/issues/753 + with open(join(OWN_DATA_DIRECTORY, 'json', '1.5', 'issue753.json')) as input_json: + json = json_loads(input_json.read()) + bom = Bom.from_json(json) + self.assertEqual(2, len(bom.components)) + bom.validate() diff --git a/tests/test_spdx.py b/tests/test_spdx.py index a174e5c0..f18c23d7 100644 --- a/tests/test_spdx.py +++ b/tests/test_spdx.py @@ -19,19 +19,62 @@ from json import load as json_load from unittest import TestCase -from ddt import data, ddt, idata, unpack +from ddt import ddt, idata, unpack from cyclonedx import spdx from cyclonedx.schema._res import SPDX_JSON # rework access with open(SPDX_JSON) as spdx_schema: - KNOWN_SPDX_IDS = json_load(spdx_schema)['enum'] + KNOWN_SPDX_IDS = set(json_load(spdx_schema)['enum']) + +# for valid test data see the spec: https://spdx.github.io/spdx-spec/v3.0.1/annexes/spdx-license-expressions/ +VALID_EXPRESSIONS = { + # region Simple license expressions + 'CDDL-1.0', + # region not supported yet #110 - https://github.com/aboutcode-org/license-expression/issues/110 + # 'CDDL-1.0+', + # endregion region not supported yet #110 + # region not supported yet #109 - https://github.com/aboutcode-org/license-expression/issues/109 + # 'LicenseRef-23', + # 'LicenseRef-MIT-Style-1', + # 'DocumentRef-spdx-tool-1.2:LicenseRef-MIT-Style-2', + # endregion region not supported yet #109 + # endregion Simple license expressions + # region Composite license expressions + 'LGPL-2.1-only OR MIT', + 'MIT or LGPL-2.1-only', + '(MIT OR LGPL-2.1-only)', + 'LGPL-2.1-only OR MIT OR BSD-3-Clause', + 'LGPL-2.1-only AND MIT', + 'MIT AND LGPL-2.1-only', + 'MIT and LGPL-2.1-only', + '(MIT AND LGPL-2.1-only)', + 'LGPL-2.1-only AND MIT AND BSD-2-Clause', + 'GPL-2.0-or-later WITH Bison-exception-2.2', + 'LGPL-2.1-only OR BSD-3-Clause AND MIT', + 'MIT AND (LGPL-2.1-or-later OR BSD-3-Clause)', + # endregion Composite license expressions + # region examples from CDX spec + 'Apache-2.0 AND (MIT OR GPL-2.0-only)', + 'GPL-3.0-only WITH Classpath-exception-2.0', + # endregion examples from CDX spec +} + +INVALID_EXPRESSIONS = { + 'MIT AND Apache-2.0 OR something-unknown' + 'something invalid', + '(c) John Doe', + 'Apache License, Version 2.0', +} -VALID_COMPOUND_EXPRESSIONS = { - # for valid test data see the spec: https://spdx.github.io/spdx-spec/v2.3/SPDX-license-expressions/ - '(MIT AND Apache-2.0)', - 'BSD-2-Clause OR Apache-2.0', +UNKNOWN_SPDX_IDS = { + '', + 'something unsupported', 'something unfixable', + 'Apache 2.0', + 'LicenseRef-custom-identifier', + *(VALID_EXPRESSIONS - KNOWN_SPDX_IDS), + *INVALID_EXPRESSIONS, } @@ -43,12 +86,11 @@ def test_positive(self, supported_value: str) -> None: actual = spdx.is_supported_id(supported_value) self.assertTrue(actual) - @data( - 'something unsupported', - # somehow case-twisted values - 'MiT', - 'mit', - ) + @idata(chain(UNKNOWN_SPDX_IDS, ( + # region somehow case-twisted values + 'MiT', 'mit', + # endregion somehow case-twisted values + ))) def test_negative(self, unsupported_value: str) -> None: actual = spdx.is_supported_id(unsupported_value) self.assertFalse(actual) @@ -60,37 +102,31 @@ class TestSpdxFixup(TestCase): @idata(chain( # original value ((v, v) for v in KNOWN_SPDX_IDS), - # somehow case-twisted values + # region somehow case-twisted values ((v.lower(), v) for v in KNOWN_SPDX_IDS), ((v.upper(), v) for v in KNOWN_SPDX_IDS) + # endregion somehow case-twisted values )) @unpack def test_positive(self, fixable: str, expected_fixed: str) -> None: actual = spdx.fixup_id(fixable) self.assertEqual(expected_fixed, actual) - @data( - 'something unfixable', - ) + @idata(UNKNOWN_SPDX_IDS) def test_negative(self, unfixable: str) -> None: actual = spdx.fixup_id(unfixable) self.assertIsNone(actual) @ddt -class TestSpdxIsCompoundExpression(TestCase): +class TestSpdxIsExpression(TestCase): - @idata(VALID_COMPOUND_EXPRESSIONS) + @idata(VALID_EXPRESSIONS) def test_positive(self, valid_expression: str) -> None: - actual = spdx.is_compound_expression(valid_expression) + actual = spdx.is_expression(valid_expression) self.assertTrue(actual) - @data( - 'MIT AND Apache-2.0 OR something-unknown' - 'something invalid', - '(c) John Doe', - 'Apache License, Version 2.0' - ) + @idata(INVALID_EXPRESSIONS) def test_negative(self, invalid_expression: str) -> None: - actual = spdx.is_compound_expression(invalid_expression) + actual = spdx.is_expression(invalid_expression) self.assertFalse(actual)