Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 33 additions & 4 deletions Lib/test/test_venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,12 @@ def _check_output_of_default_create(self):
self.assertIn('home = %s' % path, data)
self.assertIn('executable = %s' %
os.path.realpath(sys.executable), data)
copies = '' if os.name=='nt' else ' --copies'
cmd = (f'command = {sys.executable} -m venv{copies} --without-pip '
f'--without-scm-ignore-files {self.env_dir}')
expected_argv = [sys.executable, '-m', 'venv']
if os.name != 'nt':
expected_argv.append('--copies')
expected_argv.extend(['--without-pip', '--without-scm-ignore-files',
self.env_dir])
cmd = f'command = {shlex.join(expected_argv)}'
self.assertIn(cmd, data)
fn = self.get_env_file(self.bindir, self.exe)
if not os.path.exists(fn): # diagnostics for Windows buildbot failures
Expand All @@ -166,7 +169,7 @@ def test_config_file_command_key(self):
('--clear', 'clear', True),
('--upgrade', 'upgrade', True),
('--upgrade-deps', 'upgrade_deps', True),
('--prompt="foobar"', 'prompt', 'foobar'),
('--prompt', 'prompt', 'foobar'),
('--without-scm-ignore-files', 'scm_ignore_files', frozenset()),
]
for opt, attr, value in options:
Expand All @@ -190,6 +193,32 @@ def test_config_file_command_key(self):
else:
self.assertRegex(data, rf'command = .* {opt}')

def test_config_file_command_quotes_paths_with_spaces(self):
# gh-148315: the `command = ...` line written to pyvenv.cfg must be
# shell-quoted, so a venv created in a directory with whitespace in
# its path (as happens on Windows when the user directory contains a
# space, e.g. "C:\\Users\\Z B") round-trips through shlex.split as
# a single token instead of being truncated at the space.
env_dir_with_space = os.path.join(tempfile.mkdtemp(), 'with space')
self.addCleanup(rmtree, os.path.dirname(env_dir_with_space))
b = venv.EnvBuilder()
b.upgrade_dependencies = Mock()
b._setup_pip = Mock()
self.run_with_capture(b.create, env_dir_with_space)
cfg = pathlib.Path(env_dir_with_space, 'pyvenv.cfg').read_text(
encoding='utf-8')
for line in cfg.splitlines():
key, _, value = line.partition('=')
if key.strip() == 'command':
parts = shlex.split(value.strip())
break
else:
self.fail(f'pyvenv.cfg is missing a command key:\n{cfg}')
# Last token must be the full env_dir, not a space-split fragment.
self.assertEqual(parts[-1], env_dir_with_space)
# And the whole argv must be parseable by the venv CLI.
self.assertEqual(parts[1:3], ['-m', 'venv'])

def test_prompt(self):
env_name = os.path.split(self.env_dir)[1]

Expand Down
9 changes: 6 additions & 3 deletions Lib/venv/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,13 +252,16 @@ def create_configuration(self, context):
if self.upgrade_deps:
args.append('--upgrade-deps')
if self.orig_prompt is not None:
args.append(f'--prompt="{self.orig_prompt}"')
args.extend(['--prompt', self.orig_prompt])
if not self.scm_ignore_files:
args.append('--without-scm-ignore-files')

args.append(context.env_dir)
args = ' '.join(args)
f.write(f'command = {sys.executable} -m venv {args}\n')
# gh-148315: shell-quote so paths containing whitespace
# (e.g. a Windows user directory with a space) round-trip
# faithfully and the recorded command can be re-executed.
command = shlex.join([sys.executable, '-m', 'venv', *args])
f.write(f'command = {command}\n')

def symlink_or_copy(self, src, dst, relative_symlinks_ok=False):
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
:mod:`venv`: Shell-quote the ``command = ...`` line written to
``pyvenv.cfg`` so that paths containing whitespace (for example a Windows
user directory with a space) are preserved faithfully and the recorded
command can be re-executed.
Loading