@@ -4,6 +4,7 @@ use std::path::{Path, PathBuf};
44use fs_err as fs;
55use itertools:: Itertools ;
66use tracing:: debug;
7+ use uv_fs:: Simplified ;
78
89use crate :: PythonRequest ;
910
@@ -23,37 +24,54 @@ pub struct PythonVersionFile {
2324}
2425
2526impl 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 .
2728 pub async fn discover (
2829 working_directory : impl AsRef < Path > ,
2930 // TODO(zanieb): Create a `DiscoverySettings` struct for these options
3031 no_config : bool ,
3132 prefer_versions : bool ,
3233 ) -> 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+ } ;
3537
3638 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+ ) ;
4243 return Ok ( None ) ;
4344 }
4445
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]
5360 } ;
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+ }
5472 }
5573
56- Ok ( None )
74+ None
5775 }
5876
5977 /// Try to read a Python version file at the given path.
0 commit comments