mirror of https://github.com/astral-sh/uv
Support `uv add -r requirements.txt` (#6005)
## Summary Resolves https://github.com/astral-sh/uv/issues/4537 - First commit avoids overwriting dependencies with different markers. - Second commit supports adding from requirements files. ## Test Plan `cargo test`
This commit is contained in:
parent
6cfb27c5e1
commit
268c6de7fd
|
|
@ -1236,8 +1236,8 @@ pub struct PipSyncArgs {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
|
||||||
#[command(group = clap::ArgGroup::new("sources").required(true).multiple(true))]
|
#[command(group = clap::ArgGroup::new("sources").required(true).multiple(true))]
|
||||||
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
pub struct PipInstallArgs {
|
pub struct PipInstallArgs {
|
||||||
/// Install all listed packages.
|
/// Install all listed packages.
|
||||||
#[arg(group = "sources")]
|
#[arg(group = "sources")]
|
||||||
|
|
@ -1517,8 +1517,8 @@ pub struct PipInstallArgs {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
|
||||||
#[command(group = clap::ArgGroup::new("sources").required(true).multiple(true))]
|
#[command(group = clap::ArgGroup::new("sources").required(true).multiple(true))]
|
||||||
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
pub struct PipUninstallArgs {
|
pub struct PipUninstallArgs {
|
||||||
/// Uninstall all listed packages.
|
/// Uninstall all listed packages.
|
||||||
#[arg(group = "sources")]
|
#[arg(group = "sources")]
|
||||||
|
|
@ -2358,11 +2358,18 @@ pub struct LockArgs {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
|
#[command(group = clap::ArgGroup::new("sources").required(true).multiple(true))]
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
pub struct AddArgs {
|
pub struct AddArgs {
|
||||||
/// The packages to add, as PEP 508 requirements (e.g., `ruff==0.5.0`).
|
/// The packages to add, as PEP 508 requirements (e.g., `ruff==0.5.0`).
|
||||||
#[arg(required = true)]
|
#[arg(group = "sources")]
|
||||||
pub requirements: Vec<String>,
|
pub packages: Vec<String>,
|
||||||
|
|
||||||
|
/// Add all packages listed in the given `requirements.txt` files.
|
||||||
|
///
|
||||||
|
/// Implies `--raw-sources`.
|
||||||
|
#[arg(long, short, group = "sources", value_parser = parse_file_path)]
|
||||||
|
pub requirements: Vec<PathBuf>,
|
||||||
|
|
||||||
/// Add the requirements as development dependencies.
|
/// Add the requirements as development dependencies.
|
||||||
#[arg(long, conflicts_with("optional"))]
|
#[arg(long, conflicts_with("optional"))]
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use std::collections::hash_map::Entry;
|
use std::collections::hash_map::Entry;
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use cache_key::RepositoryUrl;
|
use cache_key::RepositoryUrl;
|
||||||
use owo_colors::OwoColorize;
|
use owo_colors::OwoColorize;
|
||||||
use pep508_rs::{ExtraName, Requirement, VersionOrUrl};
|
use pep508_rs::{ExtraName, Requirement, VersionOrUrl};
|
||||||
|
|
@ -71,6 +71,26 @@ pub(crate) async fn add(
|
||||||
warn_user_once!("`uv add` is experimental and may change without warning");
|
warn_user_once!("`uv add` is experimental and may change without warning");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for source in &requirements {
|
||||||
|
match source {
|
||||||
|
RequirementsSource::PyprojectToml(_) => {
|
||||||
|
bail!("Adding requirements from a `pyproject.toml` is not supported in `uv add`");
|
||||||
|
}
|
||||||
|
RequirementsSource::SetupPy(_) => {
|
||||||
|
bail!("Adding requirements from a `setup.py` is not supported in `uv add`");
|
||||||
|
}
|
||||||
|
RequirementsSource::SetupCfg(_) => {
|
||||||
|
bail!("Adding requirements from a `setup.cfg` is not supported in `uv add`");
|
||||||
|
}
|
||||||
|
RequirementsSource::RequirementsTxt(path) => {
|
||||||
|
if path == Path::new("-") {
|
||||||
|
bail!("Reading requirements from stdin is not supported in `uv add`");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let reporter = PythonDownloadReporter::single(printer);
|
let reporter = PythonDownloadReporter::single(printer);
|
||||||
|
|
||||||
let target = if let Some(script) = script {
|
let target = if let Some(script) = script {
|
||||||
|
|
|
||||||
|
|
@ -1147,14 +1147,35 @@ async fn run_project(
|
||||||
.combine(Refresh::from(args.settings.upgrade.clone())),
|
.combine(Refresh::from(args.settings.upgrade.clone())),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Use raw sources if requirements files are provided as input.
|
||||||
|
let raw_sources = if args.requirements.is_empty() {
|
||||||
|
args.raw_sources
|
||||||
|
} else {
|
||||||
|
if args.raw_sources {
|
||||||
|
warn_user!("`--raw-sources` is a no-op for `requirements.txt` files, which are always treated as raw sources");
|
||||||
|
}
|
||||||
|
true
|
||||||
|
};
|
||||||
|
|
||||||
|
let requirements = args
|
||||||
|
.packages
|
||||||
|
.into_iter()
|
||||||
|
.map(RequirementsSource::Package)
|
||||||
|
.chain(
|
||||||
|
args.requirements
|
||||||
|
.into_iter()
|
||||||
|
.map(RequirementsSource::from_requirements_file),
|
||||||
|
)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
commands::add(
|
commands::add(
|
||||||
args.locked,
|
args.locked,
|
||||||
args.frozen,
|
args.frozen,
|
||||||
args.no_sync,
|
args.no_sync,
|
||||||
args.requirements,
|
requirements,
|
||||||
args.editable,
|
args.editable,
|
||||||
args.dependency_type,
|
args.dependency_type,
|
||||||
args.raw_sources,
|
raw_sources,
|
||||||
args.rev,
|
args.rev,
|
||||||
args.tag,
|
args.tag,
|
||||||
args.branch,
|
args.branch,
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ use uv_configuration::{
|
||||||
};
|
};
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
use uv_python::{Prefix, PythonDownloads, PythonPreference, PythonVersion, Target};
|
use uv_python::{Prefix, PythonDownloads, PythonPreference, PythonVersion, Target};
|
||||||
use uv_requirements::RequirementsSource;
|
|
||||||
use uv_resolver::{AnnotationStyle, DependencyMode, ExcludeNewer, PrereleaseMode, ResolutionMode};
|
use uv_resolver::{AnnotationStyle, DependencyMode, ExcludeNewer, PrereleaseMode, ResolutionMode};
|
||||||
use uv_settings::{
|
use uv_settings::{
|
||||||
Combine, FilesystemOptions, Options, PipOptions, ResolverInstallerOptions, ResolverOptions,
|
Combine, FilesystemOptions, Options, PipOptions, ResolverInstallerOptions, ResolverOptions,
|
||||||
|
|
@ -694,7 +693,8 @@ pub(crate) struct AddSettings {
|
||||||
pub(crate) locked: bool,
|
pub(crate) locked: bool,
|
||||||
pub(crate) frozen: bool,
|
pub(crate) frozen: bool,
|
||||||
pub(crate) no_sync: bool,
|
pub(crate) no_sync: bool,
|
||||||
pub(crate) requirements: Vec<RequirementsSource>,
|
pub(crate) packages: Vec<String>,
|
||||||
|
pub(crate) requirements: Vec<PathBuf>,
|
||||||
pub(crate) dependency_type: DependencyType,
|
pub(crate) dependency_type: DependencyType,
|
||||||
pub(crate) editable: Option<bool>,
|
pub(crate) editable: Option<bool>,
|
||||||
pub(crate) extras: Vec<ExtraName>,
|
pub(crate) extras: Vec<ExtraName>,
|
||||||
|
|
@ -714,6 +714,7 @@ impl AddSettings {
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
pub(crate) fn resolve(args: AddArgs, filesystem: Option<FilesystemOptions>) -> Self {
|
pub(crate) fn resolve(args: AddArgs, filesystem: Option<FilesystemOptions>) -> Self {
|
||||||
let AddArgs {
|
let AddArgs {
|
||||||
|
packages,
|
||||||
requirements,
|
requirements,
|
||||||
dev,
|
dev,
|
||||||
optional,
|
optional,
|
||||||
|
|
@ -735,11 +736,6 @@ impl AddSettings {
|
||||||
python,
|
python,
|
||||||
} = args;
|
} = args;
|
||||||
|
|
||||||
let requirements = requirements
|
|
||||||
.into_iter()
|
|
||||||
.map(RequirementsSource::Package)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let dependency_type = if let Some(group) = optional {
|
let dependency_type = if let Some(group) = optional {
|
||||||
DependencyType::Optional(group)
|
DependencyType::Optional(group)
|
||||||
} else if dev {
|
} else if dev {
|
||||||
|
|
@ -752,6 +748,7 @@ impl AddSettings {
|
||||||
locked,
|
locked,
|
||||||
frozen,
|
frozen,
|
||||||
no_sync,
|
no_sync,
|
||||||
|
packages,
|
||||||
requirements,
|
requirements,
|
||||||
dependency_type,
|
dependency_type,
|
||||||
raw_sources,
|
raw_sources,
|
||||||
|
|
|
||||||
|
|
@ -794,7 +794,7 @@ fn add_raw_error() -> Result<()> {
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
error: the argument '--tag <TAG>' cannot be used with '--raw-sources'
|
error: the argument '--tag <TAG>' cannot be used with '--raw-sources'
|
||||||
|
|
||||||
Usage: uv add --cache-dir [CACHE_DIR] --tag <TAG> --exclude-newer <EXCLUDE_NEWER> <REQUIREMENTS>...
|
Usage: uv add --cache-dir [CACHE_DIR] --tag <TAG> --exclude-newer <EXCLUDE_NEWER> <PACKAGES|--requirements <REQUIREMENTS>>
|
||||||
|
|
||||||
For more information, try '--help'.
|
For more information, try '--help'.
|
||||||
"###);
|
"###);
|
||||||
|
|
@ -2732,7 +2732,7 @@ fn add_reject_multiple_git_ref_flags() {
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
error: the argument '--tag <TAG>' cannot be used with '--branch <BRANCH>'
|
error: the argument '--tag <TAG>' cannot be used with '--branch <BRANCH>'
|
||||||
|
|
||||||
Usage: uv add --cache-dir [CACHE_DIR] --tag <TAG> --exclude-newer <EXCLUDE_NEWER> <REQUIREMENTS>...
|
Usage: uv add --cache-dir [CACHE_DIR] --tag <TAG> --exclude-newer <EXCLUDE_NEWER> <PACKAGES|--requirements <REQUIREMENTS>>
|
||||||
|
|
||||||
For more information, try '--help'.
|
For more information, try '--help'.
|
||||||
"###
|
"###
|
||||||
|
|
@ -2753,7 +2753,7 @@ fn add_reject_multiple_git_ref_flags() {
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
error: the argument '--tag <TAG>' cannot be used with '--rev <REV>'
|
error: the argument '--tag <TAG>' cannot be used with '--rev <REV>'
|
||||||
|
|
||||||
Usage: uv add --cache-dir [CACHE_DIR] --tag <TAG> --exclude-newer <EXCLUDE_NEWER> <REQUIREMENTS>...
|
Usage: uv add --cache-dir [CACHE_DIR] --tag <TAG> --exclude-newer <EXCLUDE_NEWER> <PACKAGES|--requirements <REQUIREMENTS>>
|
||||||
|
|
||||||
For more information, try '--help'.
|
For more information, try '--help'.
|
||||||
"###
|
"###
|
||||||
|
|
@ -2774,7 +2774,7 @@ fn add_reject_multiple_git_ref_flags() {
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
error: the argument '--tag <TAG>' cannot be used multiple times
|
error: the argument '--tag <TAG>' cannot be used multiple times
|
||||||
|
|
||||||
Usage: uv add [OPTIONS] <REQUIREMENTS>...
|
Usage: uv add [OPTIONS] <PACKAGES|--requirements <REQUIREMENTS>>
|
||||||
|
|
||||||
For more information, try '--help'.
|
For more information, try '--help'.
|
||||||
"###
|
"###
|
||||||
|
|
@ -3410,6 +3410,109 @@ fn add_repeat() -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add from requirement file.
|
||||||
|
#[test]
|
||||||
|
fn add_requirements_file() -> 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_txt = context.temp_dir.child("requirements.txt");
|
||||||
|
requirements_txt.write_str("Flask==2.3.2\ngit+https://github.com/agronholm/anyio.git@4.4.0")?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.add(&[]).arg("-r").arg("requirements.txt"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `uv add` is experimental and may change without warning
|
||||||
|
Resolved [N] packages in [TIME]
|
||||||
|
Prepared [N] packages in [TIME]
|
||||||
|
Installed [N] packages in [TIME]
|
||||||
|
+ anyio==4.4.0 (from git+https://github.com/agronholm/anyio.git@053e8f0a0f7b0f4a47a012eb5c6b1d9d84344e6a)
|
||||||
|
+ blinker==1.7.0
|
||||||
|
+ click==8.1.7
|
||||||
|
+ flask==2.3.2
|
||||||
|
+ idna==3.6
|
||||||
|
+ itsdangerous==2.1.2
|
||||||
|
+ jinja2==3.1.3
|
||||||
|
+ markupsafe==2.1.5
|
||||||
|
+ project==0.1.0 (from file://[TEMP_DIR]/)
|
||||||
|
+ sniffio==1.3.1
|
||||||
|
+ werkzeug==3.0.1
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let pyproject_toml = fs_err::read_to_string(context.temp_dir.join("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 = [
|
||||||
|
"flask==2.3.2",
|
||||||
|
"anyio @ git+https://github.com/agronholm/anyio.git@4.4.0",
|
||||||
|
]
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Using `--raw-sources` with `-r` should warn.
|
||||||
|
uv_snapshot!(context.filters(), context.add(&[]).arg("-r").arg("requirements.txt").arg("--raw-sources"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `--raw-sources` is a no-op for `requirements.txt` files, which are always treated as raw sources
|
||||||
|
warning: `uv add` is experimental and may change without warning
|
||||||
|
Resolved [N] packages in [TIME]
|
||||||
|
Audited [N] packages in [TIME]
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Passing a `setup.py` should fail.
|
||||||
|
uv_snapshot!(context.filters(), context.add(&[]).arg("-r").arg("setup.py"), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `uv add` is experimental and may change without warning
|
||||||
|
error: Adding requirements from a `setup.py` is not supported in `uv add`
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Passing nothing should fail.
|
||||||
|
uv_snapshot!(context.filters(), context.add(&[]), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: the following required arguments were not provided:
|
||||||
|
<PACKAGES|--requirements <REQUIREMENTS>>
|
||||||
|
|
||||||
|
Usage: uv add --cache-dir [CACHE_DIR] --exclude-newer <EXCLUDE_NEWER> <PACKAGES|--requirements <REQUIREMENTS>>
|
||||||
|
|
||||||
|
For more information, try '--help'.
|
||||||
|
"###);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Add to a PEP 732 script.
|
/// Add to a PEP 732 script.
|
||||||
#[test]
|
#[test]
|
||||||
fn add_script() -> Result<()> {
|
fn add_script() -> Result<()> {
|
||||||
|
|
|
||||||
|
|
@ -466,12 +466,12 @@ uv will search for a project in the current directory or any parent directory. I
|
||||||
<h3 class="cli-reference">Usage</h3>
|
<h3 class="cli-reference">Usage</h3>
|
||||||
|
|
||||||
```
|
```
|
||||||
uv add [OPTIONS] <REQUIREMENTS>...
|
uv add [OPTIONS] <PACKAGES|--requirements <REQUIREMENTS>>
|
||||||
```
|
```
|
||||||
|
|
||||||
<h3 class="cli-reference">Arguments</h3>
|
<h3 class="cli-reference">Arguments</h3>
|
||||||
|
|
||||||
<dl class="cli-reference"><dt><code>REQUIREMENTS</code></dt><dd><p>The packages to add, as PEP 508 requirements (e.g., <code>ruff==0.5.0</code>)</p>
|
<dl class="cli-reference"><dt><code>PACKAGES</code></dt><dd><p>The packages to add, as PEP 508 requirements (e.g., <code>ruff==0.5.0</code>)</p>
|
||||||
|
|
||||||
</dd></dl>
|
</dd></dl>
|
||||||
|
|
||||||
|
|
@ -696,6 +696,10 @@ uv add [OPTIONS] <REQUIREMENTS>...
|
||||||
|
|
||||||
</dd><dt><code>--reinstall-package</code> <i>reinstall-package</i></dt><dd><p>Reinstall a specific package, regardless of whether it’s already installed. Implies <code>--refresh-package</code></p>
|
</dd><dt><code>--reinstall-package</code> <i>reinstall-package</i></dt><dd><p>Reinstall a specific package, regardless of whether it’s already installed. Implies <code>--refresh-package</code></p>
|
||||||
|
|
||||||
|
</dd><dt><code>--requirements</code>, <code>-r</code> <i>requirements</i></dt><dd><p>Add all packages listed in the given <code>requirements.txt</code> files.</p>
|
||||||
|
|
||||||
|
<p>Implies <code>--raw-sources</code>.</p>
|
||||||
|
|
||||||
</dd><dt><code>--resolution</code> <i>resolution</i></dt><dd><p>The strategy to use when selecting between the different compatible versions for a given package requirement.</p>
|
</dd><dt><code>--resolution</code> <i>resolution</i></dt><dd><p>The strategy to use when selecting between the different compatible versions for a given package requirement.</p>
|
||||||
|
|
||||||
<p>By default, uv will use the latest compatible version of each package (<code>highest</code>).</p>
|
<p>By default, uv will use the latest compatible version of each package (<code>highest</code>).</p>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue