Add support for -c constraints in uv add (#12209)

## Summary

Closes https://github.com/astral-sh/uv/issues/11986.
This commit is contained in:
Charlie Marsh 2025-03-17 14:27:33 -07:00 committed by GitHub
parent 72be5ffb25
commit 040a5bbe5d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 445 additions and 102 deletions

View file

@ -3289,6 +3289,16 @@ pub struct AddArgs {
#[arg(long, short, alias = "requirement", group = "sources", value_parser = parse_file_path)]
pub requirements: Vec<PathBuf>,
/// Constrain versions using the given requirements files.
///
/// Constraints files are `requirements.txt`-like files that only control the _version_ of a
/// requirement that's installed. The constraints will _not_ be added to the project's
/// `pyproject.toml` file, but _will_ be respected during dependency resolution.
///
/// This is equivalent to pip's `--constraint` option.
#[arg(long, short, alias = "constraint", env = EnvVars::UV_CONSTRAINT, value_delimiter = ' ', value_parser = parse_maybe_file_path)]
pub constraints: Vec<Maybe<PathBuf>>,
/// Apply this marker to all added packages.
#[arg(long, short, value_parser = MarkerTree::from_str)]
pub marker: Option<MarkerTree>,

View file

@ -1,4 +1,5 @@
use std::collections::hash_map::Entry;
use std::collections::BTreeMap;
use std::fmt::Write;
use std::io;
use std::path::Path;
@ -21,7 +22,9 @@ use uv_configuration::{
};
use uv_dispatch::BuildDispatch;
use uv_distribution::DistributionDatabase;
use uv_distribution_types::{Index, IndexName, IndexUrls, UnresolvedRequirement, VersionId};
use uv_distribution_types::{
Index, IndexName, IndexUrls, NameRequirementSpecification, UnresolvedRequirement, VersionId,
};
use uv_fs::Simplified;
use uv_git::GIT_STORE;
use uv_git_types::GitReference;
@ -64,6 +67,7 @@ pub(crate) async fn add(
active: Option<bool>,
no_sync: bool,
requirements: Vec<RequirementsSource>,
constraints: Vec<RequirementsSource>,
marker: Option<MarkerTree>,
editable: Option<bool>,
dependency_type: DependencyType,
@ -258,8 +262,18 @@ pub(crate) async fn add(
.allow_insecure_host(network_settings.allow_insecure_host.clone());
// Read the requirements.
let RequirementsSpecification { requirements, .. } =
RequirementsSpecification::from_simple_sources(&requirements, &client_builder).await?;
let RequirementsSpecification {
requirements,
constraints,
..
} = RequirementsSpecification::from_sources(
&requirements,
&constraints,
&[],
BTreeMap::default(),
&client_builder,
)
.await?;
// Initialize any shared state.
let state = PlatformState::default();
@ -646,6 +660,7 @@ pub(crate) async fn add(
locked,
&dependency_type,
raw_sources,
constraints,
&settings,
&network_settings,
installer_metadata,
@ -682,6 +697,7 @@ async fn lock_and_sync(
locked: bool,
dependency_type: &DependencyType,
raw_sources: bool,
constraints: Vec<NameRequirementSpecification>,
settings: &ResolverInstallerSettings,
network_settings: &NetworkSettings,
installer_metadata: bool,
@ -690,13 +706,12 @@ async fn lock_and_sync(
printer: Printer,
preview: PreviewMode,
) -> Result<(), ProjectError> {
let mut lock = project::lock::do_safe_lock(
let mut lock = project::lock::LockOperation::new(
if locked {
LockMode::Locked(target.interpreter())
} else {
LockMode::Write(target.interpreter())
},
(&target).into(),
&settings.resolver,
network_settings,
&lock_state,
@ -706,6 +721,8 @@ async fn lock_and_sync(
printer,
preview,
)
.with_constraints(constraints)
.execute((&target).into())
.await?
.into_lock();
@ -808,13 +825,12 @@ async fn lock_and_sync(
// If the file was modified, we have to lock again, though the only expected change is
// the addition of the minimum version specifiers.
lock = project::lock::do_safe_lock(
lock = project::lock::LockOperation::new(
if locked {
LockMode::Locked(target.interpreter())
} else {
LockMode::Write(target.interpreter())
},
(&target).into(),
&settings.resolver,
network_settings,
&lock_state,
@ -824,6 +840,7 @@ async fn lock_and_sync(
printer,
preview,
)
.execute((&target).into())
.await?
.into_lock();
}

View file

@ -19,7 +19,7 @@ use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace,
use crate::commands::pip::loggers::DefaultResolveLogger;
use crate::commands::project::install_target::InstallTarget;
use crate::commands::project::lock::{do_safe_lock, LockMode};
use crate::commands::project::lock::{LockMode, LockOperation};
use crate::commands::project::lock_target::LockTarget;
use crate::commands::project::{
default_dependency_groups, detect_conflicts, ProjectError, ProjectInterpreter,
@ -169,9 +169,8 @@ pub(crate) async fn export(
let state = UniversalState::default();
// Lock the project.
let lock = match do_safe_lock(
let lock = match LockOperation::new(
mode,
(&target).into(),
&settings,
&network_settings,
&state,
@ -181,6 +180,7 @@ pub(crate) async fn export(
printer,
preview,
)
.execute((&target).into())
.await
{
Ok(result) => result.into_lock(),

View file

@ -183,9 +183,8 @@ pub(crate) async fn lock(
let state = UniversalState::default();
// Perform the lock operation.
match do_safe_lock(
match LockOperation::new(
mode,
target,
&settings,
&network_settings,
&state,
@ -195,6 +194,7 @@ pub(crate) async fn lock(
printer,
preview,
)
.execute(target)
.await
{
Ok(lock) => {
@ -245,97 +245,139 @@ pub(super) enum LockMode<'env> {
Frozen,
}
/// Perform a lock operation, respecting the `--locked` and `--frozen` parameters.
#[allow(clippy::fn_params_excessive_bools)]
pub(super) async fn do_safe_lock(
mode: LockMode<'_>,
target: LockTarget<'_>,
settings: &ResolverSettings,
network_settings: &NetworkSettings,
state: &UniversalState,
/// A lock operation.
pub(super) struct LockOperation<'env> {
mode: LockMode<'env>,
constraints: Vec<NameRequirementSpecification>,
settings: &'env ResolverSettings,
network_settings: &'env NetworkSettings,
state: &'env UniversalState,
logger: Box<dyn ResolveLogger>,
concurrency: Concurrency,
cache: &Cache,
cache: &'env Cache,
printer: Printer,
preview: PreviewMode,
) -> Result<LockResult, ProjectError> {
match mode {
LockMode::Frozen => {
// Read the existing lockfile, but don't attempt to lock the project.
let existing = target
.read()
.await?
.ok_or_else(|| ProjectError::MissingLockfile)?;
Ok(LockResult::Unchanged(existing))
}
impl<'env> LockOperation<'env> {
/// Initialize a [`LockOperation`].
pub(super) fn new(
mode: LockMode<'env>,
settings: &'env ResolverSettings,
network_settings: &'env NetworkSettings,
state: &'env UniversalState,
logger: Box<dyn ResolveLogger>,
concurrency: Concurrency,
cache: &'env Cache,
printer: Printer,
preview: PreviewMode,
) -> Self {
Self {
mode,
constraints: vec![],
settings,
network_settings,
state,
logger,
concurrency,
cache,
printer,
preview,
}
LockMode::Locked(interpreter) => {
// Read the existing lockfile.
let existing = target
.read()
.await?
.ok_or_else(|| ProjectError::MissingLockfile)?;
}
// Perform the lock operation, but don't write the lockfile to disk.
let result = do_lock(
target,
interpreter,
Some(existing),
settings,
network_settings,
state,
logger,
concurrency,
cache,
printer,
preview,
)
.await?;
/// Set the external constraints for the [`LockOperation`].
#[must_use]
pub(super) fn with_constraints(
mut self,
constraints: Vec<NameRequirementSpecification>,
) -> Self {
self.constraints = constraints;
self
}
// If the lockfile changed, return an error.
if matches!(result, LockResult::Changed(_, _)) {
return Err(ProjectError::LockMismatch);
/// Perform a [`LockOperation`].
pub(super) async fn execute(self, target: LockTarget<'_>) -> Result<LockResult, ProjectError> {
match self.mode {
LockMode::Frozen => {
// Read the existing lockfile, but don't attempt to lock the project.
let existing = target
.read()
.await?
.ok_or_else(|| ProjectError::MissingLockfile)?;
Ok(LockResult::Unchanged(existing))
}
LockMode::Locked(interpreter) => {
// Read the existing lockfile.
let existing = target
.read()
.await?
.ok_or_else(|| ProjectError::MissingLockfile)?;
Ok(result)
}
LockMode::Write(interpreter) | LockMode::DryRun(interpreter) => {
// Read the existing lockfile.
let existing = match target.read().await {
Ok(Some(existing)) => Some(existing),
Ok(None) => None,
Err(ProjectError::Lock(err)) => {
warn_user!(
"Failed to read existing lockfile; ignoring locked requirements: {err}"
);
None
// Perform the lock operation, but don't write the lockfile to disk.
let result = do_lock(
target,
interpreter,
Some(existing),
self.constraints,
self.settings,
self.network_settings,
self.state,
self.logger,
self.concurrency,
self.cache,
self.printer,
self.preview,
)
.await?;
// If the lockfile changed, return an error.
if matches!(result, LockResult::Changed(_, _)) {
return Err(ProjectError::LockMismatch);
}
Err(err) => return Err(err),
};
// Perform the lock operation.
let result = do_lock(
target,
interpreter,
existing,
settings,
network_settings,
state,
logger,
concurrency,
cache,
printer,
preview,
)
.await?;
// If the lockfile changed, write it to disk.
if !matches!(mode, LockMode::DryRun(_)) {
if let LockResult::Changed(_, lock) = &result {
target.commit(lock).await?;
}
Ok(result)
}
LockMode::Write(interpreter) | LockMode::DryRun(interpreter) => {
// Read the existing lockfile.
let existing = match target.read().await {
Ok(Some(existing)) => Some(existing),
Ok(None) => None,
Err(ProjectError::Lock(err)) => {
warn_user!(
"Failed to read existing lockfile; ignoring locked requirements: {err}"
);
None
}
Err(err) => return Err(err),
};
Ok(result)
// Perform the lock operation.
let result = do_lock(
target,
interpreter,
existing,
self.constraints,
self.settings,
self.network_settings,
self.state,
self.logger,
self.concurrency,
self.cache,
self.printer,
self.preview,
)
.await?;
// If the lockfile changed, write it to disk.
if !matches!(self.mode, LockMode::DryRun(_)) {
if let LockResult::Changed(_, lock) = &result {
target.commit(lock).await?;
}
}
Ok(result)
}
}
}
}
@ -345,6 +387,7 @@ async fn do_lock(
target: LockTarget<'_>,
interpreter: &Interpreter,
existing_lock: Option<Lock>,
external: Vec<NameRequirementSpecification>,
settings: &ResolverSettings,
network_settings: &NetworkSettings,
state: &UniversalState,
@ -750,6 +793,7 @@ async fn do_lock(
.iter()
.cloned()
.map(NameRequirementSpecification::from)
.chain(external)
.collect(),
overrides
.iter()

View file

@ -276,9 +276,8 @@ pub(crate) async fn remove(
let state = UniversalState::default();
// Lock and sync the environment, if necessary.
let lock = match project::lock::do_safe_lock(
let lock = match project::lock::LockOperation::new(
mode,
(&target).into(),
&settings.resolver,
&network_settings,
&state,
@ -288,6 +287,7 @@ pub(crate) async fn remove(
printer,
preview,
)
.execute((&target).into())
.await
{
Ok(result) => result.into_lock(),

View file

@ -242,9 +242,8 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
};
// Generate a lockfile.
let lock = match project::lock::do_safe_lock(
let lock = match project::lock::LockOperation::new(
mode,
target,
&settings.resolver,
&network_settings,
&lock_state,
@ -258,6 +257,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
printer,
preview,
)
.execute(target)
.await
{
Ok(result) => result.into_lock(),
@ -661,9 +661,8 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
LockMode::Write(venv.interpreter())
};
let result = match project::lock::do_safe_lock(
let result = match project::lock::LockOperation::new(
mode,
project.workspace().into(),
&settings.resolver,
&network_settings,
&lock_state,
@ -677,6 +676,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
printer,
preview,
)
.execute(project.workspace().into())
.await
{
Ok(result) => result,

View file

@ -36,7 +36,7 @@ use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger,
use crate::commands::pip::operations;
use crate::commands::pip::operations::Modifications;
use crate::commands::project::install_target::InstallTarget;
use crate::commands::project::lock::{do_safe_lock, LockMode, LockResult};
use crate::commands::project::lock::{LockMode, LockOperation, LockResult};
use crate::commands::project::lock_target::LockTarget;
use crate::commands::project::{
default_dependency_groups, detect_conflicts, script_specification, update_environment,
@ -327,9 +327,8 @@ pub(crate) async fn sync(
SyncTarget::Script(script) => LockTarget::from(script),
};
let lock = match do_safe_lock(
let lock = match LockOperation::new(
mode,
lock_target,
&settings.resolver,
&network_settings,
&state,
@ -339,6 +338,7 @@ pub(crate) async fn sync(
printer,
preview,
)
.execute(lock_target)
.await
{
Ok(result) => {

View file

@ -20,7 +20,7 @@ use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceCache};
use crate::commands::pip::latest::LatestClient;
use crate::commands::pip::loggers::DefaultResolveLogger;
use crate::commands::pip::resolution_markers;
use crate::commands::project::lock::{do_safe_lock, LockMode};
use crate::commands::project::lock::{LockMode, LockOperation};
use crate::commands::project::lock_target::LockTarget;
use crate::commands::project::{
default_dependency_groups, ProjectError, ProjectInterpreter, ScriptInterpreter, UniversalState,
@ -132,9 +132,8 @@ pub(crate) async fn tree(
let state = UniversalState::default();
// Update the lockfile, if necessary.
let lock = match do_safe_lock(
let lock = match LockOperation::new(
mode,
target,
&settings,
network_settings,
&state,
@ -144,6 +143,7 @@ pub(crate) async fn tree(
printer,
preview,
)
.execute(target)
.await
{
Ok(result) => result.into_lock(),

View file

@ -1712,6 +1712,11 @@ async fn run_project(
.map(Ok),
)
.collect::<Result<Vec<_>>>()?;
let constraints = args
.constraints
.into_iter()
.map(RequirementsSource::from_constraints_txt)
.collect::<Vec<_>>();
Box::pin(commands::add(
project_dir,
@ -1720,6 +1725,7 @@ async fn run_project(
args.active,
args.no_sync,
requirements,
constraints,
args.marker,
args.editable,
args.dependency_type,

View file

@ -1170,6 +1170,7 @@ pub(crate) struct AddSettings {
pub(crate) no_sync: bool,
pub(crate) packages: Vec<String>,
pub(crate) requirements: Vec<PathBuf>,
pub(crate) constraints: Vec<PathBuf>,
pub(crate) marker: Option<MarkerTree>,
pub(crate) dependency_type: DependencyType,
pub(crate) editable: Option<bool>,
@ -1194,6 +1195,7 @@ impl AddSettings {
let AddArgs {
packages,
requirements,
constraints,
marker,
dev,
optional,
@ -1285,6 +1287,10 @@ impl AddSettings {
no_sync,
packages,
requirements,
constraints: constraints
.into_iter()
.filter_map(Maybe::into_option)
.collect(),
marker,
dependency_type,
raw_sources,

View file

@ -4432,6 +4432,251 @@ fn add_requirements_file_with_marker_flag() -> Result<()> {
Ok(())
}
/// Add from requirement file, with additional, external constraints.
///
/// The constraints should be respected, but they should _not_ be recorded in the `pyproject.toml`
/// or `uv.lock` file.
#[test]
fn add_requirements_file_constraints() -> Result<()> {
let context = TestContext::new("3.12").with_filtered_counts();
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"
dependencies = []
"#})?;
let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str(indoc! {r"
flask
anyio
"})?;
// Write a set of valid, but outdated compiled requirements.
//
// For reference, these are generated by compiling:
// ```txt
// flask
// anyio<4
// click<7
// ```
let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.write_str(indoc! {r"
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] requirements.in -o requirements.txt
anyio==3.7.1
# via -r requirements.in
click==6.7
# via
# -r requirements.in
# flask
flask==1.1.4
# via -r requirements.in
idna==3.6
# via anyio
itsdangerous==1.1.0
# via flask
jinja2==2.11.3
# via flask
markupsafe==2.1.5
# via jinja2
sniffio==1.3.1
# via anyio
werkzeug==1.0.1
# via flask
"})?;
// Pass the input requirements as constraints.
uv_snapshot!(context.filters(), context.add().arg("-r").arg("requirements.in").arg("-c").arg("requirements.txt"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ anyio==3.7.1
+ click==6.7
+ flask==1.1.4
+ idna==3.6
+ itsdangerous==1.1.0
+ jinja2==2.11.3
+ markupsafe==2.1.5
+ sniffio==1.3.1
+ werkzeug==1.0.1
");
let pyproject_toml = context.read("pyproject.toml");
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
pyproject_toml, @r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"anyio>=3.7.1",
"flask>=1.1.4",
]
"#
);
});
let lock = context.read("uv.lock");
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
lock, @r#"
version = 1
revision = 1
requires-python = ">=3.12"
[options]
exclude-newer = "2024-03-25T00:00:00Z"
[[package]]
name = "anyio"
version = "3.7.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/28/99/2dfd53fd55ce9838e6ff2d4dac20ce58263798bd1a0dbe18b3a9af3fcfce/anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780", size = 142927 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/19/24/44299477fe7dcc9cb58d0a57d5a7588d6af2ff403fdd2d47a246c91a3246/anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5", size = 80896 },
]
[[package]]
name = "click"
version = "6.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/95/d9/c3336b6b5711c3ab9d1d3a80f1a3e2afeb9d8c02a7166462f6cc96570897/click-6.7.tar.gz", hash = "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b", size = 279019 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/34/c1/8806f99713ddb993c5366c362b2f908f18269f8d792aff1abfd700775a77/click-6.7-py2.py3-none-any.whl", hash = "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", size = 71175 },
]
[[package]]
name = "flask"
version = "1.1.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "itsdangerous" },
{ name = "jinja2" },
{ name = "werkzeug" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4d/5b/2d145f5fe718b2f15ebe69240538f06faa8bbb76488bf962091db1f7a26d/Flask-1.1.4.tar.gz", hash = "sha256:0fbeb6180d383a9186d0d6ed954e0042ad9f18e0e8de088b2b419d526927d196", size = 635920 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e8/6d/994208daa354f68fd89a34a8bafbeaab26fda84e7af1e35bdaed02b667e6/Flask-1.1.4-py2.py3-none-any.whl", hash = "sha256:c34f04500f2cbbea882b1acb02002ad6fe6b7ffa64a6164577995657f50aed22", size = 94591 },
]
[[package]]
name = "idna"
version = "3.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 },
]
[[package]]
name = "itsdangerous"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/68/1a/f27de07a8a304ad5fa817bbe383d1238ac4396da447fa11ed937039fa04b/itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", size = 53219 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/ae/44b03b253d6fade317f32c24d100b3b35c2239807046a4c953c7b89fa49e/itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749", size = 16743 },
]
[[package]]
name = "jinja2"
version = "2.11.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4f/e7/65300e6b32e69768ded990494809106f87da1d436418d5f1367ed3966fd7/Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6", size = 257589 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/c2/1eece8c95ddbc9b1aeb64f5783a9e07a286de42191b7204d67b7496ddf35/Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", size = 125699 },
]
[[package]]
name = "markupsafe"
version = "2.1.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215 },
{ url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069 },
{ url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452 },
{ url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462 },
{ url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869 },
{ url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906 },
{ url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296 },
{ url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038 },
{ url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572 },
{ url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127 },
]
[[package]]
name = "project"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "anyio" },
{ name = "flask" },
]
[package.metadata]
requires-dist = [
{ name = "anyio", specifier = ">=3.7.1" },
{ name = "flask", specifier = ">=1.1.4" },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
]
[[package]]
name = "werkzeug"
version = "1.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/10/27/a33329150147594eff0ea4c33c2036c0eadd933141055be0ff911f7f8d04/Werkzeug-1.0.1.tar.gz", hash = "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c", size = 904455 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/94/5f7079a0e00bd6863ef8f1da638721e9da21e5bacee597595b318f71d62e/Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43", size = 298631 },
]
"#
);
});
// Re-run with `--locked`.
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved [N] packages in [TIME]
");
Ok(())
}
/// Add a requirement to a dependency group.
#[test]
fn add_group() -> Result<()> {

View file

@ -134,6 +134,14 @@ $ # Add a git dependency
$ uv add git+https://github.com/psf/requests
```
If you're migrating from a `requirements.txt` file, you can use `uv add` with the `-r` flag to add
all dependencies from the file:
```console
$ # Add all dependencies from `requirements.txt`.
$ uv add -r requirements.txt -c constraints.txt
```
To remove a package, you can use `uv remove`:
```console

View file

@ -819,6 +819,13 @@ uv add [OPTIONS] <PACKAGES|--requirements <REQUIREMENTS>>
<p>May also be set with the <code>UV_CONFIG_FILE</code> environment variable.</p>
</dd><dt id="uv-add--config-setting"><a href="#uv-add--config-setting"><code>--config-setting</code></a>, <code>-C</code> <i>config-setting</i></dt><dd><p>Settings to pass to the PEP 517 build backend, specified as <code>KEY=VALUE</code> pairs</p>
</dd><dt id="uv-add--constraints"><a href="#uv-add--constraints"><code>--constraints</code></a>, <code>-c</code> <i>constraints</i></dt><dd><p>Constrain versions using the given requirements files.</p>
<p>Constraints files are <code>requirements.txt</code>-like files that only control the <em>version</em> of a requirement that&#8217;s installed. The constraints will <em>not</em> be added to the project&#8217;s <code>pyproject.toml</code> file, but <em>will</em> be respected during dependency resolution.</p>
<p>This is equivalent to pip&#8217;s <code>--constraint</code> option.</p>
<p>May also be set with the <code>UV_CONSTRAINT</code> environment variable.</p>
</dd><dt id="uv-add--default-index"><a href="#uv-add--default-index"><code>--default-index</code></a> <i>default-index</i></dt><dd><p>The URL of the default package index (by default: &lt;https://pypi.org/simple&gt;).</p>
<p>Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.</p>