Skip to content

Commit

Permalink
Rework reading pinned python versions
Browse files Browse the repository at this point in the history
  • Loading branch information
blueraft committed Jul 15, 2024
1 parent 7e23a7d commit 5bf20d9
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 38 deletions.
131 changes: 101 additions & 30 deletions crates/uv/src/commands/python/pin.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::fmt::Write;
use std::path::PathBuf;
use std::{fmt::Write, str::FromStr};

use anyhow::{bail, Result};

Expand Down Expand Up @@ -31,34 +31,28 @@ pub(crate) async fn pin(
warn_user_once!("`uv python pin` is experimental and may change without warning.");
}

let virtual_project = VirtualProject::discover(&std::env::current_dir()?, None).await;
let virtual_project = match VirtualProject::discover(&std::env::current_dir()?, None).await {
Ok(virtual_project) if !isolated => Some(virtual_project),
Ok(_) => None,
Err(e) => {
debug!("Failed to discover virtual project: {e}");
None
}
};

let Some(request) = request else {
// Display the current pinned Python version
if let Some(pins) = requests_from_version_file().await? {
for pin in pins {
let python = match PythonInstallation::find(
&pin,
EnvironmentPreference::OnlySystem,
python_preference,
cache,
) {
Ok(python) => Some(python),
// If no matching Python version is found, don't fail unless `resolved` was requested
Err(uv_python::Error::MissingPython(err)) if !resolved => {
warn_user_once!("{}", err);
None
}
Err(err) => return Err(err.into()),
};
if !isolated {
if let (Some(python), Ok(virtual_project)) = (&python, &virtual_project) {
if let Err(e) = assert_python_compatibility(python, virtual_project) {
warn_user_once!("{}", e);
}
}
}
writeln!(printer.stdout(), "{}", pin.to_canonical_string())?;
if let Some(virtual_project) = &virtual_project {
check_request_requires_python_compatibility(
&pin,
virtual_project,
python_preference,
cache,
);
}
}
return Ok(ExitStatus::Success);
}
Expand All @@ -81,10 +75,25 @@ pub(crate) async fn pin(
Err(err) => return Err(err.into()),
};

if !isolated {
if let (Some(python), Ok(virtual_project)) = (&python, &virtual_project) {
assert_python_compatibility(python, virtual_project)?;
}
if let Some(virtual_project) = &virtual_project {
// Error if the request is incompatible with the Python requirement
if let PythonRequest::Version(version) = &request {
if let Ok(python_version) = pep440_rs::Version::from_str(&version.to_string()) {
assert_python_compatibility(&python_version, virtual_project)?;
}
} else {
if let Some(python) = &python {
// Warn if the resolved Python is incompatible with the Python requirement unless --resolved is used
if let Err(e) =
assert_python_compatibility(python.python_version(), virtual_project)
{
if resolved {
return Err(e);
};
warn_user_once!("{}", e);
}
}
};
}

let output = if resolved {
Expand Down Expand Up @@ -114,9 +123,70 @@ pub(crate) async fn pin(
Ok(ExitStatus::Success)
}

/// Check if pinned request is compatible with the workspace/project's `Requires-Python`.
fn check_request_requires_python_compatibility(
pin: &PythonRequest,
virtual_project: &VirtualProject,
python_preference: PythonPreference,
cache: &Cache,
) {
let requested_version = match pin {
PythonRequest::Version(ref version) => {
let version = pep440_rs::Version::from_str(&version.to_string());
match version {
Ok(version) => Some(version),
Err(e) => {
debug!("Failed to parse PEP440 python version from {pin}: {e}");
None
}
}
}
_ => None,
};

// Check if the requested version is compatible with the project.
// If the compatibility check fails, exit early.
if let Some(version) = requested_version {
if let Err(e) = assert_python_compatibility(&version, virtual_project) {
warn_user_once!("{}", e);
return;
}
};

// If the requested version is either not specified or compatible, attempt to resolve the request into an interpreter.
let python_version = match PythonInstallation::find(
pin,
EnvironmentPreference::OnlySystem,
python_preference,
cache,
) {
Ok(python) => Ok(python.python_version().clone()),
Err(err) => Err(err.to_string()),
};

match python_version {
Ok(python_version) => {
debug!(
"The pinned Python version {} resolves to {}",
pin, python_version
);
if let Err(e) = assert_python_compatibility(&python_version, virtual_project) {
warn_user_once!("{}", e);
}
}
Err(e) => {
warn_user_once!(
"Failed to resolve pinned Python version from {}: {}",
pin,
e
);
}
}
}

/// Checks if the pinned Python version is compatible with the workspace/project's `Requires-Python`.
fn assert_python_compatibility(
python: &uv_python::PythonInstallation,
python_version: &pep440_rs::Version,
virtual_project: &VirtualProject,
) -> Result<()> {
let (requires_python, project_type) = match virtual_project {
Expand All @@ -140,9 +210,10 @@ fn assert_python_compatibility(
};

if let Some(requires_python) = requires_python {
if !requires_python.contains(python.python_version()) {
if !requires_python.contains(python_version) {
anyhow::bail!(
"The pinned Python version is incompatible with the {}'s `Requires-Python` of {}.",
"The pinned Python version {} is incompatible with the {}'s `Requires-Python` of {}.",
python_version,
project_type,
requires_python
);
Expand Down
27 changes: 19 additions & 8 deletions crates/uv/tests/python_pin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,13 +245,13 @@ fn python_pin_compatible_with_requires_python() -> anyhow::Result<()> {
)?;

uv_snapshot!(context.filters(), context.python_pin().arg("3.10"), @r###"
success: false
exit_code: 2
----- stdout -----
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: The pinned Python version is incompatible with the project's `Requires-Python` of >=3.11.
"###);
----- stderr -----
error: The pinned Python version 3.10 is incompatible with the project's `Requires-Python` of >=3.11.
"###);

uv_snapshot!(context.filters(), context.python_pin().arg("3.11"), @r###"
success: true
Expand Down Expand Up @@ -290,7 +290,7 @@ fn python_pin_compatible_with_requires_python() -> anyhow::Result<()> {
3.11
----- stderr -----
warning: The pinned Python version is incompatible with the project's `Requires-Python` of >=3.12.
warning: The pinned Python version 3.11 is incompatible with the project's `Requires-Python` of >=3.12.
"###);

Ok(())
Expand All @@ -299,6 +299,17 @@ fn python_pin_compatible_with_requires_python() -> anyhow::Result<()> {
#[test]
fn warning_pinned_python_version_not_installed() -> anyhow::Result<()> {
let context: TestContext = TestContext::new_with_versions(&["3.10", "3.11"]);
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = ["iniconfig"]
"#,
)?;

let python_version_file = context.temp_dir.child(PYTHON_VERSION_FILENAME);
python_version_file.write_str(r"3.12")?;
if cfg!(windows) {
Expand All @@ -319,7 +330,7 @@ fn warning_pinned_python_version_not_installed() -> anyhow::Result<()> {
3.12
----- stderr -----
warning: No interpreter found for Python 3.12 in system path
warning: Failed to resolve pinned Python version from Python 3.12: No interpreter found for Python 3.12 in system path
"###);
}

Expand Down

0 comments on commit 5bf20d9

Please sign in to comment.