Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions crates/uv-python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ pub use crate::prefix::Prefix;
pub use crate::python_version::PythonVersion;
pub use crate::target::Target;
pub use crate::version_files::{
request_from_version_file, requests_from_version_file, version_file_exists,
versions_file_exists, PYTHON_VERSIONS_FILENAME, PYTHON_VERSION_FILENAME,
request_from_version_file, request_from_version_file_in, requests_from_version_file,
requests_from_version_file_in, version_file_exists, versions_file_exists,
PYTHON_VERSIONS_FILENAME, PYTHON_VERSION_FILENAME,
};
pub use crate::virtualenv::{Error as VirtualEnvError, PyVenvConfiguration, VirtualEnvironment};
mod discovery;
Expand Down
74 changes: 72 additions & 2 deletions crates/uv-python/src/version_files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,77 @@ pub static PYTHON_VERSIONS_FILENAME: &str = ".python-versions";
/// Read [`PythonRequest`]s from a version file, if present.
///
/// Prefers `.python-versions` then `.python-version`.
/// If only one Python version is desired, use [`request_from_version_files`] which prefers the `.python-version` file.
/// If only one Python version is desired, use [`request_from_version_file_in`] which prefers the `.python-version` file.
pub async fn requests_from_version_file_in(
root: &PathBuf,
) -> Result<Option<Vec<PythonRequest>>, io::Error> {
if let Some(versions) = read_versions_file_in(root).await? {
Ok(Some(
versions
.into_iter()
.map(|version| PythonRequest::parse(&version))
.collect(),
))
} else if let Some(version) = read_version_file_in(root).await? {
Ok(Some(vec![PythonRequest::parse(&version)]))
} else {
Ok(None)
}
}

/// Read a [`PythonRequest`] from a version file, if present.
///
/// Prefers `.python-version` then the first entry of `.python-versions`.
/// If multiple Python versions are desired, use [`requests_from_version_file_in`] instead.
pub async fn request_from_version_file_in(
root: &PathBuf,
) -> Result<Option<PythonRequest>, io::Error> {
if let Some(version) = read_version_file_in(root).await? {
Ok(Some(PythonRequest::parse(&version)))
} else if let Some(versions) = read_versions_file_in(root).await? {
Ok(versions
.into_iter()
.next()
.inspect(|_| debug!("Using the first version from `.python-versions`"))
.map(|version| PythonRequest::parse(&version)))
} else {
Ok(None)
}
}

async fn read_versions_file_in(root: &PathBuf) -> Result<Option<Vec<String>>, io::Error> {
let mut version_file = PathBuf::from(root);
version_file.push(PYTHON_VERSIONS_FILENAME);
if !version_file.try_exists()? {
return Ok(None);
}
debug!("Reading requests from `{}`", version_file.display());
let lines: Vec<String> = fs::tokio::read_to_string(version_file)
.await?
.lines()
.map(ToString::to_string)
.collect();
Ok(Some(lines))
}

async fn read_version_file_in(root: &PathBuf) -> Result<Option<String>, io::Error> {
let mut version_file = PathBuf::from(root);
version_file.push(PYTHON_VERSION_FILENAME);
if !version_file.try_exists()? {
return Ok(None);
}
debug!("Reading requests from `{}`", version_file.display());
Ok(fs::tokio::read_to_string(version_file)
.await?
.lines()
.next()
.map(ToString::to_string))
}

/// Read [`PythonRequest`]s from a version file, if present.
///
/// Prefers `.python-versions` then `.python-version`.
/// If only one Python version is desired, use [`request_from_version_file`] which prefers the `.python-version` file.
pub async fn requests_from_version_file() -> Result<Option<Vec<PythonRequest>>, io::Error> {
if let Some(versions) = read_versions_file().await? {
Ok(Some(
Expand All @@ -32,7 +102,7 @@ pub async fn requests_from_version_file() -> Result<Option<Vec<PythonRequest>>,
/// Read a [`PythonRequest`] from a version file, if present.
///
/// Prefers `.python-version` then the first entry of `.python-versions`.
/// If multiple Python versions are desired, use [`requests_from_version_files`] instead.
/// If multiple Python versions are desired, use [`requests_from_version_file`] instead.
pub async fn request_from_version_file() -> Result<Option<PythonRequest>, io::Error> {
if let Some(version) = read_version_file().await? {
Ok(Some(PythonRequest::parse(&version)))
Expand Down
15 changes: 11 additions & 4 deletions crates/uv/src/commands/python/pin.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
use std::fmt::Write;
use std::path::PathBuf;

use anyhow::{bail, Result};

use tracing::debug;
use uv_cache::Cache;
use uv_configuration::PreviewMode;
use uv_distribution::Workspace;
use uv_fs::Simplified;
use uv_python::{
requests_from_version_file, EnvironmentPreference, PythonInstallation, PythonPreference,
requests_from_version_file_in, EnvironmentPreference, PythonInstallation, PythonPreference,
PythonRequest, PYTHON_VERSION_FILENAME,
};
use uv_warnings::warn_user_once;
Expand All @@ -29,9 +29,16 @@ pub(crate) async fn pin(
warn_user_once!("`uv python pin` is experimental and may change without warning.");
}

let working_dir =
if let Ok(workspace) = Workspace::discover(&std::env::current_dir()?, None).await {
workspace.install_path().to_owned()
} else {
std::env::current_dir()?
};

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_in(&working_dir).await? {
for pin in pins {
writeln!(printer.stdout(), "{}", pin.to_canonical_string())?;
}
Expand Down Expand Up @@ -69,7 +76,7 @@ pub(crate) async fn pin(
};

debug!("Using pin `{}`", output);
let version_file = PathBuf::from(PYTHON_VERSION_FILENAME);
let version_file = working_dir.join(PYTHON_VERSION_FILENAME);
let exists = version_file.exists();

debug!("Writing pin to {}", version_file.user_display());
Expand Down
40 changes: 40 additions & 0 deletions crates/uv/tests/python_pin.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#![cfg(all(feature = "python", feature = "pypi"))]

use assert_fs::fixture::{FileWriteStr, PathChild, PathCreateDir};
use common::{uv_snapshot, TestContext};
use indoc::indoc;
use insta::assert_snapshot;
use uv_python::{
platform::{Arch, Os},
Expand Down Expand Up @@ -211,6 +213,44 @@ fn python_pin() {
}
}

#[test]
fn python_pin_in_workspace_subfolder() {
let context: TestContext = TestContext::new_with_versions(&["3.11"]);

// Create a simple pyproject.toml
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml
.write_str(indoc! {r#"
[project]
name = "project"
version = "0.1.0"
# ...
requires-python = ">=3.10"
dependencies = []
"#})
.unwrap();

// Create a subfolder
let working_dir = context.temp_dir.child("subfolder");
working_dir.create_dir_all().unwrap();

// Run python pin command in subfolder
uv_snapshot!(context.filters(), context.python_pin().current_dir(&working_dir).arg("3.11"), @r###"
success: true
exit_code: 0
----- stdout -----
Pinned to `3.11`

----- stderr -----
"###);

let python_version =
fs_err::read_to_string(context.temp_dir.join(PYTHON_VERSION_FILENAME)).unwrap();
assert_snapshot!(python_version, @r###"
3.11
"###);
}

/// We do not need a Python interpreter to pin without `--resolved`
/// (skip on Windows because the snapshot is different and the behavior is not platform dependent)
#[cfg(unix)]
Expand Down