Support `python find --script` (#11891)

## Summary

Resolves #11794.

When `uv python find` is given a `--script` option, either the existing
environment for that script or the Python executable that would be used
to create it will be returned. If neither are found, the command exits
with exit code 1.

`--script` is incompatible with all other options to the same command.

## Test Plan

Unit tests.
This commit is contained in:
InSync 2025-03-21 08:48:59 +07:00 committed by GitHub
parent 46967723bb
commit b128aa0499
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 270 additions and 13 deletions

View File

@ -4740,6 +4740,16 @@ pub struct PythonFindArgs {
#[arg(long, overrides_with("system"), hide = true)]
pub no_system: bool,
/// Find the environment for a Python script, rather than the current project.
#[arg(
long,
conflicts_with = "request",
conflicts_with = "no_project",
conflicts_with = "system",
conflicts_with = "no_system"
)]
pub script: Option<PathBuf>,
}
#[derive(Args)]

View File

@ -32,6 +32,7 @@ pub(crate) use project::tree::tree;
pub(crate) use publish::publish;
pub(crate) use python::dir::dir as python_dir;
pub(crate) use python::find::find as python_find;
pub(crate) use python::find::find_script as python_find_script;
pub(crate) use python::install::install as python_install;
pub(crate) use python::list::list as python_list;
pub(crate) use python::pin::pin as python_pin;

View File

