Respect `.python-version` files and `pyproject.toml` in `uv python find` (#6369)

I was surprised to find we didn't do this — we should find Python
versions as we do everywhere else.
This commit is contained in:
Zanie Blue 2024-08-21 17:08:29 -05:00 committed by GitHub
parent 9a14e028df
commit 7140cdec79
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 182 additions and 14 deletions

View File

@ -3029,6 +3029,13 @@ pub struct PythonFindArgs {
/// ///
/// See `uv help python` to view supported request formats. /// See `uv help python` to view supported request formats.
pub request: Option<String>, pub request: Option<String>,
/// 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)] #[derive(Args)]

View File

@ -52,11 +52,11 @@ pub(crate) async fn run(
isolated: bool, isolated: bool,
package: Option<PackageName>, package: Option<PackageName>,
no_project: bool, no_project: bool,
no_config: bool,
extras: ExtrasSpecification, extras: ExtrasSpecification,
dev: bool, dev: bool,
python: Option<String>, python: Option<String>,
settings: ResolverInstallerSettings, settings: ResolverInstallerSettings,
python_preference: PythonPreference, python_preference: PythonPreference,
python_downloads: PythonDownloads, python_downloads: PythonDownloads,
connectivity: Connectivity, connectivity: Connectivity,
@ -449,7 +449,9 @@ pub(crate) async fn run(
Some(PythonRequest::parse(request)) Some(PythonRequest::parse(request))
// (2) Request from `.python-version` // (2) Request from `.python-version`
} else { } else {
request_from_version_file(&CWD).await? PythonVersionFile::discover(&*CWD, no_config)
.await?
.and_then(PythonVersionFile::into_version)
}; };
let python = PythonInstallation::find_or_download( let python = PythonInstallation::find_or_download(

View File

@ -2,23 +2,60 @@ use anstream::println;
use anyhow::Result; use anyhow::Result;
use uv_cache::Cache; use uv_cache::Cache;
use uv_fs::Simplified; use uv_fs::{Simplified, CWD};
use uv_python::{EnvironmentPreference, PythonInstallation, PythonPreference, PythonRequest}; 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. /// Find a Python interpreter.
pub(crate) async fn find( pub(crate) async fn find(
request: Option<String>, request: Option<String>,
no_project: bool,
no_config: bool,
python_preference: PythonPreference, python_preference: PythonPreference,
cache: &Cache, cache: &Cache,
) -> Result<ExitStatus> { ) -> Result<ExitStatus> {
let request = match request { // (1) Explicit request from user
Some(request) => PythonRequest::parse(&request), let mut request = request.map(|request| PythonRequest::parse(&request));
None => PythonRequest::Any,
}; // (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( let python = PythonInstallation::find(
&request, &request.unwrap_or_default(),
EnvironmentPreference::OnlySystem, EnvironmentPreference::OnlySystem,
python_preference, python_preference,
cache, cache,

View File

@ -689,7 +689,13 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
} }
Commands::Project(project) => { Commands::Project(project) => {
Box::pin(run_project( Box::pin(run_project(
project, script, globals, filesystem, cache, printer, project,
script,
globals,
cli.no_config,
filesystem,
cache,
printer,
)) ))
.await .await
} }
@ -916,7 +922,14 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
// Initialize the cache. // Initialize the cache.
let cache = cache.init()?; 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 { Commands::Python(PythonNamespace {
command: PythonCommand::Pin(args), command: PythonCommand::Pin(args),
@ -951,6 +964,8 @@ async fn run_project(
project_command: Box<ProjectCommand>, project_command: Box<ProjectCommand>,
script: Option<Pep723Script>, script: Option<Pep723Script>,
globals: GlobalSettings, globals: GlobalSettings,
// TODO(zanieb): Determine a better story for passing `no_config` in here
no_config: bool,
filesystem: Option<FilesystemOptions>, filesystem: Option<FilesystemOptions>,
cache: Cache, cache: Cache,
printer: Printer, printer: Printer,
@ -1033,6 +1048,7 @@ async fn run_project(
args.isolated, args.isolated,
args.package, args.package,
args.no_project, args.no_project,
no_config,
args.extras, args.extras,
args.dev, args.dev,
args.python, args.python,

View File

@ -564,15 +564,22 @@ impl PythonUninstallSettings {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(crate) struct PythonFindSettings { pub(crate) struct PythonFindSettings {
pub(crate) request: Option<String>, pub(crate) request: Option<String>,
pub(crate) no_project: bool,
} }
impl PythonFindSettings { impl PythonFindSettings {
/// Resolve the [`PythonFindSettings`] from the CLI and workspace configuration. /// Resolve the [`PythonFindSettings`] from the CLI and workspace configuration.
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
pub(crate) fn resolve(args: PythonFindArgs, _filesystem: Option<FilesystemOptions>) -> Self { pub(crate) fn resolve(args: PythonFindArgs, _filesystem: Option<FilesystemOptions>) -> Self {
let PythonFindArgs { request } = args; let PythonFindArgs {
request,
no_project,
} = args;
Self { request } Self {
request,
no_project,
}
} }
} }

View File

@ -1,5 +1,9 @@
#![cfg(all(feature = "python", feature = "pypi"))] #![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 common::{uv_snapshot, TestContext};
use uv_python::platform::{Arch, Os}; use uv_python::platform::{Arch, Os};
@ -148,3 +152,94 @@ fn python_find() {
----- stderr ----- ----- 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 -----
"###);
}

View File

@ -3199,6 +3199,10 @@ uv python find [OPTIONS] [REQUEST]
<p>For example, spinners or progress bars.</p> <p>For example, spinners or progress bars.</p>
</dd><dt><code>--no-project</code></dt><dd><p>Avoid discovering a project or workspace.</p>
<p>Otherwise, when no request is provided, the Python requirement of a project in the current directory or parent directories will be used.</p>
</dd><dt><code>--no-python-downloads</code></dt><dd><p>Disable automatic downloads of Python</p> </dd><dt><code>--no-python-downloads</code></dt><dd><p>Disable automatic downloads of Python</p>
</dd><dt><code>--offline</code></dt><dd><p>Disable network access.</p> </dd><dt><code>--offline</code></dt><dd><p>Disable network access.</p>