@@ -3,6 +3,7 @@ use std::path::{Path, PathBuf};
3
3
use fs_err as fs;
4
4
use itertools:: Itertools ;
5
5
use tracing:: debug;
6
+ use uv_fs:: Simplified ;
6
7
7
8
use crate :: PythonRequest ;
8
9
@@ -22,37 +23,54 @@ pub struct PythonVersionFile {
22
23
}
23
24
24
25
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 .
26
27
pub async fn discover (
27
28
working_directory : impl AsRef < Path > ,
28
29
// TODO(zanieb): Create a `DiscoverySettings` struct for these options
29
30
no_config : bool ,
30
31
prefer_versions : bool ,
31
32
) -> 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
+ } ;
34
36
35
37
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
+ ) ;
41
42
return Ok ( None ) ;
42
43
}
43
44
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]
52
59
} ;
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
+ }
53
71
}
54
72
55
- Ok ( None )
73
+ None
56
74
}
57
75
58
76
/// Try to read a Python version file at the given path.
0 commit comments