mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-02 18:12:17 +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)
|
.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::borrow::Cow;
|
||||||
|
use std::str::FromStr;
|
||||||
use std::{
|
use std::{
|
||||||
env, io,
|
env, io,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
@ -10,6 +11,8 @@ use thiserror::Error;
|
||||||
use uv_pypi_types::Scheme;
|
use uv_pypi_types::Scheme;
|
||||||
use uv_static::EnvVars;
|
use uv_static::EnvVars;
|
||||||
|
|
||||||
|
use crate::{Interpreter, PythonVersion};
|
||||||
|
|
||||||
/// The layout of a virtual environment.
|
/// The layout of a virtual environment.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct VirtualEnvironment {
|
pub struct VirtualEnvironment {
|
||||||
|
@ -41,6 +44,8 @@ pub struct PyVenvConfiguration {
|
||||||
pub(crate) seed: bool,
|
pub(crate) seed: bool,
|
||||||
/// Should the virtual environment include system site packages?
|
/// Should the virtual environment include system site packages?
|
||||||
pub(crate) include_system_site_packages: bool,
|
pub(crate) include_system_site_packages: bool,
|
||||||
|
/// The Python version the virtual environment was created with
|
||||||
|
pub(crate) version: Option<PythonVersion>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
|
@ -193,6 +198,7 @@ impl PyVenvConfiguration {
|
||||||
let mut relocatable = false;
|
let mut relocatable = false;
|
||||||
let mut seed = false;
|
let mut seed = false;
|
||||||
let mut include_system_site_packages = true;
|
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
|
// 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
|
// 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" => {
|
||||||
include_system_site_packages = value.trim().to_lowercase() == "true";
|
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,
|
relocatable,
|
||||||
seed,
|
seed,
|
||||||
include_system_site_packages,
|
include_system_site_packages,
|
||||||
|
version,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,6 +270,18 @@ impl PyVenvConfiguration {
|
||||||
self.include_system_site_packages
|
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.
|
/// Set the key-value pair in the `pyvenv.cfg` file.
|
||||||
pub fn set(content: &str, key: &str, value: &str) -> String {
|
pub fn set(content: &str, key: &str, value: &str) -> String {
|
||||||
let mut lines = content.lines().map(Cow::Borrowed).collect::<Vec<_>>();
|
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?;
|
} = ScriptPython::from_request(python_request, workspace, script, no_config).await?;
|
||||||
|
|
||||||
let root = Self::root(script, active, cache);
|
let root = Self::root(script, active, cache);
|
||||||
|
|
||||||
match PythonEnvironment::from_root(&root, cache) {
|
match PythonEnvironment::from_root(&root, cache) {
|
||||||
Ok(venv) => {
|
Ok(venv) => {
|
||||||
if python_request.as_ref().is_none_or(|request| {
|
if python_request.as_ref().is_none_or(|request| {
|
||||||
|
@ -804,7 +803,12 @@ impl ProjectInterpreter {
|
||||||
let venv = workspace.venv(active);
|
let venv = workspace.venv(active);
|
||||||
match PythonEnvironment::from_root(&venv, cache) {
|
match PythonEnvironment::from_root(&venv, cache) {
|
||||||
Ok(venv) => {
|
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) {
|
if request.satisfied(venv.interpreter(), cache) {
|
||||||
debug!(
|
debug!(
|
||||||
"The virtual environment's Python version satisfies `{}`",
|
"The virtual environment's Python version satisfies `{}`",
|
||||||
|
@ -818,7 +822,8 @@ impl ProjectInterpreter {
|
||||||
);
|
);
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}) {
|
})
|
||||||
|
{
|
||||||
if let Some(requires_python) = requires_python.as_ref() {
|
if let Some(requires_python) = requires_python.as_ref() {
|
||||||
if requires_python.contains(venv.interpreter().python_version()) {
|
if requires_python.contains(venv.interpreter().python_version()) {
|
||||||
return Ok(Self::Environment(venv));
|
return Ok(Self::Environment(venv));
|
||||||
|
|
|
@ -9268,3 +9268,114 @@ fn sync_build_constraints() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
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