Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

uv lock specifier instability? #6316

Closed
bluss opened this issue Aug 21, 2024 · 5 comments · Fixed by #6332
Closed

uv lock specifier instability? #6316

bluss opened this issue Aug 21, 2024 · 5 comments · Fixed by #6332
Assignees
Labels
bug Something isn't working lock Related to universal resolution and locking

Comments

@bluss
Copy link
Contributor

bluss commented Aug 21, 2024

In some cases I see the following diff in my project's lock file. The order of the specifiers is flipping back and forth. (Both sides of the diff are using uv 0.3.0).

The source is pyproject.toml's dependencies = ["numpy>=1.25.2,<2"] (with more dependencies specified).

Is it possible there is some leeway in the ordering for these? Using uv 0.3.0.

diff --git a/uv.lock b/uv.lock
index 53e5a5c..e5da145 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1329,7 +1329,7 @@ requires-dist = [
     { name = "jinja2", specifier = ">=3.1.2" },
     { name = "jupytext", specifier = ">=1.15.1" },
     { name = "nbconvert", specifier = ">=7.8.0" },
-    { name = "numpy", specifier = "<2,>=1.25.2" },
+    { name = "numpy", specifier = ">=1.25.2,<2" },
     { name = "openpyxl", specifier = ">=3.1.2" },

Seems to reproduce with the full dependency list, after a few uv sync and uv locks.
Using uv python pin 3.11. Defacto python is cpython-3.11.7-linux-x86_64-gnu.

(Edited here: reduced problem to only two dependencies)

[project]
name = "newproj"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.11"

dependencies = [
    "papermill>=2.4.0",
    "numpy>=1.25.2,<2",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

Seems to in fact correlate with adding/removing aiohttp too, which happens without change to pyproject.toml.

$ touch pyproject.toml  && uv sync
Resolved 96 packages in 1.28s
   Built newproj @ file:///home/user/tmp/newproj
Prepared 1 package in 151ms
Uninstalled 2 packages in 0.66ms
Installed 1 package in 0.28ms
 - aiohttp==3.10.5
 ~ newproj==0.1.0 (from file:///home/user/tmp/newproj)
$ touch pyproject.toml  && uv sync
Resolved 96 packages in 2ms
   Built newproj @ file:///home/user/tmp/newproj
Prepared 2 packages in 155ms
Uninstalled 1 package in 0.15ms
Installed 2 packages in 0.77ms
 + aiohttp==3.10.5
 ~ newproj==0.1.0 (from file:///home/user/tmp/newproj)
$ touch pyproject.toml  && uv sync
Resolved 96 packages in 2ms
   Built newproj @ file:///home/user/tmp/newproj
Prepared 1 package in 154ms
Uninstalled 1 package in 0.16ms
Installed 1 package in 0.34ms
 ~ newproj==0.1.0 (from file:///home/user/tmp/newproj)

@konstin
Copy link
Member

konstin commented Aug 21, 2024

Is this happening with a lockfile initialized with 0.3.0, i.e. does 0.3.0 ever write out <2,>=1.25.2 that wasn't there before? I've seen these cases upgrading to 0.3.0, but i can't reproduce it in 0.3.0.

@konstin
Copy link
Member

konstin commented Aug 21, 2024

For context, i have confirmed that hatchling flips the order of the specifier (uvx --from build pyproject-build --installer uv && unzip -p dist/dummy-0.1.0-py3-none-any.whl dummy-0.1.0.dist-info/METADATA | grep "Requires-Dist"), but we shouldn't be reading metadata through hatchling (or if we do, that's probably the source of the bug)

@bluss
Copy link
Contributor Author

bluss commented Aug 21, 2024

Yes, it happens when starting with uv 0.3.0.

This whimsical sequence of commands ends up with a diff like in the issue report

uv init --no-config newproj
cd newproj/
git init .
git add README.md  src/ pyproject.toml 
uv add  "papermill>=2.4.0" "numpy>=1.25.2,<2"
git add -u
git commit -m first
uv sync
git add uv.lock 
git commit -m lockfile
uv lock
git diff
uv sync
uv sync
git diff
touch pyproject.toml  && uv sync
touch pyproject.toml  && uv sync
touch pyproject.toml  && uv sync
git diff
diff --git uv.lock uv.lock
index 1f890d7..f606d83 100644
--- uv.lock
+++ uv.lock
@@ -346,7 +346,7 @@ dependencies = [
 
 [package.metadata]
 requires-dist = [
-    { name = "numpy", specifier = "<2,>=1.25.2" },
+    { name = "numpy", specifier = ">=1.25.2,<2" },
     { name = "papermill", specifier = ">=2.4.0" },
 ]

I forgot to pin python in this command sequence. So it reproduced with python 3.12.4 for me there.

@charliermarsh charliermarsh added bug Something isn't working lock Related to universal resolution and locking labels Aug 21, 2024
konstin added a commit that referenced this issue Aug 21, 2024
…lly from `pyproject.toml` or dynamically from the build backend.

Python's `packaging` [sorts](https://github.com/pypa/packaging/blob/cc938f984bbbe43c5734b9656c9837ab3a28191f/src/packaging/specifiers.py#L777) specifiers before emitting them, so all build backends built on top of it - such as hatchling - will change the specifier order compared to pyproject.toml. The core metadata spec does say "If a field is not marked as Dynamic, then the value of the field in any wheel built from the sdist MUST match the value in the sdist", but it doesn't specify if "match" means string equivalent or semantically equivalent, so it's arguable if that spec conformant. This change means that the specifiers have a different ordering when coming from the build backend than when read statically from pyproject.toml.

Previously, we tried to read path dep metadata in order:
* From the (built wheel) cache (`packaging` order)
* From pyproject.toml (verbatim specifier)
* From a fresh build (`packaging` order)

This behaviour is unstable: On the first run, we cache is cold, so we read the verbatim specifier from `pyproject.toml`, then we build and store the metadata in the cache. On the second run, we read the `packaging` sorted specifier from the cache.

Reproducer:

```shell
rm -rf newproj
uv init -q --no-config newproj
cd newproj/
uv add -q "anyio>=4,<5"
cat uv.lock | grep "requires-dist"
uv sync -q
cat uv.lock | grep "requires-dist"
cd ..
```

```
requires-dist = [{ name = "anyio", specifier = ">=4,<5" }]
requires-dist = [{ name = "anyio", specifier = "<5,>=4" }]
```

A project either has static metadata, so we can read from pyproject.toml, or it doesn't, and we always read from the build through `packaging`. We can use this to stabilize the behavior by slightly switching the order.

* From pyproject.toml (verbatim specifier)
* From the (built wheel) cache (`packaging` order)
* From a fresh build (`packaging` order)

Potentially, we still want to sort the specifiers we get anyway, after all, the is no guarantee that the specifiers from a build backend are deterministic. But our metadata reading behavior should be independent of the cache state, hence changing the order in the PR.

Fixes #6316
konstin added a commit that referenced this issue Aug 21, 2024
For a path dep such as the root project, uv can read metadata statically from `pyproject.toml` or dynamically from the build backend.

Python's `packaging` [sorts](https://github.com/pypa/packaging/blob/cc938f984bbbe43c5734b9656c9837ab3a28191f/src/packaging/specifiers.py#L777) specifiers before emitting them, so all build backends built on top of it - such as hatchling - will change the specifier order compared to pyproject.toml. The core metadata spec does say "If a field is not marked as Dynamic, then the value of the field in any wheel built from the sdist MUST match the value in the sdist", but it doesn't specify if "match" means string equivalent or semantically equivalent, so it's arguable if that spec conformant. This change means that the specifiers have a different ordering when coming from the build backend than when read statically from pyproject.toml.

Previously, we tried to read path dep metadata in order:
* From the (built wheel) cache (`packaging` order)
* From pyproject.toml (verbatim specifier)
* From a fresh build (`packaging` order)

This behaviour is unstable: On the first run, we cache is cold, so we read the verbatim specifier from `pyproject.toml`, then we build and store the metadata in the cache. On the second run, we read the `packaging` sorted specifier from the cache.

Reproducer:

```shell
rm -rf newproj
uv init -q --no-config newproj
cd newproj/
uv add -q "anyio>=4,<5"
cat uv.lock | grep "requires-dist"
uv sync -q
cat uv.lock | grep "requires-dist"
cd ..
```

```
requires-dist = [{ name = "anyio", specifier = ">=4,<5" }]
requires-dist = [{ name = "anyio", specifier = "<5,>=4" }]
```

A project either has static metadata, so we can read from pyproject.toml, or it doesn't, and we always read from the build through `packaging`. We can use this to stabilize the behavior by slightly switching the order.

* From pyproject.toml (verbatim specifier)
* From the (built wheel) cache (`packaging` order)
* From a fresh build (`packaging` order)

Potentially, we still want to sort the specifiers we get anyway, after all, the is no guarantee that the specifiers from a build backend are deterministic. But our metadata reading behavior should be independent of the cache state, hence changing the order in the PR.

Fixes #6316
konstin added a commit that referenced this issue Aug 21, 2024
For a path dep such as the root project, uv can read metadata statically from `pyproject.toml` or dynamically from the build backend.

Python's `packaging` [sorts](https://github.com/pypa/packaging/blob/cc938f984bbbe43c5734b9656c9837ab3a28191f/src/packaging/specifiers.py#L777) specifiers before emitting them, so all build backends built on top of it - such as hatchling - will change the specifier order compared to pyproject.toml. The core metadata spec does say "If a field is not marked as Dynamic, then the value of the field in any wheel built from the sdist MUST match the value in the sdist", but it doesn't specify if "match" means string equivalent or semantically equivalent, so it's arguable if that spec conformant. This change means that the specifiers have a different ordering when coming from the build backend than when read statically from pyproject.toml.

Previously, we tried to read path dep metadata in order:
* From the (built wheel) cache (`packaging` order)
* From pyproject.toml (verbatim specifier)
* From a fresh build (`packaging` order)

This behaviour is unstable: On the first run, we cache is cold, so we read the verbatim specifier from `pyproject.toml`, then we build and store the metadata in the cache. On the second run, we read the `packaging` sorted specifier from the cache.

Reproducer:

```shell
rm -rf newproj
uv init -q --no-config newproj
cd newproj/
uv add -q "anyio>=4,<5"
cat uv.lock | grep "requires-dist"
uv sync -q
cat uv.lock | grep "requires-dist"
cd ..
```

```
requires-dist = [{ name = "anyio", specifier = ">=4,<5" }]
requires-dist = [{ name = "anyio", specifier = "<5,>=4" }]
```

A project either has static metadata, so we can read from pyproject.toml, or it doesn't, and we always read from the build through `packaging`. We can use this to stabilize the behavior by slightly switching the order.

* From pyproject.toml (verbatim specifier)
* From the (built wheel) cache (`packaging` order)
* From a fresh build (`packaging` order)

Potentially, we still want to sort the specifiers we get anyway, after all, the is no guarantee that the specifiers from a build backend are deterministic. But our metadata reading behavior should be independent of the cache state, hence changing the order in the PR.

Fixes #6316
@konstin
Copy link
Member

konstin commented Aug 21, 2024

Thank you for the great reproducer!

konstin added a commit that referenced this issue Aug 21, 2024
For a path dep such as the root project, uv can read metadata statically
from `pyproject.toml` or dynamically from the build backend.

Python's `packaging`
[sorts](https://github.com/pypa/packaging/blob/cc938f984bbbe43c5734b9656c9837ab3a28191f/src/packaging/specifiers.py#L777)
specifiers before emitting them, so all build backends built on top of
it - such as hatchling - will change the specifier order compared to
pyproject.toml. The core metadata spec does say "If a field is not
marked as Dynamic, then the value of the field in any wheel built from
the sdist MUST match the value in the sdist", but it doesn't specify if
"match" means string equivalent or semantically equivalent, so it's
arguable if that spec conformant. This change means that the specifiers
have a different ordering when coming from the build backend than when
read statically from pyproject.toml.

Previously, we tried to read path dep metadata in order:
* From the (built wheel) cache (`packaging` order)
* From pyproject.toml (verbatim specifier)
* From a fresh build (`packaging` order)

This behaviour is unstable: On the first run, we cache is cold, so we
read the verbatim specifier from `pyproject.toml`, then we build and
store the metadata in the cache. On the second run, we read the
`packaging` sorted specifier from the cache.

Reproducer:

```shell
rm -rf newproj
uv init -q --no-config newproj
cd newproj/
uv add -q "anyio>=4,<5"
cat uv.lock | grep "requires-dist"
uv sync -q
cat uv.lock | grep "requires-dist"
cd ..
```

```
requires-dist = [{ name = "anyio", specifier = ">=4,<5" }]
requires-dist = [{ name = "anyio", specifier = "<5,>=4" }]
```

A project either has static metadata, so we can read from
pyproject.toml, or it doesn't, and we always read from the build through
`packaging`. We can use this to stabilize the behavior by slightly
switching the order.

* From pyproject.toml (verbatim specifier)
* From the (built wheel) cache (`packaging` order)
* From a fresh build (`packaging` order)

Potentially, we still want to sort the specifiers we get anyway, after
all, the is no guarantee that the specifiers from a build backend are
deterministic. But our metadata reading behavior should be independent
of the cache state, hence changing the order in the PR.

Fixes #6316
@bluss
Copy link
Contributor Author

bluss commented Aug 21, 2024

heh, thank you, your reduced sequence was better

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working lock Related to universal resolution and locking
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants