7
7
8
8
from __future__ import annotations
9
9
10
- import hashlib
11
10
import logging
12
11
import os
13
12
import random
17
16
from shlex import quote
18
17
from string import ascii_lowercase , ascii_uppercase , digits
19
18
from subprocess import Popen
19
+ from typing import TYPE_CHECKING
20
20
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
22
27
from virtualenv .discovery .py_info import PythonInfo
23
28
from virtualenv .util .subprocess import subprocess
24
29
27
32
LOGGER = logging .getLogger (__name__ )
28
33
29
34
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 :
31
45
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 )
33
49
if isinstance (result , Exception ):
34
50
if raise_on_error :
35
51
raise result
@@ -38,63 +54,35 @@ def from_exe(cls, app_data, exe, env=None, raise_on_error=True, ignore_cache=Fal
38
54
return result
39
55
40
56
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
42
58
# note here we cannot resolve symlinks, as the symlink may trigger different prefix information if there's a
43
59
# pyenv.cfg somewhere alongside on python3.5+
44
60
exe_path = Path (exe )
45
61
if not ignore_cache and exe_path in _CACHE : # check in the in-memory cache
46
62
result = _CACHE [exe_path ]
47
63
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 )
50
65
# independent if it was from the file or in-memory cache fix the original executable location
51
66
if isinstance (result , PythonInfo ):
52
67
result .executable = exe
53
68
return result
54
69
55
70
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
98
86
return py_info
99
87
100
88
@@ -120,6 +108,8 @@ def _run_subprocess(cls, exe, app_data, env):
120
108
121
109
start_cookie = gen_cookie ()
122
110
end_cookie = gen_cookie ()
111
+ if app_data is None :
112
+ app_data = AppDataDisabled ()
123
113
with app_data .ensure_extracted (py_info_script ) as py_info_script :
124
114
cmd = [exe , str (py_info_script ), start_cookie , end_cookie ]
125
115
# prevent sys.prefix from leaking into the child process - see https://bugs.python.org/issue22490
@@ -182,8 +172,12 @@ def __repr__(self) -> str:
182
172
return cmd_repr
183
173
184
174
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 ()
187
181
_CACHE .clear ()
188
182
189
183
0 commit comments