mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
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 <contact@zanie.dev>
This commit is contained in:
parent
3ee8028bb4
commit
56203484a2
18 changed files with 709 additions and 46 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -4886,6 +4886,7 @@ dependencies = [
|
|||
"uv-torch",
|
||||
"uv-version",
|
||||
"uv-warnings",
|
||||
"uv-workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -5777,6 +5778,7 @@ dependencies = [
|
|||
"uv-static",
|
||||
"uv-torch",
|
||||
"uv-warnings",
|
||||
"uv-workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -5939,6 +5941,7 @@ version = "0.0.1"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_fs",
|
||||
"clap",
|
||||
"fs-err 3.1.0",
|
||||
"glob",
|
||||
"insta",
|
||||
|
|
|
@ -32,6 +32,7 @@ uv-static = { workspace = true }
|
|||
uv-torch = { workspace = true, features = ["clap"] }
|
||||
uv-version = { workspace = true }
|
||||
uv-warnings = { workspace = true }
|
||||
uv-workspace = { workspace = true }
|
||||
|
||||
anstream = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
|
|
|
@ -22,6 +22,7 @@ use uv_redacted::DisplaySafeUrl;
|
|||
use uv_resolver::{AnnotationStyle, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode};
|
||||
use uv_static::EnvVars;
|
||||
use uv_torch::TorchMode;
|
||||
use uv_workspace::pyproject_mut::AddBoundsKind;
|
||||
|
||||
pub mod comma;
|
||||
pub mod compat;
|
||||
|
@ -836,10 +837,6 @@ pub enum ProjectCommand {
|
|||
/// it includes markers that differ from the existing specifier in which case another entry for
|
||||
/// the dependency will be added.
|
||||
///
|
||||
/// If no constraint or URL is provided for a dependency, a lower bound is added equal to the
|
||||
/// latest compatible version of the package, e.g., `>=1.2.3`, unless `--frozen` is provided, in
|
||||
/// which case no resolution is performed.
|
||||
///
|
||||
/// The lockfile and project environment will be updated to reflect the added dependencies. To
|
||||
/// skip updating the lockfile, use `--frozen`. To skip updating the environment, use
|
||||
/// `--no-sync`.
|
||||
|
@ -3562,6 +3559,19 @@ pub struct AddArgs {
|
|||
)]
|
||||
pub raw: bool,
|
||||
|
||||
/// The kind of version specifier to use when adding dependencies.
|
||||
///
|
||||
/// When adding a dependency to the project, if no constraint or URL is provided, a constraint
|
||||
/// is added based on the latest compatible version of the package. By default, a lower bound
|
||||
/// constraint is used, e.g., `>=1.2.3`.
|
||||
///
|
||||
/// When `--frozen` is provided, no resolution is performed, and dependencies are always added
|
||||
/// without constraints.
|
||||
///
|
||||
/// This option is in preview and may change in any future release.
|
||||
#[arg(long, value_enum)]
|
||||
pub bounds: Option<AddBoundsKind>,
|
||||
|
||||
/// Commit to use when adding a dependency from Git.
|
||||
#[arg(long, group = "git-ref", action = clap::ArgAction::Set)]
|
||||
pub rev: Option<String>,
|
||||
|
|
|
@ -33,6 +33,7 @@ uv-resolver = { workspace = true, features = ["schemars", "clap"] }
|
|||
uv-static = { workspace = true }
|
||||
uv-torch = { workspace = true, features = ["schemars", "clap"] }
|
||||
uv-warnings = { workspace = true }
|
||||
uv-workspace = { workspace = true, features = ["schemars", "clap"] }
|
||||
|
||||
clap = { workspace = true }
|
||||
fs-err = { workspace = true }
|
||||
|
|
|
@ -14,6 +14,7 @@ use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
|
|||
use uv_redacted::DisplaySafeUrl;
|
||||
use uv_resolver::{AnnotationStyle, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode};
|
||||
use uv_torch::TorchMode;
|
||||
use uv_workspace::pyproject_mut::AddBoundsKind;
|
||||
|
||||
use crate::{FilesystemOptions, Options, PipOptions};
|
||||
|
||||
|
@ -74,6 +75,7 @@ macro_rules! impl_combine_or {
|
|||
};
|
||||
}
|
||||
|
||||
impl_combine_or!(AddBoundsKind);
|
||||
impl_combine_or!(AnnotationStyle);
|
||||
impl_combine_or!(ExcludeNewer);
|
||||
impl_combine_or!(ExportFormat);
|
||||
|
|
|
@ -20,6 +20,7 @@ use uv_redacted::DisplaySafeUrl;
|
|||
use uv_resolver::{AnnotationStyle, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode};
|
||||
use uv_static::EnvVars;
|
||||
use uv_torch::TorchMode;
|
||||
use uv_workspace::pyproject_mut::AddBoundsKind;
|
||||
|
||||
/// A `pyproject.toml` with an (optional) `[tool.uv]` section.
|
||||
#[allow(dead_code)]
|
||||
|
@ -53,6 +54,9 @@ pub struct Options {
|
|||
#[serde(flatten)]
|
||||
pub publish: PublishOptions,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub add: AddOptions,
|
||||
|
||||
#[option_group]
|
||||
pub pip: Option<PipOptions>,
|
||||
|
||||
|
@ -1841,6 +1845,10 @@ pub struct OptionsWire {
|
|||
trusted_publishing: Option<TrustedPublishing>,
|
||||
check_url: Option<IndexUrl>,
|
||||
|
||||
// #[serde(flatten)]
|
||||
// add: AddOptions
|
||||
add_bounds: Option<AddBoundsKind>,
|
||||
|
||||
pip: Option<PipOptions>,
|
||||
cache_keys: Option<Vec<CacheKey>>,
|
||||
|
||||
|
@ -1929,6 +1937,7 @@ impl From<OptionsWire> for Options {
|
|||
dev_dependencies,
|
||||
managed,
|
||||
package,
|
||||
add_bounds: bounds,
|
||||
// Used by the build backend
|
||||
build_backend,
|
||||
} = value;
|
||||
|
@ -1996,6 +2005,7 @@ impl From<OptionsWire> for Options {
|
|||
trusted_publishing,
|
||||
check_url,
|
||||
},
|
||||
add: AddOptions { add_bounds: bounds },
|
||||
workspace,
|
||||
sources,
|
||||
dev_dependencies,
|
||||
|
@ -2057,3 +2067,28 @@ pub struct PublishOptions {
|
|||
)]
|
||||
pub check_url: Option<IndexUrl>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, CombineOptions, OptionsMetadata)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct AddOptions {
|
||||
/// The default version specifier when adding a dependency.
|
||||
///
|
||||
/// When adding a dependency to the project, if no constraint or URL is provided, a constraint
|
||||
/// is added based on the latest compatible version of the package. By default, a lower bound
|
||||
/// constraint is used, e.g., `>=1.2.3`.
|
||||
///
|
||||
/// When `--frozen` is provided, no resolution is performed, and dependencies are always added
|
||||
/// without constraints.
|
||||
///
|
||||
/// This option is in preview and may change in any future release.
|
||||
#[option(
|
||||
default = "\"lower\"",
|
||||
value_type = "str",
|
||||
example = r#"
|
||||
add-bounds = "major"
|
||||
"#,
|
||||
possible_values = true
|
||||
)]
|
||||
pub add_bounds: Option<AddBoundsKind>,
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ uv-redacted = { workspace = true }
|
|||
uv-static = { workspace = true }
|
||||
uv-warnings = { workspace = true }
|
||||
|
||||
clap = { workspace = true, optional = true }
|
||||
fs-err = { workspace = true }
|
||||
glob = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use std::{fmt, mem};
|
||||
|
||||
use itertools::Itertools;
|
||||
use std::{fmt, iter, mem};
|
||||
use thiserror::Error;
|
||||
use toml_edit::{
|
||||
Array, ArrayOfTables, DocumentMut, Formatted, Item, RawString, Table, TomlError, Value,
|
||||
|
@ -50,6 +51,8 @@ pub enum Error {
|
|||
package_name: PackageName,
|
||||
requirements: Vec<Requirement>,
|
||||
},
|
||||
#[error("Unknown bound king {0}")]
|
||||
UnknownBoundKind(String),
|
||||
}
|
||||
|
||||
/// The result of editing an array in a TOML document.
|
||||
|
@ -83,6 +86,169 @@ impl ArrayEdit {
|
|||
}
|
||||
}
|
||||
|
||||
/// The default version specifier when adding a dependency.
|
||||
// While PEP 440 allows an arbitrary number of version digits, the `major` and `minor` build on
|
||||
// most projects sticking to two or three components and a SemVer-ish versioning system, so can
|
||||
// bump the major or minor version of a major.minor or major.minor.patch input version.
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum AddBoundsKind {
|
||||
/// Only a lower bound, e.g., `>=1.2.3`.
|
||||
#[default]
|
||||
Lower,
|
||||
/// Allow the same major version, similar to the semver caret, e.g., `>=1.2.3, <2.0.0`.
|
||||
///
|
||||
/// Leading zeroes are skipped, e.g. `>=0.1.2, <0.2.0`.
|
||||
Major,
|
||||
/// Allow the same minor version, similar to the semver tilde, e.g., `>=1.2.3, <1.3.0`.
|
||||
///
|
||||
/// Leading zeroes are skipped, e.g. `>=0.1.2, <0.1.3`.
|
||||
Minor,
|
||||
/// Pin the exact version, e.g., `==1.2.3`.
|
||||
///
|
||||
/// This option is not recommended, as versions are already pinned in the uv lockfile.
|
||||
Exact,
|
||||
}
|
||||
|
||||
impl Display for AddBoundsKind {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Lower => write!(f, "lower"),
|
||||
Self::Major => write!(f, "major"),
|
||||
Self::Minor => write!(f, "minor"),
|
||||
Self::Exact => write!(f, "exact"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AddBoundsKind {
|
||||
fn specifiers(self, version: Version) -> VersionSpecifiers {
|
||||
// Nomenclature: "major" is the most significant component of the version, "minor" is the
|
||||
// second most significant component, so most versions are either major.minor.patch or
|
||||
// 0.major.minor.
|
||||
match self {
|
||||
AddBoundsKind::Lower => {
|
||||
VersionSpecifiers::from(VersionSpecifier::greater_than_equal_version(version))
|
||||
}
|
||||
AddBoundsKind::Major => {
|
||||
let leading_zeroes = version
|
||||
.release()
|
||||
.iter()
|
||||
.take_while(|digit| **digit == 0)
|
||||
.count();
|
||||
|
||||
// Special case: The version is 0.
|
||||
if leading_zeroes == version.release().len() {
|
||||
let upper_bound = Version::new(
|
||||
[0, 1]
|
||||
.into_iter()
|
||||
.chain(iter::repeat_n(0, version.release().iter().skip(2).len())),
|
||||
);
|
||||
return VersionSpecifiers::from_iter([
|
||||
VersionSpecifier::greater_than_equal_version(version),
|
||||
VersionSpecifier::less_than_version(upper_bound),
|
||||
]);
|
||||
}
|
||||
|
||||
// Compute the new major version and pad it to the same length:
|
||||
// 1.2.3 -> 2.0.0
|
||||
// 1.2 -> 2.0
|
||||
// 1 -> 2
|
||||
// We ignore leading zeroes, adding Semver-style semantics to 0.x versions, too:
|
||||
// 0.1.2 -> 0.2.0
|
||||
// 0.0.1 -> 0.0.2
|
||||
let major = version.release().get(leading_zeroes).copied().unwrap_or(0);
|
||||
// The length of the lower bound minus the leading zero and bumped component.
|
||||
let trailing_zeros = version.release().iter().skip(leading_zeroes + 1).len();
|
||||
let upper_bound = Version::new(
|
||||
iter::repeat_n(0, leading_zeroes)
|
||||
.chain(iter::once(major + 1))
|
||||
.chain(iter::repeat_n(0, trailing_zeros)),
|
||||
);
|
||||
|
||||
VersionSpecifiers::from_iter([
|
||||
VersionSpecifier::greater_than_equal_version(version),
|
||||
VersionSpecifier::less_than_version(upper_bound),
|
||||
])
|
||||
}
|
||||
AddBoundsKind::Minor => {
|
||||
let leading_zeroes = version
|
||||
.release()
|
||||
.iter()
|
||||
.take_while(|digit| **digit == 0)
|
||||
.count();
|
||||
|
||||
// Special case: The version is 0.
|
||||
if leading_zeroes == version.release().len() {
|
||||
let upper_bound = [0, 0, 1]
|
||||
.into_iter()
|
||||
.chain(iter::repeat_n(0, version.release().iter().skip(3).len()));
|
||||
return VersionSpecifiers::from_iter([
|
||||
VersionSpecifier::greater_than_equal_version(version),
|
||||
VersionSpecifier::less_than_version(Version::new(upper_bound)),
|
||||
]);
|
||||
}
|
||||
|
||||
// If both major and minor version are 0, the concept of bumping the minor version
|
||||
// instead of the major version is not useful. Instead, we bump the next
|
||||
// non-zero part of the version. This avoids extending the three components of 0.0.1
|
||||
// to the four components of 0.0.1.1.
|
||||
if leading_zeroes >= 2 {
|
||||
let most_significant =
|
||||
version.release().get(leading_zeroes).copied().unwrap_or(0);
|
||||
// The length of the lower bound minus the leading zero and bumped component.
|
||||
let trailing_zeros = version.release().iter().skip(leading_zeroes + 1).len();
|
||||
let upper_bound = Version::new(
|
||||
iter::repeat_n(0, leading_zeroes)
|
||||
.chain(iter::once(most_significant + 1))
|
||||
.chain(iter::repeat_n(0, trailing_zeros)),
|
||||
);
|
||||
return VersionSpecifiers::from_iter([
|
||||
VersionSpecifier::greater_than_equal_version(version),
|
||||
VersionSpecifier::less_than_version(upper_bound),
|
||||
]);
|
||||
}
|
||||
|
||||
// Compute the new minor version and pad it to the same length where possible:
|
||||
// 1.2.3 -> 1.3.0
|
||||
// 1.2 -> 1.3
|
||||
// 1 -> 1.1
|
||||
// We ignore leading zero, adding Semver-style semantics to 0.x versions, too:
|
||||
// 0.1.2 -> 0.1.3
|
||||
// 0.0.1 -> 0.0.2
|
||||
|
||||
// If the version has only one digit, say `1`, or if there are only leading zeroes,
|
||||
// pad with zeroes.
|
||||
let major = version.release().get(leading_zeroes).copied().unwrap_or(0);
|
||||
let minor = version
|
||||
.release()
|
||||
.get(leading_zeroes + 1)
|
||||
.copied()
|
||||
.unwrap_or(0);
|
||||
let upper_bound = Version::new(
|
||||
iter::repeat_n(0, leading_zeroes)
|
||||
.chain(iter::once(major))
|
||||
.chain(iter::once(minor + 1))
|
||||
.chain(iter::repeat_n(
|
||||
0,
|
||||
version.release().iter().skip(leading_zeroes + 2).len(),
|
||||
)),
|
||||
);
|
||||
|
||||
VersionSpecifiers::from_iter([
|
||||
VersionSpecifier::greater_than_equal_version(version),
|
||||
VersionSpecifier::less_than_version(upper_bound),
|
||||
])
|
||||
}
|
||||
AddBoundsKind::Exact => {
|
||||
VersionSpecifiers::from_iter([VersionSpecifier::equals_version(version)])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Specifies whether dependencies are added to a script file or a `pyproject.toml` file.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum DependencyTarget {
|
||||
|
@ -523,22 +689,19 @@ impl PyProjectTomlMut {
|
|||
Ok(added)
|
||||
}
|
||||
|
||||
/// Set the minimum version for an existing dependency.
|
||||
pub fn set_dependency_minimum_version(
|
||||
/// Set the constraint for a requirement for an existing dependency.
|
||||
pub fn set_dependency_bound(
|
||||
&mut self,
|
||||
dependency_type: &DependencyType,
|
||||
index: usize,
|
||||
version: Version,
|
||||
bound_kind: AddBoundsKind,
|
||||
) -> Result<(), Error> {
|
||||
let group = match dependency_type {
|
||||
DependencyType::Production => self.set_project_dependency_minimum_version()?,
|
||||
DependencyType::Dev => self.set_dev_dependency_minimum_version()?,
|
||||
DependencyType::Optional(extra) => {
|
||||
self.set_optional_dependency_minimum_version(extra)?
|
||||
}
|
||||
DependencyType::Group(group) => {
|
||||
self.set_dependency_group_requirement_minimum_version(group)?
|
||||
}
|
||||
DependencyType::Production => self.dependencies_array()?,
|
||||
DependencyType::Dev => self.dev_dependencies_array()?,
|
||||
DependencyType::Optional(extra) => self.optional_dependencies_array(extra)?,
|
||||
DependencyType::Group(group) => self.dependency_groups_array(group)?,
|
||||
};
|
||||
|
||||
let Some(req) = group.get(index) else {
|
||||
|
@ -549,16 +712,16 @@ impl PyProjectTomlMut {
|
|||
.as_str()
|
||||
.and_then(try_parse_requirement)
|
||||
.ok_or(Error::MalformedDependencies)?;
|
||||
req.version_or_url = Some(VersionOrUrl::VersionSpecifier(VersionSpecifiers::from(
|
||||
VersionSpecifier::greater_than_equal_version(version),
|
||||
)));
|
||||
req.version_or_url = Some(VersionOrUrl::VersionSpecifier(
|
||||
bound_kind.specifiers(version),
|
||||
));
|
||||
group.replace(index, req.to_string());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the minimum version for an existing dependency in `project.dependencies`.
|
||||
fn set_project_dependency_minimum_version(&mut self) -> Result<&mut Array, Error> {
|
||||
/// Get the TOML array for `project.dependencies`.
|
||||
fn dependencies_array(&mut self) -> Result<&mut Array, Error> {
|
||||
// Get or create `project.dependencies`.
|
||||
let dependencies = self
|
||||
.project()?
|
||||
|
@ -570,8 +733,8 @@ impl PyProjectTomlMut {
|
|||
Ok(dependencies)
|
||||
}
|
||||
|
||||
/// Set the minimum version for an existing dependency in `tool.uv.dev-dependencies`.
|
||||
fn set_dev_dependency_minimum_version(&mut self) -> Result<&mut Array, Error> {
|
||||
/// Get the TOML array for `tool.uv.dev-dependencies`.
|
||||
fn dev_dependencies_array(&mut self) -> Result<&mut Array, Error> {
|
||||
// Get or create `tool.uv.dev-dependencies`.
|
||||
let dev_dependencies = self
|
||||
.doc
|
||||
|
@ -591,11 +754,8 @@ impl PyProjectTomlMut {
|
|||
Ok(dev_dependencies)
|
||||
}
|
||||
|
||||
/// Set the minimum version for an existing dependency in `project.optional-dependencies`.
|
||||
fn set_optional_dependency_minimum_version(
|
||||
&mut self,
|
||||
group: &ExtraName,
|
||||
) -> Result<&mut Array, Error> {
|
||||
/// Get the TOML array for a `project.optional-dependencies` entry.
|
||||
fn optional_dependencies_array(&mut self, group: &ExtraName) -> Result<&mut Array, Error> {
|
||||
// Get or create `project.optional-dependencies`.
|
||||
let optional_dependencies = self
|
||||
.project()?
|
||||
|
@ -623,11 +783,8 @@ impl PyProjectTomlMut {
|
|||
Ok(group)
|
||||
}
|
||||
|
||||
/// Set the minimum version for an existing dependency in `dependency-groups`.
|
||||
fn set_dependency_group_requirement_minimum_version(
|
||||
&mut self,
|
||||
group: &GroupName,
|
||||
) -> Result<&mut Array, Error> {
|
||||
/// Get the TOML array for a `dependency-groups` entry.
|
||||
fn dependency_groups_array(&mut self, group: &GroupName) -> Result<&mut Array, Error> {
|
||||
// Get or create `dependency-groups`.
|
||||
let dependency_groups = self
|
||||
.doc
|
||||
|
@ -1485,7 +1642,9 @@ fn split_specifiers(req: &str) -> (&str, &str) {
|
|||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::split_specifiers;
|
||||
use super::{AddBoundsKind, split_specifiers};
|
||||
use std::str::FromStr;
|
||||
use uv_pep440::Version;
|
||||
|
||||
#[test]
|
||||
fn split() {
|
||||
|
@ -1506,4 +1665,107 @@ mod test {
|
|||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bound_kind_to_specifiers_exact() {
|
||||
let tests = [
|
||||
("0", "==0"),
|
||||
("0.0", "==0.0"),
|
||||
("0.0.0", "==0.0.0"),
|
||||
("0.1", "==0.1"),
|
||||
("0.0.1", "==0.0.1"),
|
||||
("0.0.0.1", "==0.0.0.1"),
|
||||
("1.0.0", "==1.0.0"),
|
||||
("1.2", "==1.2"),
|
||||
("1.2.3", "==1.2.3"),
|
||||
("1.2.3.4", "==1.2.3.4"),
|
||||
("1.2.3.4a1.post1", "==1.2.3.4a1.post1"),
|
||||
];
|
||||
|
||||
for (version, expected) in tests {
|
||||
let actual = AddBoundsKind::Exact
|
||||
.specifiers(Version::from_str(version).unwrap())
|
||||
.to_string();
|
||||
assert_eq!(actual, expected, "{version}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bound_kind_to_specifiers_lower() {
|
||||
let tests = [
|
||||
("0", ">=0"),
|
||||
("0.0", ">=0.0"),
|
||||
("0.0.0", ">=0.0.0"),
|
||||
("0.1", ">=0.1"),
|
||||
("0.0.1", ">=0.0.1"),
|
||||
("0.0.0.1", ">=0.0.0.1"),
|
||||
("1", ">=1"),
|
||||
("1.0.0", ">=1.0.0"),
|
||||
("1.2", ">=1.2"),
|
||||
("1.2.3", ">=1.2.3"),
|
||||
("1.2.3.4", ">=1.2.3.4"),
|
||||
("1.2.3.4a1.post1", ">=1.2.3.4a1.post1"),
|
||||
];
|
||||
|
||||
for (version, expected) in tests {
|
||||
let actual = AddBoundsKind::Lower
|
||||
.specifiers(Version::from_str(version).unwrap())
|
||||
.to_string();
|
||||
assert_eq!(actual, expected, "{version}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bound_kind_to_specifiers_major() {
|
||||
let tests = [
|
||||
("0", ">=0, <0.1"),
|
||||
("0.0", ">=0.0, <0.1"),
|
||||
("0.0.0", ">=0.0.0, <0.1.0"),
|
||||
("0.0.0.0", ">=0.0.0.0, <0.1.0.0"),
|
||||
("0.1", ">=0.1, <0.2"),
|
||||
("0.0.1", ">=0.0.1, <0.0.2"),
|
||||
("0.0.1.1", ">=0.0.1.1, <0.0.2.0"),
|
||||
("0.0.0.1", ">=0.0.0.1, <0.0.0.2"),
|
||||
("1", ">=1, <2"),
|
||||
("1.0.0", ">=1.0.0, <2.0.0"),
|
||||
("1.2", ">=1.2, <2.0"),
|
||||
("1.2.3", ">=1.2.3, <2.0.0"),
|
||||
("1.2.3.4", ">=1.2.3.4, <2.0.0.0"),
|
||||
("1.2.3.4a1.post1", ">=1.2.3.4a1.post1, <2.0.0.0"),
|
||||
];
|
||||
|
||||
for (version, expected) in tests {
|
||||
let actual = AddBoundsKind::Major
|
||||
.specifiers(Version::from_str(version).unwrap())
|
||||
.to_string();
|
||||
assert_eq!(actual, expected, "{version}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bound_kind_to_specifiers_minor() {
|
||||
let tests = [
|
||||
("0", ">=0, <0.0.1"),
|
||||
("0.0", ">=0.0, <0.0.1"),
|
||||
("0.0.0", ">=0.0.0, <0.0.1"),
|
||||
("0.0.0.0", ">=0.0.0.0, <0.0.1.0"),
|
||||
("0.1", ">=0.1, <0.1.1"),
|
||||
("0.0.1", ">=0.0.1, <0.0.2"),
|
||||
("0.0.1.1", ">=0.0.1.1, <0.0.2.0"),
|
||||
("0.0.0.1", ">=0.0.0.1, <0.0.0.2"),
|
||||
("1", ">=1, <1.1"),
|
||||
("1.0.0", ">=1.0.0, <1.1.0"),
|
||||
("1.2", ">=1.2, <1.3"),
|
||||
("1.2.3", ">=1.2.3, <1.3.0"),
|
||||
("1.2.3.4", ">=1.2.3.4, <1.3.0.0"),
|
||||
("1.2.3.4a1.post1", ">=1.2.3.4a1.post1, <1.3.0.0"),
|
||||
];
|
||||
|
||||
for (version, expected) in tests {
|
||||
let actual = AddBoundsKind::Minor
|
||||
.specifiers(Version::from_str(version).unwrap())
|
||||
.to_string();
|
||||
assert_eq!(actual, expected, "{version}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ uv-types = { workspace = true }
|
|||
uv-version = { workspace = true }
|
||||
uv-virtualenv = { workspace = true }
|
||||
uv-warnings = { workspace = true }
|
||||
uv-workspace = { workspace = true }
|
||||
uv-workspace = { workspace = true, features = ["clap"] }
|
||||
|
||||
anstream = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
|
|
|
@ -41,7 +41,7 @@ use uv_settings::PythonInstallMirrors;
|
|||
use uv_types::{BuildIsolation, HashStrategy};
|
||||
use uv_warnings::warn_user_once;
|
||||
use uv_workspace::pyproject::{DependencyType, Source, SourceError, Sources, ToolUvSources};
|
||||
use uv_workspace::pyproject_mut::{ArrayEdit, DependencyTarget, PyProjectTomlMut};
|
||||
use uv_workspace::pyproject_mut::{AddBoundsKind, ArrayEdit, DependencyTarget, PyProjectTomlMut};
|
||||
use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace, WorkspaceCache};
|
||||
|
||||
use crate::commands::pip::loggers::{
|
||||
|
@ -74,6 +74,7 @@ pub(crate) async fn add(
|
|||
editable: Option<bool>,
|
||||
dependency_type: DependencyType,
|
||||
raw: bool,
|
||||
bounds: Option<AddBoundsKind>,
|
||||
indexes: Vec<Index>,
|
||||
rev: Option<String>,
|
||||
tag: Option<String>,
|
||||
|
@ -94,6 +95,10 @@ pub(crate) async fn add(
|
|||
printer: Printer,
|
||||
preview: PreviewMode,
|
||||
) -> Result<ExitStatus> {
|
||||
if bounds.is_some() && preview.is_disabled() {
|
||||
warn_user_once!("The bounds option is in preview and may change in any future release.");
|
||||
}
|
||||
|
||||
for source in &requirements {
|
||||
match source {
|
||||
RequirementsSource::PyprojectToml(_) => {
|
||||
|
@ -533,6 +538,7 @@ pub(crate) async fn add(
|
|||
locked,
|
||||
&dependency_type,
|
||||
raw,
|
||||
bounds,
|
||||
constraints,
|
||||
&settings,
|
||||
&network_settings,
|
||||
|
@ -759,6 +765,7 @@ async fn lock_and_sync(
|
|||
locked: bool,
|
||||
dependency_type: &DependencyType,
|
||||
raw: bool,
|
||||
bound_kind: Option<AddBoundsKind>,
|
||||
constraints: Vec<NameRequirementSpecification>,
|
||||
settings: &ResolverInstallerSettings,
|
||||
network_settings: &NetworkSettings,
|
||||
|
@ -834,6 +841,15 @@ async fn lock_and_sync(
|
|||
None => true,
|
||||
};
|
||||
if !is_empty {
|
||||
if let Some(bound_kind) = bound_kind {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{} Using explicit requirement `{}` over bounds preference `{}`",
|
||||
"note:".bold(),
|
||||
edit.requirement,
|
||||
bound_kind
|
||||
)?;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -846,7 +862,12 @@ async fn lock_and_sync(
|
|||
// For example, convert `1.2.3+local` to `1.2.3`.
|
||||
let minimum = (*minimum).clone().without_local();
|
||||
|
||||
toml.set_dependency_minimum_version(&edit.dependency_type, *index, minimum)?;
|
||||
toml.set_dependency_bound(
|
||||
&edit.dependency_type,
|
||||
*index,
|
||||
minimum,
|
||||
bound_kind.unwrap_or_default(),
|
||||
)?;
|
||||
|
||||
modified = true;
|
||||
}
|
||||
|
|
|
@ -1907,6 +1907,7 @@ async fn run_project(
|
|||
args.editable,
|
||||
args.dependency_type,
|
||||
args.raw,
|
||||
args.bounds,
|
||||
args.indexes,
|
||||
args.rev,
|
||||
args.tag,
|
||||
|
|
|
@ -45,6 +45,7 @@ use uv_static::EnvVars;
|
|||
use uv_torch::TorchMode;
|
||||
use uv_warnings::warn_user_once;
|
||||
use uv_workspace::pyproject::DependencyType;
|
||||
use uv_workspace::pyproject_mut::AddBoundsKind;
|
||||
|
||||
use crate::commands::ToolRunCommand;
|
||||
use crate::commands::{InitKind, InitProjectKind, pip::operations::Modifications};
|
||||
|
@ -1261,6 +1262,7 @@ pub(crate) struct AddSettings {
|
|||
pub(crate) editable: Option<bool>,
|
||||
pub(crate) extras: Vec<ExtraName>,
|
||||
pub(crate) raw: bool,
|
||||
pub(crate) bounds: Option<AddBoundsKind>,
|
||||
pub(crate) rev: Option<String>,
|
||||
pub(crate) tag: Option<String>,
|
||||
pub(crate) branch: Option<String>,
|
||||
|
@ -1289,6 +1291,7 @@ impl AddSettings {
|
|||
no_editable,
|
||||
extra,
|
||||
raw,
|
||||
bounds,
|
||||
rev,
|
||||
tag,
|
||||
branch,
|
||||
|
@ -1370,10 +1373,12 @@ impl AddSettings {
|
|||
}
|
||||
|
||||
let install_mirrors = filesystem
|
||||
.clone()
|
||||
.as_ref()
|
||||
.map(|fs| fs.install_mirrors.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
let bounds = bounds.or(filesystem.as_ref().and_then(|fs| fs.add.add_bounds));
|
||||
|
||||
Self {
|
||||
locked,
|
||||
frozen,
|
||||
|
@ -1388,6 +1393,7 @@ impl AddSettings {
|
|||
marker,
|
||||
dependency_type,
|
||||
raw,
|
||||
bounds,
|
||||
rev,
|
||||
tag,
|
||||
branch,
|
||||
|
|
|
@ -11841,3 +11841,232 @@ fn add_optional_normalize() -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test `uv add` with different kinds of bounds and constraints.
|
||||
#[test]
|
||||
fn add_bounds() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
// Set bounds in `uv.toml`
|
||||
let uv_toml = context.temp_dir.child("uv.toml");
|
||||
uv_toml.write_str(indoc! {r#"
|
||||
add-bounds = "exact"
|
||||
"#})?;
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
"#})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.add().arg("idna"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: The bounds option is in preview and may change in any future release.
|
||||
Resolved 2 packages in [TIME]
|
||||
Prepared 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ idna==3.6
|
||||
");
|
||||
|
||||
let pyproject_toml = context.read("pyproject.toml");
|
||||
assert_snapshot!(
|
||||
pyproject_toml, @r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"idna==3.6",
|
||||
]
|
||||
"#
|
||||
);
|
||||
|
||||
fs_err::remove_file(uv_toml)?;
|
||||
|
||||
// Set bounds in `pyproject.toml`
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[tool.uv]
|
||||
add-bounds = "major"
|
||||
"#})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.add().arg("anyio"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: The bounds option is in preview and may change in any future release.
|
||||
Resolved 4 packages in [TIME]
|
||||
Prepared 2 packages in [TIME]
|
||||
Installed 2 packages in [TIME]
|
||||
+ anyio==4.3.0
|
||||
+ sniffio==1.3.1
|
||||
");
|
||||
|
||||
let pyproject_toml = context.read("pyproject.toml");
|
||||
assert_snapshot!(
|
||||
pyproject_toml, @r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"anyio>=4.3.0,<5.0.0",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
add-bounds = "major"
|
||||
"#
|
||||
);
|
||||
|
||||
// Existing constraints take precedence over the bounds option
|
||||
uv_snapshot!(context.filters(), context.add().arg("anyio").arg("--bounds").arg("minor").arg("--preview"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 4 packages in [TIME]
|
||||
Audited 3 packages in [TIME]
|
||||
");
|
||||
|
||||
let pyproject_toml = context.read("pyproject.toml");
|
||||
assert_snapshot!(
|
||||
pyproject_toml, @r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"anyio>=4.3.0,<5.0.0",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
add-bounds = "major"
|
||||
"#
|
||||
);
|
||||
|
||||
// Explicit constraints take precedence over the bounds option
|
||||
uv_snapshot!(context.filters(), context.add().arg("anyio==4.2").arg("idna").arg("--bounds").arg("minor").arg("--preview"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 4 packages in [TIME]
|
||||
Prepared 1 package in [TIME]
|
||||
Uninstalled 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
- anyio==4.3.0
|
||||
+ anyio==4.2.0
|
||||
");
|
||||
|
||||
let pyproject_toml = context.read("pyproject.toml");
|
||||
assert_snapshot!(
|
||||
pyproject_toml, @r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"anyio==4.2",
|
||||
"idna>=3.6,<3.7",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
add-bounds = "major"
|
||||
"#
|
||||
);
|
||||
|
||||
// Set bounds on the CLI and use `--preview` to silence the warning.
|
||||
uv_snapshot!(context.filters(), context.add().arg("sniffio").arg("--bounds").arg("minor").arg("--preview"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 4 packages in [TIME]
|
||||
Audited 3 packages in [TIME]
|
||||
");
|
||||
|
||||
let pyproject_toml = context.read("pyproject.toml");
|
||||
assert_snapshot!(
|
||||
pyproject_toml, @r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"anyio==4.2",
|
||||
"idna>=3.6,<3.7",
|
||||
"sniffio>=1.3.1,<1.4.0",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
add-bounds = "major"
|
||||
"#
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Hint that we're using an explicit bound over the preferred bounds.
|
||||
#[test]
|
||||
fn add_bounds_requirement_over_bounds_kind() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
// Set bounds in `uv.toml`
|
||||
let uv_toml = context.temp_dir.child("uv.toml");
|
||||
uv_toml.write_str(indoc! {r#"
|
||||
add-bounds = "exact"
|
||||
"#})?;
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
"#})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.add().arg("anyio==4.2").arg("idna").arg("--bounds").arg("minor").arg("--preview"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 4 packages in [TIME]
|
||||
note: Using explicit requirement `anyio==4.2` over bounds preference `minor`
|
||||
Prepared 3 packages in [TIME]
|
||||
Installed 3 packages in [TIME]
|
||||
+ anyio==4.2.0
|
||||
+ idna==3.6
|
||||
+ sniffio==1.3.1
|
||||
");
|
||||
|
||||
let pyproject_toml = context.read("pyproject.toml");
|
||||
assert_snapshot!(
|
||||
pyproject_toml, @r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"anyio==4.2",
|
||||
"idna>=3.6,<3.7",
|
||||
]
|
||||
"#
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -3976,7 +3976,7 @@ fn resolve_config_file() -> anyhow::Result<()> {
|
|||
.arg("--show-settings")
|
||||
.arg("--config-file")
|
||||
.arg(config.path())
|
||||
.arg("requirements.in"), @r###"
|
||||
.arg("requirements.in"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
@ -3987,8 +3987,8 @@ fn resolve_config_file() -> anyhow::Result<()> {
|
|||
|
|
||||
1 | [project]
|
||||
| ^^^^^^^
|
||||
unknown field `project`, expected one of `required-version`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dev-dependencies`, `build-backend`
|
||||
"###
|
||||
unknown field `project`, expected one of `required-version`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `python-install-mirror`, `pypy-install-mirror`, `python-downloads-json-url`, `publish-url`, `trusted-publishing`, `check-url`, `add-bounds`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dev-dependencies`, `build-backend`
|
||||
"
|
||||
);
|
||||
|
||||
// Write an _actual_ `pyproject.toml`.
|
||||
|
|
|
@ -41,7 +41,8 @@ The [`--dev`](#development-dependencies), [`--group`](#dependency-groups), or
|
|||
field.
|
||||
|
||||
The dependency will include a constraint, e.g., `>=0.27.2`, for the most recent, compatible version
|
||||
of the package. An alternative constraint can be provided:
|
||||
of the package. The kind of bound can be adjusted with
|
||||
[`--bounds`](../../reference/settings.md#bounds), or the constraint can be provided directly:
|
||||
|
||||
```console
|
||||
$ uv add "httpx>=0.20"
|
||||
|
|
|
@ -389,8 +389,6 @@ Dependencies are added to the project's `pyproject.toml` file.
|
|||
|
||||
If a given dependency exists already, it will be updated to the new version specifier unless it includes markers that differ from the existing specifier in which case another entry for the dependency will be added.
|
||||
|
||||
If no constraint or URL is provided for a dependency, a lower bound is added equal to the latest compatible version of the package, e.g., `>=1.2.3`, unless `--frozen` is provided, in which case no resolution is performed.
|
||||
|
||||
The lockfile and project environment will be updated to reflect the added dependencies. To skip updating the lockfile, use `--frozen`. To skip updating the environment, use `--no-sync`.
|
||||
|
||||
If any of the requested dependencies cannot be found, uv will exit with an error, unless the `--frozen` flag is provided, in which case uv will add the dependencies verbatim without checking that they exist or are compatible with the project.
|
||||
|
@ -416,7 +414,17 @@ uv add [OPTIONS] <PACKAGES|--requirements <REQUIREMENTS>>
|
|||
<p>Can be provided multiple times.</p>
|
||||
<p>Expects to receive either a hostname (e.g., <code>localhost</code>), a host-port pair (e.g., <code>localhost:8080</code>), or a URL (e.g., <code>https://localhost</code>).</p>
|
||||
<p>WARNING: Hosts included in this list will not be verified against the system's certificate store. Only use <code>--allow-insecure-host</code> in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.</p>
|
||||
<p>May also be set with the <code>UV_INSECURE_HOST</code> environment variable.</p></dd><dt id="uv-add--branch"><a href="#uv-add--branch"><code>--branch</code></a> <i>branch</i></dt><dd><p>Branch to use when adding a dependency from Git</p>
|
||||
<p>May also be set with the <code>UV_INSECURE_HOST</code> environment variable.</p></dd><dt id="uv-add--bounds"><a href="#uv-add--bounds"><code>--bounds</code></a> <i>bounds</i></dt><dd><p>The kind of version specifier to use when adding dependencies.</p>
|
||||
<p>When adding a dependency to the project, if no constraint or URL is provided, a constraint is added based on the latest compatible version of the package. By default, a lower bound constraint is used, e.g., <code>>=1.2.3</code>.</p>
|
||||
<p>When <code>--frozen</code> is provided, no resolution is performed, and dependencies are always added without constraints.</p>
|
||||
<p>This option is in preview and may change in any future release.</p>
|
||||
<p>Possible values:</p>
|
||||
<ul>
|
||||
<li><code>lower</code>: Only a lower bound, e.g., <code>>=1.2.3</code></li>
|
||||
<li><code>major</code>: Allow the same major version, similar to the semver caret, e.g., <code>>=1.2.3, <2.0.0</code></li>
|
||||
<li><code>minor</code>: Allow the same minor version, similar to the semver tilde, e.g., <code>>=1.2.3, <1.3.0</code></li>
|
||||
<li><code>exact</code>: Pin the exact version, e.g., <code>==1.2.3</code></li>
|
||||
</ul></dd><dt id="uv-add--branch"><a href="#uv-add--branch"><code>--branch</code></a> <i>branch</i></dt><dd><p>Branch to use when adding a dependency from Git</p>
|
||||
</dd><dt id="uv-add--cache-dir"><a href="#uv-add--cache-dir"><code>--cache-dir</code></a> <i>cache-dir</i></dt><dd><p>Path to the cache directory.</p>
|
||||
<p>Defaults to <code>$XDG_CACHE_HOME/uv</code> or <code>$HOME/.cache/uv</code> on macOS and Linux, and <code>%LOCALAPPDATA%\uv\cache</code> on Windows.</p>
|
||||
<p>To view the location of the cache directory, run <code>uv cache dir</code>.</p>
|
||||
|
|
|
@ -592,6 +592,44 @@ members = ["member1", "path/to/member2", "libs/*"]
|
|||
---
|
||||
|
||||
## Configuration
|
||||
### [`add-bounds`](#add-bounds) {: #add-bounds }
|
||||
|
||||
The default version specifier when adding a dependency.
|
||||
|
||||
When adding a dependency to the project, if no constraint or URL is provided, a constraint
|
||||
is added based on the latest compatible version of the package. By default, a lower bound
|
||||
constraint is used, e.g., `>=1.2.3`.
|
||||
|
||||
When `--frozen` is provided, no resolution is performed, and dependencies are always added
|
||||
without constraints.
|
||||
|
||||
This option is in preview and may change in any future release.
|
||||
|
||||
**Default value**: `"lower"`
|
||||
|
||||
**Possible values**:
|
||||
|
||||
- `"lower"`: Only a lower bound, e.g., `>=1.2.3`
|
||||
- `"major"`: Allow the same major version, similar to the semver caret, e.g., `>=1.2.3, <2.0.0`
|
||||
- `"minor"`: Allow the same minor version, similar to the semver tilde, e.g., `>=1.2.3, <1.3.0`
|
||||
- `"exact"`: Pin the exact version, e.g., `==1.2.3`
|
||||
|
||||
**Example usage**:
|
||||
|
||||
=== "pyproject.toml"
|
||||
|
||||
```toml
|
||||
[tool.uv]
|
||||
add-bounds = "major"
|
||||
```
|
||||
=== "uv.toml"
|
||||
|
||||
```toml
|
||||
add-bounds = "major"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### [`allow-insecure-host`](#allow-insecure-host) {: #allow-insecure-host }
|
||||
|
||||
Allow insecure connections to host.
|
||||
|
|
44
uv.schema.json
generated
44
uv.schema.json
generated
|
@ -4,6 +4,17 @@
|
|||
"description": "Metadata and configuration for uv.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"add-bounds": {
|
||||
"description": "The default version specifier when adding a dependency.\n\nWhen adding a dependency to the project, if no constraint or URL is provided, a constraint is added based on the latest compatible version of the package. By default, a lower bound constraint is used, e.g., `>=1.2.3`.\n\nWhen `--frozen` is provided, no resolution is performed, and dependencies are always added without constraints.\n\nThis option is in preview and may change in any future release.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AddBoundsKind"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"allow-insecure-host": {
|
||||
"description": "Allow insecure connections to host.\n\nExpects to receive either a hostname (e.g., `localhost`), a host-port pair (e.g., `localhost:8080`), or a URL (e.g., `https://localhost`).\n\nWARNING: Hosts included in this list will not be verified against the system's certificate store. Only use `--allow-insecure-host` in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.",
|
||||
"type": [
|
||||
|
@ -557,6 +568,39 @@
|
|||
}
|
||||
},
|
||||
"definitions": {
|
||||
"AddBoundsKind": {
|
||||
"description": "The default version specifier when adding a dependency.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Only a lower bound, e.g., `>=1.2.3`.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"lower"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Allow the same major version, similar to the semver caret, e.g., `>=1.2.3, <2.0.0`.\n\nLeading zeroes are skipped, e.g. `>=0.1.2, <0.2.0`.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"major"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Allow the same minor version, similar to the semver tilde, e.g., `>=1.2.3, <1.3.0`.\n\nLeading zeroes are skipped, e.g. `>=0.1.2, <0.1.3`.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"minor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Pin the exact version, e.g., `==1.2.3`.\n\nThis option is not recommended, as versions are already pinned in the uv lockfile.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"exact"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"AnnotationStyle": {
|
||||
"description": "Indicate the style of annotation comments, used to indicate the dependencies that requested each package.",
|
||||
"oneOf": [
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue