Ensure virtual environment is compatible with interpreter on sync (#12884)

It was possible that a virtual environment became out of sync with the
interpreter it pointed to (for example, if a symlink was changed to an
updated Python version). In such a case, `pyvenv.cfg` and
`activate_this.py` would no longer be correct. This PR detects when the
`version` (`venv` module) or `version_info` (uv and `virtualenv`) field
in `pyvenv.cfg` is out of sync with the interpreter. In such a case, uv
recreates the virtual environment.

Closes #12461
This commit is contained in:
John Mumm
2025-04-15 12:01:14 +02:00
committed by GitHub
parent 88cd7d619f
commit 278a136bcb
4 changed files with 166 additions and 16 deletions
+9
View File
@@ -355,4 +355,13 @@ impl PythonEnvironment {
.unwrap_or(false)
}
}
/// 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)
}
}
+25
View File
@@ -1,4 +1,5 @@
use std::borrow::Cow;
use std::str::FromStr;
use std::{
env, io,
path::{Path, PathBuf},
@@ -10,6 +11,8 @@ use thiserror::Error;
use uv_pypi_types::Scheme;
use uv_static::EnvVars;
use crate::{Interpreter, PythonVersion};
/// The layout of a virtual environment.
#[derive(Debug)]
pub struct VirtualEnvironment {
@@ -41,6 +44,8 @@ pub struct PyVenvConfiguration {
pub(crate) seed: bool,
/// Should the virtual environment include system site packages?
pub(crate) include_system_site_packages: bool,
/// The Python version the virtual environment was created with
pub(crate) version: Option<PythonVersion>,
}
#[derive(Debug, Error)]
@@ -193,6 +198,7 @@ impl PyVenvConfiguration {
let mut relocatable = false;
let mut seed = false;
let mut include_system_site_packages = true;
let mut version = None;
// Per https://snarky.ca/how-virtual-environments-work/, the `pyvenv.cfg` file is not a
// valid INI file, and is instead expected to be parsed by partitioning each line on the
@@ -219,6 +225,12 @@ impl PyVenvConfiguration {
"include-system-site-packages" => {
include_system_site_packages = value.trim().to_lowercase() == "true";
}
"version" | "version_info" => {
version = Some(
PythonVersion::from_str(value.trim())
.map_err(|e| io::Error::new(std::io::ErrorKind::InvalidData, e))?,
);
}
_ => {}
}
}
@@ -229,6 +241,7 @@ impl PyVenvConfiguration {
relocatable,
seed,
include_system_site_packages,
version,
})
}
@@ -257,6 +270,18 @@ 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::<Vec<_>>();