@@ -301,306 +301,3 @@ index 0000000..17fc917
301
301
+++ b/Misc/NEWS.d/next/Library/2024-09-28-02-03-04.gh-issue-124651.bLBGtH.rst
302
302
@@ -0,0 +1 @@
303
303
+ Properly quote template strings in :mod:`venv` activation scripts.
304
- diff --git a/fix.patch b/fix.patch
305
- deleted file mode 100644
306
- index 4f63f32..0000000
307
- --- a/fix.patch
308
- +++ /dev/null
309
- @@ -1,297 +0,0 @@
310
- - From ae0d64cb185900712c40a65d7d8aa118f9903d57 Mon Sep 17 00:00:00 2001
311
- - From: Victor Stinner <[email protected] >
312
- - Date: Fri, 1 Nov 2024 14:11:47 +0100
313
- - Subject: [PATCH] [3.11] gh-124651: Quote template strings in `venv` activation
314
- - scripts (GH-124712) (GH-126185) (#126269)
315
- -
316
- - (cherry picked from commit ae961ae94bf19c8f8c7fbea3d1c25cc55ce8ae97)
317
- - ---
318
- - Lib/test/test_venv.py | 81 +++++++++++++++++++
319
- - Lib/venv/__init__.py | 42 ++++++++--
320
- - Lib/venv/scripts/common/activate | 6 +-
321
- - Lib/venv/scripts/nt/activate.bat | 4 +-
322
- - Lib/venv/scripts/posix/activate.csh | 6 +-
323
- - Lib/venv/scripts/posix/activate.fish | 6 +-
324
- - ...-09-28-02-03-04.gh-issue-124651.bLBGtH.rst | 1 +
325
- - 7 files changed, 130 insertions(+), 16 deletions(-)
326
- - create mode 100644 Misc/NEWS.d/next/Library/2024-09-28-02-03-04.gh-issue-124651.bLBGtH.rst
327
- -
328
- - diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py
329
- - index 480cb29f35a6a4..871b8314b90b05 100644
330
- - --- a/Lib/test/test_venv.py
331
- - +++ b/Lib/test/test_venv.py
332
- - @@ -14,6 +14,7 @@
333
- - import subprocess
334
- - import sys
335
- - import tempfile
336
- - +import shlex
337
- - from test.support import (captured_stdout, captured_stderr, requires_zlib,
338
- - can_symlink, EnvironmentVarGuard, rmtree,
339
- - import_module,
340
- - @@ -85,6 +86,10 @@ def get_text_file_contents(self, *args, encoding='utf-8'):
341
- - result = f.read()
342
- - return result
343
- -
344
- - + def assertEndsWith(self, string, tail):
345
- - + if not string.endswith(tail):
346
- - + self.fail(f"String {string!r} does not end with {tail!r}")
347
- - +
348
- - class BasicTest(BaseTest):
349
- - """Test venv module functionality."""
350
- -
351
- - @@ -342,6 +347,82 @@ def test_executable_symlinks(self):
352
- - 'import sys; print(sys.executable)'])
353
- - self.assertEqual(out.strip(), envpy.encode())
354
- -
355
- - + # gh-124651: test quoted strings
356
- - + @unittest.skipIf(os.name == 'nt', 'contains invalid characters on Windows')
357
- - + def test_special_chars_bash(self):
358
- - + """
359
- - + Test that the template strings are quoted properly (bash)
360
- - + """
361
- - + rmtree(self.env_dir)
362
- - + bash = shutil.which('bash')
363
- - + if bash is None:
364
- - + self.skipTest('bash required for this test')
365
- - + env_name = '"\';&&$e|\'"'
366
- - + env_dir = os.path.join(os.path.realpath(self.env_dir), env_name)
367
- - + builder = venv.EnvBuilder(clear=True)
368
- - + builder.create(env_dir)
369
- - + activate = os.path.join(env_dir, self.bindir, 'activate')
370
- - + test_script = os.path.join(self.env_dir, 'test_special_chars.sh')
371
- - + with open(test_script, "w") as f:
372
- - + f.write(f'source {shlex.quote(activate)}\n'
373
- - + 'python -c \'import sys; print(sys.executable)\'\n'
374
- - + 'python -c \'import os; print(os.environ["VIRTUAL_ENV"])\'\n'
375
- - + 'deactivate\n')
376
- - + out, err = check_output([bash, test_script])
377
- - + lines = out.splitlines()
378
- - + self.assertTrue(env_name.encode() in lines[0])
379
- - + self.assertEndsWith(lines[1], env_name.encode())
380
- - +
381
- - + # gh-124651: test quoted strings
382
- - + @unittest.skipIf(os.name == 'nt', 'contains invalid characters on Windows')
383
- - + def test_special_chars_csh(self):
384
- - + """
385
- - + Test that the template strings are quoted properly (csh)
386
- - + """
387
- - + rmtree(self.env_dir)
388
- - + csh = shutil.which('tcsh') or shutil.which('csh')
389
- - + if csh is None:
390
- - + self.skipTest('csh required for this test')
391
- - + env_name = '"\';&&$e|\'"'
392
- - + env_dir = os.path.join(os.path.realpath(self.env_dir), env_name)
393
- - + builder = venv.EnvBuilder(clear=True)
394
- - + builder.create(env_dir)
395
- - + activate = os.path.join(env_dir, self.bindir, 'activate.csh')
396
- - + test_script = os.path.join(self.env_dir, 'test_special_chars.csh')
397
- - + with open(test_script, "w") as f:
398
- - + f.write(f'source {shlex.quote(activate)}\n'
399
- - + 'python -c \'import sys; print(sys.executable)\'\n'
400
- - + 'python -c \'import os; print(os.environ["VIRTUAL_ENV"])\'\n'
401
- - + 'deactivate\n')
402
- - + out, err = check_output([csh, test_script])
403
- - + lines = out.splitlines()
404
- - + self.assertTrue(env_name.encode() in lines[0])
405
- - + self.assertEndsWith(lines[1], env_name.encode())
406
- - +
407
- - + # gh-124651: test quoted strings on Windows
408
- - + @unittest.skipUnless(os.name == 'nt', 'only relevant on Windows')
409
- - + def test_special_chars_windows(self):
410
- - + """
411
- - + Test that the template strings are quoted properly on Windows
412
- - + """
413
- - + rmtree(self.env_dir)
414
- - + env_name = "'&&^$e"
415
- - + env_dir = os.path.join(os.path.realpath(self.env_dir), env_name)
416
- - + builder = venv.EnvBuilder(clear=True)
417
- - + builder.create(env_dir)
418
- - + activate = os.path.join(env_dir, self.bindir, 'activate.bat')
419
- - + test_batch = os.path.join(self.env_dir, 'test_special_chars.bat')
420
- - + with open(test_batch, "w") as f:
421
- - + f.write('@echo off\n'
422
- - + f'"{activate}" & '
423
- - + f'{self.exe} -c "import sys; print(sys.executable)" & '
424
- - + f'{self.exe} -c "import os; print(os.environ[\'VIRTUAL_ENV\'])" & '
425
- - + 'deactivate')
426
- - + out, err = check_output([test_batch])
427
- - + lines = out.splitlines()
428
- - + self.assertTrue(env_name.encode() in lines[0])
429
- - + self.assertEndsWith(lines[1], env_name.encode())
430
- - +
431
- - @unittest.skipUnless(os.name == 'nt', 'only relevant on Windows')
432
- - def test_unicode_in_batch_file(self):
433
- - """
434
- - diff --git a/Lib/venv/__init__.py b/Lib/venv/__init__.py
435
- - index 6f1af294ae63e3..299633117e6fbe 100644
436
- - --- a/Lib/venv/__init__.py
437
- - +++ b/Lib/venv/__init__.py
438
- - @@ -11,6 +11,7 @@
439
- - import sys
440
- - import sysconfig
441
- - import types
442
- - +import shlex
443
- -
444
- -
445
- - CORE_VENV_DEPS = ('pip', 'setuptools')
446
- - @@ -348,11 +349,41 @@ def replace_variables(self, text, context):
447
- - :param context: The information for the environment creation request
448
- - being processed.
449
- - """
450
- - - text = text.replace('__VENV_DIR__', context.env_dir)
451
- - - text = text.replace('__VENV_NAME__', context.env_name)
452
- - - text = text.replace('__VENV_PROMPT__', context.prompt)
453
- - - text = text.replace('__VENV_BIN_NAME__', context.bin_name)
454
- - - text = text.replace('__VENV_PYTHON__', context.env_exe)
455
- - + replacements = {
456
- - + '__VENV_DIR__': context.env_dir,
457
- - + '__VENV_NAME__': context.env_name,
458
- - + '__VENV_PROMPT__': context.prompt,
459
- - + '__VENV_BIN_NAME__': context.bin_name,
460
- - + '__VENV_PYTHON__': context.env_exe,
461
- - + }
462
- - +
463
- - + def quote_ps1(s):
464
- - + """
465
- - + This should satisfy PowerShell quoting rules [1], unless the quoted
466
- - + string is passed directly to Windows native commands [2].
467
- - + [1]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules
468
- - + [2]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_parsing#passing-arguments-that-contain-quote-characters
469
- - + """
470
- - + s = s.replace("'", "''")
471
- - + return f"'{s}'"
472
- - +
473
- - + def quote_bat(s):
474
- - + return s
475
- - +
476
- - + # gh-124651: need to quote the template strings properly
477
- - + quote = shlex.quote
478
- - + script_path = context.script_path
479
- - + if script_path.endswith('.ps1'):
480
- - + quote = quote_ps1
481
- - + elif script_path.endswith('.bat'):
482
- - + quote = quote_bat
483
- - + else:
484
- - + # fallbacks to POSIX shell compliant quote
485
- - + quote = shlex.quote
486
- - +
487
- - + replacements = {key: quote(s) for key, s in replacements.items()}
488
- - + for key, quoted in replacements.items():
489
- - + text = text.replace(key, quoted)
490
- - return text
491
- -
492
- - def install_scripts(self, context, path):
493
- - @@ -392,6 +423,7 @@ def install_scripts(self, context, path):
494
- - with open(srcfile, 'rb') as f:
495
- - data = f.read()
496
- - if not srcfile.endswith(('.exe', '.pdb')):
497
- - + context.script_path = srcfile
498
- - try:
499
- - data = data.decode('utf-8')
500
- - data = self.replace_variables(data, context)
501
- - diff --git a/Lib/venv/scripts/common/activate b/Lib/venv/scripts/common/activate
502
- - index 45af3536aa191d..1d116ca6eda4ed 100644
503
- - --- a/Lib/venv/scripts/common/activate
504
- - +++ b/Lib/venv/scripts/common/activate
505
- - @@ -37,11 +37,11 @@ deactivate () {
506
- - # unset irrelevant variables
507
- - deactivate nondestructive
508
- -
509
- - -VIRTUAL_ENV="__VENV_DIR__"
510
- - +VIRTUAL_ENV=__VENV_DIR__
511
- - export VIRTUAL_ENV
512
- -
513
- - _OLD_VIRTUAL_PATH="$PATH"
514
- - -PATH="$VIRTUAL_ENV/__VENV_BIN_NAME__:$PATH"
515
- - +PATH="$VIRTUAL_ENV/"__VENV_BIN_NAME__":$PATH"
516
- - export PATH
517
- -
518
- - # unset PYTHONHOME if set
519
- - @@ -54,7 +54,7 @@ fi
520
- -
521
- - if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
522
- - _OLD_VIRTUAL_PS1="${PS1:-}"
523
- - - PS1="__VENV_PROMPT__${PS1:-}"
524
- - + PS1=__VENV_PROMPT__"${PS1:-}"
525
- - export PS1
526
- - fi
527
- -
528
- - diff --git a/Lib/venv/scripts/nt/activate.bat b/Lib/venv/scripts/nt/activate.bat
529
- - index af4c7e0abacb1c..5ca475a6e81879 100644
530
- - --- a/Lib/venv/scripts/nt/activate.bat
531
- - +++ b/Lib/venv/scripts/nt/activate.bat
532
- - @@ -8,7 +8,7 @@ if defined _OLD_CODEPAGE (
533
- - "%SystemRoot%\System32\chcp.com" 65001 > nul
534
- - )
535
- -
536
- - -set VIRTUAL_ENV=__VENV_DIR__
537
- - +set "VIRTUAL_ENV=__VENV_DIR__"
538
- -
539
- - if not defined PROMPT set PROMPT=$P$G
540
- -
541
- - @@ -24,7 +24,7 @@ set PYTHONHOME=
542
- - if defined _OLD_VIRTUAL_PATH set PATH=%_OLD_VIRTUAL_PATH%
543
- - if not defined _OLD_VIRTUAL_PATH set _OLD_VIRTUAL_PATH=%PATH%
544
- -
545
- - -set PATH=%VIRTUAL_ENV%\__VENV_BIN_NAME__;%PATH%
546
- - +set "PATH=%VIRTUAL_ENV%\__VENV_BIN_NAME__;%PATH%"
547
- -
548
- - :END
549
- - if defined _OLD_CODEPAGE (
550
- - diff --git a/Lib/venv/scripts/posix/activate.csh b/Lib/venv/scripts/posix/activate.csh
551
- - index 68a0dc74e1a3c7..51301139517f10 100644
552
- - --- a/Lib/venv/scripts/posix/activate.csh
553
- - +++ b/Lib/venv/scripts/posix/activate.csh
554
- - @@ -8,16 +8,16 @@ alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PA
555
- - # Unset irrelevant variables.
556
- - deactivate nondestructive
557
- -
558
- - -setenv VIRTUAL_ENV "__VENV_DIR__"
559
- - +setenv VIRTUAL_ENV __VENV_DIR__
560
- -
561
- - set _OLD_VIRTUAL_PATH="$PATH"
562
- - -setenv PATH "$VIRTUAL_ENV/__VENV_BIN_NAME__:$PATH"
563
- - +setenv PATH "$VIRTUAL_ENV/"__VENV_BIN_NAME__":$PATH"
564
- -
565
- -
566
- - set _OLD_VIRTUAL_PROMPT="$prompt"
567
- -
568
- - if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
569
- - - set prompt = "__VENV_PROMPT__$prompt"
570
- - + set prompt = __VENV_PROMPT__"$prompt"
571
- - endif
572
- -
573
- - alias pydoc python -m pydoc
574
- - diff --git a/Lib/venv/scripts/posix/activate.fish b/Lib/venv/scripts/posix/activate.fish
575
- - index 54b9ea5676b66b..62ab5312d6121b 100644
576
- - --- a/Lib/venv/scripts/posix/activate.fish
577
- - +++ b/Lib/venv/scripts/posix/activate.fish
578
- - @@ -29,10 +29,10 @@ end
579
- - # Unset irrelevant variables.
580
- - deactivate nondestructive
581
- -
582
- - -set -gx VIRTUAL_ENV "__VENV_DIR__"
583
- - +set -gx VIRTUAL_ENV __VENV_DIR__
584
- -
585
- - set -gx _OLD_VIRTUAL_PATH $PATH
586
- - -set -gx PATH "$VIRTUAL_ENV/__VENV_BIN_NAME__" $PATH
587
- - +set -gx PATH "$VIRTUAL_ENV/"__VENV_BIN_NAME__ $PATH
588
- -
589
- - # Unset PYTHONHOME if set.
590
- - if set -q PYTHONHOME
591
- - @@ -52,7 +52,7 @@ if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
592
- - set -l old_status $status
593
- -
594
- - # Output the venv prompt; color taken from the blue of the Python logo.
595
- - - printf "%s%s%s" (set_color 4B8BBE) "__VENV_PROMPT__" (set_color normal)
596
- - + printf "%s%s%s" (set_color 4B8BBE) __VENV_PROMPT__ (set_color normal)
597
- -
598
- - # Restore the return status of the previous command.
599
- - echo "exit $old_status" | .
600
- - diff --git a/Misc/NEWS.d/next/Library/2024-09-28-02-03-04.gh-issue-124651.bLBGtH.rst b/Misc/NEWS.d/next/Library/2024-09-28-02-03-04.gh-issue-124651.bLBGtH.rst
601
- - new file mode 100644
602
- - index 00000000000000..17fc9171390dd9
603
- - --- /dev/null
604
- - +++ b/Misc/NEWS.d/next/Library/2024-09-28-02-03-04.gh-issue-124651.bLBGtH.rst
605
- - @@ -0,0 +1 @@
606
- - +Properly quote template strings in :mod:`venv` activation scripts.
0 commit comments