Skip to content

Commit 03f4800

Browse files
authored
feat: Decouple FileCache from py_info (#2947)
1 parent d280b76 commit 03f4800

File tree

6 files changed

+99
-107
lines changed

6 files changed

+99
-107
lines changed

docs/changelog/2074a.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Decouple `FileCache` from `py_info` (discovery) - by :user:`esafak`.

src/virtualenv/cache/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from __future__ import annotations
2+
3+
from .cache import Cache
4+
from .file_cache import FileCache
5+
6+
__all__ = [
7+
"Cache",
8+
"FileCache",
9+
]

src/virtualenv/discovery/cache.py renamed to src/virtualenv/cache/cache.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
from __future__ import annotations
22

33
from abc import ABC, abstractmethod
4-
from typing import Any
4+
from typing import Any, Generic, Hashable, TypeVar
55

66
try:
77
from typing import Self # pragma: ≥ 3.11 cover
88
except ImportError:
99
from typing_extensions import Self # pragma: < 3.11 cover
1010

11+
K = TypeVar("K", bound=Hashable)
1112

12-
class Cache(ABC):
13+
14+
class Cache(ABC, Generic[K]):
1315
"""
1416
A generic cache interface.
1517
@@ -18,7 +20,7 @@ class Cache(ABC):
1820
"""
1921

2022
@abstractmethod
21-
def get(self, key: str) -> Any | None:
23+
def get(self, key: K) -> Any | None:
2224
"""
2325
Get a value from the cache.
2426
@@ -28,7 +30,7 @@ def get(self, key: str) -> Any | None:
2830
raise NotImplementedError
2931

3032
@abstractmethod
31-
def set(self, key: str, value: Any) -> None:
33+
def set(self, key: K, value: Any) -> None:
3234
"""
3335
Set a value in the cache.
3436
@@ -38,7 +40,7 @@ def set(self, key: str, value: Any) -> None:
3840
raise NotImplementedError
3941

4042
@abstractmethod
41-
def remove(self, key: str) -> None:
43+
def remove(self, key: K) -> None:
4244
"""
4345
Remove a value from the cache.
4446

src/virtualenv/cache/file_cache.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
5+
from virtualenv.app_data.na import AppDataDisabled
6+
from virtualenv.cache import Cache
7+
8+
if TYPE_CHECKING:
9+
from pathlib import Path
10+
11+
from virtualenv.app_data.base import AppData
12+
13+
14+
class FileCache(Cache):
15+
def __init__(self, app_data: AppData) -> None:
16+
self.app_data = app_data if app_data is not None else AppDataDisabled()
17+
18+
def get(self, key: Path) -> dict | None:
19+
"""Get a value from the file cache."""
20+
py_info, py_info_store = None, self.app_data.py_info(key)
21+
with py_info_store.locked():
22+
if py_info_store.exists():
23+
py_info = py_info_store.read()
24+
return py_info
25+
26+
def set(self, key: Path, value: dict) -> None:
27+
"""Set a value in the file cache."""
28+
py_info_store = self.app_data.py_info(key)
29+
with py_info_store.locked():
30+
py_info_store.write(value)
31+
32+
def remove(self, key: Path) -> None:
33+
"""Remove a value from the file cache."""
34+
py_info_store = self.app_data.py_info(key)
35+
with py_info_store.locked():
36+
if py_info_store.exists():
37+
py_info_store.remove()
38+
39+
def clear(self) -> None:
40+
"""Clear the entire file cache."""
41+
self.app_data.py_info_clear()
42+
43+
44+
__all__ = [
45+
"FileCache",
46+
]

src/virtualenv/discovery/cached_py_info.py

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
from __future__ import annotations
99

10+
import hashlib
11+
import importlib.util
1012
import logging
1113
import os
1214
import random
@@ -19,11 +21,11 @@
1921
from typing import TYPE_CHECKING
2022

2123
from virtualenv.app_data.na import AppDataDisabled
22-
from virtualenv.discovery.file_cache import FileCache
24+
from virtualenv.cache import FileCache
2325

2426
if TYPE_CHECKING:
2527
from virtualenv.app_data.base import AppData
26-
from virtualenv.discovery.cache import Cache
28+
from virtualenv.cache import Cache
2729
from virtualenv.discovery.py_info import PythonInfo
2830
from virtualenv.util.subprocess import subprocess
2931

@@ -69,18 +71,42 @@ def _get_from_cache(cls, app_data: AppData, exe: str, env, cache: Cache, *, igno
6971

7072

7173
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
74+
# 1. get the hash of the probing script
75+
spec = importlib.util.find_spec("virtualenv.discovery.py_info")
76+
script = Path(spec.origin)
77+
try:
78+
py_info_hash = hashlib.sha256(script.read_bytes()).hexdigest()
79+
except OSError:
80+
py_info_hash = None
81+
82+
# 2. get the mtime of the python executable
83+
try:
84+
path_modified = path.stat().st_mtime
85+
except OSError:
86+
path_modified = -1
87+
88+
# 3. check if we have a valid cache entry
89+
py_info = None
90+
data = cache.get(path)
91+
if data is not None:
92+
if data.get("path") == str(path) and data.get("st_mtime") == path_modified and data.get("hash") == py_info_hash:
93+
py_info = cls._from_dict(data.get("content"))
94+
sys_exe = py_info.system_executable
95+
if sys_exe is not None and not os.path.exists(sys_exe):
96+
py_info = None # if system executable is no longer there, this is not valid
97+
if py_info is None:
98+
cache.remove(path) # if cache is invalid, remove it
7999

80100
if py_info is None: # if not loaded run and save
81101
failure, py_info = _run_subprocess(cls, exe, app_data, env)
82102
if failure is None:
83-
cache.set(path, py_info._to_dict()) # noqa: SLF001
103+
data = {
104+
"st_mtime": path_modified,
105+
"path": str(path),
106+
"content": py_info._to_dict(), # noqa: SLF001
107+
"hash": py_info_hash,
108+
}
109+
cache.set(path, data)
84110
else:
85111
py_info = failure
86112
return py_info

src/virtualenv/discovery/file_cache.py

Lines changed: 0 additions & 92 deletions
This file was deleted.

0 commit comments

Comments
 (0)