mirror of https://github.com/astral-sh/uv
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:
parent
9a14e028df
commit
7140cdec79
|
|
@ -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)]
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 -----
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue