Skip to content

Compare versions with PEP 440 equivalence - #523

Open
dchaudhari7177 wants to merge 1 commit into
adamchainz:mainfrom
dchaudhari7177:fix/505-pep440-version-compare
Open

Compare versions with PEP 440 equivalence#523
dchaudhari7177 wants to merge 1 commit into
adamchainz:mainfrom
dchaudhari7177:fix/505-pep440-version-compare

Conversation

@dchaudhari7177

Copy link
Copy Markdown

Fixes #505.

Problem

get_mismatches() compares the pinned and the installed version as raw strings:

elif installed_version != expected_version:

so any pair that differs only in spelling is reported as a mismatch. The reported case is trailing-zero normalization — pip-compile writes gdal==3.10 into the .txt, the installed distribution reports 3.10.0, and pip-lock fails with:

* Package gdal has version 3.10 but you have version 3.10.0 installed.

even though Version("3.10") == Version("3.10.0") under PEP 440.

Change

Compare with packaging.version.Version:

def versions_equal(expected: str, installed: str) -> bool:
    """Compare two version strings under PEP 440 equivalence."""
    if expected == installed:
        return True
    try:
        return Version(expected) == Version(installed)
    except InvalidVersion:
        return False
  • Exact string equality is checked first, so the common case does not pay for parsing.
  • InvalidVersion falls back to that string comparison instead of raising, so a non-PEP-440 version string behaves exactly as it does today.

One thing to flag

The issue says packaging "is already a transitive dependency of pip-lock". That isn't quite right — pyproject.toml currently has dependencies = [], so this PR adds pip-lock's first runtime dependency. I went ahead because pip-lock's workflow is built around pip-compile output and pip-tools itself depends on packaging, so it is present in practice in any environment where pip-lock is used.

If you would rather keep dependencies = [], I am happy to swap this for a dependency-free normalization of the release segment instead — that covers the reported trailing-zero case but not the pre-release spellings. Say the word and I will push it.

Tests

Six tests added to TestGetMismatches. Verified they fail against the old comparison — with the one-line change reverted, test_no_mismatches_trailing_zero and test_no_mismatches_prerelease_spelling both fail; the four mismatch tests are regression guards that pass either way.

  • equivalence: 3.10 vs 3.10.0, 1.0alpha1 vs 1.0a1
  • still mismatches: differing release (3.10 vs 3.10.1), differing epoch (1!1.0 vs 1.0), two different non-PEP-440 strings
  • still equal: two identical non-PEP-440 strings

39 passed locally, ruff check and ruff format --check clean.

🤖 Generated with Claude Code

@dchaudhari7177
dchaudhari7177 force-pushed the fix/505-pep440-version-compare branch from e2fa6e3 to 15b7276 Compare August 14, 2026 16:23
`get_mismatches()` compared the pinned and installed versions as raw
strings, so any pair that differs only in spelling was reported as a
mismatch. The reported case is trailing-zero normalization: pip-compile
writes `gdal==3.10` while the installed distribution reports `3.10.0`,
and `pip-lock` failed with

    * Package gdal has version 3.10 but you have version 3.10.0 installed.

Compare with `packaging.version.Version` instead, which implements PEP
440 equivalence. Exact string equality is still checked first, so the
common case does not pay for parsing, and versions that are not valid
PEP 440 (`InvalidVersion`) fall back to that string comparison rather
than raising.

Genuinely different versions are unaffected: differing release segments
(`3.10` vs `3.10.1`) and differing epochs (`1!1.0` vs `1.0`) are still
mismatches, and both are covered by new tests.

Fixes adamchainz#505

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dchaudhari7177
dchaudhari7177 force-pushed the fix/505-pep440-version-compare branch from 15b7276 to d80c6fa Compare August 15, 2026 04:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant