Skip to content

Commit 2ca5bb4

Browse files
committed
Discover and respect .python-version files in parent directories
1 parent 7edd78c commit 2ca5bb4

File tree

2 files changed

+68
-18
lines changed

2 files changed

+68
-18
lines changed

crates/uv-python/src/version_files.rs

+35-17
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,37 +23,54 @@ 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
// TODO(zanieb): Create a `DiscoverySettings` struct for these options
2930
no_config: bool,
3031
prefer_versions: bool,
3132
) -> Result<Option<Self>, std::io::Error> {
32-
let versions_path = working_directory.as_ref().join(PYTHON_VERSIONS_FILENAME);
33-
let version_path = working_directory.as_ref().join(PYTHON_VERSION_FILENAME);
33+
let Some(path) = Self::find_nearest(working_directory, prefer_versions) else {
34+
return Ok(None);
35+
};
3436

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

44-
let paths = if prefer_versions {
45-
[versions_path, version_path]
46-
} else {
47-
[version_path, versions_path]
48-
};
49-
for path in paths {
50-
if let Some(result) = Self::try_from_path(path).await? {
51-
return Ok(Some(result));
45+
// Uses `try_from_path` instead of `from_path` to avoid TOCTOU failures.
46+
Self::try_from_path(path).await
47+
}
48+
49+
fn find_nearest(working_directory: impl AsRef<Path>, prefer_versions: bool) -> Option<PathBuf> {
50+
let mut current = working_directory.as_ref();
51+
loop {
52+
let version_path = current.join(PYTHON_VERSION_FILENAME);
53+
let versions_path = current.join(PYTHON_VERSIONS_FILENAME);
54+
55+
let paths = if prefer_versions {
56+
[versions_path, version_path]
57+
} else {
58+
[version_path, versions_path]
5259
};
60+
for path in paths {
61+
if path.exists() {
62+
return Some(path);
63+
}
64+
}
65+
66+
if let Some(parent) = current.parent() {
67+
current = parent;
68+
} else {
69+
break;
70+
}
5371
}
5472

55-
Ok(None)
73+
None
5674
}
5775

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

crates/uv/tests/python_find.rs

+33-1
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]

0 commit comments

Comments
 (0)