Skip to content

Commit a5946fe

Browse files
committed
Merge branch 'validate-vers' of github.com:jaimergp/packageurl-python; branch 'main' of github.com:package-url/packageurl-python into validate-vers
2 parents dd0165b + 9fb5350 commit a5946fe

File tree

7 files changed

+103
-9
lines changed

7 files changed

+103
-9
lines changed

CHANGELOG.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,24 @@
11
Changelog
22
=========
33

4+
0.17.1 (2025-06-06)
5+
-------------------
6+
7+
- Define "pkg" as a PackageURL ``SCHEME`` class attribute.
8+
https://github.com/package-url/packageurl-python/issues/184
9+
10+
- Add support for Maven in ``purl2url``.
11+
https://github.com/package-url/packageurl-python/issues/177
12+
13+
0.17.0 (2025-06-04)
14+
-------------------
15+
16+
- Fix qualifiers type annotations.
17+
https://github.com/package-url/packageurl-python/issues/169
18+
19+
- Fix parsing of names and namespaces with colons.
20+
https://github.com/package-url/packageurl-python/issues/152
21+
422
0.16.0 (2024-10-22)
523
-------------------
624

Makefile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ valid:
6666

6767
test:
6868
@echo "-> Run the test suite"
69-
${MANAGE} test --noinput
7069
bin/py.test tests
7170

7271
.PHONY: virtualenv conf dev clean isort black mypy valid test

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = packageurl-python
3-
version = 0.16.0
3+
version = 0.17.1
44
license = MIT
55
description = A purl aka. Package URL parser and builder
66
long_description = file:README.rst

src/packageurl/__init__.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
import string
2828
from collections import namedtuple
29+
from collections.abc import Mapping
2930
from typing import TYPE_CHECKING
3031
from typing import Any
3132
from typing import Union
@@ -37,6 +38,7 @@
3738
if TYPE_CHECKING:
3839
from collections.abc import Callable
3940
from collections.abc import Iterable
41+
from typing import ClassVar
4042

