mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-24 13:43:45 +00:00
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
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:
parent
88cd7d619f
commit
278a136bcb
4 changed files with 166 additions and 16 deletions
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<_>>();
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue