diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index ede53be980c2..7856a14c7a65 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -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")] @@ -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")] @@ -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, + #[arg(group = "sources")] + pub packages: Vec, + + /// 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, /// Add the requirements as development dependencies. #[arg(long, conflicts_with("optional"))] diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 3a35c685a83e..483b3cc9402c 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -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}; @@ -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 { diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 8d05c177e0a7..42bebb8ad24f 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -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::>(); + 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, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 7124da598228..0f290a47b73b 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -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, @@ -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, + pub(crate) packages: Vec, + pub(crate) requirements: Vec, pub(crate) dependency_type: DependencyType, pub(crate) editable: Option, pub(crate) extras: Vec, @@ -714,6 +714,7 @@ impl AddSettings { #[allow(clippy::needless_pass_by_value)] pub(crate) fn resolve(args: AddArgs, filesystem: Option) -> Self { let AddArgs { + packages, requirements, dev, optional, @@ -735,11 +736,6 @@ impl AddSettings { python, } = args; - let requirements = requirements - .into_iter() - .map(RequirementsSource::Package) - .collect::>(); - let dependency_type = if let Some(group) = optional { DependencyType::Optional(group) } else if dev { @@ -752,6 +748,7 @@ impl AddSettings { locked, frozen, no_sync, + packages, requirements, dependency_type, raw_sources, diff --git a/crates/uv/tests/edit.rs b/crates/uv/tests/edit.rs index f969e1d9c1d4..7b138f4e4d96 100644 --- a/crates/uv/tests/edit.rs +++ b/crates/uv/tests/edit.rs @@ -794,7 +794,7 @@ fn add_raw_error() -> Result<()> { ----- stderr ----- error: the argument '--tag ' cannot be used with '--raw-sources' - Usage: uv add --cache-dir [CACHE_DIR] --tag --exclude-newer ... + Usage: uv add --cache-dir [CACHE_DIR] --tag --exclude-newer > For more information, try '--help'. "###); @@ -2732,7 +2732,7 @@ fn add_reject_multiple_git_ref_flags() { ----- stderr ----- error: the argument '--tag ' cannot be used with '--branch ' - Usage: uv add --cache-dir [CACHE_DIR] --tag --exclude-newer ... + Usage: uv add --cache-dir [CACHE_DIR] --tag --exclude-newer > For more information, try '--help'. "### @@ -2753,7 +2753,7 @@ fn add_reject_multiple_git_ref_flags() { ----- stderr ----- error: the argument '--tag ' cannot be used with '--rev ' - Usage: uv add --cache-dir [CACHE_DIR] --tag --exclude-newer ... + Usage: uv add --cache-dir [CACHE_DIR] --tag --exclude-newer > For more information, try '--help'. "### @@ -2774,7 +2774,7 @@ fn add_reject_multiple_git_ref_flags() { ----- stderr ----- error: the argument '--tag ' cannot be used multiple times - Usage: uv add [OPTIONS] ... + Usage: uv add [OPTIONS] > For more information, try '--help'. "### @@ -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: + > + + Usage: uv add --cache-dir [CACHE_DIR] --exclude-newer > + + For more information, try '--help'. + "###); + + Ok(()) +} + /// Add to a PEP 732 script. #[test] fn add_script() -> Result<()> { diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 90cf40aae2fb..75133695b40a 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -466,12 +466,12 @@ uv will search for a project in the current directory or any parent directory. I

Usage

``` -uv add [OPTIONS] ... +uv add [OPTIONS] > ```

Arguments

-
REQUIREMENTS

The packages to add, as PEP 508 requirements (e.g., ruff==0.5.0)

+
PACKAGES

The packages to add, as PEP 508 requirements (e.g., ruff==0.5.0)

@@ -696,6 +696,10 @@ uv add [OPTIONS] ...
--reinstall-package reinstall-package

Reinstall a specific package, regardless of whether it’s already installed. Implies --refresh-package

+
--requirements, -r requirements

Add all packages listed in the given requirements.txt files.

+ +

Implies --raw-sources.

+
--resolution resolution

The strategy to use when selecting between the different compatible versions for a given package requirement.

By default, uv will use the latest compatible version of each package (highest).