mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-03 18:38:21 +00:00
uv run: List available scripts when a script is not specified (#7687)
Signed-off-by: Kemal Akkoyun <kakkoyun@gmail.com> ## Summary This PR adds the ability to list available scripts in the environment when `uv run` is invoked without any arguments. It somewhat mimics the behavior of `rye run` command (See https://rye.astral.sh/guide/commands/run). This is an attempt to fix #4024. ## Test Plan I added test cases. The CI pipeline should pass. ### Manuel Tests ```shell ❯ uv run Provide a command or script to invoke with `uv run <command>` or `uv run script.py`. The following scripts are available: normalizer python python3 python3.12 See `uv run --help` for more information. ``` --------- Signed-off-by: Kemal Akkoyun <kakkoyun@gmail.com> Co-authored-by: Zanie Blue <contact@zanie.dev>
This commit is contained in:
parent
282fab5f70
commit
1a39ffe391
12 changed files with 152 additions and 16 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -4679,12 +4679,14 @@ dependencies = [
|
||||||
"fs2",
|
"fs2",
|
||||||
"junction",
|
"junction",
|
||||||
"path-slash",
|
"path-slash",
|
||||||
|
"rustix",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
|
"winsafe 0.0.22",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -4982,7 +4984,6 @@ dependencies = [
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"reqwest-middleware",
|
"reqwest-middleware",
|
||||||
"rmp-serde",
|
"rmp-serde",
|
||||||
"rustix",
|
|
||||||
"same-file",
|
"same-file",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -5014,7 +5015,6 @@ dependencies = [
|
||||||
"windows-registry",
|
"windows-registry",
|
||||||
"windows-result 0.2.0",
|
"windows-result 0.2.0",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
"winsafe 0.0.22",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -2543,7 +2543,7 @@ pub struct RunArgs {
|
||||||
/// If the path to a Python script (i.e., ending in `.py`), it will be
|
/// If the path to a Python script (i.e., ending in `.py`), it will be
|
||||||
/// executed with the Python interpreter.
|
/// executed with the Python interpreter.
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: ExternalCommand,
|
pub command: Option<ExternalCommand>,
|
||||||
|
|
||||||
/// Run with the given packages installed.
|
/// Run with the given packages installed.
|
||||||
///
|
///
|
||||||
|
|
|
@ -29,6 +29,12 @@ tempfile = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
urlencoding = { 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]
|
[target.'cfg(windows)'.dependencies]
|
||||||
junction = { workspace = true }
|
junction = { workspace = true }
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ pub use crate::path::*;
|
||||||
|
|
||||||
pub mod cachedir;
|
pub mod cachedir;
|
||||||
mod path;
|
mod path;
|
||||||
|
pub mod which;
|
||||||
|
|
||||||
/// Reads data from the path and requires that it be valid UTF-8 or UTF-16.
|
/// Reads data from the path and requires that it be valid UTF-8 or UTF-16.
|
||||||
///
|
///
|
||||||
|
|
|
@ -3,7 +3,7 @@ use std::path::Path;
|
||||||
/// Check whether a path in PATH is a valid executable.
|
/// Check whether a path in PATH is a valid executable.
|
||||||
///
|
///
|
||||||
/// Derived from `which`'s `Checker`.
|
/// 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"))]
|
#[cfg(any(unix, target_os = "wasi", target_os = "redox"))]
|
||||||
{
|
{
|
||||||
if rustix::fs::access(path, rustix::fs::Access::EXEC_OK).is_err() {
|
if rustix::fs::access(path, rustix::fs::Access::EXEC_OK).is_err() {
|
|
@ -53,12 +53,8 @@ tracing = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
which = { 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]
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
windows-sys = { workspace = true }
|
windows-sys = { workspace = true }
|
||||||
winsafe = { workspace = true }
|
|
||||||
windows-registry = { workspace = true }
|
windows-registry = { workspace = true }
|
||||||
windows-result = { workspace = true }
|
windows-result = { workspace = true }
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ use tracing::{debug, instrument, trace};
|
||||||
use which::{which, which_all};
|
use which::{which, which_all};
|
||||||
|
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
|
use uv_fs::which::is_executable;
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
use uv_pep440::{Prerelease, Version, VersionSpecifier, VersionSpecifiers};
|
use uv_pep440::{Prerelease, Version, VersionSpecifier, VersionSpecifiers};
|
||||||
use uv_warnings::warn_user_once;
|
use uv_warnings::warn_user_once;
|
||||||
|
@ -27,7 +28,6 @@ use crate::virtualenv::{
|
||||||
conda_prefix_from_env, virtualenv_from_env, virtualenv_from_working_dir,
|
conda_prefix_from_env, virtualenv_from_env, virtualenv_from_working_dir,
|
||||||
virtualenv_python_executable,
|
virtualenv_python_executable,
|
||||||
};
|
};
|
||||||
use crate::which::is_executable;
|
|
||||||
use crate::{Interpreter, PythonVersion};
|
use crate::{Interpreter, PythonVersion};
|
||||||
|
|
||||||
/// A request to find a Python installation.
|
/// A request to find a Python installation.
|
||||||
|
|
|
@ -37,7 +37,6 @@ mod python_version;
|
||||||
mod target;
|
mod target;
|
||||||
mod version_files;
|
mod version_files;
|
||||||
mod virtualenv;
|
mod virtualenv;
|
||||||
mod which;
|
|
||||||
|
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
pub(crate) fn current_dir() -> Result<std::path::PathBuf, std::io::Error> {
|
pub(crate) fn current_dir() -> Result<std::path::PathBuf, std::io::Error> {
|
||||||
|
|
|
@ -18,9 +18,11 @@ use uv_configuration::{
|
||||||
Concurrency, DevMode, EditableMode, ExtrasSpecification, InstallOptions, SourceStrategy,
|
Concurrency, DevMode, EditableMode, ExtrasSpecification, InstallOptions, SourceStrategy,
|
||||||
};
|
};
|
||||||
use uv_distribution::LoweredRequirement;
|
use uv_distribution::LoweredRequirement;
|
||||||
|
use uv_fs::which::is_executable;
|
||||||
use uv_fs::{PythonExt, Simplified};
|
use uv_fs::{PythonExt, Simplified};
|
||||||
use uv_installer::{SatisfiesResult, SitePackages};
|
use uv_installer::{SatisfiesResult, SitePackages};
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
|
|
||||||
use uv_python::{
|
use uv_python::{
|
||||||
EnvironmentPreference, Interpreter, PythonDownloads, PythonEnvironment, PythonInstallation,
|
EnvironmentPreference, Interpreter, PythonDownloads, PythonEnvironment, PythonInstallation,
|
||||||
PythonPreference, PythonRequest, PythonVariant, PythonVersionFile, VersionRequest,
|
PythonPreference, PythonRequest, PythonVariant, PythonVersionFile, VersionRequest,
|
||||||
|
@ -51,7 +53,7 @@ use crate::settings::ResolverInstallerSettings;
|
||||||
pub(crate) async fn run(
|
pub(crate) async fn run(
|
||||||
project_dir: &Path,
|
project_dir: &Path,
|
||||||
script: Option<Pep723Script>,
|
script: Option<Pep723Script>,
|
||||||
command: RunCommand,
|
command: Option<RunCommand>,
|
||||||
requirements: Vec<RequirementsSource>,
|
requirements: Vec<RequirementsSource>,
|
||||||
show_resolution: bool,
|
show_resolution: bool,
|
||||||
locked: bool,
|
locked: bool,
|
||||||
|
@ -751,6 +753,73 @@ pub(crate) async fn run(
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or_else(|| &base_interpreter, |env| env.interpreter());
|
.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);
|
||||||
|
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())
|
||||||
|
})
|
||||||
|
.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 in the environment:\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}`");
|
debug!("Running `{command}`");
|
||||||
let mut process = command.as_command(interpreter);
|
let mut process = command.as_command(interpreter);
|
||||||
|
|
||||||
|
|
|
@ -133,7 +133,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
|
||||||
// Parse the external command, if necessary.
|
// Parse the external command, if necessary.
|
||||||
let run_command = if let Commands::Project(command) = &*cli.command {
|
let run_command = if let Commands::Project(command) = &*cli.command {
|
||||||
if let ProjectCommand::Run(uv_cli::RunArgs {
|
if let ProjectCommand::Run(uv_cli::RunArgs {
|
||||||
command,
|
command: Some(command),
|
||||||
module,
|
module,
|
||||||
script,
|
script,
|
||||||
..
|
..
|
||||||
|
@ -1239,9 +1239,6 @@ async fn run_project(
|
||||||
)
|
)
|
||||||
.collect::<Vec<_>>();
|
.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(
|
Box::pin(commands::run(
|
||||||
project_dir,
|
project_dir,
|
||||||
script,
|
script,
|
||||||
|
|
|
@ -198,6 +198,74 @@ fn run_args() -> Result<()> {
|
||||||
Ok(())
|
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 in the environment:
|
||||||
|
|
||||||
|
- 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 in the environment:
|
||||||
|
|
||||||
|
- 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
|
/// Run a PEP 723-compatible script. The script should take precedence over the workspace
|
||||||
/// dependencies.
|
/// dependencies.
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -65,7 +65,7 @@ Arguments following the command (or script) are not interpreted as arguments to
|
||||||
<h3 class="cli-reference">Usage</h3>
|
<h3 class="cli-reference">Usage</h3>
|
||||||
|
|
||||||
```
|
```
|
||||||
uv run [OPTIONS] <COMMAND>
|
uv run [OPTIONS] [COMMAND]
|
||||||
```
|
```
|
||||||
|
|
||||||
<h3 class="cli-reference">Options</h3>
|
<h3 class="cli-reference">Options</h3>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue