From 7bd0d97ce573e9fa428d2b0076d83976e772b3b7 Mon Sep 17 00:00:00 2001 From: Noam Teyssier <22600644+noamteyssier@users.noreply.github.com> Date: Fri, 11 Oct 2024 02:19:57 -0700 Subject: [PATCH] feat: add comma value-delimiter to with argument in tool run args to allow for multiple arguments in with flag (#7909) This is to address my own issue #7908 ## Summary This change makes use of the `clap` value_delimiter parser to populate the `with` `Vec` which currently can either only be empty or with 1 value for each `--with` flag. This makes use of the current code structure but allows for multiple arguments with a single `--with` flag. ## Test Plan Can be tested with the following CLI: ```bash target/debug/uv tool run --with numpy,polars,matplotlib ipython -c "import numpy;import polars;import matplotlib;" ``` And former behavior of multiple `--with` flags are kept ```bash target/debug/uv tool run --with numpy --with polars --with matplotlib ipython -c "import numpy;import polars;import matplotlib;" ``` --------- Co-authored-by: Charlie Marsh --- crates/uv-cli/src/lib.rs | 16 +- crates/uv/tests/tool_run.rs | 318 ++++++++++++++++++++++++++++++++++++ 2 files changed, 326 insertions(+), 8 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 3b27ac801..8f3cd3d4b 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2571,7 +2571,7 @@ pub struct RunArgs { /// When used in a project, these dependencies will be layered on top of /// the project environment in a separate, ephemeral environment. These /// dependencies are allowed to conflict with those specified by the project. - #[arg(long)] + #[arg(long, value_delimiter = ',')] pub with: Vec, /// Run with the given packages installed as editables. @@ -2579,7 +2579,7 @@ pub struct RunArgs { /// When used in a project, these dependencies will be layered on top of /// the project environment in a separate, ephemeral environment. These /// dependencies are allowed to conflict with those specified by the project. - #[arg(long)] + #[arg(long, value_delimiter = ',')] pub with_editable: Vec, /// Run with all packages listed in the given `requirements.txt` files. @@ -2587,7 +2587,7 @@ pub struct RunArgs { /// The same environment semantics as `--with` apply. /// /// Using `pyproject.toml`, `setup.py`, or `setup.cfg` files is not allowed. - #[arg(long, value_parser = parse_maybe_file_path)] + #[arg(long, value_delimiter = ',', value_parser = parse_maybe_file_path)] pub with_requirements: Vec>, /// Run the command in an isolated virtual environment. @@ -3373,7 +3373,7 @@ pub struct ToolRunArgs { pub from: Option, /// Run with the given packages installed. - #[arg(long)] + #[arg(long, value_delimiter = ',')] pub with: Vec, /// Run with the given packages installed as editables @@ -3381,11 +3381,11 @@ pub struct ToolRunArgs { /// When used in a project, these dependencies will be layered on top of /// the uv tool's environment in a separate, ephemeral environment. These /// dependencies are allowed to conflict with those specified. - #[arg(long)] + #[arg(long, value_delimiter = ',')] pub with_editable: Vec, /// Run with all packages listed in the given `requirements.txt` files. - #[arg(long, value_parser = parse_maybe_file_path)] + #[arg(long, value_delimiter = ',', value_parser = parse_maybe_file_path)] pub with_requirements: Vec>, /// Run the tool in an isolated virtual environment, ignoring any already-installed tools. @@ -3441,11 +3441,11 @@ pub struct ToolInstallArgs { pub from: Option, /// Include the following extra requirements. - #[arg(long)] + #[arg(long, value_delimiter = ',')] pub with: Vec, /// Run all requirements listed in the given `requirements.txt` files. - #[arg(long, value_parser = parse_maybe_file_path)] + #[arg(long, value_delimiter = ',', value_parser = parse_maybe_file_path)] pub with_requirements: Vec>, #[command(flatten)] diff --git a/crates/uv/tests/tool_run.rs b/crates/uv/tests/tool_run.rs index e8a766e18..cf443775b 100644 --- a/crates/uv/tests/tool_run.rs +++ b/crates/uv/tests/tool_run.rs @@ -829,6 +829,324 @@ fn tool_run_without_output() { "###); } +#[test] +#[cfg(not(windows))] +fn tool_run_csv_with() -> anyhow::Result<()> { + let context = TestContext::new("3.12").with_filtered_counts(); + let tool_dir = context.temp_dir.child("tools"); + let bin_dir = context.temp_dir.child("bin"); + + let anyio_local = context.temp_dir.child("src").child("anyio_local"); + copy_dir_all( + context.workspace_root.join("scripts/packages/anyio_local"), + &anyio_local, + )?; + + let black_editable = context.temp_dir.child("src").child("black_editable"); + copy_dir_all( + context + .workspace_root + .join("scripts/packages/black_editable"), + &black_editable, + )?; + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! { r#" + [project] + name = "foo" + version = "1.0.0" + requires-python = ">=3.8" + dependencies = ["anyio", "sniffio==1.3.1"] + "# + })?; + + let test_script = context.temp_dir.child("main.py"); + test_script.write_str(indoc! { r" + import sniffio + " + })?; + + // performs a tool run with CSV `with` flag + uv_snapshot!(context.filters(), context.tool_run() + .arg("--with") + .arg("numpy,pandas") + .arg("ipython") + .arg("-c") + .arg("import numpy; import pandas;") + .env("UV_TOOL_DIR", tool_dir.as_os_str()) + .env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved [N] packages in [TIME] + Prepared [N] packages in [TIME] + Installed [N] packages in [TIME] + + asttokens==2.4.1 + + decorator==5.1.1 + + executing==2.0.1 + + ipython==8.22.2 + + jedi==0.19.1 + + matplotlib-inline==0.1.6 + + numpy==1.26.4 + + pandas==2.2.1 + + parso==0.8.3 + + pexpect==4.9.0 + + prompt-toolkit==3.0.43 + + ptyprocess==0.7.0 + + pure-eval==0.2.2 + + pygments==2.17.2 + + python-dateutil==2.9.0.post0 + + pytz==2024.1 + + six==1.16.0 + + stack-data==0.6.3 + + traitlets==5.14.2 + + tzdata==2024.1 + + wcwidth==0.2.13 + "###); + + Ok(()) +} + +#[test] +#[cfg(windows)] +fn tool_run_csv_with() -> anyhow::Result<()> { + let context = TestContext::new("3.12").with_filtered_counts(); + let tool_dir = context.temp_dir.child("tools"); + let bin_dir = context.temp_dir.child("bin"); + + let anyio_local = context.temp_dir.child("src").child("anyio_local"); + copy_dir_all( + context.workspace_root.join("scripts/packages/anyio_local"), + &anyio_local, + )?; + + let black_editable = context.temp_dir.child("src").child("black_editable"); + copy_dir_all( + context + .workspace_root + .join("scripts/packages/black_editable"), + &black_editable, + )?; + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! { r#" + [project] + name = "foo" + version = "1.0.0" + requires-python = ">=3.8" + dependencies = ["anyio", "sniffio==1.3.1"] + "# + })?; + + let test_script = context.temp_dir.child("main.py"); + test_script.write_str(indoc! { r" + import sniffio + " + })?; + + // performs a tool run with CSV `with` flag + uv_snapshot!(context.filters(), context.tool_run() + .arg("--with") + .arg("numpy,pandas") + .arg("ipython") + .arg("-c") + .arg("import numpy; import pandas;") + .env("UV_TOOL_DIR", tool_dir.as_os_str()) + .env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved [N] packages in [TIME] + Prepared [N] packages in [TIME] + Installed [N] packages in [TIME] + + asttokens==2.4.1 + + decorator==5.1.1 + + executing==2.0.1 + + ipython==8.22.2 + + jedi==0.19.1 + + matplotlib-inline==0.1.6 + + numpy==1.26.4 + + pandas==2.2.1 + + parso==0.8.3 + + prompt-toolkit==3.0.43 + + pure-eval==0.2.2 + + pygments==2.17.2 + + python-dateutil==2.9.0.post0 + + pytz==2024.1 + + six==1.16.0 + + stack-data==0.6.3 + + traitlets==5.14.2 + + wcwidth==0.2.13 + "###); + + Ok(()) +} + +#[test] +#[cfg(not(windows))] +fn tool_run_repeated_with() -> anyhow::Result<()> { + let context = TestContext::new("3.12").with_filtered_counts(); + let tool_dir = context.temp_dir.child("tools"); + let bin_dir = context.temp_dir.child("bin"); + + let anyio_local = context.temp_dir.child("src").child("anyio_local"); + copy_dir_all( + context.workspace_root.join("scripts/packages/anyio_local"), + &anyio_local, + )?; + + let black_editable = context.temp_dir.child("src").child("black_editable"); + copy_dir_all( + context + .workspace_root + .join("scripts/packages/black_editable"), + &black_editable, + )?; + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! { r#" + [project] + name = "foo" + version = "1.0.0" + requires-python = ">=3.8" + dependencies = ["anyio", "sniffio==1.3.1"] + "# + })?; + + let test_script = context.temp_dir.child("main.py"); + test_script.write_str(indoc! { r" + import sniffio + " + })?; + + // performs a tool run with repeated `with` flag + uv_snapshot!(context.filters(), context.tool_run() + .arg("--with") + .arg("numpy") + .arg("--with") + .arg("pandas") + .arg("ipython") + .arg("-c") + .arg("import numpy; import pandas;") + .env("UV_TOOL_DIR", tool_dir.as_os_str()) + .env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved [N] packages in [TIME] + Prepared [N] packages in [TIME] + Installed [N] packages in [TIME] + + asttokens==2.4.1 + + decorator==5.1.1 + + executing==2.0.1 + + ipython==8.22.2 + + jedi==0.19.1 + + matplotlib-inline==0.1.6 + + numpy==1.26.4 + + pandas==2.2.1 + + parso==0.8.3 + + pexpect==4.9.0 + + prompt-toolkit==3.0.43 + + ptyprocess==0.7.0 + + pure-eval==0.2.2 + + pygments==2.17.2 + + python-dateutil==2.9.0.post0 + + pytz==2024.1 + + six==1.16.0 + + stack-data==0.6.3 + + traitlets==5.14.2 + + tzdata==2024.1 + + wcwidth==0.2.13 + "###); + + Ok(()) +} + +#[test] +#[cfg(windows)] +fn tool_run_repeated_with() -> anyhow::Result<()> { + let context = TestContext::new("3.12").with_filtered_counts(); + let tool_dir = context.temp_dir.child("tools"); + let bin_dir = context.temp_dir.child("bin"); + + let anyio_local = context.temp_dir.child("src").child("anyio_local"); + copy_dir_all( + context.workspace_root.join("scripts/packages/anyio_local"), + &anyio_local, + )?; + + let black_editable = context.temp_dir.child("src").child("black_editable"); + copy_dir_all( + context + .workspace_root + .join("scripts/packages/black_editable"), + &black_editable, + )?; + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! { r#" + [project] + name = "foo" + version = "1.0.0" + requires-python = ">=3.8" + dependencies = ["anyio", "sniffio==1.3.1"] + "# + })?; + + let test_script = context.temp_dir.child("main.py"); + test_script.write_str(indoc! { r" + import sniffio + " + })?; + + // performs a tool run with repeated `with` flag + uv_snapshot!(context.filters(), context.tool_run() + .arg("--with") + .arg("numpy") + .arg("--with") + .arg("pandas") + .arg("ipython") + .arg("-c") + .arg("import numpy; import pandas;") + .env("UV_TOOL_DIR", tool_dir.as_os_str()) + .env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved [N] packages in [TIME] + Prepared [N] packages in [TIME] + Installed [N] packages in [TIME] + + asttokens==2.4.1 + + decorator==5.1.1 + + executing==2.0.1 + + ipython==8.22.2 + + jedi==0.19.1 + + matplotlib-inline==0.1.6 + + numpy==1.26.4 + + pandas==2.2.1 + + parso==0.8.3 + + prompt-toolkit==3.0.43 + + pure-eval==0.2.2 + + pygments==2.17.2 + + python-dateutil==2.9.0.post0 + + pytz==2024.1 + + six==1.16.0 + + stack-data==0.6.3 + + traitlets==5.14.2 + + wcwidth==0.2.13 + "###); + + Ok(()) +} + #[test] fn tool_run_with_editable() -> anyhow::Result<()> { let context = TestContext::new("3.12").with_filtered_counts();