Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

uv run: List available scripts when a script is not specified #7687

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ExternalCommand>,

/// Run with the given packages installed.
///
Expand Down
6 changes: 6 additions & 0 deletions crates/uv-fs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down
1 change: 1 addition & 0 deletions crates/uv-fs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
4 changes: 0 additions & 4 deletions crates/uv-python/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down
2 changes: 1 addition & 1 deletion crates/uv-python/src/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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.
Expand Down
1 change: 0 additions & 1 deletion crates/uv-python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ mod python_version;
mod target;
mod version_files;
mod virtualenv;
mod which;

#[cfg(not(test))]
pub(crate) fn current_dir() -> Result<std::path::PathBuf, std::io::Error> {
Expand Down
68 changes: 67 additions & 1 deletion crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +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::{
EnvironmentPreference, Interpreter, PythonDownloads, PythonEnvironment, PythonInstallation,
PythonPreference, PythonRequest, PythonVersionFile, VersionRequest,
Expand Down Expand Up @@ -51,7 +53,7 @@ use crate::settings::ResolverInstallerSettings;
pub(crate) async fn run(
project_dir: &Path,
script: Option<Pep723Script>,
command: RunCommand,
command: Option<RunCommand>,
requirements: Vec<RequirementsSource>,
show_resolution: bool,
locked: bool,
Expand Down Expand Up @@ -717,6 +719,70 @@ 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.
let Some(command) = command else {
writeln!(
printer.stdout(),
"Provide a command or script to invoke with `uv run <command>` or `uv run <script>.py`.\n"
)?;

#[allow(clippy::map_identity)]
let commands = interpreter
.scripts()
.read_dir()
.ok()
.into_iter()
.flatten()
.map(|entry| match entry {
Ok(entry) => Ok(entry),
Err(err) => {
// If we can't read the entry, fail.
// This could be a symptom of a more serious problem.
warn!("Failed to read entry: {}", err);
kakkoyun marked this conversation as resolved.
Show resolved Hide resolved
Err(err)
}
})
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.filter(|entry| {
entry
.file_type()
.is_ok_and(|file_type| file_type.is_file() || file_type.is_symlink())
kakkoyun marked this conversation as resolved.
Show resolved Hide resolved
})
.map(|entry| entry.path())
.filter(|path| is_executable(path))
.map(|path| {
if cfg!(windows) {
// Remove the extensions.
path.with_extension("")
} else {
path
}
})
.map(|path| {
path.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string()
})
.filter(|command| {
!command.starts_with("activate") && !command.starts_with("deactivate")
})
.sorted()
.collect_vec();

if !commands.is_empty() {
writeln!(printer.stdout(), "The following commands are available:\n")?;
for command in commands {
writeln!(printer.stdout(), "- {command}")?;
}
}
let help = format!("See `{}` for more information.", "uv run --help".bold());
writeln!(printer.stdout(), "\n{help}")?;
return Ok(ExitStatus::Error);
};

debug!("Running `{command}`");
let mut process = command.as_command(interpreter);

Expand Down
9 changes: 5 additions & 4 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,11 @@ async fn run(cli: Cli) -> Result<ExitStatus> {

// 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 {
if let ProjectCommand::Run(uv_cli::RunArgs {
command: Some(command),
..
}) = &**command
{
Some(RunCommand::try_from(command)?)
} else {
None
Expand Down Expand Up @@ -1229,9 +1233,6 @@ async fn run_project(
)
.collect::<Vec<_>>();

// Given `ProjectCommand::Run`, we always expect a `RunCommand` to be present.
let command = command.expect("run command is required");

Box::pin(commands::run(
project_dir,
script,
Expand Down
68 changes: 68 additions & 0 deletions crates/uv/tests/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,74 @@ 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.
#[cfg(not(windows))]
uv_snapshot!(context.filters(), context.run(), @r###"
success: false
exit_code: 2
----- stdout -----
Provide a command or script to invoke with `uv run <command>` or `uv run <script>.py`.

The following commands 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]/)
"###);

#[cfg(windows)]
uv_snapshot!(context.filters(), context.run(), @r###"
success: false
exit_code: 2
----- stdout -----
Provide a command or script to invoke with `uv run <command>` or `uv run <script>.py`.

The following commands are available:

- pydoc
- python
- pythonw

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]
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Arguments following the command (or script) are not interpreted as arguments to
<h3 class="cli-reference">Usage</h3>

```
uv run [OPTIONS] <COMMAND>
uv run [OPTIONS] [COMMAND]
```

<h3 class="cli-reference">Options</h3>
Expand Down
Loading