Skip to content

Commit

Permalink
uv run supports python zipapp
Browse files Browse the repository at this point in the history
  • Loading branch information
j178 committed Sep 11, 2024
1 parent 38c7c5f commit 6186228
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 9 deletions.
1 change: 1 addition & 0 deletions crates/uv/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ tracing-tree = { workspace = true }
unicode-width = { workspace = true }
url = { workspace = true }
which = { workspace = true }
zip = { workspace = true }

[target.'cfg(target_os = "windows")'.dependencies]
mimalloc = { version = "0.1.39" }
Expand Down
48 changes: 39 additions & 9 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,9 @@ pub(crate) enum RunCommand {
PythonGuiScript(PathBuf, Vec<OsString>),
/// Execute a Python package containing a `__main__.py` file.
PythonPackage(PathBuf, Vec<OsString>),
/// Execute a Python [zipapp].
/// [zipapp]: https://docs.python.org/3/library/zipapp.html
PythonZipapp(PathBuf, Vec<OsString>),
/// Execute a `python` script provided via `stdin`.
PythonStdin(Vec<u8>),
/// Execute an external command.
Expand All @@ -792,10 +795,11 @@ impl RunCommand {
fn display_executable(&self) -> Cow<'_, str> {
match self {
Self::Python(_) => Cow::Borrowed("python"),
Self::PythonScript(_, _) | Self::PythonPackage(_, _) | Self::Empty => {
Cow::Borrowed("python")
}
Self::PythonGuiScript(_, _) => Cow::Borrowed("pythonw"),
Self::PythonScript(..)
| Self::PythonPackage(..)
| Self::PythonZipapp(..)
| Self::Empty => Cow::Borrowed("python"),
Self::PythonGuiScript(..) => Cow::Borrowed("pythonw"),
Self::PythonStdin(_) => Cow::Borrowed("python -c"),
Self::External(executable, _) => executable.to_string_lossy(),
}
Expand All @@ -809,7 +813,9 @@ impl RunCommand {
process.args(args);
process
}
Self::PythonScript(target, args) | Self::PythonPackage(target, args) => {
Self::PythonScript(target, args)
| Self::PythonPackage(target, args)
| Self::PythonZipapp(target, args) => {
let mut process = Command::new(interpreter.sys_executable());
process.arg(target);
process.args(args);
Expand Down Expand Up @@ -872,7 +878,9 @@ impl std::fmt::Display for RunCommand {
}
Ok(())
}
Self::PythonScript(target, args) | Self::PythonPackage(target, args) => {
Self::PythonScript(target, args)
| Self::PythonPackage(target, args)
| Self::PythonZipapp(target, args) => {
write!(f, "python {}", target.display())?;
for arg in args {
write!(f, " {}", arg.to_string_lossy())?;
Expand Down Expand Up @@ -916,6 +924,14 @@ impl TryFrom<&ExternalCommand> for RunCommand {
};

let target_path = PathBuf::from(&target);
let metadata = target_path.metadata();
let is_file = metadata
.as_ref()
.map_or(false, |metadata| metadata.is_file());
let is_dir = metadata
.as_ref()
.map_or(false, |metadata| metadata.is_dir());

if target.eq_ignore_ascii_case("-") {
let mut buf = Vec::with_capacity(1024);
std::io::stdin().read_to_end(&mut buf)?;
Expand All @@ -925,18 +941,20 @@ impl TryFrom<&ExternalCommand> for RunCommand {
} else if target_path
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("py") || ext.eq_ignore_ascii_case("pyc"))
&& target_path.exists()
&& is_file
{
Ok(Self::PythonScript(target_path, args.to_vec()))
} else if cfg!(windows)
&& target_path
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("pyw"))
&& target_path.exists()
&& is_file
{
Ok(Self::PythonGuiScript(target_path, args.to_vec()))
} else if target_path.is_dir() && target_path.join("__main__.py").exists() {
} else if is_dir && target_path.join("__main__.py").is_file() {
Ok(Self::PythonPackage(target_path, args.to_vec()))
} else if is_file && is_python_zipapp(&target_path) {
Ok(Self::PythonZipapp(target_path, args.to_vec()))
} else {
Ok(Self::External(
target.clone(),
Expand All @@ -945,3 +963,15 @@ impl TryFrom<&ExternalCommand> for RunCommand {
}
}
}

/// Returns `true` if the target is a ZIP archive containing a `__main__.py` file.
fn is_python_zipapp(target: &Path) -> bool {
if let Ok(file) = std::fs::File::open(target) {
if let Ok(mut archive) = zip::ZipArchive::new(file) {
return archive
.by_name("__main__.py")
.map_or(false, |f| f.is_file());
}
}
false
}
39 changes: 39 additions & 0 deletions crates/uv/tests/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1656,6 +1656,45 @@ fn run_package() -> Result<()> {
Ok(())
}

#[test]
fn run_zipapp() -> Result<()> {
let context = TestContext::new("3.12");

// Create a zipapp.
let child = context.temp_dir.child("app");
child.create_dir_all()?;

let main_script = child.child("__main__.py");
main_script.write_str(indoc! { r#"
print("Hello, world!")
"#
})?;

let zipapp = context.temp_dir.child("app.pyz");
let status = context
.run()
.arg("python")
.arg("-m")
.arg("zipapp")
.arg(child.as_ref())
.arg("--output")
.arg(zipapp.as_ref())
.status()?;
assert!(status.success());

// Run the zipapp.
uv_snapshot!(context.filters(), context.run().arg(zipapp.as_ref()), @r###"
success: true
exit_code: 0
----- stdout -----
Hello, world!
----- stderr -----
"###);

Ok(())
}

/// When the `pyproject.toml` file is invalid.
#[test]
fn run_project_toml_error() -> Result<()> {
Expand Down

0 comments on commit 6186228

Please sign in to comment.