Skip to content

Commit

Permalink
Support uv add -r requirements.txt (#6005)
Browse files Browse the repository at this point in the history
## Summary

Resolves #4537

- First commit avoids overwriting dependencies with different markers.
- Second commit supports adding from requirements files.

## Test Plan

`cargo test`
  • Loading branch information
blueraft committed Aug 16, 2024
1 parent 6cfb27c commit 268c6de
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 21 deletions.
15 changes: 11 additions & 4 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1236,8 +1236,8 @@ pub struct PipSyncArgs {
}

#[derive(Args)]
#[allow(clippy::struct_excessive_bools)]
#[command(group = clap::ArgGroup::new("sources").required(true).multiple(true))]
#[allow(clippy::struct_excessive_bools)]
pub struct PipInstallArgs {
/// Install all listed packages.
#[arg(group = "sources")]
Expand Down Expand Up @@ -1517,8 +1517,8 @@ pub struct PipInstallArgs {
}

#[derive(Args)]
#[allow(clippy::struct_excessive_bools)]
#[command(group = clap::ArgGroup::new("sources").required(true).multiple(true))]
#[allow(clippy::struct_excessive_bools)]
pub struct PipUninstallArgs {
/// Uninstall all listed packages.
#[arg(group = "sources")]
Expand Down Expand Up @@ -2358,11 +2358,18 @@ pub struct LockArgs {
}

#[derive(Args)]
#[command(group = clap::ArgGroup::new("sources").required(true).multiple(true))]
#[allow(clippy::struct_excessive_bools)]
pub struct AddArgs {
/// The packages to add, as PEP 508 requirements (e.g., `ruff==0.5.0`).
#[arg(required = true)]
pub requirements: Vec<String>,
#[arg(group = "sources")]
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.
#[arg(long, conflicts_with("optional"))]
Expand Down
24 changes: 22 additions & 2 deletions crates/uv/src/commands/project/add.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
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 owo_colors::OwoColorize;
use pep508_rs::{ExtraName, Requirement, VersionOrUrl};
Expand Down Expand Up @@ -71,6 +71,26 @@ pub(crate) async fn add(
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 target = if let Some(script) = script {
Expand Down
25 changes: 23 additions & 2 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1147,14 +1147,35 @@ async fn run_project(
.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(
args.locked,
args.frozen,
args.no_sync,
args.requirements,
requirements,
args.editable,
args.dependency_type,
args.raw_sources,
raw_sources,
args.rev,
args.tag,
args.branch,
Expand Down
11 changes: 4 additions & 7 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ use uv_configuration::{
};
use uv_normalize::PackageName;
use uv_python::{Prefix, PythonDownloads, PythonPreference, PythonVersion, Target};
use uv_requirements::RequirementsSource;
use uv_resolver::{AnnotationStyle, DependencyMode, ExcludeNewer, PrereleaseMode, ResolutionMode};
use uv_settings::{
Combine, FilesystemOptions, Options, PipOptions, ResolverInstallerOptions, ResolverOptions,
Expand Down Expand Up @@ -694,7 +693,8 @@ pub(crate) struct AddSettings {
pub(crate) locked: bool,
pub(crate) frozen: 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) editable: Option<bool>,
pub(crate) extras: Vec<ExtraName>,
Expand All @@ -714,6 +714,7 @@ impl AddSettings {
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn resolve(args: AddArgs, filesystem: Option<FilesystemOptions>) -> Self {
let AddArgs {
packages,
requirements,
dev,
optional,
Expand All @@ -735,11 +736,6 @@ impl AddSettings {
python,
} = args;

let requirements = requirements
.into_iter()
.map(RequirementsSource::Package)
.collect::<Vec<_>>();

let dependency_type = if let Some(group) = optional {
DependencyType::Optional(group)
} else if dev {
Expand All @@ -752,6 +748,7 @@ impl AddSettings {
locked,
frozen,
no_sync,
packages,
requirements,
dependency_type,
raw_sources,
Expand Down
111 changes: 107 additions & 4 deletions crates/uv/tests/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -794,7 +794,7 @@ fn add_raw_error() -> Result<()> {
----- stderr -----
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'.
"###);
Expand Down Expand Up @@ -2732,7 +2732,7 @@ fn add_reject_multiple_git_ref_flags() {
----- stderr -----
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'.
"###
Expand All @@ -2753,7 +2753,7 @@ fn add_reject_multiple_git_ref_flags() {
----- stderr -----
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'.
"###
Expand All @@ -2774,7 +2774,7 @@ fn add_reject_multiple_git_ref_flags() {
----- stderr -----
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'.
"###
Expand Down Expand Up @@ -3410,6 +3410,109 @@ fn add_repeat() -> Result<()> {
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.
#[test]
fn add_script() -> Result<()> {
Expand Down
8 changes: 6 additions & 2 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>

```
uv add [OPTIONS] <REQUIREMENTS>...
uv add [OPTIONS] <PACKAGES|--requirements <REQUIREMENTS>>
```

<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>

Expand Down Expand Up @@ -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&#8217;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>

<p>By default, uv will use the latest compatible version of each package (<code>highest</code>).</p>
Expand Down

0 comments on commit 268c6de

Please sign in to comment.