From a7b667a231e5dd83bdf460b63a67138a9378fcda Mon Sep 17 00:00:00 2001 From: repzakske <31244169+repzakske@users.noreply.github.com> Date: Fri, 16 Oct 2026 01:49:47 +0200 Subject: [PATCH 1/2] gh-159498: Fix PowerShell venv activation for paths containing brackets --- Lib/test/test_venv.py | 40 +++++++++++++++++++ Lib/venv/scripts/common/Activate.ps1 | 6 +-- ...-10-16-01-44-47.gh-issue-159498.Lp9sQ2.rst | 2 + 3 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-16-01-44-47.gh-issue-159498.Lp9sQ2.rst diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py index 2f30d3108021dc..a61b420e503c95 100644 --- a/Lib/test/test_venv.py +++ b/Lib/test/test_venv.py @@ -7,6 +7,7 @@ import contextlib import ensurepip +import json import os import os.path import pathlib @@ -846,6 +847,45 @@ def test_fish_activate_shadowed_builtins(self): self.assertNotIn('LEAK', text) self.assertIn('OLDSTATUS=7', text) + @requires_subprocess() + def test_powershell_activate_literal_paths(self): + shells = [path for name in ('pwsh', 'powershell.exe') + if (path := shutil.which(name)) is not None] + if not shells: + self.skipTest('PowerShell required for this test') + + for env_name in ('venv[1]', os.path.join('project [2026]', '.venv')): + env_dir = self.get_env_file(env_name) + builder = venv.EnvBuilder(clear=True, prompt='from-config') + builder.create(env_dir) + activate = os.path.join(env_dir, self.bindir, 'Activate.ps1') + activate = activate.replace("'", "''") + test_script = self.get_env_file('test_literal_paths.ps1') + code = ('import json, os, sys; ' + 'print(json.dumps([sys.executable, ' + "os.environ['VIRTUAL_ENV'], " + "os.environ['VIRTUAL_ENV_PROMPT']]))") + with open(test_script, 'w', encoding='utf-8-sig') as f: + f.write( + "$ErrorActionPreference = 'Stop'\n" + f". '{activate}'\n" + f'{self.exe} -c "{code}"\n' + 'deactivate\n' + ) + for shell in shells: + with self.subTest(shell=shell, env_name=env_name): + out, err = check_output( + [shell, '-NoProfile', '-NonInteractive', + '-ExecutionPolicy', 'Bypass', '-File', test_script], + ) + self.assertFalse(err) + executable, active_dir, prompt = json.loads(out) + self.assertEqual(pathlib.Path(active_dir), + pathlib.Path(env_dir)) + self.assertEqual(prompt, 'from-config') + self.assertEqual(pathlib.Path(executable), + pathlib.Path(env_dir, self.bindir, self.exe)) + # gh-124651: test quoted strings on Windows @unittest.skipUnless(os.name == 'nt', 'only relevant on Windows') def test_special_chars_windows(self): diff --git a/Lib/venv/scripts/common/Activate.ps1 b/Lib/venv/scripts/common/Activate.ps1 index d75b8fbcfc7778..51f9177abe91fd 100644 --- a/Lib/venv/scripts/common/Activate.ps1 +++ b/Lib/venv/scripts/common/Activate.ps1 @@ -135,7 +135,7 @@ function Get-PyVenvConfig( Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). - $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue + $pyvenvConfigPath = Resolve-Path -LiteralPath (Join-Path -Path $ConfigDir -ChildPath 'pyvenv.cfg') -ErrorAction Continue # An empty map will be returned if no config file is found. $pyvenvConfig = @{ } @@ -143,7 +143,7 @@ function Get-PyVenvConfig( if ($pyvenvConfigPath) { Write-Verbose "File exists, parse `key = value` lines" - $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath + $pyvenvConfigContent = Get-Content -LiteralPath $pyvenvConfigPath.ProviderPath $pyvenvConfigContent | ForEach-Object { $keyval = $PSItem -split "\s*=\s*", 2 @@ -168,7 +168,7 @@ function Get-PyVenvConfig( # Determine the containing directory of this script $VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition -$VenvExecDir = Get-Item -Path $VenvExecPath +$VenvExecDir = Get-Item -LiteralPath $VenvExecPath Write-Verbose "Activation script is located in path: '$VenvExecPath'" Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" diff --git a/Misc/NEWS.d/next/Library/2026-10-16-01-44-47.gh-issue-159498.Lp9sQ2.rst b/Misc/NEWS.d/next/Library/2026-10-16-01-44-47.gh-issue-159498.Lp9sQ2.rst new file mode 100644 index 00000000000000..914d47c98813f1 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-16-01-44-47.gh-issue-159498.Lp9sQ2.rst @@ -0,0 +1,2 @@ +Fix PowerShell activation of :mod:`venv` environments with square brackets +in their paths, including reading the intended ``pyvenv.cfg`` file. From 1c2557eb9ab98190c0dbc50073827227c95e0eca Mon Sep 17 00:00:00 2001 From: repzakske <31244169+repzakske@users.noreply.github.com> Date: Fri, 16 Oct 2026 01:56:57 +0200 Subject: [PATCH 2/2] test: focus the PowerShell regression on one bracketed path --- Lib/test/test_venv.py | 57 ++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py index a61b420e503c95..179fcd71b3c489 100644 --- a/Lib/test/test_venv.py +++ b/Lib/test/test_venv.py @@ -854,37 +854,34 @@ def test_powershell_activate_literal_paths(self): if not shells: self.skipTest('PowerShell required for this test') - for env_name in ('venv[1]', os.path.join('project [2026]', '.venv')): - env_dir = self.get_env_file(env_name) - builder = venv.EnvBuilder(clear=True, prompt='from-config') - builder.create(env_dir) - activate = os.path.join(env_dir, self.bindir, 'Activate.ps1') - activate = activate.replace("'", "''") - test_script = self.get_env_file('test_literal_paths.ps1') - code = ('import json, os, sys; ' - 'print(json.dumps([sys.executable, ' - "os.environ['VIRTUAL_ENV'], " - "os.environ['VIRTUAL_ENV_PROMPT']]))") - with open(test_script, 'w', encoding='utf-8-sig') as f: - f.write( - "$ErrorActionPreference = 'Stop'\n" - f". '{activate}'\n" - f'{self.exe} -c "{code}"\n' - 'deactivate\n' + env_dir = self.get_env_file('venv[1]') + venv.EnvBuilder(prompt='from-config').create(env_dir) + activate = os.path.join(env_dir, self.bindir, 'Activate.ps1') + activate = activate.replace("'", "''") + test_script = self.get_env_file('test_literal_paths.ps1') + code = ('import json, os, sys; ' + 'print(json.dumps([sys.executable, ' + "os.environ['VIRTUAL_ENV'], " + "os.environ['VIRTUAL_ENV_PROMPT']]))") + with open(test_script, 'w', encoding='utf-8-sig') as f: + f.write( + "$ErrorActionPreference = 'Stop'\n" + f". '{activate}'\n" + f'{self.exe} -c "{code}"\n' + 'deactivate\n' + ) + for shell in shells: + with self.subTest(shell=shell): + out, err = check_output( + [shell, '-NoProfile', '-NonInteractive', + '-ExecutionPolicy', 'Bypass', '-File', test_script], ) - for shell in shells: - with self.subTest(shell=shell, env_name=env_name): - out, err = check_output( - [shell, '-NoProfile', '-NonInteractive', - '-ExecutionPolicy', 'Bypass', '-File', test_script], - ) - self.assertFalse(err) - executable, active_dir, prompt = json.loads(out) - self.assertEqual(pathlib.Path(active_dir), - pathlib.Path(env_dir)) - self.assertEqual(prompt, 'from-config') - self.assertEqual(pathlib.Path(executable), - pathlib.Path(env_dir, self.bindir, self.exe)) + self.assertFalse(err) + executable, active_dir, prompt = json.loads(out) + self.assertEqual(pathlib.Path(active_dir), pathlib.Path(env_dir)) + self.assertEqual(prompt, 'from-config') + self.assertEqual(pathlib.Path(executable), + pathlib.Path(env_dir, self.bindir, self.exe)) # gh-124651: test quoted strings on Windows @unittest.skipUnless(os.name == 'nt', 'only relevant on Windows')