From 645a7e0a7716d235272892241e0acada68b4277d Mon Sep 17 00:00:00 2001 From: Kemal Akkoyun Date: Wed, 25 Sep 2024 17:38:31 +0200 Subject: [PATCH 1/9] uv run: List available scripts when a script is not specified Signed-off-by: Kemal Akkoyun --- crates/uv-cli/src/lib.rs | 2 +- crates/uv-python/src/lib.rs | 2 +- crates/uv-python/src/which.rs | 2 +- crates/uv/src/commands/project/run.rs | 50 +++++++++++++++++++++++++++ crates/uv/src/lib.rs | 5 ++- crates/uv/tests/run.rs | 45 ++++++++++++++++++++++++ docs/reference/cli.md | 2 +- 7 files changed, 103 insertions(+), 5 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 2699a519009f..6ed785cc1e6c 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2485,7 +2485,7 @@ pub struct RunArgs { /// If the path to a Python script (i.e., ending in `.py`), it will be /// executed with the Python interpreter. #[command(subcommand)] - pub command: ExternalCommand, + pub command: Option, /// Run with the given packages installed. /// diff --git a/crates/uv-python/src/lib.rs b/crates/uv-python/src/lib.rs index cb8e99f67f62..b13ba983c809 100644 --- a/crates/uv-python/src/lib.rs +++ b/crates/uv-python/src/lib.rs @@ -37,7 +37,7 @@ mod python_version; mod target; mod version_files; mod virtualenv; -mod which; +pub mod which; #[cfg(not(test))] pub(crate) fn current_dir() -> Result { diff --git a/crates/uv-python/src/which.rs b/crates/uv-python/src/which.rs index 352bc14f3b93..26a68203dbf2 100644 --- a/crates/uv-python/src/which.rs +++ b/crates/uv-python/src/which.rs @@ -3,7 +3,7 @@ use std::path::Path; /// Check whether a path in PATH is a valid executable. /// /// Derived from `which`'s `Checker`. -pub(crate) fn is_executable(path: &Path) -> bool { +pub fn is_executable(path: &Path) -> bool { #[cfg(any(unix, target_os = "wasi", target_os = "redox"))] { if rustix::fs::access(path, rustix::fs::Access::EXEC_OK).is_err() { diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 2dc660e34c9c..b0e4c269b684 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -21,6 +21,7 @@ use uv_distribution::LoweredRequirement; use uv_fs::{PythonExt, Simplified}; use uv_installer::{SatisfiesResult, SitePackages}; use uv_normalize::PackageName; +use uv_python::which::is_executable; use uv_python::{ EnvironmentPreference, Interpreter, PythonDownloads, PythonEnvironment, PythonInstallation, PythonPreference, PythonRequest, PythonVersionFile, VersionRequest, @@ -717,6 +718,55 @@ pub(crate) async fn run( .as_ref() .map_or_else(|| &base_interpreter, |env| env.interpreter()); + // Check if any run command is given. + // If not, print the available scripts for the current interpreter. + if let RunCommand::Empty = command { + writeln!( + printer.stdout(), + "Provide a command or script to invoke with `uv run ` or `uv run script.py`.\n" + )?; + + let mut scripts = interpreter + .scripts() + .read_dir() + .ok() + .into_iter() + .flatten() + .filter_map(|entry| match entry { + Ok(entry) => Some(entry), + Err(err) => { + debug!("Failed to read entry: {}", err); + None + } + }) + .filter(|entry| { + entry + .file_type() + .is_ok_and(|file_type| file_type.is_file() || file_type.is_symlink()) + }) + .map(|entry| entry.path()) + .filter(|path| is_executable(path)) + .map(|path| { + path.file_name() + .unwrap_or_default() + .to_string_lossy() + .to_string() + }) + .collect_vec(); + scripts.sort(); + + if !scripts.is_empty() { + writeln!(printer.stdout(), "The following scripts are available:\n")?; + writeln!(printer.stdout(), "{}", scripts.join("\n"))?; + } + writeln!( + printer.stdout(), + "\nSee `uv run --help` for more information." + )?; + + return Ok(ExitStatus::Success); + }; + debug!("Running `{command}`"); let mut process = command.as_command(interpreter); diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index d46fa74747a1..349e28026186 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -132,7 +132,10 @@ async fn run(cli: Cli) -> Result { // Parse the external command, if necessary. let run_command = if let Commands::Project(command) = &*cli.command { if let ProjectCommand::Run(uv_cli::RunArgs { command, .. }) = &**command { - Some(RunCommand::try_from(command)?) + match command { + Some(command) => Some(RunCommand::try_from(command)?), + None => Some(RunCommand::Empty), + } } else { None } diff --git a/crates/uv/tests/run.rs b/crates/uv/tests/run.rs index ed6514913689..4a06492eb090 100644 --- a/crates/uv/tests/run.rs +++ b/crates/uv/tests/run.rs @@ -197,6 +197,51 @@ fn run_args() -> Result<()> { Ok(()) } +/// Run without specifying any argunments. +/// This should list the available scripts. +#[test] +fn run_no_args() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! { r#" + [project] + name = "foo" + version = "1.0.0" + requires-python = ">=3.8" + dependencies = [] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "# + })?; + + // Run without specifying any argunments. + uv_snapshot!(context.filters(), context.run(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Provide a command or script to invoke with `uv run ` or `uv run script.py`. + + The following scripts are available: + + python + python3 + python3.12 + + See `uv run --help` for more information. + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + foo==1.0.0 (from file://[TEMP_DIR]/) + "###); + + Ok(()) +} + /// Run a PEP 723-compatible script. The script should take precedence over the workspace /// dependencies. #[test] diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 2a7f325dfed8..e64605619670 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -65,7 +65,7 @@ Arguments following the command (or script) are not interpreted as arguments to

Usage

``` -uv run [OPTIONS] +uv run [OPTIONS] [COMMAND] ```

Options

From 27369fc1564029417ade31a85576449b8fe89525 Mon Sep 17 00:00:00 2001 From: Kemal Akkoyun Date: Wed, 25 Sep 2024 21:31:11 +0200 Subject: [PATCH 2/9] Update tests for windows Signed-off-by: Kemal Akkoyun --- crates/uv/tests/run.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/crates/uv/tests/run.rs b/crates/uv/tests/run.rs index 4a06492eb090..f30c536cfe4d 100644 --- a/crates/uv/tests/run.rs +++ b/crates/uv/tests/run.rs @@ -218,6 +218,7 @@ fn run_no_args() -> Result<()> { })?; // Run without specifying any argunments. + #[cfg(not(windows))] uv_snapshot!(context.filters(), context.run(), @r###" success: true exit_code: 0 @@ -239,6 +240,35 @@ fn run_no_args() -> Result<()> { + foo==1.0.0 (from file://[TEMP_DIR]/) "###); + #[cfg(windows)] + uv_snapshot!(context.filters(), context.run(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Provide a command or script to invoke with `uv run ` or `uv run script.py`. + + The following scripts are available: + + activate.bat + activate.csh + activate.fish + activate.nu + activate.ps1 + activate_this.py + deactivate.bat + pydoc.bat + python.exe + pythonw.exe + + See `uv run --help` for more information. + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + foo==1.0.0 (from file://[TEMP_DIR]/) + "###); + Ok(()) } From 0b887b30ff61d8e477e820ef9f4c97e081f893d8 Mon Sep 17 00:00:00 2001 From: Kemal Akkoyun Date: Thu, 26 Sep 2024 14:14:54 +0200 Subject: [PATCH 3/9] Address the review comments Signed-off-by: Kemal Akkoyun --- Cargo.lock | 4 +-- crates/uv-fs/Cargo.toml | 6 ++++ crates/uv-fs/src/lib.rs | 1 + crates/{uv-python => uv-fs}/src/which.rs | 0 crates/uv-python/Cargo.toml | 4 --- crates/uv-python/src/discovery.rs | 2 +- crates/uv-python/src/lib.rs | 1 - crates/uv/src/commands/project/run.rs | 36 ++++++++++++++++++------ crates/uv/tests/run.rs | 33 ++++++++-------------- 9 files changed, 49 insertions(+), 38 deletions(-) rename crates/{uv-python => uv-fs}/src/which.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index fc5da8e685d6..e67fdfbf0dd0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4875,12 +4875,14 @@ dependencies = [ "fs2", "junction", "path-slash", + "rustix", "schemars", "serde", "tempfile", "tokio", "tracing", "urlencoding", + "winsafe 0.0.22", ] [[package]] @@ -5066,7 +5068,6 @@ dependencies = [ "reqwest", "reqwest-middleware", "rmp-serde", - "rustix", "same-file", "schemars", "serde", @@ -5091,7 +5092,6 @@ dependencies = [ "windows-registry", "windows-result 0.2.0", "windows-sys 0.59.0", - "winsafe 0.0.22", ] [[package]] diff --git a/crates/uv-fs/Cargo.toml b/crates/uv-fs/Cargo.toml index c7aa1cb1a20f..88f53659bfa0 100644 --- a/crates/uv-fs/Cargo.toml +++ b/crates/uv-fs/Cargo.toml @@ -29,6 +29,12 @@ tempfile = { workspace = true } tracing = { workspace = true } urlencoding = { workspace = true } +[target.'cfg(target_os = "windows")'.dependencies] +winsafe = { workspace = true } + +[target.'cfg(any(unix, target_os = "wasi", target_os = "redox"))'.dependencies] +rustix = { workspace = true } + [target.'cfg(windows)'.dependencies] junction = { workspace = true } diff --git a/crates/uv-fs/src/lib.rs b/crates/uv-fs/src/lib.rs index 5c69e5a962b3..658b6127ff1d 100644 --- a/crates/uv-fs/src/lib.rs +++ b/crates/uv-fs/src/lib.rs @@ -12,6 +12,7 @@ pub use crate::path::*; pub mod cachedir; mod path; +pub mod which; /// Reads data from the path and requires that it be valid UTF-8 or UTF-16. /// diff --git a/crates/uv-python/src/which.rs b/crates/uv-fs/src/which.rs similarity index 100% rename from crates/uv-python/src/which.rs rename to crates/uv-fs/src/which.rs diff --git a/crates/uv-python/Cargo.toml b/crates/uv-python/Cargo.toml index 9bd7363ecc19..87d714fc76db 100644 --- a/crates/uv-python/Cargo.toml +++ b/crates/uv-python/Cargo.toml @@ -53,12 +53,8 @@ tracing = { workspace = true } url = { workspace = true } which = { workspace = true } -[target.'cfg(any(unix, target_os = "wasi", target_os = "redox"))'.dependencies] -rustix = { workspace = true } - [target.'cfg(target_os = "windows")'.dependencies] windows-sys = { workspace = true } -winsafe = { workspace = true } windows-registry = { workspace = true } windows-result = { workspace = true } diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index a0ff76405ccd..68938eeeda8b 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -11,6 +11,7 @@ use which::{which, which_all}; use pep440_rs::{Prerelease, Version, VersionSpecifier, VersionSpecifiers}; use uv_cache::Cache; +use uv_fs::which::is_executable; use uv_fs::Simplified; use uv_warnings::warn_user_once; @@ -27,7 +28,6 @@ use crate::virtualenv::{ conda_prefix_from_env, virtualenv_from_env, virtualenv_from_working_dir, virtualenv_python_executable, }; -use crate::which::is_executable; use crate::{Interpreter, PythonVersion}; /// A request to find a Python installation. diff --git a/crates/uv-python/src/lib.rs b/crates/uv-python/src/lib.rs index b13ba983c809..0d64bc6ce37b 100644 --- a/crates/uv-python/src/lib.rs +++ b/crates/uv-python/src/lib.rs @@ -37,7 +37,6 @@ mod python_version; mod target; mod version_files; mod virtualenv; -pub mod which; #[cfg(not(test))] pub(crate) fn current_dir() -> Result { diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index b0e4c269b684..5747ac8fc06a 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -18,10 +18,11 @@ use uv_configuration::{ Concurrency, DevMode, EditableMode, ExtrasSpecification, InstallOptions, SourceStrategy, }; use uv_distribution::LoweredRequirement; +use uv_fs::which::is_executable; use uv_fs::{PythonExt, Simplified}; use uv_installer::{SatisfiesResult, SitePackages}; use uv_normalize::PackageName; -use uv_python::which::is_executable; + use uv_python::{ EnvironmentPreference, Interpreter, PythonDownloads, PythonEnvironment, PythonInstallation, PythonPreference, PythonRequest, PythonVersionFile, VersionRequest, @@ -723,10 +724,11 @@ pub(crate) async fn run( if let RunCommand::Empty = command { writeln!( printer.stdout(), - "Provide a command or script to invoke with `uv run ` or `uv run script.py`.\n" + "Provide a command or script to invoke with `uv run ` or `uv run