From 1300a47fe1835646d72138b47fd47cdaa8f432e9 Mon Sep 17 00:00:00 2001 From: Arpit Jain Date: Fri, 31 Jul 2026 09:21:50 +0900 Subject: [PATCH 1/2] Include the name in ComparablePackageURL The comparison tuple carried type, namespace, version, qualifiers and subpath but not name, so two purls that differ only by name compared equal and sorted arbitrarily. Signed-off-by: Arpit Jain --- cyclonedx/_internal/compare.py | 1 + tests/test_model.py | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/cyclonedx/_internal/compare.py b/cyclonedx/_internal/compare.py index 756260ef0..3d7577c99 100644 --- a/cyclonedx/_internal/compare.py +++ b/cyclonedx/_internal/compare.py @@ -76,6 +76,7 @@ def __new__(cls, p: 'PackageURL') -> 'ComparablePackageURL': return super().__new__(cls, ( p.type, p.namespace, + p.name, p.version, ComparableDict(p.qualifiers) if isinstance(p.qualifiers, dict) else p.qualifiers, p.subpath diff --git a/tests/test_model.py b/tests/test_model.py index 58c017e94..20703239e 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -23,7 +23,9 @@ from ddt import ddt, named_data -from cyclonedx._internal.compare import ComparableTuple +from packageurl import PackageURL + +from cyclonedx._internal.compare import ComparablePackageURL, ComparableTuple from cyclonedx.exception.model import InvalidLocaleTypeException, InvalidUriException, UnknownHashTypeException from cyclonedx.model import ( Copyright, @@ -70,6 +72,23 @@ def test_sort(self) -> None: self.assertListEqual(sorted_enums, expected_enums) +class TestComparablePackageURL(TestCase): + + def test_differs_by_name(self) -> None: + # name was missing from the comparison tuple, so two purls that differ + # only by name compared equal + purl1 = ComparablePackageURL(PackageURL(type='pypi', name='foo', version='1.0.0')) + purl2 = ComparablePackageURL(PackageURL(type='pypi', name='bar', version='1.0.0')) + self.assertNotEqual(purl1, purl2) + self.assertGreater(purl1, purl2) + self.assertLess(purl2, purl1) + + def test_equal_same_purl(self) -> None: + purl1 = ComparablePackageURL(PackageURL(type='pypi', name='foo', version='1.0.0')) + purl2 = ComparablePackageURL(PackageURL(type='pypi', name='foo', version='1.0.0')) + self.assertEqual(purl1, purl2) + + class TestComparableTuple(TestCase): def test_equal_self(self) -> None: From e91c4e8ada029ab8caa94dd8ba7edf0c2472f837 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 10 Aug 2026 14:50:57 +0200 Subject: [PATCH 2/2] tests Signed-off-by: Jan Kowalleck --- tests/test_internal/test_compare.py | 43 +++++++++++++++++++++++++++++ tests/test_model.py | 21 +------------- 2 files changed, 44 insertions(+), 20 deletions(-) create mode 100644 tests/test_internal/test_compare.py diff --git a/tests/test_internal/test_compare.py b/tests/test_internal/test_compare.py new file mode 100644 index 000000000..c76d6a063 --- /dev/null +++ b/tests/test_internal/test_compare.py @@ -0,0 +1,43 @@ +# This file is part of CycloneDX Python Library +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. + +from unittest import TestCase + +from packageurl import PackageURL + +from cyclonedx._internal.compare import ComparablePackageURL + + +class TestComparablePackageURL(TestCase): + + def test_differs_by_name(self) -> None: + """ + regression for https://github.com/CycloneDX/cyclonedx-python-lib/issues/1021 + + name was missing from the comparison tuple, so two purls that differ + only by name compared equal + """ + purl1 = ComparablePackageURL(PackageURL(type='pypi', name='foo', version='1.0.0')) + purl2 = ComparablePackageURL(PackageURL(type='pypi', name='bar', version='1.0.0')) + self.assertNotEqual(purl1, purl2) + self.assertGreater(purl1, purl2) + self.assertLess(purl2, purl1) + + def test_equal_same_purl(self) -> None: + purl1 = ComparablePackageURL(PackageURL(type='pypi', name='foo', version='1.0.0')) + purl2 = ComparablePackageURL(PackageURL(type='pypi', name='foo', version='1.0.0')) + self.assertEqual(purl1, purl2) diff --git a/tests/test_model.py b/tests/test_model.py index 20703239e..58c017e94 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -23,9 +23,7 @@ from ddt import ddt, named_data -from packageurl import PackageURL - -from cyclonedx._internal.compare import ComparablePackageURL, ComparableTuple +from cyclonedx._internal.compare import ComparableTuple from cyclonedx.exception.model import InvalidLocaleTypeException, InvalidUriException, UnknownHashTypeException from cyclonedx.model import ( Copyright, @@ -72,23 +70,6 @@ def test_sort(self) -> None: self.assertListEqual(sorted_enums, expected_enums) -class TestComparablePackageURL(TestCase): - - def test_differs_by_name(self) -> None: - # name was missing from the comparison tuple, so two purls that differ - # only by name compared equal - purl1 = ComparablePackageURL(PackageURL(type='pypi', name='foo', version='1.0.0')) - purl2 = ComparablePackageURL(PackageURL(type='pypi', name='bar', version='1.0.0')) - self.assertNotEqual(purl1, purl2) - self.assertGreater(purl1, purl2) - self.assertLess(purl2, purl1) - - def test_equal_same_purl(self) -> None: - purl1 = ComparablePackageURL(PackageURL(type='pypi', name='foo', version='1.0.0')) - purl2 = ComparablePackageURL(PackageURL(type='pypi', name='foo', version='1.0.0')) - self.assertEqual(purl1, purl2) - - class TestComparableTuple(TestCase): def test_equal_self(self) -> None: