Skip to content

Commit df3693e

Browse files
committed
Discover and respect .python-version files in parent directories
1 parent 4733b14 commit df3693e

File tree

3 files changed

+70
-16
lines changed

3 files changed

+70
-16
lines changed

crates/uv-python/src/version_files.rs

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::path::{Path, PathBuf};
33
use fs_err as fs;
44
use itertools::Itertools;
55
use tracing::debug;
6+
use uv_fs::Simplified;
67

78
use crate::PythonRequest;
89

@@ -22,31 +23,48 @@ pub struct PythonVersionFile {
2223
}
2324

2425
impl PythonVersionFile {
25-
/// Find a Python version file in the given directory.
26+
/// Find a Python version file in the given directory or any of its parents.
2627
pub async fn discover(
2728
working_directory: impl AsRef<Path>,
2829
no_config: bool,
2930
) -> Result<Option<Self>, std::io::Error> {
30-
let versions_path = working_directory.as_ref().join(PYTHON_VERSIONS_FILENAME);
31-
let version_path = working_directory.as_ref().join(PYTHON_VERSION_FILENAME);
31+
let Some(path) = Self::find_nearest(working_directory) else {
32+
return Ok(None);
33+
};
3234

3335
if no_config {
34-
if version_path.exists() {
35-
debug!("Ignoring `.python-version` file due to `--no-config`");
36-
} else if versions_path.exists() {
37-
debug!("Ignoring `.python-versions` file due to `--no-config`");
38-
};
36+
debug!(
37+
"Ignoring Python version file at `{}` due to `--no-config`",
38+
path.user_display()
39+
);
3940
return Ok(None);
4041
}
4142

42-
if let Some(result) = Self::try_from_path(version_path).await? {
43-
return Ok(Some(result));
44-
};
45-
if let Some(result) = Self::try_from_path(versions_path).await? {
46-
return Ok(Some(result));
47-
};
43+
// Use `try_from_path` instead of `from_path` to avoid TOCTOU failures.
44+
Self::try_from_path(path).await
45+
}
46+
47+
fn find_nearest(working_directory: impl AsRef<Path>) -> Option<PathBuf> {
48+
let mut current = working_directory.as_ref();
49+
loop {
50+
let version_path = current.join(PYTHON_VERSION_FILENAME);
51+
let versions_path = current.join(PYTHON_VERSIONS_FILENAME);
52+
53+
if version_path.exists() {
54+
return Some(version_path);
55+
}
56+
if versions_path.exists() {
57+
return Some(versions_path);
58+
}
59+
60+
if let Some(parent) = current.parent() {
61+
current = parent;
62+
} else {
63+
break;
64+
}
65+
}
4866

49-
Ok(None)
67+
None
5068
}
5169

5270
/// Try to read a Python version file at the given path.

crates/uv/tests/python_find.rs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#![cfg(all(feature = "python", feature = "pypi"))]
22

3-
use assert_fs::fixture::FileWriteStr;
43
use assert_fs::prelude::PathChild;
4+
use assert_fs::{fixture::FileWriteStr, prelude::PathCreateDir};
55
use indoc::indoc;
66

77
use common::{uv_snapshot, TestContext};
@@ -196,6 +196,38 @@ fn python_find_pin() {
196196
197197
----- stderr -----
198198
"###);
199+
200+
let child_dir = context.temp_dir.child("child");
201+
child_dir.create_dir_all().unwrap();
202+
203+
// We should also find pinned versions in the parent directory
204+
uv_snapshot!(context.filters(), context.python_find().current_dir(&child_dir), @r###"
205+
success: true
206+
exit_code: 0
207+
----- stdout -----
208+
[PYTHON-3.12]
209+
210+
----- stderr -----
211+
"###);
212+
213+
uv_snapshot!(context.filters(), context.python_pin().arg("3.11").current_dir(&child_dir), @r###"
214+
success: true
215+
exit_code: 0
216+
----- stdout -----
217+
Updated `.python-version` from `3.12` -> `3.11`
218+
219+
----- stderr -----
220+
"###);
221+
222+
// Unless the child directory also has a pin
223+
uv_snapshot!(context.filters(), context.python_find().current_dir(&child_dir), @r###"
224+
success: true
225+
exit_code: 0
226+
----- stdout -----
227+
[PYTHON-3.11]
228+
229+
----- stderr -----
230+
"###);
199231
}
200232

201233
#[test]

docs/reference/cli.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3201,6 +3201,10 @@ uv python find [OPTIONS] [REQUEST]
32013201

32023202
<p>For example, spinners or progress bars.</p>
32033203

3204+
</dd><dt><code>--no-project</code></dt><dd><p>Avoid discovering a project or workspace.</p>
3205+
3206+
<p>Otherwise, when no request is provided, the Python requirement of a project in the current directory or parent directories will be used.</p>
3207+
32043208
</dd><dt><code>--no-python-downloads</code></dt><dd><p>Disable automatic downloads of Python</p>
32053209

32063210
</dd><dt><code>--offline</code></dt><dd><p>Disable network access.</p>

0 commit comments

Comments
 (0)