Open lucaspar opened 2 months 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.
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.
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.
Many popular packages (fastapi, httpx, ..) use 0.X.Y versioning schema which is very difficult to work with when using ~=
. I also really dislike packages that enforce upper bound by default. I sometimes find myself stuck not being upgrade to a next major release (mostly backwards compatible) because of some random dependency that decided to safeguard me against it preemptively.
Hey @zanieb, for users exclusively using uv
as an application management tool, is there a way to change the default behavior of adding the lower bound specifier when uv add
is used without a explicit constraint?
I like to stick with semver patch versions parity between the pyproject.toml
and uv.lock
files (the ~=
version specifier), and let the CI upgrade the dependencies if the tests pass. The problem I find with the default >=
version specifier is that I really can't tell which version of a package I have installed unless I manually check the lock file. If for some reason the lock file is updated for a package that introduces breaking changes, I'm forced to manually downgrade the package (from which the ~=
specifier would save me).
Consider for example that uv itself introduces breaking changes when the minor version is updated. This is the case too for FastAPI.
A configuration option that allowed users to set the default version specifier strategy (set lower bound >=
, set strict version ==
, patch versions only ~=
, set upper bound <=
) when uv add
is called would be really helpful.
Currently, when adding a dependency without an explicit version constraint e.g.
uv add numpy
,uv
adds anumpy>=2.1.0
to thepyproject.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: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:
This is a default behavior I'd like to see from
uv
to ease future project upgrades.