4143
from typing_extensions import Literal
4244
from typing_extensions import Self
@@ -230,9 +232,12 @@ def normalize_qualifiers(
230232

231233
if not encode:
232234
return qualifiers_map
235+
return _qualifier_map_to_string(qualifiers_map) or None
233236

234-
qualifiers_list = [f"{key}={value}" for key, value in qualifiers_map.items()]
235-
return "&".join(qualifiers_list) or None
237+
238+
def _qualifier_map_to_string(qualifiers: dict[str, str]) -> str:
239+
qualifiers_list = [f"{key}={value}" for key, value in qualifiers.items()]
240+
return "&".join(qualifiers_list)
236241

237242

238243
def normalize_subpath(subpath: AnyStr | None, encode: bool | None = True) -> str | None:
@@ -319,6 +324,8 @@ class PackageURL(
319324
https://github.com/package-url/purl-spec
320325
"""
321326

327+
SCHEME: ClassVar[str] = "pkg"
328+
322329
type: str
323330
namespace: str | None
324331
name: str
@@ -400,7 +407,7 @@ def to_dict(self, encode: bool | None = False, empty: Any = None) -> dict[str, A
400407

401408
return data
402409

403-
def to_string(self) -> str:
410+
def to_string(self, encode: bool | None = True) -> str:
404411
"""
405412
Return a purl string built from components.
406413
"""
@@ -411,10 +418,10 @@ def to_string(self) -> str:
411418
self.version,
412419
self.qualifiers,
413420
self.subpath,
414-
encode=True,
421+
encode=encode,
415422
)
416423

417-
purl = ["pkg:", type, "/"]
424+
purl = [self.SCHEME, ":", type, "/"]
418425

419426
if namespace:
420427
purl.extend((namespace, "/"))
@@ -427,6 +434,8 @@ def to_string(self) -> str:
427434

428435
if qualifiers:
429436
purl.append("?")
437+
if isinstance(qualifiers, Mapping):
438+
qualifiers = _qualifier_map_to_string(qualifiers)
430439
purl.append(qualifiers)
431440

432441
if subpath:
@@ -445,8 +454,10 @@ def from_string(cls, purl: str) -> Self:
445454
raise ValueError("A purl string argument is required.")
446455

447456
scheme, sep, remainder = purl.partition(":")
448-
if not sep or scheme != "pkg":
449-
raise ValueError(f'purl is missing the required "pkg" scheme component: {purl!r}.')
457+
if not sep or scheme != cls.SCHEME:
458+
raise ValueError(
459+
f'purl is missing the required "{cls.SCHEME}" scheme component: {purl!r}.'
460+
)
450461

451462
# this strip '/, // and /// as possible in :// or :///
452463
remainder = remainder.strip().lstrip("/")

src/packageurl/contrib/purl2url.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
from packageurl.contrib.route import NoRouteAvailable
2929
from packageurl.contrib.route import Router
3030

31+
DEFAULT_MAVEN_REPOSITORY = "https://repo.maven.apache.org/maven2"
32+
3133

3234
def get_repo_download_url_by_package_type(
3335
type, namespace, name, version, archive_extension="tar.gz"
@@ -314,6 +316,24 @@ def build_cocoapods_repo_url(purl):
314316
return name and f"https://cocoapods.org/pods/{name}"
315317

316318

319+
@repo_router.route("pkg:maven/.*")
320+
def build_maven_repo_url(purl):
321+
"""
322+
Return a Maven repo URL from the `purl` string.
323+
"""
324+
purl_data = PackageURL.from_string(purl)
325+
namespace = purl_data.namespace
326+
name = purl_data.name
327+
version = purl_data.version
328+
qualifiers = purl_data.qualifiers
329+
330+
base_url = qualifiers.get("repository_url", DEFAULT_MAVEN_REPOSITORY)
331+
332+
if namespace and name and version:
333+
namespace = namespace.replace(".", "/")
334+
return f"{base_url}/{namespace}/{name}/{version}"
335+
336+
317337
# Download URLs:
318338

319339

@@ -365,6 +385,28 @@ def build_npm_download_url(purl):
365385
return f"{base_url}/{name}/-/{name}-{version}.tgz"
366386

367387

388+
@download_router.route("pkg:maven/.*")
389+
def build_maven_download_url(purl):
390+
"""
391+
Return a maven download URL from the `purl` string.
392+
"""
393+
purl_data = PackageURL.from_string(purl)
394+
395+
namespace = purl_data.namespace
396+
name = purl_data.name
397+
version = purl_data.version
398+
qualifiers = purl_data.qualifiers
399+
400+
base_url = qualifiers.get("repository_url", DEFAULT_MAVEN_REPOSITORY)
401+
maven_type = qualifiers.get("type", "jar") # default to "jar"
402+
classifier = qualifiers.get("classifier")
403+
404+
if namespace and name and version:
405+
namespace = namespace.replace(".", "/")
406+
classifier = f"-{classifier}" if classifier else ""
407+
return f"{base_url}/{namespace}/{name}/{version}/{name}-{version}{classifier}.{maven_type}"
408+
409+
368410
@download_router.route("pkg:hackage/.*")
369411
def build_hackage_download_url(purl):
370412
"""

tests/contrib/test_purl2url.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ def test_purl2url_get_repo_url():
6868
"pkg:golang/gopkg.in/[email protected]": "https://pkg.go.dev/gopkg.in/[email protected]",
6969
"pkg:cocoapods/[email protected]": "https://cocoapods.org/pods/AFNetworking",
7070
"pkg:cocoapods/[email protected]": "https://cocoapods.org/pods/MapsIndoors",
71+
"pkg:maven/org.apache.commons/[email protected]": "https://repo.maven.apache.org/maven2/org/apache/commons/commons-io/1.3.2",
72+
"pkg:maven/org.apache.commons/[email protected]?repository_url=https://repo1.maven.org/maven2": "https://repo1.maven.org/maven2/org/apache/commons/commons-io/1.3.2",
7173
}
7274

7375
for purl, url in purls_url.items():
@@ -92,6 +94,10 @@ def test_purl2url_get_download_url():
9294
"pkg:gitlab/tg1999/firebase@1a122122": "https://gitlab.com/tg1999/firebase/-/archive/1a122122/firebase-1a122122.tar.gz",
9395
"pkg:gitlab/tg1999/firebase@1a122122?version_prefix=v": "https://gitlab.com/tg1999/firebase/-/archive/v1a122122/firebase-v1a122122.tar.gz",
9496
"pkg:gitlab/hoppr/[email protected]": "https://gitlab.com/hoppr/hoppr/-/archive/v1.11.1-dev.2/hoppr-v1.11.1-dev.2.tar.gz",
97+
"pkg:maven/org.apache.commons/[email protected]": "https://repo.maven.apache.org/maven2/org/apache/commons/commons-io/1.3.2/commons-io-1.3.2.jar",
98+
"pkg:maven/org.apache.commons/[email protected]?repository_url=https://repo1.maven.org/maven2": "https://repo1.maven.org/maven2/org/apache/commons/commons-io/1.3.2/commons-io-1.3.2.jar",
99+
"pkg:maven/org.apache.commons/[email protected]?type=pom": "https://repo.maven.apache.org/maven2/org/apache/commons/commons-io/1.3.2/commons-io-1.3.2.pom",
100+
"pkg:maven/org.apache.commons/[email protected]?classifier=sources": "https://repo.maven.apache.org/maven2/org/apache/commons/commons-math3/3.6.1/commons-math3-3.6.1-sources.jar",
95101
# From `download_url` qualifier
96102
"pkg:github/yarnpkg/[email protected]?download_url=https://github.com/yarnpkg/yarn/releases/download/v1.3.2/yarn-v1.3.2.tar.gz&version_prefix=v": "https://github.com/yarnpkg/yarn/releases/download/v1.3.2/yarn-v1.3.2.tar.gz",
97103
"pkg:generic/lxc-master.tar.gz?download_url=https://salsa.debian.org/lxc-team/lxc/-/archive/master/lxc-master.tar.gz": "https://salsa.debian.org/lxc-team/lxc/-/archive/master/lxc-master.tar.gz",
@@ -150,6 +156,10 @@ def test_purl2url_get_inferred_urls():
150156
"pkg:cocoapods/[email protected]": ["https://cocoapods.org/pods/AFNetworking"],
151157
"pkg:composer/psr/[email protected]": ["https://packagist.org/packages/psr/log#1.1.3"],
152158
"pkg:rubygems/package-name": ["https://rubygems.org/gems/package-name"],
159+
"pkg:maven/org.apache.commons/[email protected]": [
160+
"https://repo.maven.apache.org/maven2/org/apache/commons/commons-io/1.3.2",
161+
"https://repo.maven.apache.org/maven2/org/apache/commons/commons-io/1.3.2/commons-io-1.3.2.jar",
162+
],
153163
"pkg:bitbucket/birkenfeld": [],
154164
}
155165

tests/test_packageurl.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,3 +386,17 @@ def test_vers_validation_ok():
386386
def test_vers_validation_fails():
387387
with pytest.raises(ValueError, match="must start with the 'vers:' URI scheme."):
388388
PackageURL.from_string("pkg:pypi/requests?vers=>=2.0")
389+
390+
391+
def test_no_encoding_to_string():
392+
p = PackageURL(
393+
type="nuget",
394+
namespace="an:odd:space",
395+
name="libiconv: character set conversion library",
396+
version="1.9",
397+
qualifiers={"package-id": "e11a609df352e292"},
398+
)
399+
assert (
400+
p.to_string(encode=False)
401+
== "pkg:nuget/an:odd:space/libiconv: character set conversion [email protected]?package-id=e11a609df352e292"
402+
)

0 commit comments

Comments
 (0)