77
88from __future__ import annotations
99
10- import hashlib
1110import logging
1211import os
1312import random
1716from shlex import quote
1817from string import ascii_lowercase , ascii_uppercase , digits
1918from subprocess import Popen
19+ from typing import TYPE_CHECKING
2020
21- from virtualenv .app_data import AppDataDisabled
21+ from virtualenv .app_data .na import AppDataDisabled
22+ from virtualenv .discovery .file_cache import FileCache
23+
24+ if TYPE_CHECKING :
25+ from virtualenv .app_data .base import AppData
26+ from virtualenv .discovery .cache import Cache
2227from virtualenv .discovery .py_info import PythonInfo
2328from virtualenv .util .subprocess import subprocess
2429
2732LOGGER = logging .getLogger (__name__ )
2833
2934
30- def from_exe (cls , app_data , exe , env = None , raise_on_error = True , ignore_cache = False ): # noqa: FBT002, PLR0913
35+ def from_exe ( # noqa: PLR0913
36+ cls ,
37+ app_data ,
38+ exe ,
39+ env = None ,
40+ * ,
41+ raise_on_error = True ,
42+ ignore_cache = False ,
43+ cache : Cache | None = None ,
44+ ) -> PythonInfo | None :
3145 env = os .environ if env is None else env
32- result = _get_from_cache (cls , app_data , exe , env , ignore_cache = ignore_cache )
46+ if cache is None :
47+ cache = FileCache (app_data )
48+ result = _get_from_cache (cls , app_data , exe , env , cache , ignore_cache = ignore_cache )
3349 if isinstance (result , Exception ):
3450 if raise_on_error :
3551 raise result
@@ -38,63 +54,35 @@ def from_exe(cls, app_data, exe, env=None, raise_on_error=True, ignore_cache=Fal
3854 return result
3955
4056
41- def _get_from_cache (cls , app_data , exe , env , ignore_cache = True ) : # noqa: FBT002
57+ def _get_from_cache (cls , app_data : AppData , exe : str , env , cache : Cache , * , ignore_cache : bool ) -> PythonInfo : # noqa: PLR0913
4258 # note here we cannot resolve symlinks, as the symlink may trigger different prefix information if there's a
4359 # pyenv.cfg somewhere alongside on python3.5+
4460 exe_path = Path (exe )
4561 if not ignore_cache and exe_path in _CACHE : # check in the in-memory cache
4662 result = _CACHE [exe_path ]
4763 else : # otherwise go through the app data cache
48- py_info = _get_via_file_cache (cls , app_data , exe_path , exe , env )
49- result = _CACHE [exe_path ] = py_info
64+ result = _CACHE [exe_path ] = _get_via_file_cache (cls , app_data , exe_path , exe , env , cache )
5065 # independent if it was from the file or in-memory cache fix the original executable location
5166 if isinstance (result , PythonInfo ):
5267 result .executable = exe
5368 return result
5469
5570
56- def _get_via_file_cache (cls , app_data , path , exe , env ):
57- path_text = str (path )
58- try :
59- path_modified = path .stat ().st_mtime
60- except OSError :
61- path_modified = - 1
62- py_info_script = Path (os .path .abspath (__file__ )).parent / "py_info.py"
63- try :
64- py_info_hash = hashlib .sha256 (py_info_script .read_bytes ()).hexdigest ()
65- except OSError :
66- py_info_hash = None
67-
68- if app_data is None :
69- app_data = AppDataDisabled ()
70- py_info , py_info_store = None , app_data .py_info (path )
71- with py_info_store .locked ():
72- if py_info_store .exists (): # if exists and matches load
73- data = py_info_store .read ()
74- of_path = data .get ("path" )
75- of_st_mtime = data .get ("st_mtime" )
76- of_content = data .get ("content" )
77- of_hash = data .get ("hash" )
78- if of_path == path_text and of_st_mtime == path_modified and of_hash == py_info_hash :
79- py_info = cls ._from_dict (of_content .copy ())
80- sys_exe = py_info .system_executable
81- if sys_exe is not None and not os .path .exists (sys_exe ):
82- py_info_store .remove ()
83- py_info = None
84- else :
85- py_info_store .remove ()
86- if py_info is None : # if not loaded run and save
87- failure , py_info = _run_subprocess (cls , exe , app_data , env )
88- if failure is None :
89- data = {
90- "st_mtime" : path_modified ,
91- "path" : path_text ,
92- "content" : py_info ._to_dict (), # noqa: SLF001
93- "hash" : py_info_hash ,
94- }
95- py_info_store .write (data )
96- else :
97- py_info = failure
71+ def _get_via_file_cache (cls , app_data : AppData , path : Path , exe : str , env , cache : Cache ) -> PythonInfo : # noqa: PLR0913
72+ py_info = cache .get (path )
73+ if py_info is not None :
74+ py_info = cls ._from_dict (py_info )
75+ sys_exe = py_info .system_executable
76+ if sys_exe is not None and not os .path .exists (sys_exe ):
77+ cache .remove (path )
78+ py_info = None
79+
80+ if py_info is None : # if not loaded run and save
81+ failure , py_info = _run_subprocess (cls , exe , app_data , env )
82+ if failure is None :
83+ cache .set (path , py_info ._to_dict ()) # noqa: SLF001
84+ else :
85+ py_info = failure
9886 return py_info
9987
10088
@@ -120,6 +108,8 @@ def _run_subprocess(cls, exe, app_data, env):
120108
121109 start_cookie = gen_cookie ()
122110 end_cookie = gen_cookie ()
111+ if app_data is None :
112+ app_data = AppDataDisabled ()
123113 with app_data .ensure_extracted (py_info_script ) as py_info_script :
124114 cmd = [exe , str (py_info_script ), start_cookie , end_cookie ]
125115 # prevent sys.prefix from leaking into the child process - see https://bugs.python.org/issue22490
@@ -182,8 +172,12 @@ def __repr__(self) -> str:
182172 return cmd_repr
183173
184174
185- def clear (app_data ):
186- app_data .py_info_clear ()
175+ def clear (app_data = None , cache = None ):
176+ """Clear the cache."""
177+ if cache is None and app_data is not None :
178+ cache = FileCache (app_data )
179+ if cache is not None :
180+ cache .clear ()
187181 _CACHE .clear ()
188182
189183
0 commit comments