From 81ff3646bfb9489e2651cec6defa02a7ddf07d77 Mon Sep 17 00:00:00 2001 From: Shantanu Suryawanshi Date: Tue, 17 Feb 2026 22:38:57 -0500 Subject: [PATCH] Adding -E shorthand for --extra flag --- crates/uv-cli/src/lib.rs | 12 +++-- crates/uv/tests/it/edit.rs | 58 +++++++++++++++++++++++ crates/uv/tests/it/export.rs | 76 +++++++++++++++++++++++++++++++ crates/uv/tests/it/pip_compile.rs | 42 +++++++++++++++++ crates/uv/tests/it/pip_install.rs | 48 +++++++++++++++++++ crates/uv/tests/it/pip_sync.rs | 51 +++++++++++++++++++++ crates/uv/tests/it/run.rs | 46 +++++++++++++++++++ crates/uv/tests/it/sync.rs | 39 ++++++++++++++++ 8 files changed, 367 insertions(+), 5 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 67a4e9aa50c28..5612aab14a92a 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -1431,7 +1431,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. @@ -1788,7 +1788,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. @@ -2157,7 +2157,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. @@ -3466,6 +3466,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", @@ -3794,6 +3795,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", @@ -4327,7 +4329,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 d5527ce5f27ec..4a908e61eebac 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -3378,6 +3378,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 154ba00105caf..3c882263d1e14 100644 --- a/crates/uv/tests/it/export.rs +++ b/crates/uv/tests/it/export.rs @@ -8829,3 +8829,79 @@ 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(()) +} diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 211652986c672..47f4012b3d577 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -18318,3 +18318,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 c30e42572fc77..5802247777d06 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -14201,3 +14201,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 8922bb71ee302..1619c02e4ee18 100644 --- a/crates/uv/tests/it/run.rs +++ b/crates/uv/tests/it/run.rs @@ -6248,3 +6248,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 f60f7f7b49976..b5d60671336aa 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(()) +}