diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 7714446651051..1a3d1a9a07ae3 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -3029,6 +3029,13 @@ pub struct PythonFindArgs { /// /// See `uv help python` to view supported request formats. pub request: Option, + + /// Avoid discovering a project or workspace. + /// + /// Otherwise, when no request is provided, the Python requirement of a project in the current + /// directory or parent directories will be used. + #[arg(long, alias = "no_workspace")] + pub no_project: bool, } #[derive(Args)] diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 18c1347e06605..43d978d16557e 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -52,11 +52,11 @@ pub(crate) async fn run( isolated: bool, package: Option, no_project: bool, + no_config: bool, extras: ExtrasSpecification, dev: bool, python: Option, settings: ResolverInstallerSettings, - python_preference: PythonPreference, python_downloads: PythonDownloads, connectivity: Connectivity, @@ -449,7 +449,9 @@ pub(crate) async fn run( Some(PythonRequest::parse(request)) // (2) Request from `.python-version` } else { - request_from_version_file(&CWD).await? + PythonVersionFile::discover(&*CWD, no_config) + .await? + .and_then(PythonVersionFile::into_version) }; let python = PythonInstallation::find_or_download( diff --git a/crates/uv/src/commands/python/find.rs b/crates/uv/src/commands/python/find.rs index 5cc6a90a649ed..d2f84052c0df3 100644 --- a/crates/uv/src/commands/python/find.rs +++ b/crates/uv/src/commands/python/find.rs @@ -2,23 +2,60 @@ use anstream::println; use anyhow::Result; use uv_cache::Cache; -use uv_fs::Simplified; -use uv_python::{EnvironmentPreference, PythonInstallation, PythonPreference, PythonRequest}; +use uv_fs::{Simplified, CWD}; +use uv_python::{ + EnvironmentPreference, PythonInstallation, PythonPreference, PythonRequest, PythonVersionFile, + VersionRequest, +}; +use uv_resolver::RequiresPython; +use uv_warnings::warn_user_once; +use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceError}; -use crate::commands::ExitStatus; +use crate::commands::{project::find_requires_python, ExitStatus}; /// Find a Python interpreter. pub(crate) async fn find( request: Option, + no_project: bool, + no_config: bool, python_preference: PythonPreference, cache: &Cache, ) -> Result { - let request = match request { - Some(request) => PythonRequest::parse(&request), - None => PythonRequest::Any, - }; + // (1) Explicit request from user + let mut request = request.map(|request| PythonRequest::parse(&request)); + + // (2) Request from `.python-version` + if request.is_none() { + request = PythonVersionFile::discover(&*CWD, no_config) + .await? + .and_then(PythonVersionFile::into_version); + } + + // (3) `Requires-Python` in `pyproject.toml` + if request.is_none() && !no_project { + let project = match VirtualProject::discover(&CWD, &DiscoveryOptions::default()).await { + Ok(project) => Some(project), + Err(WorkspaceError::MissingProject(_)) => None, + Err(WorkspaceError::MissingPyprojectToml) => None, + Err(WorkspaceError::NonWorkspace(_)) => None, + Err(err) => { + warn_user_once!("{err}"); + None + } + }; + + if let Some(project) = project { + request = find_requires_python(project.workspace())? + .as_ref() + .map(RequiresPython::specifiers) + .map(|specifiers| { + PythonRequest::Version(VersionRequest::Range(specifiers.clone())) + }); + } + } + let python = PythonInstallation::find( - &request, + &request.unwrap_or_default(), EnvironmentPreference::OnlySystem, python_preference, cache, diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index cfbb78650293f..4b63e235b416f 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -689,7 +689,13 @@ async fn run(cli: Cli) -> Result { } Commands::Project(project) => { Box::pin(run_project( - project, script, globals, filesystem, cache, printer, + project, + script, + globals, + cli.no_config, + filesystem, + cache, + printer, )) .await } @@ -916,7 +922,14 @@ async fn run(cli: Cli) -> Result { // Initialize the cache. let cache = cache.init()?; - commands::python_find(args.request, globals.python_preference, &cache).await + commands::python_find( + args.request, + args.no_project, + cli.no_config, + globals.python_preference, + &cache, + ) + .await } Commands::Python(PythonNamespace { command: PythonCommand::Pin(args), @@ -951,6 +964,8 @@ async fn run_project( project_command: Box, script: Option, globals: GlobalSettings, + // TODO(zanieb): Determine a better story for passing `no_config` in here + no_config: bool, filesystem: Option, cache: Cache, printer: Printer, @@ -1033,6 +1048,7 @@ async fn run_project( args.isolated, args.package, args.no_project, + no_config, args.extras, args.dev, args.python, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 7db2d430e1d3f..fcbb3895c3502 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -564,15 +564,22 @@ impl PythonUninstallSettings { #[derive(Debug, Clone)] pub(crate) struct PythonFindSettings { pub(crate) request: Option, + pub(crate) no_project: bool, } impl PythonFindSettings { /// Resolve the [`PythonFindSettings`] from the CLI and workspace configuration. #[allow(clippy::needless_pass_by_value)] pub(crate) fn resolve(args: PythonFindArgs, _filesystem: Option) -> Self { - let PythonFindArgs { request } = args; + let PythonFindArgs { + request, + no_project, + } = args; - Self { request } + Self { + request, + no_project, + } } } diff --git a/crates/uv/tests/python_find.rs b/crates/uv/tests/python_find.rs index fb2cd9b7526e7..7b27406a4338b 100644 --- a/crates/uv/tests/python_find.rs +++ b/crates/uv/tests/python_find.rs @@ -1,5 +1,9 @@ #![cfg(all(feature = "python", feature = "pypi"))] +use assert_fs::fixture::FileWriteStr; +use assert_fs::prelude::PathChild; +use indoc::indoc; + use common::{uv_snapshot, TestContext}; use uv_python::platform::{Arch, Os}; @@ -148,3 +152,94 @@ fn python_find() { ----- stderr ----- "###); } + +#[test] +fn python_find_pin() { + let context: TestContext = TestContext::new_with_versions(&["3.11", "3.12"]); + + // Pin to a version + uv_snapshot!(context.filters(), context.python_pin().arg("3.12"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Pinned `.python-version` to `3.12` + + ----- stderr ----- + "###); + + // We should find the pinned version, not the first on the path + uv_snapshot!(context.filters(), context.python_find(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + [PYTHON-3.12] + + ----- stderr ----- + "###); + + // Unless explicitly requested + uv_snapshot!(context.filters(), context.python_find().arg("3.11"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + [PYTHON-3.11] + + ----- stderr ----- + "###); + + // Or `--no-config` is used + uv_snapshot!(context.filters(), context.python_find().arg("--no-config"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + [PYTHON-3.11] + + ----- stderr ----- + "###); +} + +#[test] +fn python_find_project() { + let context: TestContext = TestContext::new_with_versions(&["3.11", "3.12"]); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml + .write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio==3.7.0"] + "#}) + .unwrap(); + + // We should respect the project's required version, not the first on the path + uv_snapshot!(context.filters(), context.python_find(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + [PYTHON-3.12] + + ----- stderr ----- + "###); + + // Unless explicitly requested + uv_snapshot!(context.filters(), context.python_find().arg("3.11"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + [PYTHON-3.11] + + ----- stderr ----- + "###); + + // Or `--no-project` is used + uv_snapshot!(context.filters(), context.python_find().arg("--no-project"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + [PYTHON-3.11] + + ----- stderr ----- + "###); +} diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 39620426d0610..e377ee147b023 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -3199,6 +3199,10 @@ uv python find [OPTIONS] [REQUEST]

For example, spinners or progress bars.

+
--no-project

Avoid discovering a project or workspace.

+ +

Otherwise, when no request is provided, the Python requirement of a project in the current directory or parent directories will be used.

+
--no-python-downloads

Disable automatic downloads of Python

--offline

Disable network access.