Skip to content

Commit

Permalink
Allow --script to be provided with uv run - (#10035)
Browse files Browse the repository at this point in the history
## Summary

Closes #10021.
  • Loading branch information
charliermarsh authored Dec 19, 2024
1 parent 5a3826d commit 4513ce0
Show file tree
Hide file tree
Showing 3 changed files with 182 additions and 10 deletions.
68 changes: 61 additions & 7 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1182,6 +1182,8 @@ pub(crate) enum RunCommand {
PythonZipapp(PathBuf, Vec<OsString>),
/// Execute a `python` script provided via `stdin`.
PythonStdin(Vec<u8>, Vec<OsString>),
/// Execute a `pythonw` script provided via `stdin`.
PythonGuiStdin(Vec<u8>, Vec<OsString>),
/// Execute a Python script provided via a remote URL.
PythonRemote(tempfile::NamedTempFile, Vec<OsString>),
/// Execute an external command.
Expand Down Expand Up @@ -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(),
}
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand Down
6 changes: 3 additions & 3 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,9 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
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 {
Expand Down
118 changes: 118 additions & 0 deletions crates/uv/tests/it/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<()> {
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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<()> {
Expand Down Expand Up @@ -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();
Expand Down

0 comments on commit 4513ce0

Please sign in to comment.