From f14c28f39766855420dd58d209da4ad847f3030e Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Thu, 20 Sep 2018 13:38:34 -0700 Subject: bpo-34011: Fixes missing venv files and other tests (GH-9458) --- Lib/distutils/tests/test_bdist.py | 3 ++ Lib/distutils/tests/test_bdist_wininst.py | 2 + Lib/test/test_platform.py | 36 +++++++------ Lib/test/test_site.py | 78 +++++++++++++++------------- Lib/test/test_sysconfig.py | 25 ++++++--- Lib/test/test_venv.py | 67 ++++++++++++------------ Lib/venv/__init__.py | 48 +++++++++-------- Tools/msi/distutils.command.bdist_wininst.py | 3 ++ 8 files changed, 148 insertions(+), 114 deletions(-) diff --git a/Lib/distutils/tests/test_bdist.py b/Lib/distutils/tests/test_bdist.py index f762f5d..c80b3ed 100644 --- a/Lib/distutils/tests/test_bdist.py +++ b/Lib/distutils/tests/test_bdist.py @@ -39,6 +39,9 @@ class BuildTestCase(support.TempdirManager, for name in names: subcmd = cmd.get_finalized_command(name) + if getattr(subcmd, '_unsupported', False): + # command is not supported on this build + continue self.assertTrue(subcmd.skip_build, '%s should take --skip-build from bdist' % name) diff --git a/Lib/distutils/tests/test_bdist_wininst.py b/Lib/distutils/tests/test_bdist_wininst.py index 5d17ab1..4c19bba 100644 --- a/Lib/distutils/tests/test_bdist_wininst.py +++ b/Lib/distutils/tests/test_bdist_wininst.py @@ -5,6 +5,8 @@ from test.support import run_unittest from distutils.command.bdist_wininst import bdist_wininst from distutils.tests import support +@unittest.skipIf(getattr(bdist_wininst, '_unsupported', False), + 'bdist_wininst is not supported in this install') class BuildWinInstTestCase(support.TempdirManager, support.LoggingSilencer, unittest.TestCase): diff --git a/Lib/test/test_platform.py b/Lib/test/test_platform.py index fd6da31..c26e193 100644 --- a/Lib/test/test_platform.py +++ b/Lib/test/test_platform.py @@ -3,6 +3,7 @@ import os import platform import subprocess import sys +import sysconfig import tempfile import unittest import warnings @@ -16,29 +17,34 @@ class PlatformTest(unittest.TestCase): @support.skip_unless_symlink def test_architecture_via_symlink(self): # issue3762 # On Windows, the EXE needs to know where pythonXY.dll and *.pyd is at - # so we add the directory to the path and PYTHONPATH. + # so we add the directory to the path, PYTHONHOME and PYTHONPATH. + env = None if sys.platform == "win32": - def restore_environ(old_env): - os.environ.clear() - os.environ.update(old_env) - - self.addCleanup(restore_environ, dict(os.environ)) - - os.environ["Path"] = "{};{}".format( - os.path.dirname(sys.executable), os.environ["Path"]) - os.environ["PYTHONPATH"] = os.path.dirname(sys.executable) - - def get(python): + env = {k.upper(): os.environ[k] for k in os.environ} + env["PATH"] = "{};{}".format( + os.path.dirname(sys.executable), env.get("PATH", "")) + env["PYTHONHOME"] = os.path.dirname(sys.executable) + if sysconfig.is_python_build(True): + env["PYTHONPATH"] = os.path.dirname(os.__file__) + + def get(python, env=None): cmd = [python, '-c', 'import platform; print(platform.architecture())'] - p = subprocess.Popen(cmd, stdout=subprocess.PIPE) - return p.communicate() + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, env=env) + r = p.communicate() + if p.returncode: + print(repr(r[0])) + print(repr(r[1]), file=sys.stderr) + self.fail('unexpected return code: {0} (0x{0:08X})' + .format(p.returncode)) + return r real = os.path.realpath(sys.executable) link = os.path.abspath(support.TESTFN) os.symlink(real, link) try: - self.assertEqual(get(real), get(link)) + self.assertEqual(get(real), get(link, env=env)) finally: os.remove(link) diff --git a/Lib/test/test_site.py b/Lib/test/test_site.py index 742be1e..dc59e59 100644 --- a/Lib/test/test_site.py +++ b/Lib/test/test_site.py @@ -6,7 +6,8 @@ executing have not been removed. """ import unittest import test.support -from test.support import captured_stderr, TESTFN, EnvironmentVarGuard +from test.support import (captured_stderr, TESTFN, EnvironmentVarGuard, + change_cwd) import builtins import os import sys @@ -346,40 +347,47 @@ class ImportSideEffectTests(unittest.TestCase): # __file__ if abs_paths() does not get run. sys and builtins (the # only other modules imported before site.py runs) do not have # __file__ or __cached__ because they are built-in. - parent = os.path.relpath(os.path.dirname(os.__file__)) - env = os.environ.copy() - env['PYTHONPATH'] = parent - code = ('import os, sys', - # use ASCII to avoid locale issues with non-ASCII directories - 'os_file = os.__file__.encode("ascii", "backslashreplace")', - r'sys.stdout.buffer.write(os_file + b"\n")', - 'os_cached = os.__cached__.encode("ascii", "backslashreplace")', - r'sys.stdout.buffer.write(os_cached + b"\n")') - command = '\n'.join(code) - # First, prove that with -S (no 'import site'), the paths are - # relative. - proc = subprocess.Popen([sys.executable, '-S', '-c', command], - env=env, - stdout=subprocess.PIPE) - stdout, stderr = proc.communicate() - - self.assertEqual(proc.returncode, 0) - os__file__, os__cached__ = stdout.splitlines()[:2] - self.assertFalse(os.path.isabs(os__file__)) - self.assertFalse(os.path.isabs(os__cached__)) - # Now, with 'import site', it works. - proc = subprocess.Popen([sys.executable, '-c', command], - env=env, - stdout=subprocess.PIPE) - stdout, stderr = proc.communicate() - self.assertEqual(proc.returncode, 0) - os__file__, os__cached__ = stdout.splitlines()[:2] - self.assertTrue(os.path.isabs(os__file__), - "expected absolute path, got {}" - .format(os__file__.decode('ascii'))) - self.assertTrue(os.path.isabs(os__cached__), - "expected absolute path, got {}" - .format(os__cached__.decode('ascii'))) + try: + parent = os.path.relpath(os.path.dirname(os.__file__)) + cwd = os.getcwd() + except ValueError: + # Failure to get relpath probably means we need to chdir + # to the same drive. + cwd, parent = os.path.split(os.path.dirname(os.__file__)) + with change_cwd(cwd): + env = os.environ.copy() + env['PYTHONPATH'] = parent + code = ('import os, sys', + # use ASCII to avoid locale issues with non-ASCII directories + 'os_file = os.__file__.encode("ascii", "backslashreplace")', + r'sys.stdout.buffer.write(os_file + b"\n")', + 'os_cached = os.__cached__.encode("ascii", "backslashreplace")', + r'sys.stdout.buffer.write(os_cached + b"\n")') + command = '\n'.join(code) + # First, prove that with -S (no 'import site'), the paths are + # relative. + proc = subprocess.Popen([sys.executable, '-S', '-c', command], + env=env, + stdout=subprocess.PIPE) + stdout, stderr = proc.communicate() + + self.assertEqual(proc.returncode, 0) + os__file__, os__cached__ = stdout.splitlines()[:2] + self.assertFalse(os.path.isabs(os__file__)) + self.assertFalse(os.path.isabs(os__cached__)) + # Now, with 'import site', it works. + proc = subprocess.Popen([sys.executable, '-c', command], + env=env, + stdout=subprocess.PIPE) + stdout, stderr = proc.communicate() + self.assertEqual(proc.returncode, 0) + os__file__, os__cached__ = stdout.splitlines()[:2] + self.assertTrue(os.path.isabs(os__file__), + "expected absolute path, got {}" + .format(os__file__.decode('ascii'))) + self.assertTrue(os.path.isabs(os__cached__), + "expected absolute path, got {}" + .format(os__cached__.decode('ascii'))) def test_no_duplicate_paths(self): # No duplicate paths should exist in sys.path diff --git a/Lib/test/test_sysconfig.py b/Lib/test/test_sysconfig.py index 20252be..1b19298 100644 --- a/Lib/test/test_sysconfig.py +++ b/Lib/test/test_sysconfig.py @@ -235,21 +235,34 @@ class TestSysConfig(unittest.TestCase): def test_symlink(self): # On Windows, the EXE needs to know where pythonXY.dll is at so we have # to add the directory to the path. + env = None if sys.platform == "win32": - os.environ["PATH"] = "{};{}".format( - os.path.dirname(sys.executable), os.environ["PATH"]) + env = {k.upper(): os.environ[k] for k in os.environ} + env["PATH"] = "{};{}".format( + os.path.dirname(sys.executable), env.get("PATH", "")) + # Requires PYTHONHOME as well since we locate stdlib from the + # EXE path and not the DLL path (which should be fixed) + env["PYTHONHOME"] = os.path.dirname(sys.executable) + if sysconfig.is_python_build(True): + env["PYTHONPATH"] = os.path.dirname(os.__file__) # Issue 7880 - def get(python): + def get(python, env=None): cmd = [python, '-c', 'import sysconfig; print(sysconfig.get_platform())'] - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, env=os.environ) - return p.communicate() + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, env=env) + out, err = p.communicate() + if p.returncode: + print((out, err)) + self.fail('Non-zero return code {0} (0x{0:08X})' + .format(p.returncode)) + return out, err real = os.path.realpath(sys.executable) link = os.path.abspath(TESTFN) os.symlink(real, link) try: - self.assertEqual(get(real), get(link)) + self.assertEqual(get(real), get(link, env)) finally: unlink(link) diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py index 9cea87e..c86f7a1 100644 --- a/Lib/test/test_venv.py +++ b/Lib/test/test_venv.py @@ -27,6 +27,17 @@ except ImportError: skipInVenv = unittest.skipIf(sys.prefix != sys.base_prefix, 'Test not appropriate in a venv') +def check_output(cmd, encoding=None): + p = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding=encoding) + out, err = p.communicate() + if p.returncode: + raise subprocess.CalledProcessError( + p.returncode, cmd, None, out, err) + return out, err + class BaseTest(unittest.TestCase): """Base class for venv tests.""" maxDiff = 80 * 50 @@ -134,9 +145,7 @@ class BasicTest(BaseTest): ('base_prefix', sys.prefix), ('base_exec_prefix', sys.exec_prefix)): cmd[2] = 'import sys; print(sys.%s)' % prefix - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - out, err = p.communicate() + out, err = check_output(cmd) self.assertEqual(out.strip(), expected.encode()) if sys.platform == 'win32': @@ -259,11 +268,10 @@ class BasicTest(BaseTest): """ rmtree(self.env_dir) self.run_with_capture(venv.create, self.env_dir) - envpy = os.path.join(os.path.realpath(self.env_dir), self.bindir, self.exe) - cmd = [envpy, '-c', 'import sys; print(sys.executable)'] - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - out, err = p.communicate() + envpy = os.path.join(os.path.realpath(self.env_dir), + self.bindir, self.exe) + out, err = check_output([envpy, '-c', + 'import sys; print(sys.executable)']) self.assertEqual(out.strip(), envpy.encode()) @unittest.skipUnless(can_symlink(), 'Needs symlinks') @@ -274,17 +282,16 @@ class BasicTest(BaseTest): rmtree(self.env_dir) builder = venv.EnvBuilder(clear=True, symlinks=True) builder.create(self.env_dir) - envpy = os.path.join(os.path.realpath(self.env_dir), self.bindir, self.exe) - cmd = [envpy, '-c', 'import sys; print(sys.executable)'] - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - out, err = p.communicate() + envpy = os.path.join(os.path.realpath(self.env_dir), + self.bindir, self.exe) + out, err = check_output([envpy, '-c', + 'import sys; print(sys.executable)']) self.assertEqual(out.strip(), envpy.encode()) @unittest.skipUnless(os.name == 'nt', 'only relevant on Windows') def test_unicode_in_batch_file(self): """ - Test isolation from system site-packages + Test handling of Unicode paths """ rmtree(self.env_dir) env_dir = os.path.join(os.path.realpath(self.env_dir), 'ϼўТλФЙ') @@ -292,12 +299,10 @@ class BasicTest(BaseTest): builder.create(env_dir) activate = os.path.join(env_dir, self.bindir, 'activate.bat') envpy = os.path.join(env_dir, self.bindir, self.exe) - cmd = [activate, '&', self.exe, '-c', 'print(0)'] - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, encoding='oem', - shell=True) - out, err = p.communicate() - print(err) + out, err = check_output( + [activate, '&', self.exe, '-c', 'print(0)'], + encoding='oem', + ) self.assertEqual(out.strip(), '0') @skipInVenv @@ -306,11 +311,8 @@ class EnsurePipTest(BaseTest): def assert_pip_not_installed(self): envpy = os.path.join(os.path.realpath(self.env_dir), self.bindir, self.exe) - try_import = 'try:\n import pip\nexcept ImportError:\n print("OK")' - cmd = [envpy, '-c', try_import] - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - out, err = p.communicate() + out, err = check_output([envpy, '-c', + 'try:\n import pip\nexcept ImportError:\n print("OK")']) # We force everything to text, so unittest gives the detailed diff # if we get unexpected results err = err.decode("latin-1") # Force to text, prevent decoding errors @@ -388,11 +390,8 @@ class EnsurePipTest(BaseTest): # Ensure pip is available in the virtual environment envpy = os.path.join(os.path.realpath(self.env_dir), self.bindir, self.exe) # Ignore DeprecationWarning since pip code is not part of Python - cmd = [envpy, '-W', 'ignore::DeprecationWarning', '-I', - '-m', 'pip', '--version'] - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - out, err = p.communicate() + out, err = check_output([envpy, '-W', 'ignore::DeprecationWarning', '-I', + '-m', 'pip', '--version']) # We force everything to text, so unittest gives the detailed diff # if we get unexpected results err = err.decode("latin-1") # Force to text, prevent decoding errors @@ -406,12 +405,10 @@ class EnsurePipTest(BaseTest): # http://bugs.python.org/issue19728 # Check the private uninstall command provided for the Windows # installers works (at least in a virtual environment) - cmd = [envpy, '-W', 'ignore::DeprecationWarning', '-I', - '-m', 'ensurepip._uninstall'] with EnvironmentVarGuard() as envvars: - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - out, err = p.communicate() + out, err = check_output([envpy, + '-W', 'ignore::DeprecationWarning', '-I', + '-m', 'ensurepip._uninstall']) # We force everything to text, so unittest gives the detailed diff # if we get unexpected results err = err.decode("latin-1") # Force to text, prevent decoding errors diff --git a/Lib/venv/__init__.py b/Lib/venv/__init__.py index e0ab241..0434208 100644 --- a/Lib/venv/__init__.py +++ b/Lib/venv/__init__.py @@ -208,11 +208,9 @@ class EnvBuilder: copier(context.env_exe, path, relative_symlinks_ok=True) if not os.path.islink(path): os.chmod(path, 0o755) - elif sysconfig.is_python_build(True): - # See bpo-34011. This copying code should only be needed when a - # venv is created from a source Python build (i.e. not an installed - # Python) - subdir = 'DLLs' + else: + # See bpo-34011. When using a proper install, we should only need to + # copy the top-level of DLLs. include = self.include_binary files = [f for f in os.listdir(dirname) if include(f)] for f in files: @@ -220,24 +218,28 @@ class EnvBuilder: dst = os.path.join(binpath, f) if dst != context.env_exe: # already done, above copier(src, dst) - dirname = os.path.join(dirname, subdir) - if os.path.isdir(dirname): - files = [f for f in os.listdir(dirname) if include(f)] - for f in files: - src = os.path.join(dirname, f) - dst = os.path.join(binpath, f) - copier(src, dst) - # copy init.tcl over - for root, dirs, files in os.walk(context.python_dir): - if 'init.tcl' in files: - tcldir = os.path.basename(root) - tcldir = os.path.join(context.env_dir, 'Lib', tcldir) - if not os.path.exists(tcldir): - os.makedirs(tcldir) - src = os.path.join(root, 'init.tcl') - dst = os.path.join(tcldir, 'init.tcl') - shutil.copyfile(src, dst) - break + + # When creating from a build directory, we continue to copy all files. + if sysconfig.is_python_build(True): + subdir = 'DLLs' + dirname = os.path.join(dirname, subdir) + if os.path.isdir(dirname): + files = [f for f in os.listdir(dirname) if include(f)] + for f in files: + src = os.path.join(dirname, f) + dst = os.path.join(binpath, f) + copier(src, dst) + # copy init.tcl over + for root, dirs, files in os.walk(context.python_dir): + if 'init.tcl' in files: + tcldir = os.path.basename(root) + tcldir = os.path.join(context.env_dir, 'Lib', tcldir) + if not os.path.exists(tcldir): + os.makedirs(tcldir) + src = os.path.join(root, 'init.tcl') + dst = os.path.join(tcldir, 'init.tcl') + shutil.copyfile(src, dst) + break def _setup_pip(self, context): """Installs or upgrades pip in a virtual environment""" diff --git a/Tools/msi/distutils.command.bdist_wininst.py b/Tools/msi/distutils.command.bdist_wininst.py index d586e34..548fdd0 100644 --- a/Tools/msi/distutils.command.bdist_wininst.py +++ b/Tools/msi/distutils.command.bdist_wininst.py @@ -9,6 +9,9 @@ from distutils.errors import DistutilsPlatformError class bdist_wininst(Command): description = "create an executable installer for MS Windows" + # Marker for tests that we have the unsupported bdist_wininst + _unsupported = True + def initialize_options(self): pass -- cgit v0.12