Ensure virtual environment is compatible with interpreter on sync (#12884)
Some checks are pending
CI / cargo dev generate-all (push) Blocked by required conditions
CI / cargo shear (push) Waiting to run
CI / cargo test | ubuntu (push) Blocked by required conditions
CI / Determine changes (push) Waiting to run
CI / lint (push) Waiting to run
CI / cargo clippy | ubuntu (push) Blocked by required conditions
CI / cargo clippy | windows (push) Blocked by required conditions
CI / cargo test | macos (push) Blocked by required conditions
CI / cargo test | windows (push) Blocked by required conditions
CI / check windows trampoline | aarch64 (push) Blocked by required conditions
CI / check windows trampoline | i686 (push) Blocked by required conditions
CI / build binary | windows aarch64 (push) Blocked by required conditions
CI / check windows trampoline | x86_64 (push) Blocked by required conditions
CI / test windows trampoline | i686 (push) Blocked by required conditions
CI / test windows trampoline | x86_64 (push) Blocked by required conditions
CI / typos (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / check system | python on macos x86-64 (push) Blocked by required conditions
CI / build binary | linux libc (push) Blocked by required conditions
CI / build binary | linux musl (push) Blocked by required conditions
CI / build binary | macos aarch64 (push) Blocked by required conditions
CI / build binary | macos x86_64 (push) Blocked by required conditions
CI / build binary | windows x86_64 (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / build binary | freebsd (push) Blocked by required conditions
CI / ecosystem test | pydantic/pydantic-core (push) Blocked by required conditions
CI / ecosystem test | prefecthq/prefect (push) Blocked by required conditions
CI / integration test | pypy on ubuntu (push) Blocked by required conditions
CI / ecosystem test | pallets/flask (push) Blocked by required conditions
CI / smoke test | linux (push) Blocked by required conditions
CI / check system | alpine (push) Blocked by required conditions
CI / smoke test | macos (push) Blocked by required conditions
CI / smoke test | windows x86_64 (push) Blocked by required conditions
CI / smoke test | windows aarch64 (push) Blocked by required conditions
CI / integration test | conda on ubuntu (push) Blocked by required conditions
CI / integration test | deadsnakes python3.9 on ubuntu (push) Blocked by required conditions
CI / integration test | free-threaded on linux (push) Blocked by required conditions
CI / integration test | free-threaded on windows (push) Blocked by required conditions
CI / integration test | pypy on windows (push) Blocked by required conditions
CI / integration test | graalpy on ubuntu (push) Blocked by required conditions
CI / integration test | graalpy on windows (push) Blocked by required conditions
CI / integration test | github actions (push) Blocked by required conditions
CI / integration test | free-threaded python on github actions (push) Blocked by required conditions
CI / integration test | determine publish changes (push) Blocked by required conditions
CI / integration test | uv publish (push) Blocked by required conditions
CI / integration test | uv_build (push) Blocked by required conditions
CI / check cache | ubuntu (push) Blocked by required conditions
CI / check cache | macos aarch64 (push) Blocked by required conditions
CI / check system | python on debian (push) Blocked by required conditions
CI / check system | python on fedora (push) Blocked by required conditions
CI / check system | python on ubuntu (push) Blocked by required conditions
CI / check system | python on opensuse (push) Blocked by required conditions
CI / check system | python on rocky linux 8 (push) Blocked by required conditions
CI / check system | python on rocky linux 9 (push) Blocked by required conditions
CI / check system | pypy on ubuntu (push) Blocked by required conditions
CI / check system | pyston (push) Blocked by required conditions
CI / check system | python on macos aarch64 (push) Blocked by required conditions
CI / check system | homebrew python on macos aarch64 (push) Blocked by required conditions
CI / check system | python3.10 on windows x86-64 (push) Blocked by required conditions
CI / check system | python3.10 on windows x86 (push) Blocked by required conditions
CI / check system | python3.13 on windows x86-64 (push) Blocked by required conditions
CI / check system | x86-64 python3.13 on windows aarch64 (push) Blocked by required conditions
CI / check system | windows registry (push) Blocked by required conditions
CI / check system | python3.12 via chocolatey (push) Blocked by required conditions
CI / check system | python3.9 via pyenv (push) Blocked by required conditions
CI / check system | python3.13 (push) Blocked by required conditions
CI / check system | conda3.11 on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.8 on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.11 on linux x86-64 (push) Blocked by required conditions
CI / check system | conda3.8 on linux x86-64 (push) Blocked by required conditions
CI / check system | conda3.11 on windows x86-64 (push) Blocked by required conditions
CI / check system | conda3.8 on windows x86-64 (push) Blocked by required conditions
CI / check system | amazonlinux (push) Blocked by required conditions
CI / check system | embedded python3.10 on windows x86-64 (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions

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
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 166 additions and 16 deletions

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)
}
}

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<_>>();

View file

@ -661,7 +661,6 @@ impl ScriptInterpreter {
} = ScriptPython::from_request(python_request, workspace, script, no_config).await?;
let root = Self::root(script, active, cache);
match PythonEnvironment::from_root(&root, cache) {
Ok(venv) => {
if python_request.as_ref().is_none_or(|request| {
@ -804,7 +803,12 @@ impl ProjectInterpreter {
let venv = workspace.venv(active);
match PythonEnvironment::from_root(&venv, cache) {
Ok(venv) => {
if python_request.as_ref().is_none_or(|request| {
let venv_matches_interpreter = venv.matches_interpreter(venv.interpreter());
if !venv_matches_interpreter {
debug!("The virtual environment's interpreter version does not match the version it was created from.");
}
if venv_matches_interpreter
&& python_request.as_ref().is_none_or(|request| {
if request.satisfied(venv.interpreter(), cache) {
debug!(
"The virtual environment's Python version satisfies `{}`",
@ -818,7 +822,8 @@ impl ProjectInterpreter {
);
false
}
}) {
})
{
if let Some(requires_python) = requires_python.as_ref() {
if requires_python.contains(venv.interpreter().python_version()) {
return Ok(Self::Environment(venv));

View file

@ -9268,3 +9268,114 @@ fn sync_build_constraints() -> Result<()> {
Ok(())
}
// Test that we recreate a virtual environment when `pyvenv.cfg` version
// is incompatible with the interpreter version.
#[test]
fn sync_when_virtual_environment_incompatible_with_interpreter() -> Result<()> {
let context = TestContext::new("3.12");
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 = []
"#,
)?;
// Create a virtual environment at `.venv`.
context
.venv()
.arg(context.venv.as_os_str())
.arg("--python")
.arg("3.12")
.assert()
.success();
// Simulate an incompatible `pyvenv.cfg:version` value created
// by the venv module.
let pyvenv_cfg = context.venv.child("pyvenv.cfg");
let contents = fs_err::read_to_string(&pyvenv_cfg)
.unwrap()
.lines()
.map(|line| {
if line.trim_start().starts_with("version") {
"version = 3.11.0".to_string()
} else {
line.to_string()
}
})
.collect::<Vec<_>>()
.join("\n");
fs_err::write(&pyvenv_cfg, contents)?;
// We should also be able to read from the lockfile.
uv_snapshot!(context.filters(), context.sync(), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Removed virtual environment at: .venv
Creating virtual environment at: .venv
Resolved 1 package in [TIME]
Audited in [TIME]
");
insta::with_settings!({
filters => context.filters(),
}, {
let contents = fs_err::read_to_string(&pyvenv_cfg).unwrap();
let lines: Vec<&str> = contents.split('\n').collect();
assert_snapshot!(lines[3], @r###"
version_info = 3.12.[X]
"###);
});
// Simulate an incompatible `pyvenv.cfg:version_info` value created
// by uv or virtualenv.
let pyvenv_cfg = context.venv.child("pyvenv.cfg");
let contents = fs_err::read_to_string(&pyvenv_cfg)
.unwrap()
.lines()
.map(|line| {
if line.trim_start().starts_with("version") {
"version_info = 3.11.0".to_string()
} else {
line.to_string()
}
})
.collect::<Vec<_>>()
.join("\n");
fs_err::write(&pyvenv_cfg, contents)?;
// We should also be able to read from the lockfile.
uv_snapshot!(context.filters(), context.sync(), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Removed virtual environment at: .venv
Creating virtual environment at: .venv
Resolved 1 package in [TIME]
Audited in [TIME]
");
insta::with_settings!({
filters => context.filters(),
}, {
let contents = fs_err::read_to_string(&pyvenv_cfg).unwrap();
let lines: Vec<&str> = contents.split('\n').collect();
assert_snapshot!(lines[3], @r###"
version_info = 3.12.[X]
"###);
});
Ok(())
}