Skip to content

Commit 4513ce0

Browse files
Allow --script to be provided with uv run - (#10035)
## Summary Closes #10021.
1 parent 5a3826d commit 4513ce0

File tree

3 files changed

+182
-10
lines changed

3 files changed

+182
-10
lines changed

crates/uv/src/commands/project/run.rs

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1182,6 +1182,8 @@ pub(crate) enum RunCommand {
11821182
PythonZipapp(PathBuf, Vec<OsString>),
11831183
/// Execute a `python` script provided via `stdin`.
11841184
PythonStdin(Vec<u8>, Vec<OsString>),
1185+
/// Execute a `pythonw` script provided via `stdin`.
1186+
PythonGuiStdin(Vec<u8>, Vec<OsString>),
11851187
/// Execute a Python script provided via a remote URL.
11861188
PythonRemote(tempfile::NamedTempFile, Vec<OsString>),
11871189
/// Execute an external command.
@@ -1209,6 +1211,13 @@ impl RunCommand {
12091211
}
12101212
}
12111213
Self::PythonStdin(..) => Cow::Borrowed("python -c"),
1214+
Self::PythonGuiStdin(..) => {
1215+
if cfg!(windows) {
1216+
Cow::Borrowed("pythonw -c")
1217+
} else {
1218+
Cow::Borrowed("python -c")
1219+
}
1220+
}
12121221
Self::External(executable, _) => executable.to_string_lossy(),
12131222
}
12141223
}
@@ -1280,6 +1289,38 @@ impl RunCommand {
12801289

12811290
process
12821291
}
1292+
Self::PythonGuiStdin(script, args) => {
1293+
let python_executable = interpreter.sys_executable();
1294+
1295+
// Use `pythonw.exe` if it exists, otherwise fall back to `python.exe`.
1296+
// See `install-wheel-rs::get_script_executable`.gd
1297+
let pythonw_executable = python_executable
1298+
.file_name()
1299+
.map(|name| {
1300+
let new_name = name.to_string_lossy().replace("python", "pythonw");
1301+
python_executable.with_file_name(new_name)
1302+
})
1303+
.filter(|path| path.is_file())
1304+
.unwrap_or_else(|| python_executable.to_path_buf());
1305+
1306+
let mut process = Command::new(&pythonw_executable);
1307+
process.arg("-c");
1308+
1309+
#[cfg(unix)]
1310+
{
1311+
use std::os::unix::ffi::OsStringExt;
1312+
process.arg(OsString::from_vec(script.clone()));
1313+
}
1314+
1315+
#[cfg(not(unix))]
1316+
{
1317+
let script = String::from_utf8(script.clone()).expect("script is valid UTF-8");
1318+
process.arg(script);
1319+
}
1320+
process.args(args);
1321+
1322+
process
1323+
}
12831324
Self::External(executable, args) => {
12841325
let mut process = Command::new(executable);
12851326
process.args(args);
@@ -1328,6 +1369,10 @@ impl std::fmt::Display for RunCommand {
13281369
write!(f, "python -c")?;
13291370
Ok(())
13301371
}
1372+
Self::PythonGuiStdin(..) => {
1373+
write!(f, "pythonw -c")?;
1374+
Ok(())
1375+
}
13311376
Self::External(executable, args) => {
13321377
write!(f, "{}", executable.to_string_lossy())?;
13331378
for arg in args {
@@ -1360,6 +1405,19 @@ impl RunCommand {
13601405
return Ok(Self::Empty);
13611406
};
13621407

1408+
if target.eq_ignore_ascii_case("-") {
1409+
let mut buf = Vec::with_capacity(1024);
1410+
std::io::stdin().read_to_end(&mut buf)?;
1411+
1412+
return if module {
1413+
Err(anyhow!("Cannot run a Python module from stdin"))
1414+
} else if gui_script {
1415+
Ok(Self::PythonGuiStdin(buf, args.to_vec()))
1416+
} else {
1417+
Ok(Self::PythonStdin(buf, args.to_vec()))
1418+
};
1419+
}
1420+
13631421
let target_path = PathBuf::from(target);
13641422

13651423
// Determine whether the user provided a remote script.
@@ -1402,21 +1460,17 @@ impl RunCommand {
14021460

14031461
if module {
14041462
return Ok(Self::PythonModule(target.clone(), args.to_vec()));
1405-
} else if script {
1406-
return Ok(Self::PythonScript(target.clone().into(), args.to_vec()));
14071463
} else if gui_script {
14081464
return Ok(Self::PythonGuiScript(target.clone().into(), args.to_vec()));
1465+
} else if script {
1466+
return Ok(Self::PythonScript(target.clone().into(), args.to_vec()));
14091467
}
14101468

14111469
let metadata = target_path.metadata();
14121470
let is_file = metadata.as_ref().map_or(false, std::fs::Metadata::is_file);
14131471
let is_dir = metadata.as_ref().map_or(false, std::fs::Metadata::is_dir);
14141472

1415-
if target.eq_ignore_ascii_case("-") {
1416-
let mut buf = Vec::with_capacity(1024);
1417-
std::io::stdin().read_to_end(&mut buf)?;
1418-
Ok(Self::PythonStdin(buf, args.to_vec()))
1419-
} else if target.eq_ignore_ascii_case("python") {
1473+
if target.eq_ignore_ascii_case("python") {
14201474
Ok(Self::Python(args.to_vec()))
14211475
} else if target_path
14221476
.extension()

crates/uv/src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,9 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
175175
Some(RunCommand::PythonRemote(script, _)) => {
176176
Pep723Metadata::read(&script).await?.map(Pep723Item::Remote)
177177
}
178-
Some(RunCommand::PythonStdin(contents, _)) => {
179-
Pep723Metadata::parse(contents)?.map(Pep723Item::Stdin)
180-
}
178+
Some(
179+
RunCommand::PythonStdin(contents, _) | RunCommand::PythonGuiStdin(contents, _),
180+
) => Pep723Metadata::parse(contents)?.map(Pep723Item::Stdin),
181181
_ => None,
182182
}
183183
} else if let ProjectCommand::Remove(uv_cli::RemoveArgs {

crates/uv/tests/it/run.rs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2544,6 +2544,20 @@ fn run_module() {
25442544
"#);
25452545
}
25462546

2547+
#[test]
2548+
fn run_module_stdin() {
2549+
let context = TestContext::new("3.12");
2550+
2551+
uv_snapshot!(context.filters(), context.run().arg("-m").arg("-"), @r###"
2552+
success: false
2553+
exit_code: 2
2554+
----- stdout -----
2555+
2556+
----- stderr -----
2557+
error: Cannot run a Python module from stdin
2558+
"###);
2559+
}
2560+
25472561
/// When the `pyproject.toml` file is invalid.
25482562
#[test]
25492563
fn run_project_toml_error() -> Result<()> {
@@ -2874,6 +2888,40 @@ fn run_script_explicit() -> Result<()> {
28742888
Ok(())
28752889
}
28762890

2891+
#[test]
2892+
fn run_script_explicit_stdin() -> Result<()> {
2893+
let context = TestContext::new("3.12");
2894+
2895+
let test_script = context.temp_dir.child("script");
2896+
test_script.write_str(indoc! { r#"
2897+
# /// script
2898+
# requires-python = ">=3.11"
2899+
# dependencies = [
2900+
# "iniconfig",
2901+
# ]
2902+
# ///
2903+
import iniconfig
2904+
print("Hello, world!")
2905+
"#
2906+
})?;
2907+
2908+
uv_snapshot!(context.filters(), context.run().arg("--script").arg("-").stdin(std::fs::File::open(test_script)?), @r###"
2909+
success: true
2910+
exit_code: 0
2911+
----- stdout -----
2912+
Hello, world!
2913+
2914+
----- stderr -----
2915+
Reading inline script metadata from `stdin`
2916+
Resolved 1 package in [TIME]
2917+
Prepared 1 package in [TIME]
2918+
Installed 1 package in [TIME]
2919+
+ iniconfig==2.0.0
2920+
"###);
2921+
2922+
Ok(())
2923+
}
2924+
28772925
#[test]
28782926
fn run_script_explicit_no_file() {
28792927
let context = TestContext::new("3.12");
@@ -2942,6 +2990,41 @@ fn run_gui_script_explicit_windows() -> Result<()> {
29422990
Ok(())
29432991
}
29442992

2993+
#[test]
2994+
#[cfg(windows)]
2995+
fn run_gui_script_explicit_stdin_windows() -> Result<()> {
2996+
let context = TestContext::new("3.12");
2997+
2998+
let test_script = context.temp_dir.child("script");
2999+
test_script.write_str(indoc! { r#"
3000+
# /// script
3001+
# requires-python = ">=3.11"
3002+
# dependencies = [
3003+
# "iniconfig",
3004+
# ]
3005+
# ///
3006+
import iniconfig
3007+
print("Hello, world!")
3008+
"#
3009+
})?;
3010+
3011+
uv_snapshot!(context.filters(), context.run().arg("--gui-script").arg("-").stdin(std::fs::File::open(test_script)?), @r###"
3012+
success: true
3013+
exit_code: 0
3014+
----- stdout -----
3015+
Hello, world!
3016+
3017+
----- stderr -----
3018+
Reading inline script metadata from `stdin`
3019+
Resolved 1 package in [TIME]
3020+
Prepared 1 package in [TIME]
3021+
Installed 1 package in [TIME]
3022+
+ iniconfig==2.0.0
3023+
"###);
3024+
3025+
Ok(())
3026+
}
3027+
29453028
#[test]
29463029
#[cfg(not(windows))]
29473030
fn run_gui_script_explicit_unix() -> Result<()> {
@@ -2974,6 +3057,41 @@ fn run_gui_script_explicit_unix() -> Result<()> {
29743057
Ok(())
29753058
}
29763059

3060+
#[test]
3061+
#[cfg(not(windows))]
3062+
fn run_gui_script_explicit_stdin_unix() -> Result<()> {
3063+
let context = TestContext::new("3.12");
3064+
3065+
let test_script = context.temp_dir.child("script");
3066+
test_script.write_str(indoc! { r#"
3067+
# /// script
3068+
# requires-python = ">=3.11"
3069+
# dependencies = [
3070+
# "iniconfig",
3071+
# ]
3072+
# ///
3073+
import iniconfig
3074+
print("Hello, world!")
3075+
"#
3076+
})?;
3077+
3078+
uv_snapshot!(context.filters(), context.run().arg("--gui-script").arg("-").stdin(std::fs::File::open(test_script)?), @r###"
3079+
success: true
3080+
exit_code: 0
3081+
----- stdout -----
3082+
Hello, world!
3083+
3084+
----- stderr -----
3085+
Reading inline script metadata from `stdin`
3086+
Resolved 1 package in [TIME]
3087+
Prepared 1 package in [TIME]
3088+
Installed 1 package in [TIME]
3089+
+ iniconfig==2.0.0
3090+
"###);
3091+
3092+
Ok(())
3093+
}
3094+
29773095
#[test]
29783096
fn run_remote_pep723_script() {
29793097
let context = TestContext::new("3.12").with_filtered_python_names();

0 commit comments

Comments
 (0)