Skip to content

refactor!: streamline comparison/hashing functions #755

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Feb 11, 2025
37 changes: 11 additions & 26 deletions cyclonedx/_internal/compare.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,40 +58,25 @@ def __gt__(self, other: Any) -> bool:
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):
"""
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
))
132 changes: 69 additions & 63 deletions cyclonedx/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,22 +133,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}>'
Expand Down Expand Up @@ -236,22 +237,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}>'
Expand Down Expand Up @@ -515,22 +517,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}>'
Expand Down Expand Up @@ -733,7 +736,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:
Expand Down Expand Up @@ -892,25 +895,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}>'
Expand Down Expand Up @@ -969,22 +971,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}>'
Expand Down Expand Up @@ -1060,22 +1063,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}>'
Expand Down Expand Up @@ -1144,22 +1148,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}>'
Expand Down Expand Up @@ -1234,22 +1239,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}>'
Expand Down Expand Up @@ -1287,16 +1293,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}>'
32 changes: 21 additions & 11 deletions cyclonedx/model/bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import 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 (
Expand Down Expand Up @@ -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}>'
Expand Down Expand Up @@ -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)}>'
Loading