astral-sh / uv

An extremely fast Python package and project manager, written in Rust.
https://docs.astral.sh/uv
Apache License 2.0
20.08k stars 597 forks source link

Consider defaulting to `~=` version_cmp instead of `>=` #6783

Open lucaspar opened 2 weeks ago

lucaspar commented 2 weeks ago

This "issue" might be the intended behavior of uv, but I believe it should be changed.

Currently, when adding a dependency without an explicit version constraint e.g. uv add numpy, uv adds a numpy>=2.1.0 to the pyproject.toml in order to track this requirement.

Most semver upgrades are minors and patches, so this is usually fine, but >= can be problematic when said package introduces a new major version (e.g. 3.0.0).

The default behavior of Poetry, for example, is to use the caret, where ^2.1.0 is equivalent to >=2.1.0 <3.0.0, thus protecting the project from an unintended breaking upgrade.

The uv's specifier >= however, will upgrade to the most recent major by default. PEP 440 introduced the ~= "compatible release clause" / tilde, which - IMO - makes more sense to serve as the default version constraint:

~= 2.1.0
# equals to
>= 2.1.0, == 2.1.*

Note this behavior is different from Poetry's caret notation, so, unless specified, the patch version could be safely omitted by default to allow minor upgrades, while still preventing major ones:

~= 2.1
# equals to
>= 2.1, == 2.*

This is a default behavior I'd like to see from uv to ease future project upgrades.

zanieb commented 2 weeks ago

Now that we have an application / library split I wonder if we can use that to determine if we should add upper bounds by default. Previously, we were quite opposed to this due to its effect on the ecosystem of published packages.

Some prior discussion at https://github.com/astral-sh/uv/issues/5178 — I think the rest of it was private. I previously compiled some references on the topic though.

lucaspar commented 2 weeks ago

ha, I figured this must had already been discussed before. Reading some of that convinced me that not setting upper bounds might be the best default for libraries. Most of my projects are applications though, and I often prefer to reserve some time to handle major upgrades manually. I see good points for either option though.

palotasb commented 2 weeks ago

I hope this is useful: https://iscinumpy.dev/post/bound-version-constraints/ It's a very detailed (and thus long...) article arguing for why the >= default is better than ~= for Python dependency constraints.

IMHO while >= is a no-contest winner for libraries, it makes sense for applications too. Develop using the >= constraints by default, explicitly update the lockfile regularly, test the changes, and use the lockfile when needing the stability in prod. Only add ~=/<= constraints when updating breaks something in practice.