Checklist
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
- Lock once → succeeds;
idna is 3.10 (URL-pinned), requests is current.
- 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.) 🤖
Checklist
What happened?
When a pip dependency is pinned by a direct URL (e.g.
foo @ https://.../foo-1.2.3-...whl) and alockfile 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 thevendored Poetry solver with a placeholder version of
0.0.0instead of its real, already-knownversion.
The
0.0.0is not just cosmetic. Because the reconcile solve holds the URL-pinned package at0.0.0, any other package that constrains it by version (e.g. a transitivefoo >=1.0) can nolonger be satisfied at a normal version, and the solver reacts in one of two ways:
versions old enough that they no longer require a modern
foo, and emitsfooitself as0.0.0in the lockfile.(
SolverProblemErroron currentmain; older conda-lock surfaced this asKeyError: '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
idnaplusrequests(which requiresidna >=2.5,<4):idnais3.10(URL-pinned),requestsis current.idnais emitted as0.0.0andrequestsis silently downgraded to an ancient version (e.g.2.15.1) so that itsidnaconstraint no longer applies.
Tightening the constraint so no downgrade is possible (e.g.
requests >=2.30) turns the silentcorruption 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 thereconcile
solve_pypitwice; the0.0.0 → real versionchange is the only variable that flips theoutcome. 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):For a URL-sourced locked dependency the real version is available in
locked.version, butget_packagereplaces it with"0.0.0". The siblingelsebranch (same function) already does theright thing.
git blametraces the0.0.0line to the original URL-source support (2021) with noexplanatory comment, so it looks like an oversight rather than a deliberate choice.
Note that a
LockedDependency'ssourcecan only be a URL source (DependencySource.typeisLiteral["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
elsebranch:The dependency stays URL-pinned (
source_type/source_urlunchanged); only the bogus version iscorrected. Happy to open a PR with this change plus a regression test.
conda-lock version
0.0.0line is present unchanged onmainandv4.0.2(latest release).(Reported with AI assistance.) 🤖