mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 19:08:04 +00:00
Add support for -c
constraints in uv add
(#12209)
## Summary Closes https://github.com/astral-sh/uv/issues/11986.
This commit is contained in:
parent
72be5ffb25
commit
040a5bbe5d
13 changed files with 445 additions and 102 deletions
|
@ -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>,
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<()> {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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’s installed. The constraints will <em>not</em> be added to the project’s <code>pyproject.toml</code> file, but <em>will</em> be respected during dependency resolution.</p>
|
||||
|
||||
<p>This is equivalent to pip’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: <https://pypi.org/simple>).</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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue