Skip to content

Commit 31fccbf

Browse files
saquibsaifeeclaude
andcommitted
refactor!: remove packageurl-python dependency
Removes packageurl-python entirely — no runtime dependency, no dev dependency. The library now treats PURL as an opaque string, which matches the CycloneDX specification (PURL is a plain string field in the schema). Changes: - Component.purl accepts and returns Optional[str] only - Bom.get_component_by_purl() now takes Optional[str] - Removed PackageUrl serialization helper and ComparablePackageURL utility - Removed all packageurl imports from source and test files - Updated test fixtures to use PURL string format directly - Regenerated snapshots with updated PURL string representation BREAKING CHANGE: Component.purl type changed from Optional[PackageURL] to Optional[str]. Users who need structured PURL access should parse the string themselves using the packageurl-python library directly. Signed-off-by: Saquib Saifee <saquibsaifee2@gmail.com> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4ef5bc3 commit 31fccbf

23 files changed

Lines changed: 48 additions & 133 deletions

cyclonedx/_internal/compare.py

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,7 @@
2222
"""
2323

2424
from itertools import zip_longest
25-
from typing import TYPE_CHECKING, Any, Optional
26-
27-
if TYPE_CHECKING: # pragma: no cover
28-
from packageurl import PackageURL
25+
from typing import Any, Optional
2926

3027

3128
class ComparableTuple(tuple[Optional[Any], ...]):
@@ -65,18 +62,3 @@ class ComparableDict(ComparableTuple):
6562

6663
def __new__(cls, d: dict[Any, Any]) -> 'ComparableDict':
6764
return super().__new__(cls, sorted(d.items()))
68-
69-
70-
class ComparablePackageURL(ComparableTuple):
71-
"""
72-
Allows comparison of PackageURL, allowing for qualifiers.
73-
"""
74-
75-
def __new__(cls, p: 'PackageURL') -> 'ComparablePackageURL':
76-
return super().__new__(cls, (
77-
p.type,
78-
p.namespace,
79-
p.version,
80-
ComparableDict(p.qualifiers) if isinstance(p.qualifiers, dict) else p.qualifiers,
81-
p.subpath
82-
))

cyclonedx/model/bom.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from datetime import datetime
2121
from enum import Enum
2222
from itertools import chain
23-
from typing import TYPE_CHECKING, Optional, Union
23+
from typing import Optional, Union
2424
from uuid import UUID, uuid4
2525
from warnings import warn
2626

@@ -54,9 +54,6 @@
5454
from .tool import Tool, ToolRepository, _ToolRepositoryHelper
5555
from .vulnerability import Vulnerability
5656

57-
if TYPE_CHECKING: # pragma: no cover
58-
from packageurl import PackageURL
59-
6057

6158
@serializable.serializable_enum
6259
class TlpClassification(str, Enum):
@@ -694,13 +691,13 @@ def definitions(self) -> Optional[Definitions]:
694691
def definitions(self, definitions: Definitions) -> None:
695692
self._definitions = definitions
696693

697-
def get_component_by_purl(self, purl: Optional['PackageURL']) -> Optional[Component]:
694+
def get_component_by_purl(self, purl: Optional[str]) -> Optional[Component]:
698695
"""
699696
Get a Component already in the Bom by its PURL
700697
701698
Args:
702699
purl:
703-
An instance of `packageurl.PackageURL` to look and find `Component`.
700+
A PURL string to look and find `Component`.
704701
705702
Returns:
706703
`Component` or `None`

cyclonedx/model/component.py

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,11 @@
2727
else:
2828
from typing_extensions import deprecated
2929

30-
# See https://github.com/package-url/packageurl-python/issues/65
3130
import py_serializable as serializable
32-
from packageurl import PackageURL
3331
from sortedcontainers import SortedSet
3432