@ -1,17 +1,24 @@
use anstream::println;
use anyhow::Result;
use std::fmt::Write;
use std::path::Path;
use uv_cache::Cache;
use uv_fs::Simplified;
use uv_python::{EnvironmentPreference, PythonInstallation, PythonPreference, PythonRequest};
use uv_python::{
EnvironmentPreference, PythonDownloads, PythonInstallation, PythonPreference, PythonRequest,
};
use uv_scripts::Pep723ItemRef;
use uv_settings::PythonInstallMirrors;
use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceCache, WorkspaceError};
use crate::commands::{
project::{validate_project_requires_python, WorkspacePython},
project::{validate_project_requires_python, ScriptInterpreter, WorkspacePython},
ExitStatus,
};
use crate::printer::Printer;
use crate::settings::NetworkSettings;
/// Find a Python interpreter.
pub(crate) async fn find(
@ -88,3 +95,48 @@ pub(crate) async fn find(
Ok(ExitStatus::Success)
}
pub(crate) async fn find_script(
script: Pep723ItemRef<'_>,
network_settings: &NetworkSettings,
python_preference: PythonPreference,
python_downloads: PythonDownloads,
no_config: bool,
cache: &Cache,
printer: Printer,
) -> Result<ExitStatus> {
match ScriptInterpreter::discover(
script,
None,
network_settings,
python_preference,
python_downloads,
&PythonInstallMirrors::default(),
no_config,
Some(false),
cache,
printer,
)
.await
{
Err(error) => {
writeln!(printer.stderr(), "{error}")?;
Ok(ExitStatus::Failure)
}
Ok(ScriptInterpreter::Interpreter(interpreter)) => {
let path = interpreter.sys_executable();
println!("{}", std::path::absolute(path)?.simplified_display());
Ok(ExitStatus::Success)
}
Ok(ScriptInterpreter::Environment(environment)) => {
let path = environment.interpreter().sys_executable();
println!("{}", std::path::absolute(path)?.simplified_display());
Ok(ExitStatus::Success)
}
}
}

View File

@ -31,7 +31,7 @@ use uv_pep508::VersionOrUrl;
use uv_pypi_types::{ParsedDirectoryUrl, ParsedUrl};
use uv_requirements::RequirementsSource;
use uv_requirements_txt::RequirementsTxtRequirement;
use uv_scripts::{Pep723Error, Pep723Item, Pep723Metadata, Pep723Script};
use uv_scripts::{Pep723Error, Pep723Item, Pep723ItemRef, Pep723Metadata, Pep723Script};
use uv_settings::{Combine, FilesystemOptions, Options};
use uv_static::EnvVars;
use uv_warnings::{warn_user, warn_user_once};
@ -246,6 +246,32 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
},
_ => None,
}
} else if let Commands::Python(uv_cli::PythonNamespace {
command:
PythonCommand::Find(uv_cli::PythonFindArgs {
script: Some(script),
..
}),
}) = &*cli.command
{
match Pep723Script::read(&script).await {
Ok(Some(script)) => Some(Pep723Item::Script(script)),
Ok(None) => {
bail!(
"`{}` does not contain a PEP 723 metadata tag; run `{}` to initialize the script",
script.user_display().cyan(),
format!("uv init --script {}", script.user_display()).green()
)
}
Err(Pep723Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => {
bail!(
"Failed to read `{}` (not found); run `{}` to create a PEP 723 script",
script.user_display().cyan(),
format!("uv init --script {}", script.user_display()).green()
)
}
Err(err) => return Err(err.into()),
}
} else {
None
};
@ -1306,16 +1332,29 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
// Initialize the cache.
let cache = cache.init()?;
commands::python_find(
&project_dir,
args.request,
args.no_project,
cli.top_level.no_config,
args.system,
globals.python_preference,
&cache,
)
.await
if let Some(Pep723Item::Script(script)) = script {
commands::python_find_script(
Pep723ItemRef::Script(&script),
&globals.network_settings,
globals.python_preference,
globals.python_downloads,
cli.top_level.no_config,
&cache,
printer,
)
.await
} else {
commands::python_find(
&project_dir,
args.request,
args.no_project,
cli.top_level.no_config,
args.system,
globals.python_preference,
&cache,
)
.await
}
}
Commands::Python(PythonNamespace {
command: PythonCommand::Pin(args),

View File

@ -976,6 +976,7 @@ impl PythonFindSettings {
no_project,
system,
no_system,
script: _,
} = args;
Self {

View File

@ -707,3 +707,155 @@ fn python_required_python_major_minor() {
error: No interpreter found for Python >3.11.[X], <3.12 in virtual environments, managed installations, or search path
"###);
}
#[test]
fn python_find_script() {
let context = TestContext::new("3.13")
.with_filtered_exe_suffix()
.with_filtered_virtualenv_bin()
.with_filtered_python_names();
let filters = context
.filters()
.into_iter()
.chain(vec![(
r"environments-v2/[\w-]+",
"environments-v2/[HASHEDNAME]",
)])
.collect::<Vec<_>>();
uv_snapshot!(filters, context.init().arg("--script").arg("foo.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Initialized script at `foo.py`
"###);
uv_snapshot!(filters, context.sync().arg("--script").arg("foo.py"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Creating script environment at: [CACHE_DIR]/environments-v2/[HASHEDNAME]
Resolved in [TIME]
Audited in [TIME]
");
uv_snapshot!(filters, context.python_find().arg("--script").arg("foo.py"), @r"
success: true
exit_code: 0
----- stdout -----
[CACHE_DIR]/environments-v2/[HASHEDNAME]/[BIN]/python
----- stderr -----
");
}
#[test]
fn python_find_script_no_environment() {
let context = TestContext::new("3.13")
.with_filtered_exe_suffix()
.with_filtered_virtualenv_bin()
.with_filtered_python_names();
let script = context.temp_dir.child("foo.py");
script
.write_str(indoc! {r"
# /// script
# dependencies = []
# ///
"})
.unwrap();
uv_snapshot!(context.filters(), context.python_find().arg("--script").arg("foo.py"), @r"
success: true
exit_code: 0
----- stdout -----
[VENV]/[BIN]/python
----- stderr -----
");
}
#[test]
fn python_find_script_python_not_found() {
let context = TestContext::new_with_versions(&[]).with_filtered_python_sources();
let script = context.temp_dir.child("foo.py");
script
.write_str(indoc! {r"
# /// script
# dependencies = []
# ///
"})
.unwrap();
uv_snapshot!(context.filters(), context.python_find().arg("--script").arg("foo.py"), @r"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
No interpreter found in [PYTHON SOURCES]
");
}
#[test]
fn python_find_script_no_such_version() {
let context = TestContext::new("3.13")
.with_filtered_exe_suffix()
.with_filtered_virtualenv_bin()
.with_filtered_python_names()
.with_filtered_python_sources();
let filters = context
.filters()
.into_iter()
.chain(vec![(
r"environments-v2/[\w-]+",
"environments-v2/[HASHEDNAME]",
)])
.collect::<Vec<_>>();
let script = context.temp_dir.child("foo.py");
script
.write_str(indoc! {r#"
# /// script
# requires-python = ">=3.13"
# dependencies = []
# ///
"#})
.unwrap();
uv_snapshot!(filters, context.sync().arg("--script").arg("foo.py"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Creating script environment at: [CACHE_DIR]/environments-v2/[HASHEDNAME]
Resolved in [TIME]
Audited in [TIME]
");
script
.write_str(indoc! {r#"
# /// script
# requires-python = ">=3.14"
# dependencies = []
# ///
"#})
.unwrap();
uv_snapshot!(filters, context.python_find().arg("--script").arg("foo.py"), @r"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
No interpreter found for Python >=3.14 in [PYTHON SOURCES]
");
}

View File

@ -5018,6 +5018,8 @@ uv python find [OPTIONS] [REQUEST]
</dd><dt id="uv-python-find--quiet"><a href="#uv-python-find--quiet"><code>--quiet</code></a>, <code>-q</code></dt><dd><p>Do not print any output</p>
</dd><dt id="uv-python-find--script"><a href="#uv-python-find--script"><code>--script</code></a> <i>script</i></dt><dd><p>Find the environment for a Python script, rather than the current project</p>
</dd><dt id="uv-python-find--system"><a href="#uv-python-find--system"><code>--system</code></a></dt><dd><p>Only find system Python interpreters.</p>
<p>By default, uv will report the first Python interpreter it would use, including those in an active virtual environment or a virtual environment in the current working directory or any parent directory.</p>