From 916db9cf237bac01f138c8fd9cc33461b587dfd4 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 21 Aug 2024 13:54:13 -0500 Subject: [PATCH] Discover and respect `.python-version` files in parent directories --- crates/uv-python/src/version_files.rs | 48 ++++++++++++++++++--------- crates/uv/tests/python_find.rs | 34 ++++++++++++++++++- 2 files changed, 66 insertions(+), 16 deletions(-) diff --git a/crates/uv-python/src/version_files.rs b/crates/uv-python/src/version_files.rs index c592e14339494..11f4a03ec8eb5 100644 --- a/crates/uv-python/src/version_files.rs +++ b/crates/uv-python/src/version_files.rs @@ -3,6 +3,7 @@ use std::path::{Path, PathBuf}; use fs_err as fs; use itertools::Itertools; use tracing::debug; +use uv_fs::Simplified; use crate::PythonRequest; @@ -22,31 +23,48 @@ pub struct PythonVersionFile { } impl PythonVersionFile { - /// Find a Python version file in the given directory. + /// Find a Python version file in the given directory or any of its parents. pub async fn discover( working_directory: impl AsRef, no_config: bool, ) -> Result, std::io::Error> { - let versions_path = working_directory.as_ref().join(PYTHON_VERSIONS_FILENAME); - let version_path = working_directory.as_ref().join(PYTHON_VERSION_FILENAME); + let Some(path) = Self::find_nearest(working_directory) else { + return Ok(None); + }; if no_config { - if version_path.exists() { - debug!("Ignoring `.python-version` file due to `--no-config`"); - } else if versions_path.exists() { - debug!("Ignoring `.python-versions` file due to `--no-config`"); - }; + debug!( + "Ignoring Python version file at `{}` due to `--no-config`", + path.user_display() + ); return Ok(None); } - if let Some(result) = Self::try_from_path(version_path).await? { - return Ok(Some(result)); - }; - if let Some(result) = Self::try_from_path(versions_path).await? { - return Ok(Some(result)); - }; + // Use `try_from_path` instead of `from_path` to avoid TOCTOU failures. + Self::try_from_path(path).await + } + + fn find_nearest(working_directory: impl AsRef) -> Option { + let mut current = working_directory.as_ref(); + loop { + let version_path = current.join(PYTHON_VERSION_FILENAME); + let versions_path = current.join(PYTHON_VERSIONS_FILENAME); + + if version_path.exists() { + return Some(version_path); + } + if versions_path.exists() { + return Some(versions_path); + } + + if let Some(parent) = current.parent() { + current = parent; + } else { + break; + } + } - Ok(None) + None } /// Try to read a Python version file at the given path. diff --git a/crates/uv/tests/python_find.rs b/crates/uv/tests/python_find.rs index 7b27406a4338b..ca184aaed9ac3 100644 --- a/crates/uv/tests/python_find.rs +++ b/crates/uv/tests/python_find.rs @@ -1,7 +1,7 @@ #![cfg(all(feature = "python", feature = "pypi"))] -use assert_fs::fixture::FileWriteStr; use assert_fs::prelude::PathChild; +use assert_fs::{fixture::FileWriteStr, prelude::PathCreateDir}; use indoc::indoc; use common::{uv_snapshot, TestContext}; @@ -196,6 +196,38 @@ fn python_find_pin() { ----- stderr ----- "###); + + let child_dir = context.temp_dir.child("child"); + child_dir.create_dir_all().unwrap(); + + // We should also find pinned versions in the parent directory + uv_snapshot!(context.filters(), context.python_find().current_dir(&child_dir), @r###" + success: true + exit_code: 0 + ----- stdout ----- + [PYTHON-3.12] + + ----- stderr ----- + "###); + + uv_snapshot!(context.filters(), context.python_pin().arg("3.11").current_dir(&child_dir), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Updated `.python-version` from `3.12` -> `3.11` + + ----- stderr ----- + "###); + + // Unless the child directory also has a pin + uv_snapshot!(context.filters(), context.python_find().current_dir(&child_dir), @r###" + success: true + exit_code: 0 + ----- stdout ----- + [PYTHON-3.11] + + ----- stderr ----- + "###); } #[test]