Skip to content

Commit 5620348

Browse files
konstinzanieb
andauthored
Add uv add --bounds to configure the version constraint (#12946)
By default, uv uses only a lower bound in `uv add`, which avoids dependency conflicts due to upper bounds. With this PR, this cna be changed by setting a different bound kind. The bound kind can be configured in `uv.toml`, as a user preference, in `pyproject.toml`, as a project preference, or on the CLI, when adding a specific project. We add two options that add an upper bound on the constraint, one for SemVer (`>=1.2.3,<2.0.0`, dubbed "major", modeled after the SemVer caret) and another one for dependencies that make breaking changes in minor version (`>=1.2.3,<1.3.0`, dubbed "minor", modeled after the SemVer tilde). Intuitively, the major option bumps the most significant version component, while the minor option bumps the second most significant version component. There is also an exact bounds option (`==1.2.3`), though generally we recommend setting a wider bound and using the lockfile for pinning. Versions can have leading zeroes, such as `0.1` or `0.0.1`. For a single leading 0, we shift the the meaning of major and minor similar to cargo. For two or more leading zeroes, the difference between major and minor becomes inapplicable, instead both bump the most significant component: - major: `0.1` -> `>=0.1,<0.2` - major: `0.0.1` -> `>=0.0.1,<0.0.2` - major: `0.0.1.1` -> `>=0.0.1.1,<0.0.2.0` - major: `0.0.0.1` -> `>=0.0.0.1,<0.0.0.2` - minor: `0.1` -> `>=0.1,<0.1.1` - minor: `0.0.1` -> `>=0.0.1,<0.0.2` - minor: `0.0.1.1` -> `>=0.0.1.1,<0.0.2.0` - minor: `0.0.0.1` -> `>=0.0.0.1,<0.0.0.2` For a consistent appearance, we try to preserve the number of components in the upper bound. For example, adding a version `2.17` with the major option is stored as `>=2.17,<3.0`. If a version uses three components and is greater than 0, both bounds will also use three components (SemVer versions always have three components). Of the top 100 PyPI packages, 8 use a non-three-component version (docutils, idna, pycparser and soupsieve with two components, packaging, pytz and tzdata with two component, CalVer and trove-classifiers with four component CalVer). Example `pyproject.toml` files with the top 100 packages: [`--bounds major`](https://gist.github.com/konstin/0aaffa9ea53c4834c22759e8865409f4) and [`--bounds minor`](https://gist.github.com/konstin/e77f5e990a7efe8a3c8a97c5c5b76964). While many projects follow version scheme that roughly or directly matches the major or minor options, these compatibility ranges are usually not applicable for the also popular CalVer versioning. For pre-release versions, there are two framings we could take: One is that pre-releases generally make no guarantees about compatibility between them and are used to introduce breaking changes, so we should pin them exactly. In many cases however, pre-release specifiers are used because a project needs a bugfix or a feature that hasn't made it into a stable release, or because a project is compatible with the next version before a final version for that release is published. In those cases, compatibility with other packages that depend on the same library is more important, so the desired bound is the same as it would be for the stable release, except with the lower bound lowered to include pre-release. The names of the bounds and the name of the flag is up for bikeshedding. Currently, the option is call `tool.uv.bounds`, but we could also move it under `tool.uv.edit.bounds`, where it would be the first/only entry. Fixes #6783 --------- Co-authored-by: Zanie Blue <[email protected]>
1 parent 3ee8028 commit 5620348

File tree

18 files changed

+709
-46
lines changed

18 files changed

+709
-46
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/uv-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ uv-static = { workspace = true }
3232
uv-torch = { workspace = true, features = ["clap"] }
3333
uv-version = { workspace = true }
3434
uv-warnings = { workspace = true }
35+
uv-workspace = { workspace = true }
3536

3637
anstream = { workspace = true }
3738
anyhow = { workspace = true }

crates/uv-cli/src/lib.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use uv_redacted::DisplaySafeUrl;
2222
use uv_resolver::{AnnotationStyle, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode};
2323
use uv_static::EnvVars;
2424
use uv_torch::TorchMode;
25+
use uv_workspace::pyproject_mut::AddBoundsKind;
2526

2627
pub mod comma;
2728
pub mod compat;
@@ -836,10 +837,6 @@ pub enum ProjectCommand {
836837
/// it includes markers that differ from the existing specifier in which case another entry for
837838
/// the dependency will be added.
838839
///
839-
/// If no constraint or URL is provided for a dependency, a lower bound is added equal to the
840-
/// latest compatible version of the package, e.g., `>=1.2.3`, unless `--frozen` is provided, in
841-
/// which case no resolution is performed.
842-
///
843840
/// The lockfile and project environment will be updated to reflect the added dependencies. To
844841
/// skip updating the lockfile, use `--frozen`. To skip updating the environment, use
845842
/// `--no-sync`.
@@ -3562,6 +3559,19 @@ pub struct AddArgs {
35623559
)]
35633560
pub raw: bool,
35643561

3562+
/// The kind of version specifier to use when adding dependencies.
3563+
///
3564+
/// When adding a dependency to the project, if no constraint or URL is provided, a constraint
3565+
/// is added based on the latest compatible version of the package. By default, a lower bound
3566+
/// constraint is used, e.g., `>=1.2.3`.
3567+
///
3568+
/// When `--frozen` is provided, no resolution is performed, and dependencies are always added
3569+
/// without constraints.
3570+
///
3571+
/// This option is in preview and may change in any future release.
3572+
#[arg(long, value_enum)]
3573+
pub bounds: Option<AddBoundsKind>,
3574+
35653575
/// Commit to use when adding a dependency from Git.
35663576
#[arg(long, group = "git-ref", action = clap::ArgAction::Set)]
35673577
pub rev: Option<String>,

crates/uv-settings/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ uv-resolver = { workspace = true, features = ["schemars", "clap"] }
3333
uv-static = { workspace = true }
3434
uv-torch = { workspace = true, features = ["schemars", "clap"] }
3535
uv-warnings = { workspace = true }
36+
uv-workspace = { workspace = true, features = ["schemars", "clap"] }
3637

3738
clap = { workspace = true }
3839
fs-err = { workspace = true }

crates/uv-settings/src/combine.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
1414
use uv_redacted::DisplaySafeUrl;
1515
use uv_resolver::{AnnotationStyle, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode};
1616
use uv_torch::TorchMode;
17+
use uv_workspace::pyproject_mut::AddBoundsKind;
1718

1819
use crate::{FilesystemOptions, Options, PipOptions};
1920

@@ -74,6 +75,7 @@ macro_rules! impl_combine_or {
7475
};
7576
}
7677

78+
impl_combine_or!(AddBoundsKind);
7779
impl_combine_or!(AnnotationStyle);
7880
impl_combine_or!(ExcludeNewer);
7981
impl_combine_or!(ExportFormat);

crates/uv-settings/src/settings.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use uv_redacted::DisplaySafeUrl;
2020
use uv_resolver::{AnnotationStyle, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode};
2121
use uv_static::EnvVars;
2222
use uv_torch::TorchMode;
23+
use uv_workspace::pyproject_mut::AddBoundsKind;
2324

2425
/// A `pyproject.toml` with an (optional) `[tool.uv]` section.
2526
#[allow(dead_code)]
@@ -53,6 +54,9 @@ pub struct Options {
5354
#[serde(flatten)]
5455
pub publish: PublishOptions,
5556

57+
#[serde(flatten)]
58+
pub add: AddOptions,
59+
5660
#[option_group]
5761
pub pip: Option<PipOptions>,
5862

@@ -1841,6 +1845,10 @@ pub struct OptionsWire {
18411845
trusted_publishing: Option<TrustedPublishing>,
18421846
check_url: Option<IndexUrl>,
18431847

1848+
// #[serde(flatten)]
1849+
// add: AddOptions
1850+
add_bounds: Option<AddBoundsKind>,
1851+
18441852
pip: Option<PipOptions>,
18451853
cache_keys: Option<Vec<CacheKey>>,
18461854

@@ -1929,6 +1937,7 @@ impl From<OptionsWire> for Options {
19291937
dev_dependencies,
19301938
managed,
19311939
package,
1940+
add_bounds: bounds,
19321941
// Used by the build backend
19331942
build_backend,
19341943
} = value;
@@ -1996,6 +2005,7 @@ impl From<OptionsWire> for Options {
19962005
trusted_publishing,
19972006
check_url,
19982007
},
2008+
add: AddOptions { add_bounds: bounds },
19992009
workspace,
20002010
sources,
20012011
dev_dependencies,
@@ -2057,3 +2067,28 @@ pub struct PublishOptions {
20572067
)]
20582068
pub check_url: Option<IndexUrl>,
20592069
}
2070+
2071+
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, CombineOptions, OptionsMetadata)]
2072+
#[serde(rename_all = "kebab-case")]
2073+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2074+
pub struct AddOptions {
2075+
/// The default version specifier when adding a dependency.
2076+
///
2077+
/// When adding a dependency to the project, if no constraint or URL is provided, a constraint
2078+
/// is added based on the latest compatible version of the package. By default, a lower bound
2079+
/// constraint is used, e.g., `>=1.2.3`.
2080+
///
2081+
/// When `--frozen` is provided, no resolution is performed, and dependencies are always added
2082+
/// without constraints.
2083+
///
2084+
/// This option is in preview and may change in any future release.
2085+
#[option(
2086+
default = "\"lower\"",
2087+
value_type = "str",
2088+
example = r#"
2089+
add-bounds = "major"
2090+
"#,
2091+
possible_values = true
2092+
)]
2093+
pub add_bounds: Option<AddBoundsKind>,
2094+
}

crates/uv-workspace/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ uv-redacted = { workspace = true }
3131
uv-static = { workspace = true }
3232
uv-warnings = { workspace = true }
3333

34+
clap = { workspace = true, optional = true }
3435
fs-err = { workspace = true }
3536
glob = { workspace = true }
3637
itertools = { workspace = true }

0 commit comments

Comments
 (0)