diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py index 2f30d3108021dc..179fcd71b3c489 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,42 @@ 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') + + 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], + ) + 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.