@@ -4,6 +4,7 @@ use std::path::{Path, PathBuf};
4
4
use fs_err as fs;
5
5
use itertools:: Itertools ;
6
6
use tracing:: debug;
7
+ use uv_fs:: Simplified ;
7
8
8
9
use crate :: PythonRequest ;
9
10
@@ -23,37 +24,54 @@ pub struct PythonVersionFile {
23
24
}
24
25
25
26
impl PythonVersionFile {
26
- /// Find a Python version file in the given directory.
27
+ /// Find a Python version file in the given directory or any of its parents .
27
28
pub async fn discover (
28
29
working_directory : impl AsRef < Path > ,
29
30
// TODO(zanieb): Create a `DiscoverySettings` struct for these options
30
31
no_config : bool ,
31
32
prefer_versions : bool ,
32
33
) -> Result < Option < Self > , std:: io:: Error > {
33
- let versions_path = working_directory. as_ref ( ) . join ( PYTHON_VERSIONS_FILENAME ) ;
34
- let version_path = working_directory. as_ref ( ) . join ( PYTHON_VERSION_FILENAME ) ;
34
+ let Some ( path) = Self :: find_nearest ( working_directory, prefer_versions) else {
35
+ return Ok ( None ) ;
36
+ } ;
35
37
36
38
if no_config {
37
- if version_path. exists ( ) {
38
- debug ! ( "Ignoring `.python-version` file due to `--no-config`" ) ;
39
- } else if versions_path. exists ( ) {
40
- debug ! ( "Ignoring `.python-versions` file due to `--no-config`" ) ;
41
- } ;
39
+ debug ! (
40
+ "Ignoring Python version file at `{}` due to `--no-config`" ,
41
+ path. user_display( )
42
+ ) ;
42
43
return Ok ( None ) ;
43
44
}
44
45
45
- let paths = if prefer_versions {
46
- [ versions_path, version_path]
47
- } else {
48
- [ version_path, versions_path]
49
- } ;
50
- for path in paths {
51
- if let Some ( result) = Self :: try_from_path ( path) . await ? {
52
- return Ok ( Some ( result) ) ;
46
+ // Uses `try_from_path` instead of `from_path` to avoid TOCTOU failures.
47
+ Self :: try_from_path ( path) . await
48
+ }
49
+
50
+ fn find_nearest ( working_directory : impl AsRef < Path > , prefer_versions : bool ) -> Option < PathBuf > {
51
+ let mut current = working_directory. as_ref ( ) ;
52
+ loop {
53
+ let version_path = current. join ( PYTHON_VERSION_FILENAME ) ;
54
+ let versions_path = current. join ( PYTHON_VERSIONS_FILENAME ) ;
55
+
56
+ let paths = if prefer_versions {
57
+ [ versions_path, version_path]
58
+ } else {
59
+ [ version_path, versions_path]
53
60
} ;
61
+ for path in paths {
62
+ if path. exists ( ) {
63
+ return Some ( path) ;
64
+ }
65
+ }
66
+
67
+ if let Some ( parent) = current. parent ( ) {
68
+ current = parent;
69
+ } else {
70
+ break ;
71
+ }
54
72
}
55
73
56
- Ok ( None )
74
+ None
57
75
}
58
76
59
77
/// Try to read a Python version file at the given path.
0 commit comments