From fb0811680054a8aea9d8babdbf6877c6e3da9afe Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Fri, 25 Apr 2025 13:06:46 -0500 Subject: [PATCH] Report Python versions in `pyvenv.cfg` version mismatch (#13027) When working on #13025 I noticed this message was lacking versions, which seems frustrating if you're debugging things. I refactored the general `matches_interpreter` utilities that were added in https://github.com/astral-sh/uv/pull/12884 into a more purpose-fit function that returns an `Option` with the versions if there's a mismatch. --- crates/uv-python/src/environment.rs | 25 ++++++++++++++++++------- crates/uv-python/src/interpreter.rs | 16 +++++++++++----- crates/uv-python/src/python_version.rs | 11 +++++++++++ crates/uv-python/src/virtualenv.rs | 14 +------------- crates/uv/src/commands/project/mod.rs | 4 ++-- 5 files changed, 43 insertions(+), 27 deletions(-) diff --git a/crates/uv-python/src/environment.rs b/crates/uv-python/src/environment.rs index dfc65359a..ce943d703 100644 --- a/crates/uv-python/src/environment.rs +++ b/crates/uv-python/src/environment.rs @@ -10,6 +10,7 @@ use tracing::debug; use uv_cache::Cache; use uv_cache_key::cache_digest; use uv_fs::{LockedFile, Simplified}; +use uv_pep440::Version; use crate::discovery::find_python_installation; use crate::installation::PythonInstallation; @@ -356,12 +357,22 @@ impl PythonEnvironment { } } - /// If this is a virtual environment (indicated by the presence of - /// a `pyvenv.cfg` file), this returns true if the `pyvenv.cfg` version - /// is the same as the interpreter Python version. Also returns true - /// if this is not a virtual environment. - pub fn matches_interpreter(&self, interpreter: &Interpreter) -> bool { - let Ok(cfg) = self.cfg() else { return true }; - cfg.matches_interpreter(interpreter) + /// Check if the `pyvenv.cfg` version is the same as the interpreter's Python version. + /// + /// Returns [`None`] if the versions are the consistent or there is no `pyvenv.cfg`. If the + /// versions do not match, returns a tuple of the `pyvenv.cfg` and interpreter's Python versions + /// for display. + pub fn get_pyvenv_version_conflict(&self) -> Option<(Version, Version)> { + let cfg = self.cfg().ok()?; + let cfg_version = cfg.version?.into_version(); + + // Determine if we should be checking for patch-level equality + let exe_version = if cfg_version.release().get(2).is_none() { + self.interpreter().python_minor_version() + } else { + self.interpreter().python_patch_version() + }; + + (cfg_version != exe_version).then_some((cfg_version, exe_version)) } } diff --git a/crates/uv-python/src/interpreter.rs b/crates/uv-python/src/interpreter.rs index 4e4132b0d..42e3c8db7 100644 --- a/crates/uv-python/src/interpreter.rs +++ b/crates/uv-python/src/interpreter.rs @@ -318,31 +318,37 @@ impl Interpreter { &self.markers.python_full_version().version } - /// Returns the full minor Python version. + /// Returns the Python version up to the minor component. #[inline] pub fn python_minor_version(&self) -> Version { Version::new(self.python_version().release().iter().take(2).copied()) } - /// Return the major version of this Python version. + /// Returns the Python version up to the patch component. + #[inline] + pub fn python_patch_version(&self) -> Version { + Version::new(self.python_version().release().iter().take(3).copied()) + } + + /// Return the major version component of this Python version. pub fn python_major(&self) -> u8 { let major = self.markers.python_full_version().version.release()[0]; u8::try_from(major).expect("invalid major version") } - /// Return the minor version of this Python version. + /// Return the minor version component of this Python version. pub fn python_minor(&self) -> u8 { let minor = self.markers.python_full_version().version.release()[1]; u8::try_from(minor).expect("invalid minor version") } - /// Return the patch version of this Python version. + /// Return the patch version component of this Python version. pub fn python_patch(&self) -> u8 { let minor = self.markers.python_full_version().version.release()[2]; u8::try_from(minor).expect("invalid patch version") } - /// Returns the Python version as a simple tuple. + /// Returns the Python version as a simple tuple, e.g., `(3, 12)`. pub fn python_tuple(&self) -> (u8, u8) { (self.python_major(), self.python_minor()) } diff --git a/crates/uv-python/src/python_version.rs b/crates/uv-python/src/python_version.rs index d71f60cba..30dfccecd 100644 --- a/crates/uv-python/src/python_version.rs +++ b/crates/uv-python/src/python_version.rs @@ -8,6 +8,12 @@ use uv_pep508::{MarkerEnvironment, StringVersion}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct PythonVersion(StringVersion); +impl From for PythonVersion { + fn from(version: StringVersion) -> Self { + Self(version) + } +} + impl Deref for PythonVersion { type Target = StringVersion; @@ -176,6 +182,11 @@ impl PythonVersion { &self.0.version } + /// Return the full parsed Python version. + pub fn into_version(self) -> Version { + self.0.version + } + /// Return the major version of this Python version. pub fn major(&self) -> u8 { u8::try_from(self.0.release().first().copied().unwrap_or(0)).expect("invalid major version") diff --git a/crates/uv-python/src/virtualenv.rs b/crates/uv-python/src/virtualenv.rs index a930b0513..7d72188fc 100644 --- a/crates/uv-python/src/virtualenv.rs +++ b/crates/uv-python/src/virtualenv.rs @@ -11,7 +11,7 @@ use thiserror::Error; use uv_pypi_types::Scheme; use uv_static::EnvVars; -use crate::{Interpreter, PythonVersion}; +use crate::PythonVersion; /// The layout of a virtual environment. #[derive(Debug)] @@ -270,18 +270,6 @@ impl PyVenvConfiguration { self.include_system_site_packages } - /// Returns true if the virtual environment has the same `pyvenv.cfg` version - /// as the interpreter Python version. Also returns true if there is no version. - pub fn matches_interpreter(&self, interpreter: &Interpreter) -> bool { - self.version.as_ref().is_none_or(|version| { - interpreter.python_major() == version.major() - && interpreter.python_minor() == version.minor() - && version - .patch() - .is_none_or(|patch| patch == interpreter.python_patch()) - }) - } - /// Set the key-value pair in the `pyvenv.cfg` file. pub fn set(content: &str, key: &str, value: &str) -> String { let mut lines = content.lines().map(Cow::Borrowed).collect::>(); diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index f522a132d..1677754a2 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -773,8 +773,8 @@ fn environment_is_usable( requires_python: Option<&RequiresPython>, cache: &Cache, ) -> bool { - if !environment.matches_interpreter(environment.interpreter()) { - debug!("The virtual environment's interpreter version does not match the version it was created from."); + if let Some((cfg_version, int_version)) = environment.get_pyvenv_version_conflict() { + debug!("The interpreter in the virtual environment has different version ({int_version}) than it was created with ({cfg_version})"); return false; }