3533
from .._internal.bom_ref import bom_ref_from_str as _bom_ref_from_str
36-
from .._internal.compare import ComparablePackageURL as _ComparablePackageURL, ComparableTuple as _ComparableTuple
34+
from .._internal.compare import ComparableTuple as _ComparableTuple
3735
from ..exception.model import InvalidOmniBorIdException, InvalidSwhidException
3836
from ..exception.serialization import (
3937
CycloneDxDeserializationException,
@@ -51,7 +49,6 @@
5149
SchemaVersion1Dot6,
5250
SchemaVersion1Dot7,
5351
)
54-
from ..serialization import PackageUrl as PackageUrlSH
5552
from . import (
5653
AttachedText,
5754
ExternalReference,
@@ -974,11 +971,10 @@ def for_file(absolute_file_path: str, path_for_bom: Optional[str]) -> 'Component
974971
component = ComponentBuilder().make_for_file(absolute_file_path, name=path_for_bom)
975972
sha1_hash = next((h.content for h in component.hashes if h.alg is HashAlgorithm.SHA_1), None)
976973
assert sha1_hash is not None
977-
component.version = f'0.0.0-{sha1_hash[0:12]}'
978-
component.purl = PackageURL( # DEPRECATED: a file has no PURL!
979-
type='generic', name=path_for_bom if path_for_bom else absolute_file_path,
980-
version=f'0.0.0-{sha1_hash[0:12]}'
981-
)
974+
version = f'0.0.0-{sha1_hash[0:12]}'
975+
name = path_for_bom if path_for_bom else absolute_file_path
976+
component.version = version
977+
component.purl = f'pkg:generic/{name}@{version}' # DEPRECATED: a file has no PURL!
982978
return component
983979

984980
def __init__(
@@ -996,7 +992,7 @@ def __init__(
996992
hashes: Optional[Iterable[HashType]] = None,
997993
licenses: Optional[Iterable[License]] = None,
998994
copyright: Optional[str] = None,
999-
purl: Optional[PackageURL] = None,
995+
purl: Optional[str] = None,
1000996
external_references: Optional[Iterable[ExternalReference]] = None,
1001997
properties: Optional[Iterable[Property]] = None,
1002998
release_notes: Optional[ReleaseNotes] = None,
@@ -1377,22 +1373,22 @@ def cpe(self, cpe: Optional[str]) -> None:
13771373
self._cpe = cpe
13781374

13791375
@property
1380-
@serializable.type_mapping(PackageUrlSH)
13811376
@serializable.xml_sequence(15)
1382-
def purl(self) -> Optional[PackageURL]:
1377+
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
1378+
def purl(self) -> Optional[str]:
13831379
"""
13841380
Specifies the package-url (PURL).
13851381
13861382
The purl, if specified, must be valid and conform to the specification defined at:
13871383
https://github.com/package-url/purl-spec
13881384
13891385
Returns:
1390-
`PackageURL` or `None`
1386+
`str` or `None`
13911387
"""
13921388
return self._purl
13931389

13941390
@purl.setter
1395-
def purl(self, purl: Optional[PackageURL]) -> None:
1391+
def purl(self, purl: Optional[str]) -> None:
13961392
self._purl = purl
13971393

13981394
@property
@@ -1679,7 +1675,7 @@ def __comparable_tuple(self) -> _ComparableTuple:
16791675
return _ComparableTuple((
16801676
self.type, self.group, self.name, self.version,
16811677
self.bom_ref.value,
1682-
None if self.purl is None else _ComparablePackageURL(self.purl),
1678+
self.purl,
16831679
self.swid, self.cpe, _ComparableTuple(self.swhids),
16841680
self.supplier, self.author, self.publisher,
16851681
self.description,

cyclonedx/model/component_evidence.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
from warnings import warn
2525
from xml.etree.ElementTree import Element as XmlElement # nosec B405
2626

27-
# See https://github.com/package-url/packageurl-python/issues/65
2827
import py_serializable as serializable
2928
from sortedcontainers import SortedSet
3029

cyclonedx/serialization/__init__.py

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@
2424
from typing import Any, Optional
2525
from uuid import UUID
2626

27-
# See https://github.com/package-url/packageurl-python/issues/65
28-
from packageurl import PackageURL
2927
from py_serializable.helpers import BaseHelper
3028

3129
if sys.version_info >= (3, 13):
@@ -57,25 +55,6 @@ def deserialize(cls, o: Any) -> BomRef:
5755
return BomRef.deserialize(o)
5856

5957

60-
class PackageUrl(BaseHelper):
61-
62-
@classmethod
63-
def serialize(cls, o: Any, ) -> str:
64-
if isinstance(o, PackageURL):
65-
return str(o.to_string())
66-
raise SerializationOfUnexpectedValueException(
67-
f'Attempt to serialize a non-PackageURL: {o!r}')
68-
69-
@classmethod
70-
def deserialize(cls, o: Any) -> PackageURL:
71-
try:
72-
return PackageURL.from_string(purl=str(o))
73-
except ValueError as err:
74-
raise CycloneDxDeserializationException(
75-
f'PURL string supplied does not parse: {o!r}'
76-
) from err
77-
78-
7958
class UrnUuidHelper(BaseHelper):
8059

8160
@classmethod

examples/complex_serialize.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818
import sys
1919
from typing import TYPE_CHECKING
2020

21-
from packageurl import PackageURL
22-
2321
from cyclonedx.contrib.license.factories import LicenseFactory
2422
from cyclonedx.contrib.this.builders import this_component as cdx_lib_component
2523
from cyclonedx.exception import MissingOptionalDependencyException
@@ -68,7 +66,7 @@
6866
urls=[XsUri('https://www.acme.org')]
6967
),
7068
bom_ref='myComponent@1.33.7-beta.1',
71-
purl=PackageURL('generic', 'acme', 'some-component', '1.33.7-beta.1')
69+
purl='pkg:generic/acme/some-component@1.33.7-beta.1'
7270
)
7371
bom.components.add(component1)
7472
bom.register_dependency(root_component, [component1])

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ keywords = [
7070

7171
[tool.poetry.dependencies]
7272
python = "^3.9"
73-
packageurl-python = ">=0.11, <2"
7473
py-serializable = "^2.1.0"
7574
sortedcontainers = "^2.4.0"
7675
license-expression = "^30"

tests/_data/models.py

Lines changed: 9 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,6 @@
2424
from typing import Any, Optional
2525
from uuid import UUID
2626

27-
# See https://github.com/package-url/packageurl-python/issues/65
28-
from packageurl import PackageURL
29-
3027
from cyclonedx.builder.this import this_component, this_tool
3128
from cyclonedx.model import (
3229
AttachedText,
@@ -482,9 +479,7 @@ def get_bom_with_component_evidence() -> Bom:
482479
component = Component(
483480
name='setuptools', version='50.3.2',
484481
bom_ref='pkg:pypi/setuptools@50.3.2?extension=tar.gz',
485-
purl=PackageURL(
486-
type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz'
487-
),
482+
purl='pkg:pypi/setuptools@50.3.2?extension=tar.gz',
488483
licenses=[DisjunctiveLicense(id='MIT')],
489484
author='Test Author'
490485
)
@@ -550,7 +545,7 @@ def get_bom_with_component_setuptools_with_vulnerability() -> Bom:
550545
),
551546
affects=[
552547
BomTarget(
553-
ref=component.purl.to_string(),
548+
ref=str(component.purl),
554549
versions=[BomTargetVersionRange(
555550
range='49.0.0 - 54.0.0', status=ImpactAnalysisAffectedStatus.AFFECTED
556551
)]
@@ -845,9 +840,7 @@ def get_component_setuptools_simple(
845840
return Component(
846841
name='setuptools', version='50.3.2',
847842
bom_ref=bom_ref,
848-
purl=PackageURL(
849-
type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz'
850-
),
843+
purl='pkg:pypi/setuptools@50.3.2?extension=tar.gz',
851844
licenses=[DisjunctiveLicense(id='MIT')],
852845
author='Test Author'
853846
)
@@ -856,9 +849,7 @@ def get_component_setuptools_simple(
856849
def get_component_setuptools_simple_no_version(bom_ref: Optional[str] = None) -> Component:
857850
return Component(
858851
name='setuptools', bom_ref=bom_ref or 'pkg:pypi/setuptools?extension=tar.gz',
859-
purl=PackageURL(
860-
type='pypi', name='setuptools', qualifiers='extension=tar.gz'
861-
),
852+
purl='pkg:pypi/setuptools?extension=tar.gz',
862853
licenses=[DisjunctiveLicense(id='MIT')],
863854
author='Test Author'
864855
)
@@ -867,9 +858,7 @@ def get_component_setuptools_simple_no_version(bom_ref: Optional[str] = None) ->
867858
def get_component_toml_with_hashes_with_references(bom_ref: Optional[str] = None) -> Component:
868859
return Component(
869860
name='toml', version='0.10.2', bom_ref=bom_ref or 'pkg:pypi/toml@0.10.2?extension=tar.gz',
870-
purl=PackageURL(
871-
type='pypi', name='toml', version='0.10.2', qualifiers='extension=tar.gz'
872-
), hashes=[
861+
purl='pkg:pypi/toml@0.10.2?extension=tar.gz', hashes=[
873862
HashType.from_composite_str('sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b')
874863
], external_references=[
875864
get_external_reference_1()
@@ -1365,19 +1354,13 @@ def get_bom_for_issue_598_multiple_components_with_purl_qualifiers() -> Bom:
13651354
return _make_bom(components=[
13661355
Component(
13671356
name='dummy', version='2.3.5', bom_ref='dummy-a',
1368-
purl=PackageURL(
1369-
type='pypi', namespace=None, name='pathlib2', version='2.3.5', subpath=None,
1370-
qualifiers={}
1371-
)
1357+
purl='pkg:pypi/pathlib2@2.3.5'
13721358
),
13731359
Component(
13741360
name='dummy', version='2.3.5', bom_ref='dummy-b',
1375-
purl=PackageURL(
1376-
type='pypi', namespace=None, name='pathlib2', version='2.3.5', subpath=None,
1377-
qualifiers={
1378-
'vcs_url': 'git+https://github.com/jazzband/pathlib2.git@5a6a88db3cc1d08dbc86fbe15edfb69fb5f5a3d6'
1379-
}
1380-
)
1361+
purl='pkg:pypi/pathlib2@2.3.5'
1362+
'?vcs_url=git%2Bhttps%3A%2F%2Fgithub.com%2Fjazzband%2Fpathlib2.git'
1363+
'%405a6a88db3cc1d08dbc86fbe15edfb69fb5f5a3d6'
13811364
)
13821365
])
13831366

tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.0.xml.bin

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<component type="library">
1111
<name>dummy</name>
1212
<version>2.3.5</version>
13-
<purl>pkg:pypi/pathlib2@2.3.5?vcs_url=git%2Bhttps://github.com/jazzband/pathlib2.git%405a6a88db3cc1d08dbc86fbe15edfb69fb5f5a3d6</purl>
13+
<purl>pkg:pypi/pathlib2@2.3.5?vcs_url=git%2Bhttps%3A%2F%2Fgithub.com%2Fjazzband%2Fpathlib2.git%405a6a88db3cc1d08dbc86fbe15edfb69fb5f5a3d6</purl>
1414
<modified>false</modified>
1515
</component>
1616
</components>

tests/_data/snapshots/get_bom_for_issue_598_multiple_components_with_purl_qualifiers-1.1.xml.bin

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<component type="library" bom-ref="dummy-b">
1010
<name>dummy</name>
1111
<version>2.3.5</version>
12-
<purl>pkg:pypi/pathlib2@2.3.5?vcs_url=git%2Bhttps://github.com/jazzband/pathlib2.git%405a6a88db3cc1d08dbc86fbe15edfb69fb5f5a3d6</purl>
12+
<purl>pkg:pypi/pathlib2@2.3.5?vcs_url=git%2Bhttps%3A%2F%2Fgithub.com%2Fjazzband%2Fpathlib2.git%405a6a88db3cc1d08dbc86fbe15edfb69fb5f5a3d6</purl>
1313
</component>
1414
</components>
1515
</bom>

0 commit comments

Comments
 (0)