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.