@@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize};
1515use thiserror:: Error ;
1616use tracing:: { debug, trace, warn} ;
1717
18- use uv_cache:: { Cache , CacheBucket , CachedByTimestamp , Freshness } ;
18+ use uv_cache:: { Cache , CacheBucket , CacheEntry , CachedByTimestamp , Freshness } ;
1919use uv_cache_info:: Timestamp ;
2020use uv_cache_key:: cache_digest;
2121use uv_fs:: {
@@ -69,17 +69,8 @@ pub struct Interpreter {
6969impl 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 ( ( ) )
0 commit comments