Skip to content

Commit 7bd0d97

Browse files
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<String>` 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. <!-- What's the purpose of the change? What does it do, and why? --> ## 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;" ``` <!-- How was it tested? --> --------- Co-authored-by: Charlie Marsh <[email protected]>
1 parent 2506c1c commit 7bd0d97

File tree

2 files changed

+326
-8
lines changed

2 files changed

+326
-8
lines changed

crates/uv-cli/src/lib.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2571,23 +2571,23 @@ pub struct RunArgs {
25712571
/// When used in a project, these dependencies will be layered on top of
25722572
/// the project environment in a separate, ephemeral environment. These
25732573
/// dependencies are allowed to conflict with those specified by the project.
2574-
#[arg(long)]
2574+
#[arg(long, value_delimiter = ',')]
25752575
pub with: Vec<String>,
25762576

25772577
/// Run with the given packages installed as editables.
25782578
///
25792579
/// When used in a project, these dependencies will be layered on top of
25802580
/// the project environment in a separate, ephemeral environment. These
25812581
/// dependencies are allowed to conflict with those specified by the project.
2582-
#[arg(long)]
2582+
#[arg(long, value_delimiter = ',')]
25832583
pub with_editable: Vec<String>,
25842584

25852585
/// Run with all packages listed in the given `requirements.txt` files.
25862586
///
25872587
/// The same environment semantics as `--with` apply.
25882588
///
25892589
/// Using `pyproject.toml`, `setup.py`, or `setup.cfg` files is not allowed.
2590-
#[arg(long, value_parser = parse_maybe_file_path)]
2590+
#[arg(long, value_delimiter = ',', value_parser = parse_maybe_file_path)]
25912591
pub with_requirements: Vec<Maybe<PathBuf>>,
25922592

25932593
/// Run the command in an isolated virtual environment.
@@ -3373,19 +3373,19 @@ pub struct ToolRunArgs {
33733373
pub from: Option<String>,
33743374

33753375
/// Run with the given packages installed.
3376-
#[arg(long)]
3376+
#[arg(long, value_delimiter = ',')]
33773377
pub with: Vec<String>,
33783378

33793379
/// Run with the given packages installed as editables
33803380
///
33813381
/// When used in a project, these dependencies will be layered on top of
33823382
/// the uv tool's environment in a separate, ephemeral environment. These
33833383
/// dependencies are allowed to conflict with those specified.
3384-
#[arg(long)]
3384+
#[arg(long, value_delimiter = ',')]
33853385
pub with_editable: Vec<String>,
33863386

33873387
/// Run with all packages listed in the given `requirements.txt` files.
3388-
#[arg(long, value_parser = parse_maybe_file_path)]
3388+
#[arg(long, value_delimiter = ',', value_parser = parse_maybe_file_path)]
33893389
pub with_requirements: Vec<Maybe<PathBuf>>,
33903390

33913391
/// Run the tool in an isolated virtual environment, ignoring any already-installed tools.
@@ -3441,11 +3441,11 @@ pub struct ToolInstallArgs {
34413441
pub from: Option<String>,
34423442

34433443
/// Include the following extra requirements.
3444-
#[arg(long)]
3444+
#[arg(long, value_delimiter = ',')]
34453445
pub with: Vec<String>,
34463446

34473447
/// Run all requirements listed in the given `requirements.txt` files.
3448-
#[arg(long, value_parser = parse_maybe_file_path)]
3448+
#[arg(long, value_delimiter = ',', value_parser = parse_maybe_file_path)]
34493449
pub with_requirements: Vec<Maybe<PathBuf>>,
34503450

34513451
#[command(flatten)]

crates/uv/tests/tool_run.rs

Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -829,6 +829,324 @@ fn tool_run_without_output() {
829829
"###);
830830
}
831831

832+
#[test]
833+
#[cfg(not(windows))]
834+
fn tool_run_csv_with() -> anyhow::Result<()> {
835+
let context = TestContext::new("3.12").with_filtered_counts();
836+
let tool_dir = context.temp_dir.child("tools");
837+
let bin_dir = context.temp_dir.child("bin");
838+
839+
let anyio_local = context.temp_dir.child("src").child("anyio_local");
840+
copy_dir_all(
841+
context.workspace_root.join("scripts/packages/anyio_local"),
842+
&anyio_local,
843+
)?;
844+
845+
let black_editable = context.temp_dir.child("src").child("black_editable");
846+
copy_dir_all(
847+
context
848+
.workspace_root
849+
.join("scripts/packages/black_editable"),
850+
&black_editable,
851+
)?;
852+
853+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
854+
pyproject_toml.write_str(indoc! { r#"
855+
[project]
856+
name = "foo"
857+
version = "1.0.0"
858+
requires-python = ">=3.8"
859+
dependencies = ["anyio", "sniffio==1.3.1"]
860+
"#
861+
})?;
862+
863+
let test_script = context.temp_dir.child("main.py");
864+
test_script.write_str(indoc! { r"
865+
import sniffio
866+
"
867+
})?;
868+
869+
// performs a tool run with CSV `with` flag
870+
uv_snapshot!(context.filters(), context.tool_run()
871+
.arg("--with")
872+
.arg("numpy,pandas")
873+
.arg("ipython")
874+
.arg("-c")
875+
.arg("import numpy; import pandas;")
876+
.env("UV_TOOL_DIR", tool_dir.as_os_str())
877+
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
878+
success: true
879+
exit_code: 0
880+
----- stdout -----
881+
882+
----- stderr -----
883+
Resolved [N] packages in [TIME]
884+
Prepared [N] packages in [TIME]
885+
Installed [N] packages in [TIME]
886+
+ asttokens==2.4.1
887+
+ decorator==5.1.1
888+
+ executing==2.0.1
889+
+ ipython==8.22.2
890+
+ jedi==0.19.1
891+
+ matplotlib-inline==0.1.6
892+
+ numpy==1.26.4
893+
+ pandas==2.2.1
894+
+ parso==0.8.3
895+
+ pexpect==4.9.0
896+
+ prompt-toolkit==3.0.43
897+
+ ptyprocess==0.7.0
898+
+ pure-eval==0.2.2
899+
+ pygments==2.17.2
900+
+ python-dateutil==2.9.0.post0
901+
+ pytz==2024.1
902+
+ six==1.16.0
903+
+ stack-data==0.6.3
904+
+ traitlets==5.14.2
905+
+ tzdata==2024.1
906+
+ wcwidth==0.2.13
907+
"###);
908+
909+
Ok(())
910+
}
911+
912+
#[test]
913+
#[cfg(windows)]
914+
fn tool_run_csv_with() -> anyhow::Result<()> {
915+
let context = TestContext::new("3.12").with_filtered_counts();
916+
let tool_dir = context.temp_dir.child("tools");
917+
let bin_dir = context.temp_dir.child("bin");
918+
919+
let anyio_local = context.temp_dir.child("src").child("anyio_local");
920+
copy_dir_all(
921+
context.workspace_root.join("scripts/packages/anyio_local"),
922+
&anyio_local,
923+
)?;
924+
925+
let black_editable = context.temp_dir.child("src").child("black_editable");
926+
copy_dir_all(
927+
context
928+
.workspace_root
929+
.join("scripts/packages/black_editable"),
930+
&black_editable,
931+
)?;
932+
933+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
934+
pyproject_toml.write_str(indoc! { r#"
935+
[project]
936+
name = "foo"
937+
version = "1.0.0"
938+
requires-python = ">=3.8"
939+
dependencies = ["anyio", "sniffio==1.3.1"]
940+
"#
941+
})?;
942+
943+
let test_script = context.temp_dir.child("main.py");
944+
test_script.write_str(indoc! { r"
945+
import sniffio
946+
"
947+
})?;
948+
949+
// performs a tool run with CSV `with` flag
950+
uv_snapshot!(context.filters(), context.tool_run()
951+
.arg("--with")
952+
.arg("numpy,pandas")
953+
.arg("ipython")
954+
.arg("-c")
955+
.arg("import numpy; import pandas;")
956+
.env("UV_TOOL_DIR", tool_dir.as_os_str())
957+
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
958+
success: true
959+
exit_code: 0
960+
----- stdout -----
961+
962+
----- stderr -----
963+
Resolved [N] packages in [TIME]
964+
Prepared [N] packages in [TIME]
965+
Installed [N] packages in [TIME]
966+
+ asttokens==2.4.1
967+
+ decorator==5.1.1
968+
+ executing==2.0.1
969+
+ ipython==8.22.2
970+
+ jedi==0.19.1
971+
+ matplotlib-inline==0.1.6
972+
+ numpy==1.26.4
973+
+ pandas==2.2.1
974+
+ parso==0.8.3
975+
+ prompt-toolkit==3.0.43
976+
+ pure-eval==0.2.2
977+
+ pygments==2.17.2
978+
+ python-dateutil==2.9.0.post0
979+
+ pytz==2024.1
980+
+ six==1.16.0
981+
+ stack-data==0.6.3
982+
+ traitlets==5.14.2
983+
+ wcwidth==0.2.13
984+
"###);
985+
986+
Ok(())
987+
}
988+
989+
#[test]
990+
#[cfg(not(windows))]
991+
fn tool_run_repeated_with() -> anyhow::Result<()> {
992+
let context = TestContext::new("3.12").with_filtered_counts();
993+
let tool_dir = context.temp_dir.child("tools");
994+
let bin_dir = context.temp_dir.child("bin");
995+
996+
let anyio_local = context.temp_dir.child("src").child("anyio_local");
997+
copy_dir_all(
998+
context.workspace_root.join("scripts/packages/anyio_local"),
999+
&anyio_local,
1000+
)?;
1001+
1002+
let black_editable = context.temp_dir.child("src").child("black_editable");
1003+
copy_dir_all(
1004+
context
1005+
.workspace_root
1006+
.join("scripts/packages/black_editable"),
1007+
&black_editable,
1008+
)?;
1009+
1010+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
1011+
pyproject_toml.write_str(indoc! { r#"
1012+
[project]
1013+
name = "foo"
1014+
version = "1.0.0"
1015+
requires-python = ">=3.8"
1016+
dependencies = ["anyio", "sniffio==1.3.1"]
1017+
"#
1018+
})?;
1019+
1020+
let test_script = context.temp_dir.child("main.py");
1021+
test_script.write_str(indoc! { r"
1022+
import sniffio
1023+
"
1024+
})?;
1025+
1026+
// performs a tool run with repeated `with` flag
1027+
uv_snapshot!(context.filters(), context.tool_run()
1028+
.arg("--with")
1029+
.arg("numpy")
1030+
.arg("--with")
1031+
.arg("pandas")
1032+
.arg("ipython")
1033+
.arg("-c")
1034+
.arg("import numpy; import pandas;")
1035+
.env("UV_TOOL_DIR", tool_dir.as_os_str())
1036+
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
1037+
success: true
1038+
exit_code: 0
1039+
----- stdout -----
1040+
1041+
----- stderr -----
1042+
Resolved [N] packages in [TIME]
1043+
Prepared [N] packages in [TIME]
1044+
Installed [N] packages in [TIME]
1045+
+ asttokens==2.4.1
1046+
+ decorator==5.1.1
1047+
+ executing==2.0.1
1048+
+ ipython==8.22.2
1049+
+ jedi==0.19.1
1050+
+ matplotlib-inline==0.1.6
1051+
+ numpy==1.26.4
1052+
+ pandas==2.2.1
1053+
+ parso==0.8.3
1054+
+ pexpect==4.9.0
1055+
+ prompt-toolkit==3.0.43
1056+
+ ptyprocess==0.7.0
1057+
+ pure-eval==0.2.2
1058+
+ pygments==2.17.2
1059+
+ python-dateutil==2.9.0.post0
1060+
+ pytz==2024.1
1061+
+ six==1.16.0
1062+
+ stack-data==0.6.3
1063+
+ traitlets==5.14.2
1064+
+ tzdata==2024.1
1065+
+ wcwidth==0.2.13
1066+
"###);
1067+
1068+
Ok(())
1069+
}
1070+
1071+
#[test]
1072+
#[cfg(windows)]
1073+
fn tool_run_repeated_with() -> anyhow::Result<()> {
1074+
let context = TestContext::new("3.12").with_filtered_counts();
1075+
let tool_dir = context.temp_dir.child("tools");
1076+
let bin_dir = context.temp_dir.child("bin");
1077+
1078+
let anyio_local = context.temp_dir.child("src").child("anyio_local");
1079+
copy_dir_all(
1080+
context.workspace_root.join("scripts/packages/anyio_local"),
1081+
&anyio_local,
1082+
)?;
1083+
1084+
let black_editable = context.temp_dir.child("src").child("black_editable");
1085+
copy_dir_all(
1086+
context
1087+
.workspace_root
1088+
.join("scripts/packages/black_editable"),
1089+
&black_editable,
1090+
)?;
1091+
1092+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
1093+
pyproject_toml.write_str(indoc! { r#"
1094+
[project]
1095+
name = "foo"
1096+
version = "1.0.0"
1097+
requires-python = ">=3.8"
1098+
dependencies = ["anyio", "sniffio==1.3.1"]
1099+
"#
1100+
})?;
1101+
1102+
let test_script = context.temp_dir.child("main.py");
1103+
test_script.write_str(indoc! { r"
1104+
import sniffio
1105+
"
1106+
})?;
1107+
1108+
// performs a tool run with repeated `with` flag
1109+
uv_snapshot!(context.filters(), context.tool_run()
1110+
.arg("--with")
1111+
.arg("numpy")
1112+
.arg("--with")
1113+
.arg("pandas")
1114+
.arg("ipython")
1115+
.arg("-c")
1116+
.arg("import numpy; import pandas;")
1117+
.env("UV_TOOL_DIR", tool_dir.as_os_str())
1118+
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
1119+
success: true
1120+
exit_code: 0
1121+
----- stdout -----
1122+
1123+
----- stderr -----
1124+
Resolved [N] packages in [TIME]
1125+
Prepared [N] packages in [TIME]
1126+
Installed [N] packages in [TIME]
1127+
+ asttokens==2.4.1
1128+
+ decorator==5.1.1
1129+
+ executing==2.0.1
1130+
+ ipython==8.22.2
1131+
+ jedi==0.19.1
1132+
+ matplotlib-inline==0.1.6
1133+
+ numpy==1.26.4
1134+
+ pandas==2.2.1
1135+
+ parso==0.8.3
1136+
+ prompt-toolkit==3.0.43
1137+
+ pure-eval==0.2.2
1138+
+ pygments==2.17.2
1139+
+ python-dateutil==2.9.0.post0
1140+
+ pytz==2024.1
1141+
+ six==1.16.0
1142+
+ stack-data==0.6.3
1143+
+ traitlets==5.14.2
1144+
+ wcwidth==0.2.13
1145+
"###);
1146+
1147+
Ok(())
1148+
}
1149+
8321150
#[test]
8331151
fn tool_run_with_editable() -> anyhow::Result<()> {
8341152
let context = TestContext::new("3.12").with_filtered_counts();

0 commit comments

Comments
 (0)