Skip to content

Commit e3d4fed

Browse files
authored
gh-99370: Calculate zip path from prefix when in a venv (GH-99371)
Before python3.11, when in a venv the zip path is calculated from prefix on POSIX platforms. In python3.11 the behavior is accidentally changed to calculating from default prefix. This change will break venv created from a non-installed python with a stdlib zip file. This commit restores the behavior back to before python3.11.
1 parent a3ac923 commit e3d4fed

File tree

4 files changed

+99
-2
lines changed

4 files changed

+99
-2
lines changed

Lib/test/test_getpath.py

+33
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,39 @@ def test_venv_changed_name_posix(self):
382382
actual = getpath(ns, expected)
383383
self.assertEqual(expected, actual)
384384

385+
def test_venv_non_installed_zip_path_posix(self):
386+
"Test a venv created from non-installed python has correct zip path."""
387+
ns = MockPosixNamespace(
388+
argv0="/venv/bin/python",
389+
PREFIX="/usr",
390+
ENV_PATH="/venv/bin:/usr/bin",
391+
)
392+
ns.add_known_xfile("/path/to/non-installed/bin/python")
393+
ns.add_known_xfile("/venv/bin/python")
394+
ns.add_known_link("/venv/bin/python",
395+
"/path/to/non-installed/bin/python")
396+
ns.add_known_file("/path/to/non-installed/lib/python9.8/os.py")
397+
ns.add_known_dir("/path/to/non-installed/lib/python9.8/lib-dynload")
398+
ns.add_known_file("/venv/pyvenv.cfg", [
399+
r"home = /path/to/non-installed"
400+
])
401+
expected = dict(
402+
executable="/venv/bin/python",
403+
prefix="/path/to/non-installed",
404+
exec_prefix="/path/to/non-installed",
405+
base_executable="/path/to/non-installed/bin/python",
406+
base_prefix="/path/to/non-installed",
407+
base_exec_prefix="/path/to/non-installed",
408+
module_search_paths_set=1,
409+
module_search_paths=[
410+
"/path/to/non-installed/lib/python98.zip",
411+
"/path/to/non-installed/lib/python9.8",
412+
"/path/to/non-installed/lib/python9.8/lib-dynload",
413+
],
414+
)
415+
actual = getpath(ns, expected)
416+
self.assertEqual(expected, actual)
417+
385418
def test_venv_changed_name_copy_posix(self):
386419
"Test a venv --copies layout on *nix that lacks a distributed 'python'"
387420
ns = MockPosixNamespace(

Lib/test/test_venv.py

+63
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,69 @@ def test_pathsep_error(self):
537537
self.assertRaises(ValueError, venv.create, bad_itempath)
538538
self.assertRaises(ValueError, venv.create, pathlib.Path(bad_itempath))
539539

540+
@unittest.skipIf(os.name == 'nt', 'not relevant on Windows')
541+
@requireVenvCreate
542+
def test_zippath_from_non_installed_posix(self):
543+
"""
544+
Test that when create venv from non-installed python, the zip path
545+
value is as expected.
546+
"""
547+
rmtree(self.env_dir)
548+
# First try to create a non-installed python. It's not a real full
549+
# functional non-installed python, but enough for this test.
550+
non_installed_dir = os.path.realpath(tempfile.mkdtemp())
551+
try:
552+
bindir = os.path.join(non_installed_dir, self.bindir)
553+
os.mkdir(bindir)
554+
shutil.copy2(sys.executable, bindir)
555+
libdir = os.path.join(non_installed_dir, *self.lib)
556+
os.makedirs(libdir)
557+
landmark = os.path.join(libdir, "os.py")
558+
stdlib_zip = "python%d%d.zip" % sys.version_info[:2]
559+
zip_landmark = os.path.join(non_installed_dir,
560+
self.lib[0],
561+
stdlib_zip)
562+
additional_pythonpath_for_non_installed = []
563+
# Copy stdlib files to the non-installed python so venv can
564+
# correctly calculate the prefix.
565+
for eachpath in sys.path:
566+
if eachpath.endswith(".zip"):
567+
if os.path.isfile(eachpath):
568+
shutil.copyfile(
569+
eachpath,
570+
os.path.join(non_installed_dir, self.lib[0]))
571+
elif os.path.isfile(os.path.join(eachpath, "os.py")):
572+
for name in os.listdir(eachpath):
573+
if name == "site-packages":
574+
continue
575+
fn = os.path.join(eachpath, name)
576+
if os.path.isfile(fn):
577+
shutil.copy(fn, libdir)
578+
elif os.path.isdir(fn):
579+
shutil.copytree(fn, os.path.join(libdir, name))
580+
else:
581+
additional_pythonpath_for_non_installed.append(
582+
eachpath)
583+
cmd = [os.path.join(non_installed_dir, self.bindir, self.exe),
584+
"-m",
585+
"venv",
586+
"--without-pip",
587+
self.env_dir]
588+
# Our fake non-installed python is not fully functional because
589+
# it cannot find the extensions. Set PYTHONPATH so it can run the
590+
# venv module correctly.
591+
pythonpath = os.pathsep.join(
592+
additional_pythonpath_for_non_installed)
593+
subprocess.check_call(cmd, env={"PYTHONPATH": pythonpath})
594+
envpy = os.path.join(self.env_dir, self.bindir, self.exe)
595+
# Now check the venv created from the non-installed python has
596+
# correct zip path in pythonpath.
597+
cmd = [envpy, '-S', '-c', 'import sys; print(sys.path)']
598+
out, err = check_output(cmd)
599+
self.assertTrue(zip_landmark.encode() in out)
600+
finally:
601+
rmtree(non_installed_dir)
602+
540603
@requireVenvCreate
541604
class EnsurePipTest(BaseTest):
542605
"""Test venv module installation of pip."""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix zip path for venv created from a non-installed python on POSIX
2+
platforms.

Modules/getpath.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -679,9 +679,8 @@ def search_up(prefix, *landmarks, test=isfile):
679679
else:
680680
library_dir = executable_dir
681681
pythonpath.append(joinpath(library_dir, ZIP_LANDMARK))
682-
elif build_prefix or venv_prefix:
682+
elif build_prefix:
683683
# QUIRK: POSIX uses the default prefix when in the build directory
684-
# or a venv
685684
pythonpath.append(joinpath(PREFIX, ZIP_LANDMARK))
686685
else:
687686
pythonpath.append(joinpath(prefix, ZIP_LANDMARK))

0 commit comments

Comments
 (0)