Skip to content

Commit 31eb8b9

Browse files
fix cache invalidation for PythonInfo (#2925)
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
1 parent ec1c83e commit 31eb8b9

File tree

4 files changed

+48
-2
lines changed

4 files changed

+48
-2
lines changed

docs/changelog/2467.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix cache invalidation for PythonInfo by hashing `py_info.py`.
2+
Contributed by :user:`esafak`.

src/virtualenv/discovery/cached_py_info.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from __future__ import annotations
99

10+
import hashlib
1011
import logging
1112
import os
1213
import random
@@ -58,14 +59,23 @@ def _get_via_file_cache(cls, app_data, path, exe, env):
5859
path_modified = path.stat().st_mtime
5960
except OSError:
6061
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+
6168
if app_data is None:
6269
app_data = AppDataDisabled()
6370
py_info, py_info_store = None, app_data.py_info(path)
6471
with py_info_store.locked():
6572
if py_info_store.exists(): # if exists and matches load
6673
data = py_info_store.read()
67-
of_path, of_st_mtime, of_content = data["path"], data["st_mtime"], data["content"]
68-
if of_path == path_text and of_st_mtime == path_modified:
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:
6979
py_info = cls._from_dict(of_content.copy())
7080
sys_exe = py_info.system_executable
7181
if sys_exe is not None and not os.path.exists(sys_exe):
@@ -80,6 +90,7 @@ def _get_via_file_cache(cls, app_data, path, exe, env):
8090
"st_mtime": path_modified,
8191
"path": path_text,
8292
"content": py_info._to_dict(), # noqa: SLF001
93+
"hash": py_info_hash,
8394
}
8495
py_info_store.write(data)
8596
else:

tests/unit/discovery/py_info/test_py_info.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import pytest
1616

17+
from virtualenv.create.via_global_ref.builtin.cpython.common import is_macos_brew
1718
from virtualenv.discovery import cached_py_info
1819
from virtualenv.discovery.py_info import PythonInfo, VersionInfo
1920
from virtualenv.discovery.py_spec import PythonSpec
@@ -154,6 +155,36 @@ def test_py_info_cache_clear(mocker, session_app_data):
154155
assert spy.call_count >= 2 * count
155156

156157

158+
def test_py_info_cache_invalidation_on_py_info_change(mocker, session_app_data):
159+
# 1. Get a PythonInfo object for the current executable, this will cache it.
160+
PythonInfo.from_exe(sys.executable, session_app_data)
161+
162+
# 2. Spy on _run_subprocess
163+
spy = mocker.spy(cached_py_info, "_run_subprocess")
164+
165+
# 3. Modify the content of py_info.py
166+
py_info_script = Path(cached_py_info.__file__).parent / "py_info.py"
167+
original_content = py_info_script.read_text(encoding="utf-8")
168+
169+
try:
170+
# 4. Clear the in-memory cache
171+
mocker.patch.dict(cached_py_info._CACHE, {}, clear=True) # noqa: SLF001
172+
py_info_script.write_text(original_content + "\n# a comment", encoding="utf-8")
173+
174+
# 5. Get the PythonInfo object again
175+
info = PythonInfo.from_exe(sys.executable, session_app_data)
176+
177+
# 6. Assert that _run_subprocess was called again
178+
if is_macos_brew(info):
179+
assert spy.call_count in {2, 3}
180+
else:
181+
assert spy.call_count == 2
182+
183+
finally:
184+
# Restore the original content
185+
py_info_script.write_text(original_content, encoding="utf-8")
186+
187+
157188
@pytest.mark.skipif(not fs_supports_symlink(), reason="symlink is not supported")
158189
@pytest.mark.xfail(
159190
# https://doc.pypy.org/en/latest/install.html?highlight=symlink#download-a-pre-built-pypy

tests/unit/discovery/test_discovery.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ def test_absolute_path_does_not_exist(tmp_path):
257257
capture_output=True,
258258
text=True,
259259
check=False,
260+
encoding="utf-8",
260261
)
261262

262263
# Check that the command was successful
@@ -283,6 +284,7 @@ def test_absolute_path_does_not_exist_fails(tmp_path):
283284
capture_output=True,
284285
text=True,
285286
check=False,
287+
encoding="utf-8",
286288
)
287289

288290
# Check that the command failed

0 commit comments

Comments
 (0)