Skip to content

Commit ad7737a

Browse files
committed
Respect .python-version files and pyproject.toml in uv python find
1 parent d2053f4 commit ad7737a

File tree

5 files changed

+164
-11
lines changed

5 files changed

+164
-11
lines changed

crates/uv-cli/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3031,6 +3031,13 @@ pub struct PythonFindArgs {
30313031
///
30323032
/// See `uv help python` to view supported request formats.
30333033
pub request: Option<String>,
3034+
3035+
/// Avoid discovering a project or workspace.
3036+
///
3037+
/// Otherwise, when no request is provided, the Python requirement of a project in the current
3038+
/// directory or parent directories will be used.
3039+
#[arg(long, alias = "no_workspace")]
3040+
pub no_project: bool,
30343041
}
30353042

30363043
#[derive(Args)]

crates/uv/src/commands/python/find.rs

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,60 @@ use anstream::println;
22
use anyhow::Result;
33

44
use uv_cache::Cache;
5-
use uv_fs::Simplified;
6-
use uv_python::{EnvironmentPreference, PythonInstallation, PythonPreference, PythonRequest};
5+
use uv_fs::{Simplified, CWD};
6+
use uv_python::{
7+
EnvironmentPreference, PythonInstallation, PythonPreference, PythonRequest, PythonVersionFile,
8+
VersionRequest,
9+
};
10+
use uv_resolver::RequiresPython;
11+
use uv_warnings::warn_user_once;
12+
use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceError};
713

8-
use crate::commands::ExitStatus;
14+
use crate::commands::{project::find_requires_python, ExitStatus};
915

