Skip to content

Commit d280b76

Browse files
CI: Add PyPy 3.11 to CI checks (#2934)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 78ebc61 commit d280b76

File tree

4 files changed

+63
-33
lines changed

4 files changed

+63
-33
lines changed

.github/workflows/check.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ jobs:
2828
- "3.10"
2929
- "3.9"
3030
- "3.8"
31+
- pypy-3.11
3132
- pypy-3.10
3233
- pypy-3.9
3334
- pypy-3.8

docs/changelog/2932.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add PyPy 3.11 support. Contributed by :user:`esafak`.

src/virtualenv/discovery/py_info.py

Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -192,34 +192,57 @@ def _get_tcl_tk_libs():
192192

193193
def _fast_get_system_executable(self):
194194
"""Try to get the system executable by just looking at properties."""
195-
if self.real_prefix or ( # noqa: PLR1702
196-
self.base_prefix is not None and self.base_prefix != self.prefix
197-
): # if this is a virtual environment
198-
if self.real_prefix is None:
199-
base_executable = getattr(sys, "_base_executable", None) # some platforms may set this to help us
200-
if base_executable is not None: # noqa: SIM102 # use the saved system executable if present
201-
if sys.executable != base_executable: # we know we're in a virtual environment, cannot be us
202-
if os.path.exists(base_executable):
203-
return base_executable
204-
# Python may return "python" because it was invoked from the POSIX virtual environment
205-
# however some installs/distributions do not provide a version-less "python" binary in
206-
# the system install location (see PEP 394) so try to fallback to a versioned binary.
207-
#
208-
# Gate this to Python 3.11 as `sys._base_executable` path resolution is now relative to
209-
# the 'home' key from pyvenv.cfg which often points to the system install location.
210-
major, minor = self.version_info.major, self.version_info.minor
211-
if self.os == "posix" and (major, minor) >= (3, 11):
212-
# search relative to the directory of sys._base_executable
213-
base_dir = os.path.dirname(base_executable)
214-
for base_executable in [
215-
os.path.join(base_dir, exe) for exe in (f"python{major}", f"python{major}.{minor}")
216-
]:
217-
if os.path.exists(base_executable):
218-
return base_executable
219-
return None # in this case we just can't tell easily without poking around FS and calling them, bail
220195
# if we're not in a virtual environment, this is already a system python, so return the original executable
221196
# note we must choose the original and not the pure executable as shim scripts might throw us off
222-
return self.original_executable
197+
if not (self.real_prefix or (self.base_prefix is not None and self.base_prefix != self.prefix)):
198+
return self.original_executable
199+
200+
# if this is NOT a virtual environment, can't determine easily, bail out
201+
if self.real_prefix is not None:
202+
return None
203+
204+
base_executable = getattr(sys, "_base_executable", None) # some platforms may set this to help us
205+
if base_executable is None: # use the saved system executable if present
206+
return None
207+
208+
# we know we're in a virtual environment, can not be us
209+
if sys.executable == base_executable:
210+
return None
211+
212+
# We're not in a venv and base_executable exists; use it directly
213+
if os.path.exists(base_executable):
214+
return base_executable
215+
216+
# Try fallback for POSIX virtual environments
217+
return self._try_posix_fallback_executable(base_executable)
218+
219+
def _try_posix_fallback_executable(self, base_executable):
220+
"""
221+
Try to find a versioned Python binary as fallback for POSIX virtual environments.
222+
223+
Python may return "python" because it was invoked from the POSIX virtual environment
224+
however some installs/distributions do not provide a version-less "python" binary in
225+
the system install location (see PEP 394) so try to fallback to a versioned binary.
226+
227+
Gate this to Python 3.11 as `sys._base_executable` path resolution is now relative to
228+
the 'home' key from pyvenv.cfg which often points to the system install location.
229+
"""
230+
major, minor = self.version_info.major, self.version_info.minor
231+
if self.os != "posix" or (major, minor) < (3, 11):
232+
return None
233+
234+
# search relative to the directory of sys._base_executable
235+
base_dir = os.path.dirname(base_executable)
236+
candidates = [f"python{major}", f"python{major}.{minor}"]
237+
if self.implementation == "PyPy":
238+
candidates.extend(["pypy", "pypy3", f"pypy{major}", f"pypy{major}.{minor}"])
239+
240+
for candidate in candidates:
241+
full_path = os.path.join(base_dir, candidate)
242+
if os.path.exists(full_path):
243+
return full_path
244+
245+
return None # in this case we just can't tell easily without poking around FS and calling them, bail
223246

224247
def install_path(self, key):
225248
result = self.distutils_install.get(key)

tests/unit/discovery/py_info/test_py_info.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -162,27 +162,30 @@ def test_py_info_cache_invalidation_on_py_info_change(mocker, session_app_data):
162162
# 2. Spy on _run_subprocess
163163
spy = mocker.spy(cached_py_info, "_run_subprocess")
164164

165-
# 3. Modify the content of py_info.py
165+
# 3. Backup py_info.py
166166
py_info_script = Path(cached_py_info.__file__).parent / "py_info.py"
167167
original_content = py_info_script.read_text(encoding="utf-8")
168168
original_stat = py_info_script.stat()
169169

170170
try:
171171
# 4. Clear the in-memory cache
172172
mocker.patch.dict(cached_py_info._CACHE, {}, clear=True) # noqa: SLF001
173+
174+
# 5. Modify py_info.py to invalidate the cache
173175
py_info_script.write_text(original_content + "\n# a comment", encoding="utf-8")
174176

175-
# 5. Get the PythonInfo object again
177+
# 6. Get the PythonInfo object again
176178
info = PythonInfo.from_exe(sys.executable, session_app_data)
177179

178-
# 6. Assert that _run_subprocess was called again
180+
# 7. Assert that _run_subprocess was called again
181+
native_difference = 1 if info.system_executable == info.executable else 0
179182
if is_macos_brew(info):
180-
assert spy.call_count in {2, 3}
183+
assert spy.call_count + native_difference in {2, 3}
181184
else:
182-
assert spy.call_count == 2
185+
assert spy.call_count + native_difference == 2
183186

184187
finally:
185-
# Restore the original content and timestamp
188+
# 8. Restore the original content and timestamp
186189
py_info_script.write_text(original_content, encoding="utf-8")
187190
os.utime(str(py_info_script), (original_stat.st_atime, original_stat.st_mtime))
188191

@@ -433,7 +436,9 @@ def test_custom_venv_install_scheme_is_prefered(mocker):
433436
assert pyinfo.install_path("purelib").replace(os.sep, "/") == f"lib/python{pyver}/site-packages"
434437

435438

436-
@pytest.mark.skipif(not (os.name == "posix" and sys.version_info[:2] >= (3, 11)), reason="POSIX 3.11+ specific")
439+
@pytest.mark.skipif(
440+
IS_PYPY or not (os.name == "posix" and sys.version_info[:2] >= (3, 11)), reason="POSIX 3.11+ specific"
441+
)
437442
def test_fallback_existent_system_executable(mocker):
438443
current = PythonInfo()
439444
# Posix may execute a "python" out of a venv but try to set the base_executable

0 commit comments

Comments
 (0)