Skip to content

Commit

Permalink
Discover and respect .python-version files in parent directories
Browse files Browse the repository at this point in the history
  • Loading branch information
zanieb committed Aug 21, 2024
1 parent 4733b14 commit df3693e
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 16 deletions.
48 changes: 33 additions & 15 deletions crates/uv-python/src/version_files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<Path>,
no_config: bool,
) -> Result<Option<Self>, 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<Path>) -> Option<PathBuf> {
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.
Expand Down
34 changes: 33 additions & 1 deletion crates/uv/tests/python_find.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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]
Expand Down
4 changes: 4 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -3201,6 +3201,10 @@ uv python find [OPTIONS] [REQUEST]

<p>For example, spinners or progress bars.</p>

</dd><dt><code>--no-project</code></dt><dd><p>Avoid discovering a project or workspace.</p>

<p>Otherwise, when no request is provided, the Python requirement of a project in the current directory or parent directories will be used.</p>

</dd><dt><code>--no-python-downloads</code></dt><dd><p>Disable automatic downloads of Python</p>

</dd><dt><code>--offline</code></dt><dd><p>Disable network access.</p>
Expand Down

0 comments on commit df3693e

Please sign in to comment.