diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index e7266b49d8ea..cb51e7c8264f 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -1182,6 +1182,8 @@ pub(crate) enum RunCommand { PythonZipapp(PathBuf, Vec), /// Execute a `python` script provided via `stdin`. PythonStdin(Vec, Vec), + /// Execute a `pythonw` script provided via `stdin`. + PythonGuiStdin(Vec, Vec), /// Execute a Python script provided via a remote URL. PythonRemote(tempfile::NamedTempFile, Vec), /// Execute an external command. @@ -1209,6 +1211,13 @@ impl RunCommand { } } Self::PythonStdin(..) => Cow::Borrowed("python -c"), + Self::PythonGuiStdin(..) => { + if cfg!(windows) { + Cow::Borrowed("pythonw -c") + } else { + Cow::Borrowed("python -c") + } + } Self::External(executable, _) => executable.to_string_lossy(), } } @@ -1280,6 +1289,38 @@ impl RunCommand { process } + Self::PythonGuiStdin(script, args) => { + let python_executable = interpreter.sys_executable(); + + // Use `pythonw.exe` if it exists, otherwise fall back to `python.exe`. + // See `install-wheel-rs::get_script_executable`.gd + let pythonw_executable = python_executable + .file_name() + .map(|name| { + let new_name = name.to_string_lossy().replace("python", "pythonw"); + python_executable.with_file_name(new_name) + }) + .filter(|path| path.is_file()) + .unwrap_or_else(|| python_executable.to_path_buf()); + + let mut process = Command::new(&pythonw_executable); + process.arg("-c"); + + #[cfg(unix)] + { + use std::os::unix::ffi::OsStringExt; + process.arg(OsString::from_vec(script.clone())); + } + + #[cfg(not(unix))] + { + let script = String::from_utf8(script.clone()).expect("script is valid UTF-8"); + process.arg(script); + } + process.args(args); + + process + } Self::External(executable, args) => { let mut process = Command::new(executable); process.args(args); @@ -1328,6 +1369,10 @@ impl std::fmt::Display for RunCommand { write!(f, "python -c")?; Ok(()) } + Self::PythonGuiStdin(..) => { + write!(f, "pythonw -c")?; + Ok(()) + } Self::External(executable, args) => { write!(f, "{}", executable.to_string_lossy())?; for arg in args { @@ -1360,6 +1405,19 @@ impl RunCommand { return Ok(Self::Empty); }; + if target.eq_ignore_ascii_case("-") { + let mut buf = Vec::with_capacity(1024); + std::io::stdin().read_to_end(&mut buf)?; + + return if module { + Err(anyhow!("Cannot run a Python module from stdin")) + } else if gui_script { + Ok(Self::PythonGuiStdin(buf, args.to_vec())) + } else { + Ok(Self::PythonStdin(buf, args.to_vec())) + }; + } + let target_path = PathBuf::from(target); // Determine whether the user provided a remote script. @@ -1402,21 +1460,17 @@ impl RunCommand { if module { return Ok(Self::PythonModule(target.clone(), args.to_vec())); - } else if script { - return Ok(Self::PythonScript(target.clone().into(), args.to_vec())); } else if gui_script { return Ok(Self::PythonGuiScript(target.clone().into(), args.to_vec())); + } else if script { + return Ok(Self::PythonScript(target.clone().into(), args.to_vec())); } let metadata = target_path.metadata(); let is_file = metadata.as_ref().map_or(false, std::fs::Metadata::is_file); let is_dir = metadata.as_ref().map_or(false, std::fs::Metadata::is_dir); - if target.eq_ignore_ascii_case("-") { - let mut buf = Vec::with_capacity(1024); - std::io::stdin().read_to_end(&mut buf)?; - Ok(Self::PythonStdin(buf, args.to_vec())) - } else if target.eq_ignore_ascii_case("python") { + if target.eq_ignore_ascii_case("python") { Ok(Self::Python(args.to_vec())) } else if target_path .extension() diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 17a2e8581e8c..f734dc250a2f 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -175,9 +175,9 @@ async fn run(mut cli: Cli) -> Result { Some(RunCommand::PythonRemote(script, _)) => { Pep723Metadata::read(&script).await?.map(Pep723Item::Remote) } - Some(RunCommand::PythonStdin(contents, _)) => { - Pep723Metadata::parse(contents)?.map(Pep723Item::Stdin) - } + Some( + RunCommand::PythonStdin(contents, _) | RunCommand::PythonGuiStdin(contents, _), + ) => Pep723Metadata::parse(contents)?.map(Pep723Item::Stdin), _ => None, } } else if let ProjectCommand::Remove(uv_cli::RemoveArgs { diff --git a/crates/uv/tests/it/run.rs b/crates/uv/tests/it/run.rs index 3fd44aa22ef1..2de84dc0ceb4 100644 --- a/crates/uv/tests/it/run.rs +++ b/crates/uv/tests/it/run.rs @@ -2544,6 +2544,20 @@ fn run_module() { "#); } +#[test] +fn run_module_stdin() { + let context = TestContext::new("3.12"); + + uv_snapshot!(context.filters(), context.run().arg("-m").arg("-"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Cannot run a Python module from stdin + "###); +} + /// When the `pyproject.toml` file is invalid. #[test] fn run_project_toml_error() -> Result<()> { @@ -2874,6 +2888,40 @@ fn run_script_explicit() -> Result<()> { Ok(()) } +#[test] +fn run_script_explicit_stdin() -> Result<()> { + let context = TestContext::new("3.12"); + + let test_script = context.temp_dir.child("script"); + test_script.write_str(indoc! { r#" + # /// script + # requires-python = ">=3.11" + # dependencies = [ + # "iniconfig", + # ] + # /// + import iniconfig + print("Hello, world!") + "# + })?; + + uv_snapshot!(context.filters(), context.run().arg("--script").arg("-").stdin(std::fs::File::open(test_script)?), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Hello, world! + + ----- stderr ----- + Reading inline script metadata from `stdin` + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 + "###); + + Ok(()) +} + #[test] fn run_script_explicit_no_file() { let context = TestContext::new("3.12"); @@ -2942,6 +2990,41 @@ fn run_gui_script_explicit_windows() -> Result<()> { Ok(()) } +#[test] +#[cfg(windows)] +fn run_gui_script_explicit_stdin_windows() -> Result<()> { + let context = TestContext::new("3.12"); + + let test_script = context.temp_dir.child("script"); + test_script.write_str(indoc! { r#" + # /// script + # requires-python = ">=3.11" + # dependencies = [ + # "iniconfig", + # ] + # /// + import iniconfig + print("Hello, world!") + "# + })?; + + uv_snapshot!(context.filters(), context.run().arg("--gui-script").arg("-").stdin(std::fs::File::open(test_script)?), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Hello, world! + + ----- stderr ----- + Reading inline script metadata from `stdin` + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 + "###); + + Ok(()) +} + #[test] #[cfg(not(windows))] fn run_gui_script_explicit_unix() -> Result<()> { @@ -2974,6 +3057,41 @@ fn run_gui_script_explicit_unix() -> Result<()> { Ok(()) } +#[test] +#[cfg(not(windows))] +fn run_gui_script_explicit_stdin_unix() -> Result<()> { + let context = TestContext::new("3.12"); + + let test_script = context.temp_dir.child("script"); + test_script.write_str(indoc! { r#" + # /// script + # requires-python = ">=3.11" + # dependencies = [ + # "iniconfig", + # ] + # /// + import iniconfig + print("Hello, world!") + "# + })?; + + uv_snapshot!(context.filters(), context.run().arg("--gui-script").arg("-").stdin(std::fs::File::open(test_script)?), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Hello, world! + + ----- stderr ----- + Reading inline script metadata from `stdin` + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 + "###); + + Ok(()) +} + #[test] fn run_remote_pep723_script() { let context = TestContext::new("3.12").with_filtered_python_names();