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_android, is_apple_mobile, 24 is_emscripten, is_wasi, 25 requires_venv_with_pip, TEST_HOME_DIR, 26 requires_resource, copy_python_src_ignore) 27from test.support.os_helper import (can_symlink, EnvironmentVarGuard, rmtree, 28 TESTFN, FakePath) 29import unittest 30import venv 31from unittest.mock import patch, Mock 32 33try: 34 import ctypes 35except ImportError: 36 ctypes = None 37 38# Platforms that set sys._base_executable can create venvs from within 39# another venv, so no need to skip tests that require venv.create(). 40requireVenvCreate = unittest.skipUnless( 41 sys.prefix == sys.base_prefix 42 or sys._base_executable != sys.executable, 43 'cannot run venv.create from within a venv on this platform') 44 45if is_android or is_apple_mobile or is_emscripten or is_wasi: 46 raise unittest.SkipTest("venv is not available on this platform") 47 48@requires_subprocess() 49def check_output(cmd, encoding=None): 50 p = subprocess.Popen(cmd, 51 stdout=subprocess.PIPE, 52 stderr=subprocess.PIPE, 53 env={**os.environ, "PYTHONHOME": ""}) 54 out, err = p.communicate() 55 if p.returncode: 56 if verbose and err: 57 print(err.decode(encoding or 'utf-8', 'backslashreplace')) 58 raise subprocess.CalledProcessError( 59 p.returncode, cmd, out, err) 60 if encoding: 61 return ( 62 out.decode(encoding, 'backslashreplace'), 63 err.decode(encoding, 'backslashreplace'), 64 ) 65 return out, err 66 67class BaseTest(unittest.TestCase): 68 """Base class for venv tests.""" 69 maxDiff = 80 * 50 70 71 def setUp(self): 72 self.env_dir = os.path.realpath(tempfile.mkdtemp()) 73 if os.name == 'nt': 74 self.bindir = 'Scripts' 75 self.lib = ('Lib',) 76 self.include = 'Include' 77 else: 78 self.bindir = 'bin' 79 self.lib = ('lib', f'python{sysconfig._get_python_version_abi()}') 80 self.include = 'include' 81 executable = sys._base_executable 82 self.exe = os.path.split(executable)[-1] 83 if (sys.platform == 'win32' 84 and os.path.lexists(executable) 85 and not os.path.exists(executable)): 86 self.cannot_link_exe = True 87 else: 88 self.cannot_link_exe = False 89 90 def tearDown(self): 91 rmtree(self.env_dir) 92 93 def envpy(self, *, real_env_dir=False): 94 if real_env_dir: 95 env_dir = os.path.realpath(self.env_dir) 96 else: 97 env_dir = self.env_dir 98 return os.path.join(env_dir, self.bindir, self.exe) 99 100 def run_with_capture(self, func, *args, **kwargs): 101 with captured_stdout() as output: 102 with captured_stderr() as error: 103 func(*args, **kwargs) 104 return output.getvalue(), error.getvalue() 105 106 def get_env_file(self, *args): 107 return os.path.join(self.env_dir, *args) 108 109 def get_text_file_contents(self, *args, encoding='utf-8'): 110 with open(self.get_env_file(*args), 'r', encoding=encoding) as f: 111 result = f.read() 112 return result 113 114 def assertEndsWith(self, string, tail): 115 if not string.endswith(tail): 116 self.fail(f"String {string!r} does not end with {tail!r}") 117 118class BasicTest(BaseTest): 119 """Test venv module functionality.""" 120 121 def isdir(self, *args): 122 fn = self.get_env_file(*args) 123 self.assertTrue(os.path.isdir(fn)) 124 125 def test_defaults_with_str_path(self): 126 """ 127 Test the create function with default arguments and a str path. 128 """ 129 rmtree(self.env_dir) 130 self.run_with_capture(venv.create, self.env_dir) 131 self._check_output_of_default_create() 132 133 def test_defaults_with_pathlike(self): 134 """ 135 Test the create function with default arguments and a path-like path. 136 """ 137 rmtree(self.env_dir) 138 self.run_with_capture(venv.create, FakePath(self.env_dir)) 139 self._check_output_of_default_create() 140 141 def _check_output_of_default_create(self): 142 self.isdir(self.bindir) 143 self.isdir(self.include) 144 self.isdir(*self.lib) 145 # Issue 21197 146 p = self.get_env_file('lib64') 147 conditions = ((struct.calcsize('P') == 8) and (os.name == 'posix') and 148 (sys.platform != 'darwin')) 149 if conditions: 150 self.assertTrue(os.path.islink(p)) 151 else: 152 self.assertFalse(os.path.exists(p)) 153 data = self.get_text_file_contents('pyvenv.cfg') 154 executable = sys._base_executable 155 path = os.path.dirname(executable) 156 self.assertIn('home = %s' % path, data) 157 self.assertIn('executable = %s' % 158 os.path.realpath(sys.executable), data) 159 copies = '' if os.name=='nt' else ' --copies' 160 cmd = (f'command = {sys.executable} -m venv{copies} --without-pip ' 161 f'--without-scm-ignore-files {self.env_dir}') 162 self.assertIn(cmd, data) 163 fn = self.get_env_file(self.bindir, self.exe) 164 if not os.path.exists(fn): # diagnostics for Windows buildbot failures 165 bd = self.get_env_file(self.bindir) 166 print('Contents of %r:' % bd) 167 print(' %r' % os.listdir(bd)) 168 self.assertTrue(os.path.exists(fn), 'File %r should exist.' % fn) 169 170 def test_config_file_command_key(self): 171 options = [ 172 (None, None, None), # Default case. 173 ('--copies', 'symlinks', False), 174 ('--without-pip', 'with_pip', False), 175 ('--system-site-packages', 'system_site_packages', True), 176 ('--clear', 'clear', True), 177 ('--upgrade', 'upgrade', True), 178 ('--upgrade-deps', 'upgrade_deps', True), 179 ('--prompt="foobar"', 'prompt', 'foobar'), 180 ('--without-scm-ignore-files', 'scm_ignore_files', frozenset()), 181 ] 182 for opt, attr, value in options: 183 with self.subTest(opt=opt, attr=attr, value=value): 184 rmtree(self.env_dir) 185 if not attr: 186 kwargs = {} 187 else: 188 kwargs = {attr: value} 189 b = venv.EnvBuilder(**kwargs) 190 b.upgrade_dependencies = Mock() # avoid pip command to upgrade deps 191 b._setup_pip = Mock() # avoid pip setup 192 self.run_with_capture(b.create, self.env_dir) 193 data = self.get_text_file_contents('pyvenv.cfg') 194 if not attr or opt.endswith('git'): 195 for opt in ('--system-site-packages', '--clear', '--upgrade', 196 '--upgrade-deps', '--prompt'): 197 self.assertNotRegex(data, rf'command = .* {opt}') 198 elif os.name=='nt' and attr=='symlinks': 199 pass 200 else: 201 self.assertRegex(data, rf'command = .* {opt}') 202 203 def test_prompt(self): 204 env_name = os.path.split(self.env_dir)[1] 205 206 rmtree(self.env_dir) 207 builder = venv.EnvBuilder() 208 self.run_with_capture(builder.create, self.env_dir) 209 context = builder.ensure_directories(self.env_dir) 210 data = self.get_text_file_contents('pyvenv.cfg') 211 self.assertEqual(context.prompt, env_name) 212 self.assertNotIn("prompt = ", data) 213 214 rmtree(self.env_dir) 215 builder = venv.EnvBuilder(prompt='My prompt') 216 self.run_with_capture(builder.create, self.env_dir) 217 context = builder.ensure_directories(self.env_dir) 218 data = self.get_text_file_contents('pyvenv.cfg') 219 self.assertEqual(context.prompt, 'My prompt') 220 self.assertIn("prompt = 'My prompt'\n", data) 221 222 rmtree(self.env_dir) 223 builder = venv.EnvBuilder(prompt='.') 224 cwd = os.path.basename(os.getcwd()) 225 self.run_with_capture(builder.create, self.env_dir) 226 context = builder.ensure_directories(self.env_dir) 227 data = self.get_text_file_contents('pyvenv.cfg') 228 self.assertEqual(context.prompt, cwd) 229 self.assertIn("prompt = '%s'\n" % cwd, data) 230 231 def test_upgrade_dependencies(self): 232 builder = venv.EnvBuilder() 233 bin_path = 'bin' 234 python_exe = os.path.split(sys.executable)[1] 235 if sys.platform == 'win32': 236 bin_path = 'Scripts' 237 if os.path.normcase(os.path.splitext(python_exe)[0]).endswith('_d'): 238 python_exe = 'python_d.exe' 239 else: 240 python_exe = 'python.exe' 241 with tempfile.TemporaryDirectory() as fake_env_dir: 242 expect_exe = os.path.normcase( 243 os.path.join(fake_env_dir, bin_path, python_exe) 244 ) 245 if sys.platform == 'win32': 246 expect_exe = os.path.normcase(os.path.realpath(expect_exe)) 247 248 def pip_cmd_checker(cmd, **kwargs): 249 cmd[0] = os.path.normcase(cmd[0]) 250 self.assertEqual( 251 cmd, 252 [ 253 expect_exe, 254 '-m', 255 'pip', 256 'install', 257 '--upgrade', 258 'pip', 259 ] 260 ) 261 262 fake_context = builder.ensure_directories(fake_env_dir) 263 with patch('venv.subprocess.check_output', pip_cmd_checker): 264 builder.upgrade_dependencies(fake_context) 265 266 @requireVenvCreate 267 def test_prefixes(self): 268 """ 269 Test that the prefix values are as expected. 270 """ 271 # check a venv's prefixes 272 rmtree(self.env_dir) 273 self.run_with_capture(venv.create, self.env_dir) 274 cmd = [self.envpy(), '-c', None] 275 for prefix, expected in ( 276 ('prefix', self.env_dir), 277 ('exec_prefix', self.env_dir), 278 ('base_prefix', sys.base_prefix), 279 ('base_exec_prefix', sys.base_exec_prefix)): 280 cmd[2] = 'import sys; print(sys.%s)' % prefix 281 out, err = check_output(cmd) 282 self.assertEqual(pathlib.Path(out.strip().decode()), 283 pathlib.Path(expected), prefix) 284 285 @requireVenvCreate 286 def test_sysconfig(self): 287 """ 288 Test that the sysconfig functions work in a virtual environment. 289 """ 290 rmtree(self.env_dir) 291 self.run_with_capture(venv.create, self.env_dir, symlinks=False) 292 cmd = [self.envpy(), '-c', None] 293 for call, expected in ( 294 # installation scheme 295 ('get_preferred_scheme("prefix")', 'venv'), 296 ('get_default_scheme()', 'venv'), 297 # build environment 298 ('is_python_build()', str(sysconfig.is_python_build())), 299 ('get_makefile_filename()', sysconfig.get_makefile_filename()), 300 ('get_config_h_filename()', sysconfig.get_config_h_filename()), 301 ('get_config_var("Py_GIL_DISABLED")', 302 str(sysconfig.get_config_var("Py_GIL_DISABLED")))): 303 with self.subTest(call): 304 cmd[2] = 'import sysconfig; print(sysconfig.%s)' % call 305 out, err = check_output(cmd, encoding='utf-8') 306 self.assertEqual(out.strip(), expected, err) 307 for attr, expected in ( 308 ('executable', self.envpy()), 309 # Usually compare to sys.executable, but if we're running in our own 310 # venv then we really need to compare to our base executable 311 ('_base_executable', sys._base_executable), 312 ): 313 with self.subTest(attr): 314 cmd[2] = f'import sys; print(sys.{attr})' 315 out, err = check_output(cmd, encoding='utf-8') 316 self.assertEqual(out.strip(), expected, err) 317 318 @requireVenvCreate 319 @unittest.skipUnless(can_symlink(), 'Needs symlinks') 320 def test_sysconfig_symlinks(self): 321 """ 322 Test that the sysconfig functions work in a virtual environment. 323 """ 324 rmtree(self.env_dir) 325 self.run_with_capture(venv.create, self.env_dir, symlinks=True) 326 cmd = [self.envpy(), '-c', None] 327 for call, expected in ( 328 # installation scheme 329 ('get_preferred_scheme("prefix")', 'venv'), 330 ('get_default_scheme()', 'venv'), 331 # build environment 332 ('is_python_build()', str(sysconfig.is_python_build())), 333 ('get_makefile_filename()', sysconfig.get_makefile_filename()), 334 ('get_config_h_filename()', sysconfig.get_config_h_filename()), 335 ('get_config_var("Py_GIL_DISABLED")', 336 str(sysconfig.get_config_var("Py_GIL_DISABLED")))): 337 with self.subTest(call): 338 cmd[2] = 'import sysconfig; print(sysconfig.%s)' % call 339 out, err = check_output(cmd, encoding='utf-8') 340 self.assertEqual(out.strip(), expected, err) 341 for attr, expected in ( 342 ('executable', self.envpy()), 343 # Usually compare to sys.executable, but if we're running in our own 344 # venv then we really need to compare to our base executable 345 # HACK: Test fails on POSIX with unversioned binary (PR gh-113033) 346 #('_base_executable', sys._base_executable), 347 ): 348 with self.subTest(attr): 349 cmd[2] = f'import sys; print(sys.{attr})' 350 out, err = check_output(cmd, encoding='utf-8') 351 self.assertEqual(out.strip(), expected, err) 352 353 if sys.platform == 'win32': 354 ENV_SUBDIRS = ( 355 ('Scripts',), 356 ('Include',), 357 ('Lib',), 358 ('Lib', 'site-packages'), 359 ) 360 else: 361 ENV_SUBDIRS = ( 362 ('bin',), 363 ('include',), 364 ('lib',), 365 ('lib', 'python%d.%d' % sys.version_info[:2]), 366 ('lib', 'python%d.%d' % sys.version_info[:2], 'site-packages'), 367 ) 368 369 def create_contents(self, paths, filename): 370 """ 371 Create some files in the environment which are unrelated 372 to the virtual environment. 373 """ 374 for subdirs in paths: 375 d = os.path.join(self.env_dir, *subdirs) 376 os.mkdir(d) 377 fn = os.path.join(d, filename) 378 with open(fn, 'wb') as f: 379 f.write(b'Still here?') 380 381 def test_overwrite_existing(self): 382 """ 383 Test creating environment in an existing directory. 384 """ 385 self.create_contents(self.ENV_SUBDIRS, 'foo') 386 venv.create(self.env_dir) 387 for subdirs in self.ENV_SUBDIRS: 388 fn = os.path.join(self.env_dir, *(subdirs + ('foo',))) 389 self.assertTrue(os.path.exists(fn)) 390 with open(fn, 'rb') as f: 391 self.assertEqual(f.read(), b'Still here?') 392 393 builder = venv.EnvBuilder(clear=True) 394 builder.create(self.env_dir) 395 for subdirs in self.ENV_SUBDIRS: 396 fn = os.path.join(self.env_dir, *(subdirs + ('foo',))) 397 self.assertFalse(os.path.exists(fn)) 398 399 def clear_directory(self, path): 400 for fn in os.listdir(path): 401 fn = os.path.join(path, fn) 402 if os.path.islink(fn) or os.path.isfile(fn): 403 os.remove(fn) 404 elif os.path.isdir(fn): 405 rmtree(fn) 406 407 def test_unoverwritable_fails(self): 408 #create a file clashing with directories in the env dir 409 for paths in self.ENV_SUBDIRS[:3]: 410 fn = os.path.join(self.env_dir, *paths) 411 with open(fn, 'wb') as f: 412 f.write(b'') 413 self.assertRaises((ValueError, OSError), venv.create, self.env_dir) 414 self.clear_directory(self.env_dir) 415 416 def test_upgrade(self): 417 """ 418 Test upgrading an existing environment directory. 419 """ 420 # See Issue #21643: the loop needs to run twice to ensure 421 # that everything works on the upgrade (the first run just creates 422 # the venv). 423 for upgrade in (False, True): 424 builder = venv.EnvBuilder(upgrade=upgrade) 425 self.run_with_capture(builder.create, self.env_dir) 426 self.isdir(self.bindir) 427 self.isdir(self.include) 428 self.isdir(*self.lib) 429 fn = self.get_env_file(self.bindir, self.exe) 430 if not os.path.exists(fn): 431 # diagnostics for Windows buildbot failures 432 bd = self.get_env_file(self.bindir) 433 print('Contents of %r:' % bd) 434 print(' %r' % os.listdir(bd)) 435 self.assertTrue(os.path.exists(fn), 'File %r should exist.' % fn) 436 437 def test_isolation(self): 438 """ 439 Test isolation from system site-packages 440 """ 441 for ssp, s in ((True, 'true'), (False, 'false')): 442 builder = venv.EnvBuilder(clear=True, system_site_packages=ssp) 443 builder.create(self.env_dir) 444 data = self.get_text_file_contents('pyvenv.cfg') 445 self.assertIn('include-system-site-packages = %s\n' % s, data) 446 447 @unittest.skipUnless(can_symlink(), 'Needs symlinks') 448 def test_symlinking(self): 449 """ 450 Test symlinking works as expected 451 """ 452 for usl in (False, True): 453 builder = venv.EnvBuilder(clear=True, symlinks=usl) 454 builder.create(self.env_dir) 455 fn = self.get_env_file(self.bindir, self.exe) 456 # Don't test when False, because e.g. 'python' is always 457 # symlinked to 'python3.3' in the env, even when symlinking in 458 # general isn't wanted. 459 if usl: 460 if self.cannot_link_exe: 461 # Symlinking is skipped when our executable is already a 462 # special app symlink 463 self.assertFalse(os.path.islink(fn)) 464 else: 465 self.assertTrue(os.path.islink(fn)) 466 467 # If a venv is created from a source build and that venv is used to 468 # run the test, the pyvenv.cfg in the venv created in the test will 469 # point to the venv being used to run the test, and we lose the link 470 # to the source build - so Python can't initialise properly. 471 @requireVenvCreate 472 def test_executable(self): 473 """ 474 Test that the sys.executable value is as expected. 475 """ 476 rmtree(self.env_dir) 477 self.run_with_capture(venv.create, self.env_dir) 478 envpy = self.envpy(real_env_dir=True) 479 out, err = check_output([envpy, '-c', 480 'import sys; print(sys.executable)']) 481 self.assertEqual(out.strip(), envpy.encode()) 482 483 @unittest.skipUnless(can_symlink(), 'Needs symlinks') 484 def test_executable_symlinks(self): 485 """ 486 Test that the sys.executable value is as expected. 487 """ 488 rmtree(self.env_dir) 489 builder = venv.EnvBuilder(clear=True, symlinks=True) 490 builder.create(self.env_dir) 491 envpy = self.envpy(real_env_dir=True) 492 out, err = check_output([envpy, '-c', 493 'import sys; print(sys.executable)']) 494 self.assertEqual(out.strip(), envpy.encode()) 495 496 # gh-124651: test quoted strings 497 @unittest.skipIf(os.name == 'nt', 'contains invalid characters on Windows') 498 def test_special_chars_bash(self): 499 """ 500 Test that the template strings are quoted properly (bash) 501 """ 502 rmtree(self.env_dir) 503 bash = shutil.which('bash') 504 if bash is None: 505 self.skipTest('bash required for this test') 506 env_name = '"\';&&$e|\'"' 507 env_dir = os.path.join(os.path.realpath(self.env_dir), env_name) 508 builder = venv.EnvBuilder(clear=True) 509 builder.create(env_dir) 510 activate = os.path.join(env_dir, self.bindir, 'activate') 511 test_script = os.path.join(self.env_dir, 'test_special_chars.sh') 512 with open(test_script, "w") as f: 513 f.write(f'source {shlex.quote(activate)}\n' 514 'python -c \'import sys; print(sys.executable)\'\n' 515 'python -c \'import os; print(os.environ["VIRTUAL_ENV"])\'\n' 516 'deactivate\n') 517 out, err = check_output([bash, test_script]) 518 lines = out.splitlines() 519 self.assertTrue(env_name.encode() in lines[0]) 520 self.assertEndsWith(lines[1], env_name.encode()) 521 522 # gh-124651: test quoted strings 523 @unittest.skipIf(os.name == 'nt', 'contains invalid characters on Windows') 524 def test_special_chars_csh(self): 525 """ 526 Test that the template strings are quoted properly (csh) 527 """ 528 rmtree(self.env_dir) 529 csh = shutil.which('tcsh') or shutil.which('csh') 530 if csh is None: 531 self.skipTest('csh required for this test') 532 env_name = '"\';&&$e|\'"' 533 env_dir = os.path.join(os.path.realpath(self.env_dir), env_name) 534 builder = venv.EnvBuilder(clear=True) 535 builder.create(env_dir) 536 activate = os.path.join(env_dir, self.bindir, 'activate.csh') 537 test_script = os.path.join(self.env_dir, 'test_special_chars.csh') 538 with open(test_script, "w") as f: 539 f.write(f'source {shlex.quote(activate)}\n' 540 'python -c \'import sys; print(sys.executable)\'\n' 541 'python -c \'import os; print(os.environ["VIRTUAL_ENV"])\'\n' 542 'deactivate\n') 543 out, err = check_output([csh, test_script]) 544 lines = out.splitlines() 545 self.assertTrue(env_name.encode() in lines[0]) 546 self.assertEndsWith(lines[1], env_name.encode()) 547 548 # gh-124651: test quoted strings on Windows 549 @unittest.skipUnless(os.name == 'nt', 'only relevant on Windows') 550 def test_special_chars_windows(self): 551 """ 552 Test that the template strings are quoted properly on Windows 553 """ 554 rmtree(self.env_dir) 555 env_name = "'&&^$e" 556 env_dir = os.path.join(os.path.realpath(self.env_dir), env_name) 557 builder = venv.EnvBuilder(clear=True) 558 builder.create(env_dir) 559 activate = os.path.join(env_dir, self.bindir, 'activate.bat') 560 test_batch = os.path.join(self.env_dir, 'test_special_chars.bat') 561 with open(test_batch, "w") as f: 562 f.write('@echo off\n' 563 f'"{activate}" & ' 564 f'{self.exe} -c "import sys; print(sys.executable)" & ' 565 f'{self.exe} -c "import os; print(os.environ[\'VIRTUAL_ENV\'])" & ' 566 'deactivate') 567 out, err = check_output([test_batch]) 568 lines = out.splitlines() 569 self.assertTrue(env_name.encode() in lines[0]) 570 self.assertEndsWith(lines[1], env_name.encode()) 571 572 @unittest.skipUnless(os.name == 'nt', 'only relevant on Windows') 573 def test_unicode_in_batch_file(self): 574 """ 575 Test handling of Unicode paths 576 """ 577 rmtree(self.env_dir) 578 env_dir = os.path.join(os.path.realpath(self.env_dir), 'ϼўТλФЙ') 579 builder = venv.EnvBuilder(clear=True) 580 builder.create(env_dir) 581 activate = os.path.join(env_dir, self.bindir, 'activate.bat') 582 out, err = check_output( 583 [activate, '&', self.exe, '-c', 'print(0)'], 584 encoding='oem', 585 ) 586 self.assertEqual(out.strip(), '0') 587 588 @unittest.skipUnless(os.name == 'nt' and can_symlink(), 589 'symlinks on Windows') 590 def test_failed_symlink(self): 591 """ 592 Test handling of failed symlinks on Windows. 593 """ 594 rmtree(self.env_dir) 595 env_dir = os.path.join(os.path.realpath(self.env_dir), 'venv') 596 with patch('os.symlink') as mock_symlink: 597 mock_symlink.side_effect = OSError() 598 builder = venv.EnvBuilder(clear=True, symlinks=True) 599 _, err = self.run_with_capture(builder.create, env_dir) 600 filepath_regex = r"'[A-Z]:\\\\(?:[^\\\\]+\\\\)*[^\\\\]+'" 601 self.assertRegex(err, rf"Unable to symlink {filepath_regex} to {filepath_regex}") 602 603 @requireVenvCreate 604 def test_multiprocessing(self): 605 """ 606 Test that the multiprocessing is able to spawn. 607 """ 608 # bpo-36342: Instantiation of a Pool object imports the 609 # multiprocessing.synchronize module. Skip the test if this module 610 # cannot be imported. 611 skip_if_broken_multiprocessing_synchronize() 612 613 rmtree(self.env_dir) 614 self.run_with_capture(venv.create, self.env_dir) 615 out, err = check_output([self.envpy(real_env_dir=True), '-c', 616 'from multiprocessing import Pool; ' 617 'pool = Pool(1); ' 618 'print(pool.apply_async("Python".lower).get(3)); ' 619 'pool.terminate()']) 620 self.assertEqual(out.strip(), "python".encode()) 621 622 @requireVenvCreate 623 def test_multiprocessing_recursion(self): 624 """ 625 Test that the multiprocessing is able to spawn itself 626 """ 627 skip_if_broken_multiprocessing_synchronize() 628 629 rmtree(self.env_dir) 630 self.run_with_capture(venv.create, self.env_dir) 631 script = os.path.join(TEST_HOME_DIR, '_test_venv_multiprocessing.py') 632 subprocess.check_call([self.envpy(real_env_dir=True), "-I", script]) 633 634 @unittest.skipIf(os.name == 'nt', 'not relevant on Windows') 635 def test_deactivate_with_strict_bash_opts(self): 636 bash = shutil.which("bash") 637 if bash is None: 638 self.skipTest("bash required for this test") 639 rmtree(self.env_dir) 640 builder = venv.EnvBuilder(clear=True) 641 builder.create(self.env_dir) 642 activate = os.path.join(self.env_dir, self.bindir, "activate") 643 test_script = os.path.join(self.env_dir, "test_strict.sh") 644 with open(test_script, "w") as f: 645 f.write("set -euo pipefail\n" 646 f"source {activate}\n" 647 "deactivate\n") 648 out, err = check_output([bash, test_script]) 649 self.assertEqual(out, "".encode()) 650 self.assertEqual(err, "".encode()) 651 652 653 @unittest.skipUnless(sys.platform == 'darwin', 'only relevant on macOS') 654 def test_macos_env(self): 655 rmtree(self.env_dir) 656 builder = venv.EnvBuilder() 657 builder.create(self.env_dir) 658 659 out, err = check_output([self.envpy(real_env_dir=True), '-c', 660 'import os; print("__PYVENV_LAUNCHER__" in os.environ)']) 661 self.assertEqual(out.strip(), 'False'.encode()) 662 663 def test_pathsep_error(self): 664 """ 665 Test that venv creation fails when the target directory contains 666 the path separator. 667 """ 668 rmtree(self.env_dir) 669 bad_itempath = self.env_dir + os.pathsep 670 self.assertRaises(ValueError, venv.create, bad_itempath) 671 self.assertRaises(ValueError, venv.create, FakePath(bad_itempath)) 672 673 @unittest.skipIf(os.name == 'nt', 'not relevant on Windows') 674 @requireVenvCreate 675 def test_zippath_from_non_installed_posix(self): 676 """ 677 Test that when create venv from non-installed python, the zip path 678 value is as expected. 679 """ 680 rmtree(self.env_dir) 681 # First try to create a non-installed python. It's not a real full 682 # functional non-installed python, but enough for this test. 683 platlibdir = sys.platlibdir 684 non_installed_dir = os.path.realpath(tempfile.mkdtemp()) 685 self.addCleanup(rmtree, non_installed_dir) 686 bindir = os.path.join(non_installed_dir, self.bindir) 687 os.mkdir(bindir) 688 shutil.copy2(sys.executable, bindir) 689 libdir = os.path.join(non_installed_dir, platlibdir, self.lib[1]) 690 os.makedirs(libdir) 691 landmark = os.path.join(libdir, "os.py") 692 abi_thread = "t" if sysconfig.get_config_var("Py_GIL_DISABLED") else "" 693 stdlib_zip = f"python{sys.version_info.major}{sys.version_info.minor}{abi_thread}" 694 zip_landmark = os.path.join(non_installed_dir, 695 platlibdir, 696 stdlib_zip) 697 additional_pythonpath_for_non_installed = [] 698 699 # Copy stdlib files to the non-installed python so venv can 700 # correctly calculate the prefix. 701 for eachpath in sys.path: 702 if eachpath.endswith(".zip"): 703 if os.path.isfile(eachpath): 704 shutil.copyfile( 705 eachpath, 706 os.path.join(non_installed_dir, platlibdir)) 707 elif os.path.isfile(os.path.join(eachpath, "os.py")): 708 names = os.listdir(eachpath) 709 ignored_names = copy_python_src_ignore(eachpath, names) 710 for name in names: 711 if name in ignored_names: 712 continue 713 if name == "site-packages": 714 continue 715 fn = os.path.join(eachpath, name) 716 if os.path.isfile(fn): 717 shutil.copy(fn, libdir) 718 elif os.path.isdir(fn): 719 shutil.copytree(fn, os.path.join(libdir, name), 720 ignore=copy_python_src_ignore) 721 else: 722 additional_pythonpath_for_non_installed.append( 723 eachpath) 724 cmd = [os.path.join(non_installed_dir, self.bindir, self.exe), 725 "-m", 726 "venv", 727 "--without-pip", 728 "--without-scm-ignore-files", 729 self.env_dir] 730 # Our fake non-installed python is not fully functional because 731 # it cannot find the extensions. Set PYTHONPATH so it can run the 732 # venv module correctly. 733 pythonpath = os.pathsep.join( 734 additional_pythonpath_for_non_installed) 735 # For python built with shared enabled. We need to set 736 # LD_LIBRARY_PATH so the non-installed python can find and link 737 # libpython.so 738 ld_library_path = sysconfig.get_config_var("LIBDIR") 739 if not ld_library_path or sysconfig.is_python_build(): 740 ld_library_path = os.path.abspath(os.path.dirname(sys.executable)) 741 if sys.platform == 'darwin': 742 ld_library_path_env = "DYLD_LIBRARY_PATH" 743 else: 744 ld_library_path_env = "LD_LIBRARY_PATH" 745 child_env = { 746 "PYTHONPATH": pythonpath, 747 ld_library_path_env: ld_library_path, 748 } 749 if asan_options := os.environ.get("ASAN_OPTIONS"): 750 # prevent https://github.com/python/cpython/issues/104839 751 child_env["ASAN_OPTIONS"] = asan_options 752 subprocess.check_call(cmd, env=child_env) 753 # Now check the venv created from the non-installed python has 754 # correct zip path in pythonpath. 755 cmd = [self.envpy(), '-S', '-c', 'import sys; print(sys.path)'] 756 out, err = check_output(cmd) 757 self.assertTrue(zip_landmark.encode() in out) 758 759 @requireVenvCreate 760 def test_activate_shell_script_has_no_dos_newlines(self): 761 """ 762 Test that the `activate` shell script contains no CR LF. 763 This is relevant for Cygwin, as the Windows build might have 764 converted line endings accidentally. 765 """ 766 venv_dir = pathlib.Path(self.env_dir) 767 rmtree(venv_dir) 768 [[scripts_dir], *_] = self.ENV_SUBDIRS 769 script_path = venv_dir / scripts_dir / "activate" 770 venv.create(venv_dir) 771 with open(script_path, 'rb') as script: 772 for i, line in enumerate(script, 1): 773 error_message = f"CR LF found in line {i}" 774 self.assertFalse(line.endswith(b'\r\n'), error_message) 775 776 @requireVenvCreate 777 def test_scm_ignore_files_git(self): 778 """ 779 Test that a .gitignore file is created when "git" is specified. 780 The file should contain a `*\n` line. 781 """ 782 self.run_with_capture(venv.create, self.env_dir, 783 scm_ignore_files={'git'}) 784 file_lines = self.get_text_file_contents('.gitignore').splitlines() 785 self.assertIn('*', file_lines) 786 787 @requireVenvCreate 788 def test_create_scm_ignore_files_multiple(self): 789 """ 790 Test that ``scm_ignore_files`` can work with multiple SCMs. 791 """ 792 bzrignore_name = ".bzrignore" 793 contents = "# For Bazaar.\n*\n" 794 795 class BzrEnvBuilder(venv.EnvBuilder): 796 def create_bzr_ignore_file(self, context): 797 gitignore_path = os.path.join(context.env_dir, bzrignore_name) 798 with open(gitignore_path, 'w', encoding='utf-8') as file: 799 file.write(contents) 800 801 builder = BzrEnvBuilder(scm_ignore_files={'git', 'bzr'}) 802 self.run_with_capture(builder.create, self.env_dir) 803 804 gitignore_lines = self.get_text_file_contents('.gitignore').splitlines() 805 self.assertIn('*', gitignore_lines) 806 807 bzrignore = self.get_text_file_contents(bzrignore_name) 808 self.assertEqual(bzrignore, contents) 809 810 @requireVenvCreate 811 def test_create_scm_ignore_files_empty(self): 812 """ 813 Test that no default ignore files are created when ``scm_ignore_files`` 814 is empty. 815 """ 816 # scm_ignore_files is set to frozenset() by default. 817 self.run_with_capture(venv.create, self.env_dir) 818 with self.assertRaises(FileNotFoundError): 819 self.get_text_file_contents('.gitignore') 820 821 self.assertIn("--without-scm-ignore-files", 822 self.get_text_file_contents('pyvenv.cfg')) 823 824 @requireVenvCreate 825 def test_cli_with_scm_ignore_files(self): 826 """ 827 Test that default SCM ignore files are created by default via the CLI. 828 """ 829 self.run_with_capture(venv.main, ['--without-pip', self.env_dir]) 830 831 gitignore_lines = self.get_text_file_contents('.gitignore').splitlines() 832 self.assertIn('*', gitignore_lines) 833 834 @requireVenvCreate 835 def test_cli_without_scm_ignore_files(self): 836 """ 837 Test that ``--without-scm-ignore-files`` doesn't create SCM ignore files. 838 """ 839 args = ['--without-pip', '--without-scm-ignore-files', self.env_dir] 840 self.run_with_capture(venv.main, args) 841 842 with self.assertRaises(FileNotFoundError): 843 self.get_text_file_contents('.gitignore') 844 845 def test_venv_same_path(self): 846 same_path = venv.EnvBuilder._same_path 847 if sys.platform == 'win32': 848 # Case-insensitive, and handles short/long names 849 tests = [ 850 (True, TESTFN, TESTFN), 851 (True, TESTFN.lower(), TESTFN.upper()), 852 ] 853 import _winapi 854 # ProgramFiles is the most reliable path that will have short/long 855 progfiles = os.getenv('ProgramFiles') 856 if progfiles: 857 tests = [ 858 *tests, 859 (True, progfiles, progfiles), 860 (True, _winapi.GetShortPathName(progfiles), _winapi.GetLongPathName(progfiles)), 861 ] 862 else: 863 # Just a simple case-sensitive comparison 864 tests = [ 865 (True, TESTFN, TESTFN), 866 (False, TESTFN.lower(), TESTFN.upper()), 867 ] 868 for r, path1, path2 in tests: 869 with self.subTest(f"{path1}-{path2}"): 870 if r: 871 self.assertTrue(same_path(path1, path2)) 872 else: 873 self.assertFalse(same_path(path1, path2)) 874 875 # gh-126084: venvwlauncher should run pythonw, not python 876 @requireVenvCreate 877 @unittest.skipUnless(os.name == 'nt', 'only relevant on Windows') 878 def test_venvwlauncher(self): 879 """ 880 Test that the GUI launcher runs the GUI python. 881 """ 882 rmtree(self.env_dir) 883 venv.create(self.env_dir) 884 exename = self.exe 885 # Retain the debug suffix if present 886 if "python" in exename and not "pythonw" in exename: 887 exename = exename.replace("python", "pythonw") 888 envpyw = os.path.join(self.env_dir, self.bindir, exename) 889 try: 890 subprocess.check_call([envpyw, "-c", "import sys; " 891 "assert sys._base_executable.endswith('%s')" % exename]) 892 except subprocess.CalledProcessError: 893 self.fail("venvwlauncher.exe did not run %s" % exename) 894 895 896@requireVenvCreate 897class EnsurePipTest(BaseTest): 898 """Test venv module installation of pip.""" 899 def assert_pip_not_installed(self): 900 out, err = check_output([self.envpy(real_env_dir=True), '-c', 901 'try:\n import pip\nexcept ImportError:\n print("OK")']) 902 # We force everything to text, so unittest gives the detailed diff 903 # if we get unexpected results 904 err = err.decode("latin-1") # Force to text, prevent decoding errors 905 self.assertEqual(err, "") 906 out = out.decode("latin-1") # Force to text, prevent decoding errors 907 self.assertEqual(out.strip(), "OK") 908 909 910 def test_no_pip_by_default(self): 911 rmtree(self.env_dir) 912 self.run_with_capture(venv.create, self.env_dir) 913 self.assert_pip_not_installed() 914 915 def test_explicit_no_pip(self): 916 rmtree(self.env_dir) 917 self.run_with_capture(venv.create, self.env_dir, with_pip=False) 918 self.assert_pip_not_installed() 919 920 def test_devnull(self): 921 # Fix for issue #20053 uses os.devnull to force a config file to 922 # appear empty. However http://bugs.python.org/issue20541 means 923 # that doesn't currently work properly on Windows. Once that is 924 # fixed, the "win_location" part of test_with_pip should be restored 925 with open(os.devnull, "rb") as f: 926 self.assertEqual(f.read(), b"") 927 928 self.assertTrue(os.path.exists(os.devnull)) 929 930 def do_test_with_pip(self, system_site_packages): 931 rmtree(self.env_dir) 932 with EnvironmentVarGuard() as envvars: 933 # pip's cross-version compatibility may trigger deprecation 934 # warnings in current versions of Python. Ensure related 935 # environment settings don't cause venv to fail. 936 envvars["PYTHONWARNINGS"] = "ignore" 937 # ensurepip is different enough from a normal pip invocation 938 # that we want to ensure it ignores the normal pip environment 939 # variable settings. We set PIP_NO_INSTALL here specifically 940 # to check that ensurepip (and hence venv) ignores it. 941 # See http://bugs.python.org/issue19734 942 envvars["PIP_NO_INSTALL"] = "1" 943 # Also check that we ignore the pip configuration file 944 # See http://bugs.python.org/issue20053 945 with tempfile.TemporaryDirectory() as home_dir: 946 envvars["HOME"] = home_dir 947 bad_config = "[global]\nno-install=1" 948 # Write to both config file names on all platforms to reduce 949 # cross-platform variation in test code behaviour 950 win_location = ("pip", "pip.ini") 951 posix_location = (".pip", "pip.conf") 952 # Skips win_location due to http://bugs.python.org/issue20541 953 for dirname, fname in (posix_location,): 954 dirpath = os.path.join(home_dir, dirname) 955 os.mkdir(dirpath) 956 fpath = os.path.join(dirpath, fname) 957 with open(fpath, 'w') as f: 958 f.write(bad_config) 959 960 # Actually run the create command with all that unhelpful 961 # config in place to ensure we ignore it 962 with self.nicer_error(): 963 self.run_with_capture(venv.create, self.env_dir, 964 system_site_packages=system_site_packages, 965 with_pip=True) 966 # Ensure pip is available in the virtual environment 967 # Ignore DeprecationWarning since pip code is not part of Python 968 out, err = check_output([self.envpy(real_env_dir=True), 969 '-W', 'ignore::DeprecationWarning', 970 '-W', 'ignore::ImportWarning', '-I', 971 '-m', 'pip', '--version']) 972 # We force everything to text, so unittest gives the detailed diff 973 # if we get unexpected results 974 err = err.decode("latin-1") # Force to text, prevent decoding errors 975 self.assertEqual(err, "") 976 out = out.decode("latin-1") # Force to text, prevent decoding errors 977 expected_version = "pip {}".format(ensurepip.version()) 978 self.assertEqual(out[:len(expected_version)], expected_version) 979 env_dir = os.fsencode(self.env_dir).decode("latin-1") 980 self.assertIn(env_dir, out) 981 982 # http://bugs.python.org/issue19728 983 # Check the private uninstall command provided for the Windows 984 # installers works (at least in a virtual environment) 985 with EnvironmentVarGuard() as envvars: 986 with self.nicer_error(): 987 # It seems ensurepip._uninstall calls subprocesses which do not 988 # inherit the interpreter settings. 989 envvars["PYTHONWARNINGS"] = "ignore" 990 out, err = check_output([self.envpy(real_env_dir=True), 991 '-W', 'ignore::DeprecationWarning', 992 '-W', 'ignore::ImportWarning', '-I', 993 '-m', 'ensurepip._uninstall']) 994 # We force everything to text, so unittest gives the detailed diff 995 # if we get unexpected results 996 err = err.decode("latin-1") # Force to text, prevent decoding errors 997 # Ignore the warning: 998 # "The directory '$HOME/.cache/pip/http' or its parent directory 999 # is not owned by the current user and the cache has been disabled. 1000 # Please check the permissions and owner of that directory. If 1001 # executing pip with sudo, you may want sudo's -H flag." 1002 # where $HOME is replaced by the HOME environment variable. 1003 err = re.sub("^(WARNING: )?The directory .* or its parent directory " 1004 "is not owned or is not writable by the current user.*$", "", 1005 err, flags=re.MULTILINE) 1006 # Ignore warning about missing optional module: 1007 try: 1008 import ssl 1009 except ImportError: 1010 err = re.sub( 1011 "^WARNING: Disabling truststore since ssl support is missing$", 1012 "", 1013 err, flags=re.MULTILINE) 1014 self.assertEqual(err.rstrip(), "") 1015 # Being fairly specific regarding the expected behaviour for the 1016 # initial bundling phase in Python 3.4. If the output changes in 1017 # future pip versions, this test can likely be relaxed further. 1018 out = out.decode("latin-1") # Force to text, prevent decoding errors 1019 self.assertIn("Successfully uninstalled pip", out) 1020 # Check pip is now gone from the virtual environment. This only 1021 # applies in the system_site_packages=False case, because in the 1022 # other case, pip may still be available in the system site-packages 1023 if not system_site_packages: 1024 self.assert_pip_not_installed() 1025 1026 @contextlib.contextmanager 1027 def nicer_error(self): 1028 """ 1029 Capture output from a failed subprocess for easier debugging. 1030 1031 The output this handler produces can be a little hard to read, 1032 but at least it has all the details. 1033 """ 1034 try: 1035 yield 1036 except subprocess.CalledProcessError as exc: 1037 out = (exc.output or b'').decode(errors="replace") 1038 err = (exc.stderr or b'').decode(errors="replace") 1039 self.fail( 1040 f"{exc}\n\n" 1041 f"**Subprocess Output**\n{out}\n\n" 1042 f"**Subprocess Error**\n{err}" 1043 ) 1044 1045 @requires_venv_with_pip() 1046 @requires_resource('cpu') 1047 def test_with_pip(self): 1048 self.do_test_with_pip(False) 1049 self.do_test_with_pip(True) 1050 1051 1052if __name__ == "__main__": 1053 unittest.main() 1054