diff --git a/src/packaging/pylock.py b/src/packaging/pylock.py index 663f69687..976395a9a 100644 --- a/src/packaging/pylock.py +++ b/src/packaging/pylock.py @@ -243,6 +243,14 @@ def _validate_path_url(path: str | None, url: str | None) -> None: raise PylockValidationError("path or url must be provided") +def _validate_absolute_url(url: str | None, context: str) -> None: + if url is None: + return + parsed = urlparse(url) + if not parsed.scheme or not parsed.netloc: + raise PylockValidationError("URL must be absolute", context=context) + + def _path_name(path: str | None) -> str | None: if not path: return None @@ -352,6 +360,7 @@ def _from_dict(cls, d: Mapping[str, Any]) -> Self: subdirectory=_get(d, str, "subdirectory"), ) _validate_path_url(package_vcs.path, package_vcs.url) + _validate_absolute_url(package_vcs.url, "url") return package_vcs @@ -420,6 +429,7 @@ def _from_dict(cls, d: Mapping[str, Any]) -> Self: subdirectory=_get(d, str, "subdirectory"), ) _validate_path_url(package_archive.path, package_archive.url) + _validate_absolute_url(package_archive.url, "url") return package_archive @@ -461,6 +471,7 @@ def _from_dict(cls, d: Mapping[str, Any]) -> Self: hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract] ) _validate_path_url(package_sdist.path, package_sdist.url) + _validate_absolute_url(package_sdist.url, "url") return package_sdist @property @@ -510,6 +521,7 @@ def _from_dict(cls, d: Mapping[str, Any]) -> Self: hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract] ) _validate_path_url(package_wheel.path, package_wheel.url) + _validate_absolute_url(package_wheel.url, "url") return package_wheel @property @@ -599,6 +611,7 @@ def _from_dict(cls, d: Mapping[str, Any]) -> Self: "Exactly one of vcs, directory, archive must be set " "if sdist and wheels are not set" ) + _validate_absolute_url(package.index, "index") for i, wheel in enumerate(package.wheels or []): try: (name, version, _, _) = parse_wheel_filename(wheel.filename) diff --git a/tests/test_pylock.py b/tests/test_pylock.py index f99a82d5c..1850abf81 100644 --- a/tests/test_pylock.py +++ b/tests/test_pylock.py @@ -281,6 +281,54 @@ def test_pylock_invalid_vcs() -> None: assert str(exc_info.value) == "path or url must be provided" +@pytest.mark.parametrize( + ("data", "expected"), + [ + ( + { + "lock-version": "1.0", + "created-by": "pip", + "packages": [ + { + "name": "example", + "index": "not-a-url", + "wheels": [ + { + "name": "example-1.0-py3-none-any.whl", + "url": "https://example.com/example-1.0-py3-none-any.whl", + "hashes": {"sha256": "f" * 40}, + } + ], + } + ], + }, + "URL must be absolute in 'packages[0].index'", + ), + ( + { + "lock-version": "1.0", + "created-by": "pip", + "packages": [ + { + "name": "example", + "vcs": { + "type": "git", + "url": "not-a-url", + "commit-id": "f" * 40, + }, + } + ], + }, + "URL must be absolute in 'packages[0].vcs.url'", + ), + ], +) +def test_pylock_invalid_urls(data: dict[str, Any], expected: str) -> None: + with pytest.raises(PylockValidationError) as exc_info: + Pylock.from_dict(data) + assert str(exc_info.value) == expected + + @pytest.mark.parametrize( ("dist", "expected_filename"), [