Skip to content

Commit 034fca1

Browse files
zaniebastral-automations-bot[bot]
authored andcommitted
Clear stale interpreter cache entries before discovery
1 parent ac4a931 commit 034fca1

2 files changed

Lines changed: 77 additions & 55 deletions

File tree

crates/uv-python/src/interpreter.rs

Lines changed: 74 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize};
1515
use thiserror::Error;
1616
use tracing::{debug, trace, warn};
1717

18-
use uv_cache::{Cache, CacheBucket, CachedByTimestamp, Freshness};
18+
use uv_cache::{Cache, CacheBucket, CacheEntry, CachedByTimestamp, Freshness};
1919
use uv_cache_info::Timestamp;
2020
use uv_cache_key::cache_digest;
2121
use uv_fs::{
@@ -69,17 +69,8 @@ pub struct Interpreter {
6969
impl Interpreter {
7070
/// Detect the interpreter info for the given Python executable.
7171
pub fn query(executable: impl AsRef<Path>, cache: &Cache) -> Result<Self, Error> {
72-
Self::query_with_cache(executable, cache, false)
73-
}
74-
75-
/// Detect interpreter info, optionally refreshing its cached metadata.
76-
pub fn query_with_cache(
77-
executable: impl AsRef<Path>,
78-
cache: &Cache,
79-
refresh: bool,
80-
) -> Result<Self, Error> {
8172
let executable = executable.as_ref();
82-
let info = InterpreterInfo::query_cached(executable, cache, refresh)?;
73+
let info = InterpreterInfo::query_cached(executable, cache)?;
8374

8475
debug_assert!(
8576
info.sys_executable.is_absolute(),
@@ -111,6 +102,19 @@ impl Interpreter {
111102
})
112103
}
113104

105+
/// Remove any cached metadata for the given Python executable.
106+
pub fn clear_cache(executable: impl AsRef<Path>, cache: &Cache) -> Result<(), Error> {
107+
let absolute = std::path::absolute(executable.as_ref())?;
108+
let canonical = canonicalize_executable(&absolute)?;
109+
let cache_entry = InterpreterInfo::cache_entry(&absolute, &canonical, cache);
110+
111+
match fs::remove_file(cache_entry.path()) {
112+
Ok(()) => Ok(()),
113+
Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()),
114+
Err(err) => Err(err.into()),
115+
}
116+
}
117+
114118
/// Return a new [`Interpreter`] with the given virtual environment root.
115119
#[must_use]
116120
pub fn with_virtualenv(self, virtualenv: VirtualEnvironment) -> Self {
@@ -1127,12 +1131,47 @@ impl InterpreterInfo {
11271131
Ok(())
11281132
}
11291133

1134+
/// Return the cache entry for an interpreter's absolute and canonical executable paths.
1135+
fn cache_entry(absolute: &Path, canonical: &Path, cache: &Cache) -> CacheEntry {
1136+
let python_executable = env::var_os(EnvVars::PYTHONEXECUTABLE).map(PathBuf::from);
1137+
let pyvenv_launcher = env::var_os(EnvVars::PYVENV_LAUNCHER).map(PathBuf::from);
1138+
1139+
cache.entry(
1140+
CacheBucket::Interpreter,
1141+
// Shard interpreter metadata by host architecture, operating system, and version, to
1142+
// invalidate the cache (e.g.) on OS upgrades.
1143+
cache_digest(&(
1144+
ARCH,
1145+
uv_platform::OsType::from_env()
1146+
.map(|os_type| os_type.to_string())
1147+
.unwrap_or_default(),
1148+
uv_platform::OsRelease::from_env()
1149+
.map(|os_release| os_release.to_string())
1150+
.unwrap_or_default(),
1151+
)),
1152+
// We use the absolute path for the cache entry to avoid cache collisions for relative
1153+
// paths. But we don't want to query the executable with symbolic links resolved because
1154+
// that can change reported values, e.g., `sys.executable`. We include the canonical
1155+
// path in the cache entry as well, otherwise we can have cache collisions if an
1156+
// absolute path refers to different interpreters with matching ctimes, e.g., if you
1157+
// have a `.venv/bin/python` pointing to both Python 3.12 and Python 3.13 that were
1158+
// modified at the same time.
1159+
//
1160+
// Launcher overrides can also change the reported executable and virtual environment
1161+
// without changing either executable path.
1162+
format!(
1163+
"{}.msgpack",
1164+
cache_digest(&(absolute, canonical, &python_executable, &pyvenv_launcher))
1165+
),
1166+
)
1167+
}
1168+
11301169
/// A wrapper around [`markers::query_interpreter_info`] to cache the computed markers.
11311170
///
11321171
/// Running a Python script is (relatively) expensive, and the markers won't change
11331172
/// unless the Python executable changes, so we use the executable's last modified
11341173
/// time as a cache key.
1135-
fn query_cached(executable: &Path, cache: &Cache, refresh: bool) -> Result<Self, Error> {
1174+
fn query_cached(executable: &Path, cache: &Cache) -> Result<Self, Error> {
11361175
let absolute = std::path::absolute(executable)?;
11371176

11381177
// Provide a better error message if the link is broken or the file does not exist. Since
@@ -1160,47 +1199,16 @@ impl InterpreterInfo {
11601199
};
11611200

11621201
let canonical = canonicalize_executable(&absolute).map_err(handle_io_error)?;
1163-
let python_executable = env::var_os(EnvVars::PYTHONEXECUTABLE).map(PathBuf::from);
1164-
let pyvenv_launcher = env::var_os(EnvVars::PYVENV_LAUNCHER).map(PathBuf::from);
1165-
1166-
let cache_entry = cache.entry(
1167-
CacheBucket::Interpreter,
1168-
// Shard interpreter metadata by host architecture, operating system, and version, to
1169-
// invalidate the cache (e.g.) on OS upgrades.
1170-
cache_digest(&(
1171-
ARCH,
1172-
uv_platform::OsType::from_env()
1173-
.map(|os_type| os_type.to_string())
1174-
.unwrap_or_default(),
1175-
uv_platform::OsRelease::from_env()
1176-
.map(|os_release| os_release.to_string())
1177-
.unwrap_or_default(),
1178-
)),
1179-
// We use the absolute path for the cache entry to avoid cache collisions for relative
1180-
// paths. But we don't want to query the executable with symbolic links resolved because
1181-
// that can change reported values, e.g., `sys.executable`. We include the canonical
1182-
// path in the cache entry as well, otherwise we can have cache collisions if an
1183-
// absolute path refers to different interpreters with matching ctimes, e.g., if you
1184-
// have a `.venv/bin/python` pointing to both Python 3.12 and Python 3.13 that were
1185-
// modified at the same time.
1186-
//
1187-
// Launcher overrides can also change the reported executable and virtual environment
1188-
// without changing either executable path.
1189-
format!(
1190-
"{}.msgpack",
1191-
cache_digest(&(&absolute, &canonical, &python_executable, &pyvenv_launcher))
1192-
),
1193-
);
1202+
let cache_entry = Self::cache_entry(&absolute, &canonical, cache);
11941203

11951204
// We check the timestamp of the canonicalized executable to check if an underlying
11961205
// interpreter has been modified.
11971206
let modified = Timestamp::from_path(canonical).map_err(handle_io_error)?;
11981207

11991208
// Read from the cache.
1200-
if !refresh
1201-
&& cache
1202-
.freshness(&cache_entry, None, None)
1203-
.is_ok_and(Freshness::is_fresh)
1209+
if cache
1210+
.freshness(&cache_entry, None, None)
1211+
.is_ok_and(Freshness::is_fresh)
12041212
{
12051213
if let Ok(data) = fs::read(cache_entry.path()) {
12061214
match rmp_serde::from_slice::<CachedByTimestamp<Self>>(&data) {
@@ -1494,10 +1502,11 @@ mod tests {
14941502
}
14951503

14961504
#[tokio::test]
1497-
async fn test_cache_refresh_with_unchanged_executable() -> Result<()> {
1505+
async fn test_cache_eviction_with_unchanged_executable() -> Result<()> {
14981506
let mock_dir = tempdir()?;
14991507
let mocked_interpreter = mock_dir.path().join("python");
15001508
let response_file = mock_dir.path().join("response.json");
1509+
let query_count = mock_dir.path().join("queries");
15011510

15021511
let mut response = serde_json::from_str::<Value>(mocked_interpreter_response())?;
15031512
response["sys_executable"] = serde_json::to_value(&mocked_interpreter)?;
@@ -1506,8 +1515,9 @@ mod tests {
15061515
&mocked_interpreter,
15071516
formatdoc! {r#"
15081517
#!/bin/sh
1518+
printf '.' >> "{}"
15091519
cat "{}"
1510-
"#, response_file.display()},
1520+
"#, query_count.display(), response_file.display()},
15111521
)?;
15121522
fs::set_permissions(
15131523
&mocked_interpreter,
@@ -1532,15 +1542,27 @@ mod tests {
15321542
&original_version,
15331543
"an unchanged executable should retain its cached interpreter metadata"
15341544
);
1545+
1546+
Interpreter::clear_cache(&mocked_interpreter, &cache)?;
15351547
assert_eq!(
1536-
Interpreter::query_with_cache(&mocked_interpreter, &cache, true)?.python_version(),
1548+
fs::read_to_string(&query_count)?,
1549+
".",
1550+
"clearing cached metadata should not query the interpreter"
1551+
);
1552+
assert_eq!(
1553+
Interpreter::query(&mocked_interpreter, &cache)?.python_version(),
15371554
&updated_version,
1538-
"refreshing should query the interpreter despite its unchanged executable"
1555+
"clearing the cache should force the next query to run the interpreter"
15391556
);
15401557
assert_eq!(
15411558
Interpreter::query(&mocked_interpreter, &cache)?.python_version(),
15421559
&updated_version,
1543-
"refreshing should persist the updated interpreter metadata"
1560+
"the next query should persist the updated interpreter metadata"
1561+
);
1562+
assert_eq!(
1563+
fs::read_to_string(&query_count)?,
1564+
"..",
1565+
"the updated interpreter metadata should be cached again"
15441566
);
15451567

15461568
Ok(())

crates/uv/src/commands/project/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,7 +1148,7 @@ fn discover_project_environment(
11481148
);
11491149

11501150
// Conflicting versions for the same base interpreter indicate its cached metadata may be
1151-
// corrupted. Refresh the entry before interpreter discovery can select stale metadata.
1151+
// corrupted. Clear the entry before interpreter discovery can select stale metadata.
11521152
if matches!(
11531153
&compatibility,
11541154
Err(EnvironmentIncompatibilityError::PyenvVersionConflict(..))
@@ -1158,12 +1158,12 @@ fn discover_project_environment(
11581158
&& environment.interpreter().python_version() != base_interpreter.python_version()
11591159
{
11601160
debug!(
1161-
"Refreshing cached interpreter info for {} after finding conflicting Python versions ({} and {})",
1161+
"Clearing cached interpreter info for {} after finding conflicting Python versions ({} and {})",
11621162
base_executable.user_display(),
11631163
base_interpreter.python_version(),
11641164
environment.interpreter().python_version(),
11651165
);
1166-
Interpreter::query_with_cache(&base_executable, cache, true)?;
1166+
Interpreter::clear_cache(&base_executable, cache)?;
11671167
}
11681168

11691169
match compatibility {

0 commit comments

Comments
 (0)