Skip to content

Commit

Permalink
Support .python-version outside current directory
Browse files Browse the repository at this point in the history
Preparing for supporting this in uv run, update the .python-version
reading functions so that they can read from more than just the current
directory.

Minor behaviour change(!). In some cases previously it could
end up with different python versions depending on from which directory
inside a wider workspace an operation was called from (if there were
different .python-version files). I think this is clear from
get_or_init_environment's use of discover.
  • Loading branch information
bluss committed Jul 29, 2024
1 parent 194904b commit f932512
Show file tree
Hide file tree
Showing 6 changed files with 41 additions and 18 deletions.
45 changes: 33 additions & 12 deletions crates/uv-python/src/version_files.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::path::Path;

use fs_err as fs;
use tracing::debug;

Expand All @@ -13,15 +15,17 @@ pub static PYTHON_VERSIONS_FILENAME: &str = ".python-versions";
///
/// Prefers `.python-versions` then `.python-version`.
/// If only one Python version is desired, use [`request_from_version_files`] which prefers the `.python-version` file.
pub async fn requests_from_version_file() -> Result<Option<Vec<PythonRequest>>, std::io::Error> {
if let Some(versions) = read_versions_file().await? {
pub async fn requests_from_version_file(
directory: Option<&Path>,
) -> Result<Option<Vec<PythonRequest>>, std::io::Error> {
if let Some(versions) = read_versions_file(directory).await? {
Ok(Some(
versions
.into_iter()
.map(|version| PythonRequest::parse(&version))
.collect(),
))
} else if let Some(version) = read_version_file().await? {
} else if let Some(version) = read_version_file(directory).await? {
Ok(Some(vec![PythonRequest::parse(&version)]))
} else {
Ok(None)
Expand All @@ -30,12 +34,17 @@ pub async fn requests_from_version_file() -> Result<Option<Vec<PythonRequest>>,

/// Read a [`PythonRequest`] from a version file, if present.
///
/// Find the version file inside directory, or the current directory
/// if None.
///
/// Prefers `.python-version` then the first entry of `.python-versions`.
/// If multiple Python versions are desired, use [`requests_from_version_files`] instead.
pub async fn request_from_version_file() -> Result<Option<PythonRequest>, std::io::Error> {
if let Some(version) = read_version_file().await? {
pub async fn request_from_version_file(
directory: Option<&Path>,
) -> Result<Option<PythonRequest>, std::io::Error> {
if let Some(version) = read_version_file(directory).await? {
Ok(Some(PythonRequest::parse(&version)))
} else if let Some(versions) = read_versions_file().await? {
} else if let Some(versions) = read_versions_file(directory).await? {
Ok(versions
.into_iter()
.next()
Expand All @@ -52,10 +61,17 @@ pub async fn write_version_file(version: &str) -> Result<(), std::io::Error> {
fs::tokio::write(PYTHON_VERSION_FILENAME, format!("{version}\n")).await
}

async fn read_versions_file() -> Result<Option<Vec<String>>, std::io::Error> {
match fs::tokio::read_to_string(PYTHON_VERSIONS_FILENAME).await {
async fn read_versions_file(
directory: Option<&Path>,
) -> Result<Option<Vec<String>>, std::io::Error> {
let file_path = directory.map(|pth| pth.join(PYTHON_VERSIONS_FILENAME));
let path = file_path
.as_deref()
.unwrap_or(Path::new(PYTHON_VERSIONS_FILENAME));

match fs::tokio::read_to_string(path).await {
Ok(content) => {
debug!("Reading requests from `{PYTHON_VERSIONS_FILENAME}`");
debug!("Reading requests from `{}`", path.display());
Ok(Some(
content
.lines()
Expand All @@ -73,10 +89,15 @@ async fn read_versions_file() -> Result<Option<Vec<String>>, std::io::Error> {
}
}

async fn read_version_file() -> Result<Option<String>, std::io::Error> {
match fs::tokio::read_to_string(PYTHON_VERSION_FILENAME).await {
async fn read_version_file(directory: Option<&Path>) -> Result<Option<String>, std::io::Error> {
let file_path = directory.map(|pth| pth.join(PYTHON_VERSION_FILENAME));
let path = file_path
.as_deref()
.unwrap_or(Path::new(PYTHON_VERSION_FILENAME));

match fs::tokio::read_to_string(path).await {
Ok(content) => {
debug!("Reading requests from `{PYTHON_VERSION_FILENAME}`");
debug!("Reading requests from `{}`", path.display());
Ok(content
.lines()
.find(|line| {
Expand Down
4 changes: 3 additions & 1 deletion crates/uv/src/commands/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,9 @@ impl FoundInterpreter {
let python_request = if let Some(request) = python_request {
Some(request)
// (2) Request from `.python-version`
} else if let Some(request) = request_from_version_file().await? {
} else if let Some(request) =
request_from_version_file(Some(workspace.install_path())).await?
{
Some(request)
// (3) `Requires-Python` in `pyproject.toml`
} else {
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ pub(crate) async fn run(
let python_request = if let Some(request) = python.as_deref() {
Some(PythonRequest::parse(request))
// (2) Request from `.python-version`
} else if let Some(request) = request_from_version_file().await? {
} else if let Some(request) = request_from_version_file(None).await? {
Some(request)
// (3) `Requires-Python` in `pyproject.toml`
} else {
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/python/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ pub(crate) async fn install(
}
None
} else {
requests_from_version_file().await?
requests_from_version_file(None).await?
};
version_file_requests.unwrap_or_else(|| vec![PythonRequest::Any])
} else {
Expand Down
4 changes: 2 additions & 2 deletions crates/uv/src/commands/python/pin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ pub(crate) async fn pin(

let Some(request) = request else {
// Display the current pinned Python version
if let Some(pins) = requests_from_version_file().await? {
if let Some(pins) = requests_from_version_file(None).await? {
for pin in pins {
writeln!(printer.stdout(), "{}", pin.to_canonical_string())?;
if let Some(virtual_project) = &virtual_project {
Expand Down Expand Up @@ -126,7 +126,7 @@ pub(crate) async fn pin(
request.to_canonical_string()
};

let existing = request_from_version_file().await.ok().flatten();
let existing = request_from_version_file(None).await.ok().flatten();
write_version_file(&output).await?;

if let Some(existing) = existing
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/venv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ async fn venv_impl(

let mut interpreter_request = python_request.map(PythonRequest::parse);
if preview.is_enabled() && interpreter_request.is_none() {
interpreter_request = request_from_version_file().await.into_diagnostic()?;
interpreter_request = request_from_version_file(None).await.into_diagnostic()?;
}
if preview.is_disabled() && relocatable {
warn_user_once!("`--relocatable` is experimental and may change without warning");
Expand Down

0 comments on commit f932512

Please sign in to comment.