Skip to content

URL-pinned pip deps are reconciled with a 0.0.0 placeholder version on re-lock, corrupting the solve #922

Description

@thomas-dresselhaus

Checklist

  • I added a descriptive title
  • I searched open reports and couldn't find a duplicate

What happened?

When a pip dependency is pinned by a direct URL (e.g. foo @ https://.../foo-1.2.3-...whl) and a
lockfile already exists, re-locking (any solve that reconciles against the existing lockfile —
--update, or a changed input under --check-input-hash) feeds that dependency back into the
vendored Poetry solver with a placeholder version of 0.0.0 instead of its real, already-known
version.

The 0.0.0 is not just cosmetic. Because the reconcile solve holds the URL-pinned package at
0.0.0, any other package that constrains it by version (e.g. a transitive foo >=1.0) can no
longer be satisfied at a normal version, and the solver reacts in one of two ways:

  • Silent corruption (common): it walks the constraining packages backwards to ancient
    versions old enough that they no longer require a modern foo, and emits foo itself as
    0.0.0 in the lockfile.
  • Hard failure: if there is no such downgrade escape, version solving fails outright
    (SolverProblemError on current main; older conda-lock surfaced this as KeyError: 'None').

A first-time lock (no existing lockfile) is unaffected, because the URL dependency is then resolved
fresh and gets its real version. The problem only appears on the second lock (reconciliation),
which makes it look intermittent.

Minimal reproduction

Two direct pip deps are enough — a URL-pinned idna plus requests (which requires
idna >=2.5,<4):

channels: [conda-forge]
dependencies:
  - python =3.12
  - pip
  - pip:
      - requests
      - idna @ https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl
  1. Lock once → succeeds; idna is 3.10 (URL-pinned), requests is current.
  2. Re-lock (reconcile against the existing lockfile) → idna is emitted as 0.0.0 and
    requests is silently downgraded to an ancient version (e.g. 2.15.1) so that its idna
    constraint no longer applies.

Tightening the constraint so no downgrade is possible (e.g. requests >=2.30) turns the silent
corruption into a hard SolverProblemError ("… depends on both idna @ and requests (>=2.30),
version solving failed").

(Reproduced against main / conda-lock 4.0.2 with the vendored Poetry 2.0.1, by driving the
reconcile solve_pypi twice; the 0.0.0 → real version change is the only variable that flips the
outcome. Older conda-lock versions failed with KeyError: 'None' on the same class of graph.)

Root cause

conda_lock/pypi_solver.py, get_package (used only in the reconcile solve):

def get_package(locked: LockedDependency) -> PoetryPackage:
    if locked.source is not None:
        return PoetryPackage(
            locked.name,
            source_type="url",
            source_url=locked.source.url,
            version="0.0.0",          # <-- discards the real version
        )
    else:
        return PoetryPackage(locked.name, version=locked.version)   # <-- non-URL path is correct

For a URL-sourced locked dependency the real version is available in locked.version, but
get_package replaces it with "0.0.0". The sibling else branch (same function) already does the
right thing. git blame traces the 0.0.0 line to the original URL-source support (2021) with no
explanatory comment, so it looks like an oversight rather than a deliberate choice.

Note that a LockedDependency's source can only be a URL source (DependencySource.type is
Literal["url"]), so this branch always represents a URL pin.

Expected

Reconciling a URL-sourced locked dependency should preserve its real version (while keeping the URL
pin), so that transitive version constraints still resolve.

Proposed fix

Use the real version in the URL branch, mirroring the else branch:

    if locked.source is not None:
        return PoetryPackage(
            locked.name,
            source_type="url",
            source_url=locked.source.url,
            version=locked.version,
        )

The dependency stays URL-pinned (source_type/source_url unchanged); only the bogus version is
corrected. Happy to open a PR with this change plus a regression test.

conda-lock version

0.0.0 line is present unchanged on main and v4.0.2 (latest release).

(Reported with AI assistance.) 🤖

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions