From 837a42db0c8f99dcd29562071010f1955f4fce14 Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Sat, 11 Apr 2026 13:16:11 +0530 Subject: [PATCH 1/3] gh-148315: Shell-quote the `command` line in pyvenv.cfg The `command = ...` record written to `pyvenv.cfg` by `venv.EnvBuilder.create_configuration` was assembled with a plain `' '.join(args)` and an unquoted `sys.executable`. When any of these tokens contained whitespace (for example a Windows user directory like `C:\Users\Z B\...`), the recorded command was no longer a faithful reproduction and truncated at the first space when shell-parsed. Build the full argv as a list and emit it via `shlex.join`, and split `--prompt="..."` into two separate argv tokens so its value is quoted by `shlex.join` as well. --- Lib/venv/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Lib/venv/__init__.py b/Lib/venv/__init__.py index 21f82125f5a7c4..8e1beece2fafe3 100644 --- a/Lib/venv/__init__.py +++ b/Lib/venv/__init__.py @@ -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): """ From 4e8b52064e51a81a8d7444fee55f9744b807bb65 Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Sat, 11 Apr 2026 13:16:36 +0530 Subject: [PATCH 2/3] gh-148315: Update test_venv for shell-quoted command line --- Lib/test/test_venv.py | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py index 78461abcd69f33..09c2d510c2e28b 100644 --- a/Lib/test/test_venv.py +++ b/Lib/test/test_venv.py @@ -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 @@ -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: @@ -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] From a8202d9558ae55eb64c9565dea152122c1b05f60 Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Sat, 11 Apr 2026 13:16:50 +0530 Subject: [PATCH 3/3] gh-148315: Add NEWS entry --- .../Library/2026-04-11-00-00-00.gh-issue-148315.9X8UQm.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-04-11-00-00-00.gh-issue-148315.9X8UQm.rst diff --git a/Misc/NEWS.d/next/Library/2026-04-11-00-00-00.gh-issue-148315.9X8UQm.rst b/Misc/NEWS.d/next/Library/2026-04-11-00-00-00.gh-issue-148315.9X8UQm.rst new file mode 100644 index 00000000000000..e1308798841508 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-04-11-00-00-00.gh-issue-148315.9X8UQm.rst @@ -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.