• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""
2Test harness for the venv module.
3
4Copyright (C) 2011-2012 Vinay Sajip.
5Licensed to the PSF under a contributor agreement.
6"""
7
8import contextlib
9import ensurepip
10import os
11import os.path
12import pathlib
13import re
14import shutil
15import struct
16import subprocess
17import sys
18import sysconfig
19import tempfile
20import shlex
21from test.support import (captured_stdout, captured_stderr,
22                          skip_if_broken_multiprocessing_synchronize, verbose,
23                          requires_subprocess, is_emscripten, is_wasi,
24                          requires_venv_with_pip, TEST_HOME_DIR)
25from test.support.os_helper import (can_symlink, EnvironmentVarGuard, rmtree)
26import unittest
27import venv
28from unittest.mock import patch, Mock
29
30try:
31    import ctypes
32except ImportError:
33    ctypes = None
34
35# Platforms that set sys._base_executable can create venvs from within
36# another venv, so no need to skip tests that require venv.create().
37requireVenvCreate = unittest.skipUnless(
38    sys.prefix == sys.base_prefix
39    or sys._base_executable != sys.executable,
40    'cannot run venv.create from within a venv on this platform')
41
42if is_emscripten or is_wasi:
43    raise unittest.SkipTest("venv is not available on Emscripten/WASI.")
44
45@requires_subprocess()
46def check_output(cmd, encoding=None):
47    p = subprocess.Popen(cmd,
48        stdout=subprocess.PIPE,
49        stderr=subprocess.PIPE,
50        encoding=encoding)
51    out, err = p.communicate()
52    if p.returncode:
53        if verbose and err:
54            print(err.decode('utf-8', 'backslashreplace'))
55        raise subprocess.CalledProcessError(
56            p.returncode, cmd, out, err)
57    return out, err
58
59class BaseTest(unittest.TestCase):
60    """Base class for venv tests."""
61    maxDiff = 80 * 50
62
63    def setUp(self):
64        self.env_dir = os.path.realpath(tempfile.mkdtemp())
65        if os.name == 'nt':
66            self.bindir = 'Scripts'
67            self.lib = ('Lib',)
68            self.include = 'Include'
69        else:
70            self.bindir = 'bin'
71            self.lib = ('lib', 'python%d.%d' % sys.version_info[:2])
72            self.include = 'include'
73        executable = sys._base_executable
74        self.exe = os.path.split(executable)[-1]
75        if (sys.platform == 'win32'
76            and os.path.lexists(executable)
77            and not os.path.exists(executable)):
78            self.cannot_link_exe = True
79        else:
80            self.cannot_link_exe = False
81
82    def tearDown(self):
83        rmtree(self.env_dir)
84
85    def run_with_capture(self, func, *args, **kwargs):
86        with captured_stdout() as output:
87            with captured_stderr() as error:
88                func(*args, **kwargs)
89        return output.getvalue(), error.getvalue()
90
91    def get_env_file(self, *args):
92        return os.path.join(self.env_dir, *args)
93
94    def get_text_file_contents(self, *args, encoding='utf-8'):
95        with open(self.get_env_file(*args), 'r', encoding=encoding) as f:
96            result = f.read()
97        return result
98
99class BasicTest(BaseTest):
100    """Test venv module functionality."""
101
102    def isdir(self, *args):
103        fn = self.get_env_file(*args)
104        self.assertTrue(os.path.isdir(fn))
105
106    def test_defaults_with_str_path(self):
107        """
108        Test the create function with default arguments and a str path.
109        """
110        rmtree(self.env_dir)
111        self.run_with_capture(venv.create, self.env_dir)
112        self._check_output_of_default_create()
113
114    def test_defaults_with_pathlib_path(self):
115        """
116        Test the create function with default arguments and a pathlib.Path path.
117        """
118        rmtree(self.env_dir)
119        self.run_with_capture(venv.create, pathlib.Path(self.env_dir))
120        self._check_output_of_default_create()
121
122    def _check_output_of_default_create(self):
123        self.isdir(self.bindir)
124        self.isdir(self.include)
125        self.isdir(*self.lib)
126        # Issue 21197
127        p = self.get_env_file('lib64')
128        conditions = ((struct.calcsize('P') == 8) and (os.name == 'posix') and
129                      (sys.platform != 'darwin'))
130        if conditions:
131            self.assertTrue(os.path.islink(p))
132        else:
133            self.assertFalse(os.path.exists(p))
134        data = self.get_text_file_contents('pyvenv.cfg')
135        executable = sys._base_executable
136        path = os.path.dirname(executable)
137        self.assertIn('home = %s' % path, data)
138        self.assertIn('executable = %s' %
139                      os.path.realpath(sys.executable), data)
140        copies = '' if os.name=='nt' else ' --copies'
141        cmd = f'command = {sys.executable} -m venv{copies} --without-pip {self.env_dir}'
142        self.assertIn(cmd, data)
143        fn = self.get_env_file(self.bindir, self.exe)
144        if not os.path.exists(fn):  # diagnostics for Windows buildbot failures
145            bd = self.get_env_file(self.bindir)
146            print('Contents of %r:' % bd)
147            print('    %r' % os.listdir(bd))
148        self.assertTrue(os.path.exists(fn), 'File %r should exist.' % fn)
149
150    def test_config_file_command_key(self):
151        attrs = [
152            (None, None),
153            ('symlinks', '--copies'),
154            ('with_pip', '--without-pip'),
155            ('system_site_packages', '--system-site-packages'),
156            ('clear', '--clear'),
157            ('upgrade', '--upgrade'),
158            ('upgrade_deps', '--upgrade-deps'),
159            ('prompt', '--prompt'),
160        ]
161        for attr, opt in attrs:
162            rmtree(self.env_dir)
163            if not attr:
164                b = venv.EnvBuilder()
165            else:
166                b = venv.EnvBuilder(
167                    **{attr: False if attr in ('with_pip', 'symlinks') else True})
168            b.upgrade_dependencies = Mock() # avoid pip command to upgrade deps
169            b._setup_pip = Mock() # avoid pip setup
170            self.run_with_capture(b.create, self.env_dir)
171            data = self.get_text_file_contents('pyvenv.cfg')
172            if not attr:
173                for opt in ('--system-site-packages', '--clear', '--upgrade',
174                        '--upgrade-deps', '--prompt'):
175                    self.assertNotRegex(data, rf'command = .* {opt}')
176            elif os.name=='nt' and attr=='symlinks':
177                pass
178            else:
179                self.assertRegex(data, rf'command = .* {opt}')
180
181    def test_prompt(self):
182        env_name = os.path.split(self.env_dir)[1]
183
184        rmtree(self.env_dir)
185        builder = venv.EnvBuilder()
186        self.run_with_capture(builder.create, self.env_dir)
187        context = builder.ensure_directories(self.env_dir)
188        data = self.get_text_file_contents('pyvenv.cfg')
189        self.assertEqual(context.prompt, '(%s) ' % env_name)
190        self.assertNotIn("prompt = ", data)
191
192        rmtree(self.env_dir)
193        builder = venv.EnvBuilder(prompt='My prompt')
194        self.run_with_capture(builder.create, self.env_dir)
195        context = builder.ensure_directories(self.env_dir)
196        data = self.get_text_file_contents('pyvenv.cfg')
197        self.assertEqual(context.prompt, '(My prompt) ')
198        self.assertIn("prompt = 'My prompt'\n", data)
199
200        rmtree(self.env_dir)
201        builder = venv.EnvBuilder(prompt='.')
202        cwd = os.path.basename(os.getcwd())
203        self.run_with_capture(builder.create, self.env_dir)
204        context = builder.ensure_directories(self.env_dir)
205        data = self.get_text_file_contents('pyvenv.cfg')
206        self.assertEqual(context.prompt, '(%s) ' % cwd)
207        self.assertIn("prompt = '%s'\n" % cwd, data)
208
209    def test_upgrade_dependencies(self):
210        builder = venv.EnvBuilder()
211        bin_path = 'Scripts' if sys.platform == 'win32' else 'bin'
212        python_exe = os.path.split(sys.executable)[1]
213        with tempfile.TemporaryDirectory() as fake_env_dir:
214            expect_exe = os.path.normcase(
215                os.path.join(fake_env_dir, bin_path, python_exe)
216            )
217            if sys.platform == 'win32':
218                expect_exe = os.path.normcase(os.path.realpath(expect_exe))
219
220            def pip_cmd_checker(cmd, **kwargs):
221                cmd[0] = os.path.normcase(cmd[0])
222                self.assertEqual(
223                    cmd,
224                    [
225                        expect_exe,
226                        '-m',
227                        'pip',
228                        'install',
229                        '--upgrade',
230                        'pip',
231                        'setuptools'
232                    ]
233                )
234
235            fake_context = builder.ensure_directories(fake_env_dir)
236            with patch('venv.subprocess.check_output', pip_cmd_checker):
237                builder.upgrade_dependencies(fake_context)
238
239    @requireVenvCreate
240    def test_prefixes(self):
241        """
242        Test that the prefix values are as expected.
243        """
244        # check a venv's prefixes
245        rmtree(self.env_dir)
246        self.run_with_capture(venv.create, self.env_dir)
247        envpy = os.path.join(self.env_dir, self.bindir, self.exe)
248        cmd = [envpy, '-c', None]
249        for prefix, expected in (
250            ('prefix', self.env_dir),
251            ('exec_prefix', self.env_dir),
252            ('base_prefix', sys.base_prefix),
253            ('base_exec_prefix', sys.base_exec_prefix)):
254            cmd[2] = 'import sys; print(sys.%s)' % prefix
255            out, err = check_output(cmd)
256            self.assertEqual(out.strip(), expected.encode(), prefix)
257
258    @requireVenvCreate
259    def test_sysconfig(self):
260        """
261        Test that the sysconfig functions work in a virtual environment.
262        """
263        rmtree(self.env_dir)
264        self.run_with_capture(venv.create, self.env_dir, symlinks=False)
265        envpy = os.path.join(self.env_dir, self.bindir, self.exe)
266        cmd = [envpy, '-c', None]
267        for call, expected in (
268            # installation scheme
269            ('get_preferred_scheme("prefix")', 'venv'),
270            ('get_default_scheme()', 'venv'),
271            # build environment
272            ('is_python_build()', str(sysconfig.is_python_build())),
273            ('get_makefile_filename()', sysconfig.get_makefile_filename()),
274            ('get_config_h_filename()', sysconfig.get_config_h_filename())):
275            with self.subTest(call):
276                cmd[2] = 'import sysconfig; print(sysconfig.%s)' % call
277                out, err = check_output(cmd)
278                self.assertEqual(out.strip(), expected.encode(), err)
279
280    @requireVenvCreate
281    @unittest.skipUnless(can_symlink(), 'Needs symlinks')
282    def test_sysconfig_symlinks(self):
283        """
284        Test that the sysconfig functions work in a virtual environment.
285        """
286        rmtree(self.env_dir)
287        self.run_with_capture(venv.create, self.env_dir, symlinks=True)
288        envpy = os.path.join(self.env_dir, self.bindir, self.exe)
289        cmd = [envpy, '-c', None]
290        for call, expected in (
291            # installation scheme
292            ('get_preferred_scheme("prefix")', 'venv'),
293            ('get_default_scheme()', 'venv'),
294            # build environment
295            ('is_python_build()', str(sysconfig.is_python_build())),
296            ('get_makefile_filename()', sysconfig.get_makefile_filename()),
297            ('get_config_h_filename()', sysconfig.get_config_h_filename())):
298            with self.subTest(call):
299                cmd[2] = 'import sysconfig; print(sysconfig.%s)' % call
300                out, err = check_output(cmd)
301                self.assertEqual(out.strip(), expected.encode(), err)
302
303    if sys.platform == 'win32':
304        ENV_SUBDIRS = (
305            ('Scripts',),
306            ('Include',),
307            ('Lib',),
308            ('Lib', 'site-packages'),
309        )
310    else:
311        ENV_SUBDIRS = (
312            ('bin',),
313            ('include',),
314            ('lib',),
315            ('lib', 'python%d.%d' % sys.version_info[:2]),
316            ('lib', 'python%d.%d' % sys.version_info[:2], 'site-packages'),
317        )
318
319    def create_contents(self, paths, filename):
320        """
321        Create some files in the environment which are unrelated
322        to the virtual environment.
323        """
324        for subdirs in paths:
325            d = os.path.join(self.env_dir, *subdirs)
326            os.mkdir(d)
327            fn = os.path.join(d, filename)
328            with open(fn, 'wb') as f:
329                f.write(b'Still here?')
330
331    def test_overwrite_existing(self):
332        """
333        Test creating environment in an existing directory.
334        """
335        self.create_contents(self.ENV_SUBDIRS, 'foo')
336        venv.create(self.env_dir)
337        for subdirs in self.ENV_SUBDIRS:
338            fn = os.path.join(self.env_dir, *(subdirs + ('foo',)))
339            self.assertTrue(os.path.exists(fn))
340            with open(fn, 'rb') as f:
341                self.assertEqual(f.read(), b'Still here?')
342
343        builder = venv.EnvBuilder(clear=True)
344        builder.create(self.env_dir)
345        for subdirs in self.ENV_SUBDIRS:
346            fn = os.path.join(self.env_dir, *(subdirs + ('foo',)))
347            self.assertFalse(os.path.exists(fn))
348
349    def clear_directory(self, path):
350        for fn in os.listdir(path):
351            fn = os.path.join(path, fn)
352            if os.path.islink(fn) or os.path.isfile(fn):
353                os.remove(fn)
354            elif os.path.isdir(fn):
355                rmtree(fn)
356
357    def test_unoverwritable_fails(self):
358        #create a file clashing with directories in the env dir
359        for paths in self.ENV_SUBDIRS[:3]:
360            fn = os.path.join(self.env_dir, *paths)
361            with open(fn, 'wb') as f:
362                f.write(b'')
363            self.assertRaises((ValueError, OSError), venv.create, self.env_dir)
364            self.clear_directory(self.env_dir)
365
366    def test_upgrade(self):
367        """
368        Test upgrading an existing environment directory.
369        """
370        # See Issue #21643: the loop needs to run twice to ensure
371        # that everything works on the upgrade (the first run just creates
372        # the venv).
373        for upgrade in (False, True):
374            builder = venv.EnvBuilder(upgrade=upgrade)
375            self.run_with_capture(builder.create, self.env_dir)
376            self.isdir(self.bindir)
377            self.isdir(self.include)
378            self.isdir(*self.lib)
379            fn = self.get_env_file(self.bindir, self.exe)
380            if not os.path.exists(fn):
381                # diagnostics for Windows buildbot failures
382                bd = self.get_env_file(self.bindir)
383                print('Contents of %r:' % bd)
384                print('    %r' % os.listdir(bd))
385            self.assertTrue(os.path.exists(fn), 'File %r should exist.' % fn)
386
387    def test_isolation(self):
388        """
389        Test isolation from system site-packages
390        """
391        for ssp, s in ((True, 'true'), (False, 'false')):
392            builder = venv.EnvBuilder(clear=True, system_site_packages=ssp)
393            builder.create(self.env_dir)
394            data = self.get_text_file_contents('pyvenv.cfg')
395            self.assertIn('include-system-site-packages = %s\n' % s, data)
396
397    @unittest.skipUnless(can_symlink(), 'Needs symlinks')
398    def test_symlinking(self):
399        """
400        Test symlinking works as expected
401        """
402        for usl in (False, True):
403            builder = venv.EnvBuilder(clear=True, symlinks=usl)
404            builder.create(self.env_dir)
405            fn = self.get_env_file(self.bindir, self.exe)
406            # Don't test when False, because e.g. 'python' is always
407            # symlinked to 'python3.3' in the env, even when symlinking in
408            # general isn't wanted.
409            if usl:
410                if self.cannot_link_exe:
411                    # Symlinking is skipped when our executable is already a
412                    # special app symlink
413                    self.assertFalse(os.path.islink(fn))
414                else:
415                    self.assertTrue(os.path.islink(fn))
416
417    # If a venv is created from a source build and that venv is used to
418    # run the test, the pyvenv.cfg in the venv created in the test will
419    # point to the venv being used to run the test, and we lose the link
420    # to the source build - so Python can't initialise properly.
421    @requireVenvCreate
422    def test_executable(self):
423        """
424        Test that the sys.executable value is as expected.
425        """
426        rmtree(self.env_dir)
427        self.run_with_capture(venv.create, self.env_dir)
428        envpy = os.path.join(os.path.realpath(self.env_dir),
429                             self.bindir, self.exe)
430        out, err = check_output([envpy, '-c',
431            'import sys; print(sys.executable)'])
432        self.assertEqual(out.strip(), envpy.encode())
433
434    @unittest.skipUnless(can_symlink(), 'Needs symlinks')
435    def test_executable_symlinks(self):
436        """
437        Test that the sys.executable value is as expected.
438        """
439        rmtree(self.env_dir)
440        builder = venv.EnvBuilder(clear=True, symlinks=True)
441        builder.create(self.env_dir)
442        envpy = os.path.join(os.path.realpath(self.env_dir),
443                             self.bindir, self.exe)
444        out, err = check_output([envpy, '-c',
445            'import sys; print(sys.executable)'])
446        self.assertEqual(out.strip(), envpy.encode())
447
448    # gh-124651: test quoted strings
449    @unittest.skipIf(os.name == 'nt', 'contains invalid characters on Windows')
450    def test_special_chars_bash(self):
451        """
452        Test that the template strings are quoted properly (bash)
453        """
454        rmtree(self.env_dir)
455        bash = shutil.which('bash')
456        if bash is None:
457            self.skipTest('bash required for this test')
458        env_name = '"\';&&$e|\'"'
459        env_dir = os.path.join(os.path.realpath(self.env_dir), env_name)
460        builder = venv.EnvBuilder(clear=True)
461        builder.create(env_dir)
462        activate = os.path.join(env_dir, self.bindir, 'activate')
463        test_script = os.path.join(self.env_dir, 'test_special_chars.sh')
464        with open(test_script, "w") as f:
465            f.write(f'source {shlex.quote(activate)}\n'
466                    'python -c \'import sys; print(sys.executable)\'\n'
467                    'python -c \'import os; print(os.environ["VIRTUAL_ENV"])\'\n'
468                    'deactivate\n')
469        out, err = check_output([bash, test_script])
470        lines = out.splitlines()
471        self.assertTrue(env_name.encode() in lines[0])
472        self.assertEndsWith(lines[1], env_name.encode())
473
474    # gh-124651: test quoted strings
475    @unittest.skipIf(os.name == 'nt', 'contains invalid characters on Windows')
476    def test_special_chars_csh(self):
477        """
478        Test that the template strings are quoted properly (csh)
479        """
480        rmtree(self.env_dir)
481        csh = shutil.which('tcsh') or shutil.which('csh')
482        if csh is None:
483            self.skipTest('csh required for this test')
484        env_name = '"\';&&$e|\'"'
485        env_dir = os.path.join(os.path.realpath(self.env_dir), env_name)
486        builder = venv.EnvBuilder(clear=True)
487        builder.create(env_dir)
488        activate = os.path.join(env_dir, self.bindir, 'activate.csh')
489        test_script = os.path.join(self.env_dir, 'test_special_chars.csh')
490        with open(test_script, "w") as f:
491            f.write(f'source {shlex.quote(activate)}\n'
492                    'python -c \'import sys; print(sys.executable)\'\n'
493                    'python -c \'import os; print(os.environ["VIRTUAL_ENV"])\'\n'
494                    'deactivate\n')
495        out, err = check_output([csh, test_script])
496        lines = out.splitlines()
497        self.assertTrue(env_name.encode() in lines[0])
498        self.assertEndsWith(lines[1], env_name.encode())
499
500    # gh-124651: test quoted strings on Windows
501    @unittest.skipUnless(os.name == 'nt', 'only relevant on Windows')
502    def test_special_chars_windows(self):
503        """
504        Test that the template strings are quoted properly on Windows
505        """
506        rmtree(self.env_dir)
507        env_name = "'&&^$e"
508        env_dir = os.path.join(os.path.realpath(self.env_dir), env_name)
509        builder = venv.EnvBuilder(clear=True)
510        builder.create(env_dir)
511        activate = os.path.join(env_dir, self.bindir, 'activate.bat')
512        test_batch = os.path.join(self.env_dir, 'test_special_chars.bat')
513        with open(test_batch, "w") as f:
514            f.write('@echo off\n'
515                    f'"{activate}" & '
516                    f'{self.exe} -c "import sys; print(sys.executable)" & '
517                    f'{self.exe} -c "import os; print(os.environ[\'VIRTUAL_ENV\'])" & '
518                    'deactivate')
519        out, err = check_output([test_batch])
520        lines = out.splitlines()
521        self.assertTrue(env_name.encode() in lines[0])
522        self.assertEndsWith(lines[1], env_name.encode())
523
524    @unittest.skipUnless(os.name == 'nt', 'only relevant on Windows')
525    def test_unicode_in_batch_file(self):
526        """
527        Test handling of Unicode paths
528        """
529        rmtree(self.env_dir)
530        env_dir = os.path.join(os.path.realpath(self.env_dir), 'ϼўТλФЙ')
531        builder = venv.EnvBuilder(clear=True)
532        builder.create(env_dir)
533        activate = os.path.join(env_dir, self.bindir, 'activate.bat')
534        envpy = os.path.join(env_dir, self.bindir, self.exe)
535        out, err = check_output(
536            [activate, '&', self.exe, '-c', 'print(0)'],
537            encoding='oem',
538        )
539        self.assertEqual(out.strip(), '0')
540
541    @requireVenvCreate
542    def test_multiprocessing(self):
543        """
544        Test that the multiprocessing is able to spawn.
545        """
546        # bpo-36342: Instantiation of a Pool object imports the
547        # multiprocessing.synchronize module. Skip the test if this module
548        # cannot be imported.
549        skip_if_broken_multiprocessing_synchronize()
550
551        rmtree(self.env_dir)
552        self.run_with_capture(venv.create, self.env_dir)
553        envpy = os.path.join(os.path.realpath(self.env_dir),
554                             self.bindir, self.exe)
555        out, err = check_output([envpy, '-c',
556            'from multiprocessing import Pool; '
557            'pool = Pool(1); '
558            'print(pool.apply_async("Python".lower).get(3)); '
559            'pool.terminate()'])
560        self.assertEqual(out.strip(), "python".encode())
561
562    @requireVenvCreate
563    def test_multiprocessing_recursion(self):
564        """
565        Test that the multiprocessing is able to spawn itself
566        """
567        skip_if_broken_multiprocessing_synchronize()
568
569        rmtree(self.env_dir)
570        self.run_with_capture(venv.create, self.env_dir)
571        envpy = os.path.join(os.path.realpath(self.env_dir),
572                             self.bindir, self.exe)
573        script = os.path.join(TEST_HOME_DIR, '_test_venv_multiprocessing.py')
574        subprocess.check_call([envpy, script])
575
576    @unittest.skipIf(os.name == 'nt', 'not relevant on Windows')
577    def test_deactivate_with_strict_bash_opts(self):
578        bash = shutil.which("bash")
579        if bash is None:
580            self.skipTest("bash required for this test")
581        rmtree(self.env_dir)
582        builder = venv.EnvBuilder(clear=True)
583        builder.create(self.env_dir)
584        activate = os.path.join(self.env_dir, self.bindir, "activate")
585        test_script = os.path.join(self.env_dir, "test_strict.sh")
586        with open(test_script, "w") as f:
587            f.write("set -euo pipefail\n"
588                    f"source {activate}\n"
589                    "deactivate\n")
590        out, err = check_output([bash, test_script])
591        self.assertEqual(out, "".encode())
592        self.assertEqual(err, "".encode())
593
594
595    @unittest.skipUnless(sys.platform == 'darwin', 'only relevant on macOS')
596    def test_macos_env(self):
597        rmtree(self.env_dir)
598        builder = venv.EnvBuilder()
599        builder.create(self.env_dir)
600
601        envpy = os.path.join(os.path.realpath(self.env_dir),
602                             self.bindir, self.exe)
603        out, err = check_output([envpy, '-c',
604            'import os; print("__PYVENV_LAUNCHER__" in os.environ)'])
605        self.assertEqual(out.strip(), 'False'.encode())
606
607    def test_pathsep_error(self):
608        """
609        Test that venv creation fails when the target directory contains
610        the path separator.
611        """
612        rmtree(self.env_dir)
613        bad_itempath = self.env_dir + os.pathsep
614        self.assertRaises(ValueError, venv.create, bad_itempath)
615        self.assertRaises(ValueError, venv.create, pathlib.Path(bad_itempath))
616
617    @unittest.skipIf(os.name == 'nt', 'not relevant on Windows')
618    @requireVenvCreate
619    def test_zippath_from_non_installed_posix(self):
620        """
621        Test that when create venv from non-installed python, the zip path
622        value is as expected.
623        """
624        rmtree(self.env_dir)
625        # First try to create a non-installed python. It's not a real full
626        # functional non-installed python, but enough for this test.
627        platlibdir = sys.platlibdir
628        non_installed_dir = os.path.realpath(tempfile.mkdtemp())
629        self.addCleanup(rmtree, non_installed_dir)
630        bindir = os.path.join(non_installed_dir, self.bindir)
631        os.mkdir(bindir)
632        shutil.copy2(sys.executable, bindir)
633        libdir = os.path.join(non_installed_dir, platlibdir, self.lib[1])
634        os.makedirs(libdir)
635        landmark = os.path.join(libdir, "os.py")
636        stdlib_zip = "python%d%d.zip" % sys.version_info[:2]
637        zip_landmark = os.path.join(non_installed_dir,
638                                    platlibdir,
639                                    stdlib_zip)
640        additional_pythonpath_for_non_installed = []
641        # Copy stdlib files to the non-installed python so venv can
642        # correctly calculate the prefix.
643        for eachpath in sys.path:
644            if eachpath.endswith(".zip"):
645                if os.path.isfile(eachpath):
646                    shutil.copyfile(
647                        eachpath,
648                        os.path.join(non_installed_dir, platlibdir))
649            elif os.path.isfile(os.path.join(eachpath, "os.py")):
650                for name in os.listdir(eachpath):
651                    if name == "site-packages":
652                        continue
653                    fn = os.path.join(eachpath, name)
654                    if os.path.isfile(fn):
655                        shutil.copy(fn, libdir)
656                    elif os.path.isdir(fn):
657                        shutil.copytree(fn, os.path.join(libdir, name))
658            else:
659                additional_pythonpath_for_non_installed.append(
660                    eachpath)
661        cmd = [os.path.join(non_installed_dir, self.bindir, self.exe),
662               "-m",
663               "venv",
664               "--without-pip",
665               self.env_dir]
666        # Our fake non-installed python is not fully functional because
667        # it cannot find the extensions. Set PYTHONPATH so it can run the
668        # venv module correctly.
669        pythonpath = os.pathsep.join(
670            additional_pythonpath_for_non_installed)
671        # For python built with shared enabled. We need to set
672        # LD_LIBRARY_PATH so the non-installed python can find and link
673        # libpython.so
674        ld_library_path = sysconfig.get_config_var("LIBDIR")
675        if not ld_library_path or sysconfig.is_python_build():
676            ld_library_path = os.path.abspath(os.path.dirname(sys.executable))
677        if sys.platform == 'darwin':
678            ld_library_path_env = "DYLD_LIBRARY_PATH"
679        else:
680            ld_library_path_env = "LD_LIBRARY_PATH"
681        subprocess.check_call(cmd,
682                              env={"PYTHONPATH": pythonpath,
683                                   ld_library_path_env: ld_library_path})
684        envpy = os.path.join(self.env_dir, self.bindir, self.exe)
685        # Now check the venv created from the non-installed python has
686        # correct zip path in pythonpath.
687        cmd = [envpy, '-S', '-c', 'import sys; print(sys.path)']
688        out, err = check_output(cmd)
689        self.assertTrue(zip_landmark.encode() in out)
690
691@requireVenvCreate
692class EnsurePipTest(BaseTest):
693    """Test venv module installation of pip."""
694    def assert_pip_not_installed(self):
695        envpy = os.path.join(os.path.realpath(self.env_dir),
696                             self.bindir, self.exe)
697        out, err = check_output([envpy, '-c',
698            'try:\n import pip\nexcept ImportError:\n print("OK")'])
699        # We force everything to text, so unittest gives the detailed diff
700        # if we get unexpected results
701        err = err.decode("latin-1") # Force to text, prevent decoding errors
702        self.assertEqual(err, "")
703        out = out.decode("latin-1") # Force to text, prevent decoding errors
704        self.assertEqual(out.strip(), "OK")
705
706
707    def test_no_pip_by_default(self):
708        rmtree(self.env_dir)
709        self.run_with_capture(venv.create, self.env_dir)
710        self.assert_pip_not_installed()
711
712    def test_explicit_no_pip(self):
713        rmtree(self.env_dir)
714        self.run_with_capture(venv.create, self.env_dir, with_pip=False)
715        self.assert_pip_not_installed()
716
717    def test_devnull(self):
718        # Fix for issue #20053 uses os.devnull to force a config file to
719        # appear empty. However http://bugs.python.org/issue20541 means
720        # that doesn't currently work properly on Windows. Once that is
721        # fixed, the "win_location" part of test_with_pip should be restored
722        with open(os.devnull, "rb") as f:
723            self.assertEqual(f.read(), b"")
724
725        self.assertTrue(os.path.exists(os.devnull))
726
727    def do_test_with_pip(self, system_site_packages):
728        rmtree(self.env_dir)
729        with EnvironmentVarGuard() as envvars:
730            # pip's cross-version compatibility may trigger deprecation
731            # warnings in current versions of Python. Ensure related
732            # environment settings don't cause venv to fail.
733            envvars["PYTHONWARNINGS"] = "ignore"
734            # ensurepip is different enough from a normal pip invocation
735            # that we want to ensure it ignores the normal pip environment
736            # variable settings. We set PIP_NO_INSTALL here specifically
737            # to check that ensurepip (and hence venv) ignores it.
738            # See http://bugs.python.org/issue19734
739            envvars["PIP_NO_INSTALL"] = "1"
740            # Also check that we ignore the pip configuration file
741            # See http://bugs.python.org/issue20053
742            with tempfile.TemporaryDirectory() as home_dir:
743                envvars["HOME"] = home_dir
744                bad_config = "[global]\nno-install=1"
745                # Write to both config file names on all platforms to reduce
746                # cross-platform variation in test code behaviour
747                win_location = ("pip", "pip.ini")
748                posix_location = (".pip", "pip.conf")
749                # Skips win_location due to http://bugs.python.org/issue20541
750                for dirname, fname in (posix_location,):
751                    dirpath = os.path.join(home_dir, dirname)
752                    os.mkdir(dirpath)
753                    fpath = os.path.join(dirpath, fname)
754                    with open(fpath, 'w') as f:
755                        f.write(bad_config)
756
757                # Actually run the create command with all that unhelpful
758                # config in place to ensure we ignore it
759                with self.nicer_error():
760                    self.run_with_capture(venv.create, self.env_dir,
761                                          system_site_packages=system_site_packages,
762                                          with_pip=True)
763        # Ensure pip is available in the virtual environment
764        envpy = os.path.join(os.path.realpath(self.env_dir), self.bindir, self.exe)
765        # Ignore DeprecationWarning since pip code is not part of Python
766        out, err = check_output([envpy, '-W', 'ignore::DeprecationWarning',
767               '-W', 'ignore::ImportWarning', '-I',
768               '-m', 'pip', '--version'])
769        # We force everything to text, so unittest gives the detailed diff
770        # if we get unexpected results
771        err = err.decode("latin-1") # Force to text, prevent decoding errors
772        self.assertEqual(err, "")
773        out = out.decode("latin-1") # Force to text, prevent decoding errors
774        expected_version = "pip {}".format(ensurepip.version())
775        self.assertEqual(out[:len(expected_version)], expected_version)
776        env_dir = os.fsencode(self.env_dir).decode("latin-1")
777        self.assertIn(env_dir, out)
778
779        # http://bugs.python.org/issue19728
780        # Check the private uninstall command provided for the Windows
781        # installers works (at least in a virtual environment)
782        with EnvironmentVarGuard() as envvars:
783            with self.nicer_error():
784                # It seems ensurepip._uninstall calls subprocesses which do not
785                # inherit the interpreter settings.
786                envvars["PYTHONWARNINGS"] = "ignore"
787                out, err = check_output([envpy,
788                    '-W', 'ignore::DeprecationWarning',
789                    '-W', 'ignore::ImportWarning', '-I',
790                    '-m', 'ensurepip._uninstall'])
791        # We force everything to text, so unittest gives the detailed diff
792        # if we get unexpected results
793        err = err.decode("latin-1") # Force to text, prevent decoding errors
794        # Ignore the warning:
795        #   "The directory '$HOME/.cache/pip/http' or its parent directory
796        #    is not owned by the current user and the cache has been disabled.
797        #    Please check the permissions and owner of that directory. If
798        #    executing pip with sudo, you may want sudo's -H flag."
799        # where $HOME is replaced by the HOME environment variable.
800        err = re.sub("^(WARNING: )?The directory .* or its parent directory "
801                     "is not owned or is not writable by the current user.*$", "",
802                     err, flags=re.MULTILINE)
803        self.assertEqual(err.rstrip(), "")
804        # Being fairly specific regarding the expected behaviour for the
805        # initial bundling phase in Python 3.4. If the output changes in
806        # future pip versions, this test can likely be relaxed further.
807        out = out.decode("latin-1") # Force to text, prevent decoding errors
808        self.assertIn("Successfully uninstalled pip", out)
809        self.assertIn("Successfully uninstalled setuptools", out)
810        # Check pip is now gone from the virtual environment. This only
811        # applies in the system_site_packages=False case, because in the
812        # other case, pip may still be available in the system site-packages
813        if not system_site_packages:
814            self.assert_pip_not_installed()
815
816    @contextlib.contextmanager
817    def nicer_error(self):
818        """
819        Capture output from a failed subprocess for easier debugging.
820
821        The output this handler produces can be a little hard to read,
822        but at least it has all the details.
823        """
824        try:
825            yield
826        except subprocess.CalledProcessError as exc:
827            out = (exc.output or b'').decode(errors="replace")
828            err = (exc.stderr or b'').decode(errors="replace")
829            self.fail(
830                f"{exc}\n\n"
831                f"**Subprocess Output**\n{out}\n\n"
832                f"**Subprocess Error**\n{err}"
833            )
834
835    @requires_venv_with_pip()
836    def test_with_pip(self):
837        self.do_test_with_pip(False)
838        self.do_test_with_pip(True)
839
840
841if __name__ == "__main__":
842    unittest.main()
843