1016
/// Find a Python interpreter.
1117
pub(crate) async fn find(
1218
request: Option<String>,
19+
no_project: bool,
20+
no_config: bool,
1321
python_preference: PythonPreference,
1422
cache: &Cache,
1523
) -> Result<ExitStatus> {
16-
let request = match request {
17-
Some(request) => PythonRequest::parse(&request),
18-
None => PythonRequest::Any,
19-
};
24+
// (1) Explicit request from user
25+
let mut request = request.map(|request| PythonRequest::parse(&request));
26+
27+
// (2) Request from `.python-version`
28+
if request.is_none() {
29+
request = PythonVersionFile::discover(&*CWD, no_config)
30+
.await?
31+
.and_then(PythonVersionFile::into_version);
32+
}
33+
34+
// (3) `Requires-Python` in `pyproject.toml`
35+
if request.is_none() && !no_project {
36+
let project = match VirtualProject::discover(&CWD, &DiscoveryOptions::default()).await {
37+
Ok(project) => Some(project),
38+
Err(WorkspaceError::MissingProject(_)) => None,
39+
Err(WorkspaceError::MissingPyprojectToml) => None,
40+
Err(WorkspaceError::NonWorkspace(_)) => None,
41+
Err(err) => {
42+
warn_user_once!("{err}");
43+
None
44+
}
45+
};
46+
47+
if let Some(project) = project {
48+
request = find_requires_python(project.workspace())?
49+
.as_ref()
50+
.map(RequiresPython::specifiers)
51+
.map(|specifiers| {
52+
PythonRequest::Version(VersionRequest::Range(specifiers.clone()))
53+
});
54+
}
55+
}
56+
2057
let python = PythonInstallation::find(
21-
&request,
58+
&request.unwrap_or_default(),
2259
EnvironmentPreference::OnlySystem,
2360
python_preference,
2461
cache,

crates/uv/src/lib.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -916,7 +916,14 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
916916
// Initialize the cache.
917917
let cache = cache.init()?;
918918

919-
commands::python_find(args.request, globals.python_preference, &cache).await
919+
commands::python_find(
920+
args.request,
921+
args.no_project,
922+
cli.no_config,
923+
globals.python_preference,
924+
&cache,
925+
)
926+
.await
920927
}
921928
Commands::Python(PythonNamespace {
922929
command: PythonCommand::Pin(args),

crates/uv/src/settings.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -564,15 +564,22 @@ impl PythonUninstallSettings {
564564
#[derive(Debug, Clone)]
565565
pub(crate) struct PythonFindSettings {
566566
pub(crate) request: Option<String>,
567+
pub(crate) no_project: bool,
567568
}
568569

569570
impl PythonFindSettings {
570571
/// Resolve the [`PythonFindSettings`] from the CLI and workspace configuration.
571572
#[allow(clippy::needless_pass_by_value)]
572573
pub(crate) fn resolve(args: PythonFindArgs, _filesystem: Option<FilesystemOptions>) -> Self {
573-
let PythonFindArgs { request } = args;
574+
let PythonFindArgs {
575+
request,
576+
no_project,
577+
} = args;
574578

575-
Self { request }
579+
Self {
580+
request,
581+
no_project,
582+
}
576583
}
577584
}
578585

crates/uv/tests/python_find.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
#![cfg(all(feature = "python", feature = "pypi"))]
22

3+
use assert_fs::fixture::FileWriteStr;
4+
use assert_fs::prelude::PathChild;
5+
use indoc::indoc;
6+
37
use common::{uv_snapshot, TestContext};
48
use uv_python::platform::{Arch, Os};
59

@@ -148,3 +152,94 @@ fn python_find() {
148152
----- stderr -----
149153
"###);
150154
}
155+
156+
#[test]
157+
fn python_find_pin() {
158+
let context: TestContext = TestContext::new_with_versions(&["3.11", "3.12"]);
159+
160+
// Pin to a version
161+
uv_snapshot!(context.filters(), context.python_pin().arg("3.12"), @r###"
162+
success: true
163+
exit_code: 0
164+
----- stdout -----
165+
Pinned `.python-version` to `3.12`
166+
167+
----- stderr -----
168+
"###);
169+
170+
// We should find the pinned version, not the first on the path
171+
uv_snapshot!(context.filters(), context.python_find(), @r###"
172+
success: true
173+
exit_code: 0
174+
----- stdout -----
175+
[PYTHON-3.12]
176+
177+
----- stderr -----
178+
"###);
179+
180+
// Unless explictly requested
181+
uv_snapshot!(context.filters(), context.python_find().arg("3.11"), @r###"
182+
success: true
183+
exit_code: 0
184+
----- stdout -----
185+
[PYTHON-3.11]
186+
187+
----- stderr -----
188+
"###);
189+
190+
// Or `--no-config` is used
191+
uv_snapshot!(context.filters(), context.python_find().arg("--no-config"), @r###"
192+
success: true
193+
exit_code: 0
194+
----- stdout -----
195+
[PYTHON-3.11]
196+
197+
----- stderr -----
198+
"###);
199+
}
200+
201+
#[test]
202+
fn python_find_project() {
203+
let context: TestContext = TestContext::new_with_versions(&["3.11", "3.12"]);
204+
205+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
206+
pyproject_toml
207+
.write_str(indoc! {r#"
208+
[project]
209+
name = "project"
210+
version = "0.1.0"
211+
requires-python = ">=3.12"
212+
dependencies = ["anyio==3.7.0"]
213+
"#})
214+
.unwrap();
215+
216+
// We should respect the project's required version, not the first on the path
217+
uv_snapshot!(context.filters(), context.python_find(), @r###"
218+
success: true
219+
exit_code: 0
220+
----- stdout -----
221+
[PYTHON-3.12]
222+
223+
----- stderr -----
224+
"###);
225+
226+
// Unless explictly requested
227+
uv_snapshot!(context.filters(), context.python_find().arg("3.11"), @r###"
228+
success: true
229+
exit_code: 0
230+
----- stdout -----
231+
[PYTHON-3.11]
232+
233+
----- stderr -----
234+
"###);
235+
236+
// Or `--no-project` is used
237+
uv_snapshot!(context.filters(), context.python_find().arg("--no-project"), @r###"
238+
success: true
239+
exit_code: 0
240+
----- stdout -----
241+
[PYTHON-3.11]
242+
243+
----- stderr -----
244+
"###);
245+
}

0 commit comments

Comments
 (0)