From b128aa04997abfcdb9c934e3ddcd1567ff57b0a7 Mon Sep 17 00:00:00 2001 From: InSync Date: Fri, 21 Mar 2025 08:48:59 +0700 Subject: [PATCH] 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. --- crates/uv-cli/src/lib.rs | 10 ++ crates/uv/src/commands/mod.rs | 1 + crates/uv/src/commands/python/find.rs | 56 +++++++++- crates/uv/src/lib.rs | 61 +++++++++-- crates/uv/src/settings.rs | 1 + crates/uv/tests/it/python_find.rs | 152 ++++++++++++++++++++++++++ docs/reference/cli.md | 2 + 7 files changed, 270 insertions(+), 13 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 4387fc58f..a78c0e2a6 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -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, } #[derive(Args)] diff --git a/crates/uv/src/commands/mod.rs b/crates/uv/src/commands/mod.rs index 39fe1650a..83f48b7c2 100644 --- a/crates/uv/src/commands/mod.rs +++ b/crates/uv/src/commands/mod.rs @@ -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; diff --git a/crates/uv/src/commands/python/find.rs b/crates/uv/src/commands/python/find.rs index 72951537b..26e8e8a88 100644 --- a/crates/uv/src/commands/python/find.rs +++ b/crates/uv/src/commands/python/find.rs @@ -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 { + 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) + } + } +} diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index f5a5e25f8..2e9b3908c 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -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 { }, _ => 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 { // 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), diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 8e916178c..95b53f8c2 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -976,6 +976,7 @@ impl PythonFindSettings { no_project, system, no_system, + script: _, } = args; Self { diff --git a/crates/uv/tests/it/python_find.rs b/crates/uv/tests/it/python_find.rs index 72c9f4381..e5d29024b 100644 --- a/crates/uv/tests/it/python_find.rs +++ b/crates/uv/tests/it/python_find.rs @@ -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::>(); + + 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::>(); + + 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] + "); +} diff --git a/docs/reference/cli.md b/docs/reference/cli.md index d9c428a81..b8a04e630 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -5018,6 +5018,8 @@ uv python find [OPTIONS] [REQUEST]
--quiet, -q

Do not print any output

+
--script script

Find the environment for a Python script, rather than the current project

+
--system

Only find system Python interpreters.

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.