Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support uv add -r requirements.txt #6005

Merged
merged 8 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading