diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 9809a3d9c3a77..ae59f289c15bb 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -1432,7 +1432,7 @@ pub struct PipCompileArgs { /// Include optional dependencies from the specified extra name; may be provided more than once. /// /// Only applies to `pyproject.toml`, `setup.py`, and `setup.cfg` sources. - #[arg(long, value_delimiter = ',', conflicts_with = "all_extras", value_parser = extra_name_with_clap_error)] + #[arg(short = 'E', long, value_delimiter = ',', conflicts_with = "all_extras", value_parser = extra_name_with_clap_error)] pub extra: Option>, /// Include all optional dependencies. @@ -1789,7 +1789,7 @@ pub struct PipSyncArgs { /// Include optional dependencies from the specified extra name; may be provided more than once. /// /// Only applies to `pylock.toml`, `pyproject.toml`, `setup.py`, and `setup.cfg` sources. - #[arg(long, value_delimiter = ',', conflicts_with = "all_extras", value_parser = extra_name_with_clap_error)] + #[arg(short = 'E', long, value_delimiter = ',', conflicts_with = "all_extras", value_parser = extra_name_with_clap_error)] pub extra: Option>, /// Include all optional dependencies. @@ -2158,7 +2158,7 @@ pub struct PipInstallArgs { /// Include optional dependencies from the specified extra name; may be provided more than once. /// /// Only applies to `pylock.toml`, `pyproject.toml`, `setup.py`, and `setup.cfg` sources. - #[arg(long, value_delimiter = ',', conflicts_with = "all_extras", value_parser = extra_name_with_clap_error)] + #[arg(short = 'E', long, value_delimiter = ',', conflicts_with = "all_extras", value_parser = extra_name_with_clap_error)] pub extra: Option>, /// Include all optional dependencies. @@ -3467,6 +3467,7 @@ pub struct RunArgs { /// /// This option is only available when running in a project. #[arg( + short = 'E', long, conflicts_with = "all_extras", conflicts_with = "only_group", @@ -3795,6 +3796,7 @@ pub struct SyncArgs { /// Note that all optional dependencies are always included in the resolution; this option only /// affects the selection of packages to install. #[arg( + short = 'E', long, conflicts_with = "all_extras", conflicts_with = "only_group", @@ -4328,7 +4330,7 @@ pub struct AddArgs { /// May be provided more than once. /// /// To add this dependency to an optional extra instead, see `--optional`. - #[arg(long, value_hint = ValueHint::Other)] + #[arg(short = 'E', long, value_hint = ValueHint::Other)] pub extra: Option>, /// Avoid syncing the virtual environment [env: UV_NO_SYNC=] @@ -4805,7 +4807,7 @@ pub struct ExportArgs { /// Include optional dependencies from the specified extra name. /// /// May be provided more than once. - #[arg(long, value_delimiter = ',', conflicts_with = "all_extras", conflicts_with = "only_group", value_parser = extra_name_with_clap_error)] + #[arg(short = 'E', long, value_delimiter = ',', conflicts_with = "all_extras", conflicts_with = "only_group", value_parser = extra_name_with_clap_error)] pub extra: Option>, /// Include all optional dependencies. diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index bfd714d134c05..fc2bf51b761ce 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -3377,6 +3377,64 @@ fn update() -> Result<()> { Ok(()) } +/// Verify that `-E` works as a shorthand for `--extra` in `uv add`. +#[test] +#[cfg(feature = "test-git")] +fn add_extra_shorthand() -> Result<()> { + let context = uv_test::test_context!("3.12"); + + 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 = [ + "requests==2.31.0", + ] + "#})?; + + // Use `-E` to enable extras (multiple times). + uv_snapshot!(context.filters(), context.add().arg("requests; python_version > '3.7'").args(["-E", "use_chardet_on_py3", "-E", "socks"]), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 8 packages in [TIME] + Prepared 7 packages in [TIME] + Installed 7 packages in [TIME] + + certifi==2024.2.2 + + chardet==5.2.0 + + charset-normalizer==3.3.2 + + idna==3.6 + + pysocks==1.7.1 + + requests==2.31.0 + + urllib3==2.2.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 = [ + "requests==2.31.0", + "requests[socks,use-chardet-on-py3]>=2.31.0 ; python_full_version >= '3.8'", + ] + "# + ); + }); + + Ok(()) +} + /// Add and update a requirement, with different markers #[test] fn add_update_marker() -> Result<()> { diff --git a/crates/uv/tests/it/export.rs b/crates/uv/tests/it/export.rs index 775605443861a..349e01a937fce 100644 --- a/crates/uv/tests/it/export.rs +++ b/crates/uv/tests/it/export.rs @@ -8830,6 +8830,82 @@ fn cyclonedx_export_virtual_workspace_fixture() -> Result<()> { Ok(()) } +#[test] +fn export_extra_shorthand() -> Result<()> { + let context = uv_test::test_context!("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + + [project.optional-dependencies] + async = ["anyio==3.7.0"] + pytest = ["iniconfig"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.export().arg("-E").arg("pytest").arg("-E").arg("async").arg("--no-extra").arg("pytest"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] -E pytest -E async --no-extra pytest + -e . + anyio==3.7.0 \ + --hash=sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce \ + --hash=sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0 + # via project + idna==3.6 \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f + # via anyio + sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + # via anyio + typing-extensions==4.10.0 \ + --hash=sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475 \ + --hash=sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb + # via project + + ----- stderr ----- + Resolved 6 packages in [TIME] + "#); + + uv_snapshot!(context.filters(), context.export().arg("-E").arg("pytest"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] -E pytest + -e . + iniconfig==2.0.0 \ + --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ + --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 + # via project + typing-extensions==4.10.0 \ + --hash=sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475 \ + --hash=sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb + # via project + + ----- stderr ----- + Resolved 6 packages in [TIME] + "#); + + Ok(()) +} + #[test] fn pylock_toml_filter_by_requires_python() -> Result<()> { let context = uv_test::test_context!("3.12"); diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 92373fc054bae..824bf727137d5 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -18317,3 +18317,45 @@ async fn compile_missing_python_download_error_warning() { Caused by: tunnel error: unsuccessful "); } + +#[test] +fn pip_compile_extra_shorthand() -> Result<()> { + let context = uv_test::test_context!("3.12"); + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#"[build-system] +requires = ["setuptools>=42"] + +[project] +name = "project" +version = "0.1.0" +dependencies = [] +optional-dependencies.foo = [ + "anyio==3.7.0", +] +"#, + )?; + + uv_snapshot!(context.filters(), context.pip_compile() + .arg("pyproject.toml") + .arg("-E") + .arg("foo"), @" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] pyproject.toml -E foo + anyio==3.7.0 + # via project (pyproject.toml) + idna==3.6 + # via anyio + sniffio==1.3.1 + # via anyio + + ----- stderr ----- + Resolved 3 packages in [TIME] + " + ); + + Ok(()) +} diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 3d29517ca24d4..5ba2992719bfb 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -14195,3 +14195,51 @@ fn warn_on_lzma_wheel() { " ); } + +#[test] +fn pip_install_extra_shorthand() -> Result<()> { + let context = uv_test::test_context!("3.12"); + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [tool.poetry] +name = "poetry-editable" +version = "0.1.0" +description = "" +authors = ["Astral Software Inc. "] + +[tool.poetry.dependencies] +python = "^3.10" +anyio = "^3" +iniconfig = { version = "*", optional = true } + +[tool.poetry.extras] +test = ["iniconfig"] + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" +"#, + })?; + + uv_snapshot!(context.pip_install() + .arg("-r") + .arg("pyproject.toml") + .arg("-E") + .arg("test"), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + Prepared 4 packages in [TIME] + Installed 4 packages in [TIME] + + anyio==3.7.1 + + idna==3.6 + + iniconfig==2.0.0 + + sniffio==1.3.1 + " + ); + + Ok(()) +} diff --git a/crates/uv/tests/it/pip_sync.rs b/crates/uv/tests/it/pip_sync.rs index 20959f383e239..e9095a943cc99 100644 --- a/crates/uv/tests/it/pip_sync.rs +++ b/crates/uv/tests/it/pip_sync.rs @@ -6214,3 +6214,54 @@ fn sync_with_target_installs_missing_python() -> Result<()> { ); Ok(()) } + +#[test] +fn pip_sync_extra_shorthand() -> Result<()> { + let context = uv_test::test_context!("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [project.optional-dependencies] + types = ["typing-extensions>=4"] + async = ["anyio>3"] + "#, + )?; + + // Generate a lock file with the extras. + context + .export() + .arg("-o") + .arg("pylock.toml") + .arg("-E") + .arg("types") + .arg("-E") + .arg("async") + .assert() + .success(); + + uv_snapshot!(context.filters(), context.pip_sync() + .arg("--preview") + .arg("pylock.toml") + .args(["-E", "types", "-E", "async"]), @r#" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Prepared 4 packages in [TIME] + Installed 4 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + sniffio==1.3.1 + + typing-extensions==4.10.0 + "#); + + Ok(()) +} diff --git a/crates/uv/tests/it/run.rs b/crates/uv/tests/it/run.rs index 2dc8d532f6e89..f0617754e0f49 100644 --- a/crates/uv/tests/it/run.rs +++ b/crates/uv/tests/it/run.rs @@ -6246,3 +6246,49 @@ fn run_target_workspace_discovery_bare_script() -> Result<()> { Ok(()) } + +#[test] +fn run_extra_shorthand() -> Result<()> { + let context = uv_test::test_context!("3.12"); + + 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.0" + dependencies = [] + + [project.optional-dependencies] + foo = ["iniconfig==2.0.0"] + bar = ["iniconfig==1.1.1"] + + [tool.uv] + conflicts = [ + [ + { extra = "foo" }, + { extra = "bar" }, + ], + ] + "# + })?; + + uv_snapshot!(context.filters(), context.run() + .arg("-E") + .arg("foo") + .arg("python") + .arg("-c") + .arg("import iniconfig"), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 + "); + + Ok(()) +} diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 9039c2ff4aeb0..7f7a742c31424 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -15613,3 +15613,42 @@ fn sync_reinstalls_on_version_change() -> Result<()> { Ok(()) } + +#[test] +fn sync_extra_shorthand() -> Result<()> { + let context = uv_test::test_context!("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [project.optional-dependencies] + types = ["typing-extensions>=4"] + async = ["anyio>3"] + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.sync().args(["-E", "types", "-E", "async"]), @r#" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 5 packages in [TIME] + Prepared 4 packages in [TIME] + Installed 4 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + sniffio==1.3.1 + + typing-extensions==4.10.0 + "#); + + Ok(()) +}