mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 02:48:17 +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",
|
||||
"junction",
|
||||
"path-slash",
|
||||
"rustix",
|
||||
"schemars",
|
||||
"serde",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"urlencoding",
|
||||
"winsafe 0.0.22",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -4982,7 +4984,6 @@ dependencies = [
|
|||
"reqwest",
|
||||
"reqwest-middleware",
|
||||
"rmp-serde",
|
||||
"rustix",
|
||||
"same-file",
|
||||
"schemars",
|
||||
"serde",
|
||||
|
@ -5014,7 +5015,6 @@ dependencies = [
|
|||
"windows-registry",
|
||||
"windows-result 0.2.0",
|
||||
"windows-sys 0.59.0",
|
||||
"winsafe 0.0.22",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -2543,7 +2543,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.
|
||||
///
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -8,6 +8,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.
|
||||
///
|
||||
|
|
|
@ -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() {
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ use tracing::{debug, instrument, trace};
|
|||
use which::{which, which_all};
|
||||
|
||||
use uv_cache::Cache;
|
||||
use uv_fs::which::is_executable;
|
||||
use uv_fs::Simplified;
|
||||
use uv_pep440::{Prerelease, Version, VersionSpecifier, VersionSpecifiers};
|
||||
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.
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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, PythonVariant, PythonVersionFile, VersionRequest,
|
||||
|
@ -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,
|
||||
|
@ -751,6 +753,73 @@ 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);
|
||||
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}`");
|
||||
let mut process = command.as_command(interpreter);
|
||||
|
||||
|
|
|
@ -133,7 +133,7 @@ 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: Some(command),
|
||||
module,
|
||||
script,
|
||||
..
|
||||
|
@ -1239,9 +1239,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,
|
||||
|
|
|
@ -198,6 +198,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 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
|
||||
/// dependencies.
|
||||
#[test]
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue