@@ -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:: {
@@ -68,17 +68,8 @@ pub struct Interpreter {
6868impl Interpreter {
6969 /// Detect the interpreter info for the given Python executable.
7070 pub fn query ( executable : impl AsRef < Path > , cache : & Cache ) -> Result < Self , Error > {
71- Self :: query_with_cache ( executable, cache, false )
72- }
73-
74- /// Detect interpreter info, optionally refreshing its cached metadata.
75- pub fn query_with_cache (
76- executable : impl AsRef < Path > ,
77- cache : & Cache ,
78- refresh : bool ,
79- ) -> Result < Self , Error > {
8071 let executable = executable. as_ref ( ) ;
81- let info = InterpreterInfo :: query_cached ( executable, cache, refresh ) ?;
72+ let info = InterpreterInfo :: query_cached ( executable, cache) ?;
8273
8374 debug_assert ! (
8475 info. sys_executable. is_absolute( ) ,
@@ -110,6 +101,19 @@ impl Interpreter {
110101 } )
111102 }
112103
104+ /// Remove any cached metadata for the given Python executable.
105+ pub fn clear_cache ( executable : impl AsRef < Path > , cache : & Cache ) -> Result < ( ) , Error > {
106+ let absolute = std:: path:: absolute ( executable. as_ref ( ) ) ?;
107+ let canonical = canonicalize_executable ( & absolute) ?;
108+ let cache_entry = InterpreterInfo :: cache_entry ( & absolute, & canonical, cache) ;
109+
110+ match fs:: remove_file ( cache_entry. path ( ) ) {
111+ Ok ( ( ) ) => Ok ( ( ) ) ,
112+ Err ( err) if err. kind ( ) == io:: ErrorKind :: NotFound => Ok ( ( ) ) ,
113+ Err ( err) => Err ( err. into ( ) ) ,
114+ }
115+ }
116+
113117 /// Return a new [`Interpreter`] with the given virtual environment root.
114118 #[ must_use]
115119 pub fn with_virtualenv ( self , virtualenv : VirtualEnvironment ) -> Self {
@@ -1126,12 +1130,38 @@ impl InterpreterInfo {
11261130 Ok ( ( ) )
11271131 }
11281132
1133+ /// Return the cache entry for an interpreter's absolute and canonical executable paths.
1134+ fn cache_entry ( absolute : & Path , canonical : & Path , cache : & Cache ) -> CacheEntry {
1135+ cache. entry (
1136+ CacheBucket :: Interpreter ,
1137+ // Shard interpreter metadata by host architecture, operating system, and version, to
1138+ // invalidate the cache (e.g.) on OS upgrades.
1139+ cache_digest ( & (
1140+ ARCH ,
1141+ uv_platform:: OsType :: from_env ( )
1142+ . map ( |os_type| os_type. to_string ( ) )
1143+ . unwrap_or_default ( ) ,
1144+ uv_platform:: OsRelease :: from_env ( )
1145+ . map ( |os_release| os_release. to_string ( ) )
1146+ . unwrap_or_default ( ) ,
1147+ ) ) ,
1148+ // We use the absolute path for the cache entry to avoid cache collisions for relative
1149+ // paths. But we don't want to query the executable with symbolic links resolved because
1150+ // that can change reported values, e.g., `sys.executable`. We include the canonical
1151+ // path in the cache entry as well, otherwise we can have cache collisions if an
1152+ // absolute path refers to different interpreters with matching ctimes, e.g., if you
1153+ // have a `.venv/bin/python` pointing to both Python 3.12 and Python 3.13 that were
1154+ // modified at the same time.
1155+ format ! ( "{}.msgpack" , cache_digest( & ( absolute, canonical) ) ) ,
1156+ )
1157+ }
1158+
11291159 /// A wrapper around [`markers::query_interpreter_info`] to cache the computed markers.
11301160 ///
11311161 /// Running a Python script is (relatively) expensive, and the markers won't change
11321162 /// unless the Python executable changes, so we use the executable's last modified
11331163 /// time as a cache key.
1134- fn query_cached ( executable : & Path , cache : & Cache , refresh : bool ) -> Result < Self , Error > {
1164+ fn query_cached ( executable : & Path , cache : & Cache ) -> Result < Self , Error > {
11351165 let absolute = std:: path:: absolute ( executable) ?;
11361166
11371167 // Provide a better error message if the link is broken or the file does not exist. Since
@@ -1159,39 +1189,16 @@ impl InterpreterInfo {
11591189 } ;
11601190
11611191 let canonical = canonicalize_executable ( & absolute) . map_err ( handle_io_error) ?;
1162-
1163- let cache_entry = cache. entry (
1164- CacheBucket :: Interpreter ,
1165- // Shard interpreter metadata by host architecture, operating system, and version, to
1166- // invalidate the cache (e.g.) on OS upgrades.
1167- cache_digest ( & (
1168- ARCH ,
1169- uv_platform:: OsType :: from_env ( )
1170- . map ( |os_type| os_type. to_string ( ) )
1171- . unwrap_or_default ( ) ,
1172- uv_platform:: OsRelease :: from_env ( )
1173- . map ( |os_release| os_release. to_string ( ) )
1174- . unwrap_or_default ( ) ,
1175- ) ) ,
1176- // We use the absolute path for the cache entry to avoid cache collisions for relative
1177- // paths. But we don't want to query the executable with symbolic links resolved because
1178- // that can change reported values, e.g., `sys.executable`. We include the canonical
1179- // path in the cache entry as well, otherwise we can have cache collisions if an
1180- // absolute path refers to different interpreters with matching ctimes, e.g., if you
1181- // have a `.venv/bin/python` pointing to both Python 3.12 and Python 3.13 that were
1182- // modified at the same time.
1183- format ! ( "{}.msgpack" , cache_digest( & ( & absolute, & canonical) ) ) ,
1184- ) ;
1192+ let cache_entry = Self :: cache_entry ( & absolute, & canonical, cache) ;
11851193
11861194 // We check the timestamp of the canonicalized executable to check if an underlying
11871195 // interpreter has been modified.
11881196 let modified = Timestamp :: from_path ( canonical) . map_err ( handle_io_error) ?;
11891197
11901198 // Read from the cache.
1191- if !refresh
1192- && cache
1193- . freshness ( & cache_entry, None , None )
1194- . is_ok_and ( Freshness :: is_fresh)
1199+ if cache
1200+ . freshness ( & cache_entry, None , None )
1201+ . is_ok_and ( Freshness :: is_fresh)
11951202 {
11961203 if let Ok ( data) = fs:: read ( cache_entry. path ( ) ) {
11971204 match rmp_serde:: from_slice :: < CachedByTimestamp < Self > > ( & data) {
@@ -1485,10 +1492,11 @@ mod tests {
14851492 }
14861493
14871494 #[ tokio:: test]
1488- async fn test_cache_refresh_with_unchanged_executable ( ) -> Result < ( ) > {
1495+ async fn test_cache_eviction_with_unchanged_executable ( ) -> Result < ( ) > {
14891496 let mock_dir = tempdir ( ) ?;
14901497 let mocked_interpreter = mock_dir. path ( ) . join ( "python" ) ;
14911498 let response_file = mock_dir. path ( ) . join ( "response.json" ) ;
1499+ let query_count = mock_dir. path ( ) . join ( "queries" ) ;
14921500
14931501 let mut response = serde_json:: from_str :: < Value > ( mocked_interpreter_response ( ) ) ?;
14941502 response[ "sys_executable" ] = serde_json:: to_value ( & mocked_interpreter) ?;
@@ -1497,8 +1505,9 @@ mod tests {
14971505 & mocked_interpreter,
14981506 formatdoc ! { r#"
14991507 #!/bin/sh
1508+ printf '.' >> "{}"
15001509 cat "{}"
1501- "# , response_file. display( ) } ,
1510+ "# , query_count . display ( ) , response_file. display( ) } ,
15021511 ) ?;
15031512 fs:: set_permissions (
15041513 & mocked_interpreter,
@@ -1523,15 +1532,27 @@ mod tests {
15231532 & original_version,
15241533 "an unchanged executable should retain its cached interpreter metadata"
15251534 ) ;
1535+
1536+ Interpreter :: clear_cache ( & mocked_interpreter, & cache) ?;
1537+ assert_eq ! (
1538+ fs:: read_to_string( & query_count) ?,
1539+ "." ,
1540+ "clearing cached metadata should not query the interpreter"
1541+ ) ;
15261542 assert_eq ! (
1527- Interpreter :: query_with_cache ( & mocked_interpreter, & cache, true ) ?. python_version( ) ,
1543+ Interpreter :: query ( & mocked_interpreter, & cache) ?. python_version( ) ,
15281544 & updated_version,
1529- "refreshing should query the interpreter despite its unchanged executable "
1545+ "clearing the cache should force the next query to run the interpreter "
15301546 ) ;
15311547 assert_eq ! (
15321548 Interpreter :: query( & mocked_interpreter, & cache) ?. python_version( ) ,
15331549 & updated_version,
1534- "refreshing should persist the updated interpreter metadata"
1550+ "the next query should persist the updated interpreter metadata"
1551+ ) ;
1552+ assert_eq ! (
1553+ fs:: read_to_string( & query_count) ?,
1554+ ".." ,
1555+ "the updated interpreter metadata should be cached again"
15351556 ) ;
15361557
15371558 Ok ( ( ) )
0 commit comments