Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ Unreleased

* Support Python 3.15.

* Compare versions using PEP 440 equivalence rather than exact string equality.
Previously a requirement pinned as ``gdal==3.10`` was reported as a mismatch against the installed ``3.10.0``, even though the two versions are equal under PEP 440.
This adds a dependency on `packaging <https://pypi.org/project/packaging/>`__.

* Switch package build backend from setuptools to `uv_build <https://docs.astral.sh/uv/concepts/build-backend/>`__.
This makes builds with uv about nine times faster, since uv runs the backend natively, without creating a build environment or spawning a Python process.
Additionally, source distributions no longer include test files, which setuptools previously included incompletely, missing the files needed to actually run them.
Expand Down
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ classifiers = [
"Topic :: Utilities",
"Typing :: Typed",
]
dependencies = []
dependencies = [
"packaging>=22",
]
urls = { Changelog = "https://github.com/adamchainz/pip-lock/blob/main/CHANGELOG.rst", Funding = "https://adamj.eu/books/", Repository = "https://github.com/adamchainz/pip-lock" }

[dependency-groups]
Expand Down
16 changes: 15 additions & 1 deletion src/pip_lock/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from collections.abc import Iterable
from importlib.metadata import distributions as get_distributions

from packaging.version import InvalidVersion, Version


def read_pip(filename: str) -> list[str]:
"""Return lines in pip file, concatenating included requirement files."""
Expand Down Expand Up @@ -55,6 +57,18 @@ def normalize_name(name: str) -> str:
return name.lower().replace("_", "-").replace(".", "-")


def versions_equal(expected: str, installed: str) -> bool:
"""Compare two version strings under PEP 440 equivalence."""
if expected == installed:
return True
try:
return bool(Version(expected) == Version(installed))
except InvalidVersion:
# Not PEP 440 versions, so exact string equality above was the only
# comparison available.
return False


def get_mismatches(requirements_file_path: str) -> dict[str, tuple[str, str | None]]:
"""Return a dictionary of requirement mismatches."""
pip_lines = read_pip(requirements_file_path)
Expand All @@ -66,7 +80,7 @@ def get_mismatches(requirements_file_path: str) -> dict[str, tuple[str, str | No
installed_version = installed.get(name)
if installed_version is None:
mismatches[name] = (expected_version, None)
elif installed_version != expected_version:
elif not versions_equal(expected_version, installed_version):
mismatches[name] = (expected_version, installed_version)

return mismatches
Expand Down
54 changes: 54 additions & 0 deletions tests/test_pip_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,60 @@ def test_package_with_extra(self, tmp_path):

assert result == {}

def test_no_mismatches_trailing_zero(self, tmp_path):
requirements = tmp_path / "requirements.txt"
requirements.write_text("package==3.10\n")

with mock_get_distributions({"package": "3.10.0"}):
result = get_mismatches(str(requirements))

assert result == {}

def test_no_mismatches_prerelease_spelling(self, tmp_path):
requirements = tmp_path / "requirements.txt"
requirements.write_text("package==1.0alpha1\n")

with mock_get_distributions({"package": "1.0a1"}):
result = get_mismatches(str(requirements))

assert result == {}

def test_mismatch_differing_release(self, tmp_path):
requirements = tmp_path / "requirements.txt"
requirements.write_text("package==3.10\n")

with mock_get_distributions({"package": "3.10.1"}):
result = get_mismatches(str(requirements))

assert result == {"package": ("3.10", "3.10.1")}

def test_mismatch_differing_epoch(self, tmp_path):
requirements = tmp_path / "requirements.txt"
requirements.write_text("package==1!1.0\n")

with mock_get_distributions({"package": "1.0"}):
result = get_mismatches(str(requirements))

assert result == {"package": ("1!1.0", "1.0")}

def test_mismatch_non_pep440_versions(self, tmp_path):
requirements = tmp_path / "requirements.txt"
requirements.write_text("package==not-a-version\n")

with mock_get_distributions({"package": "also-not-a-version"}):
result = get_mismatches(str(requirements))

assert result == {"package": ("not-a-version", "also-not-a-version")}

def test_no_mismatches_identical_non_pep440_versions(self, tmp_path):
requirements = tmp_path / "requirements.txt"
requirements.write_text("package==not-a-version\n")

with mock_get_distributions({"package": "not-a-version"}):
result = get_mismatches(str(requirements))

assert result == {}


class TestPrintErrors:
def test_errors(self, capsys):
Expand Down