diff --git a/docs/changelog/+batch-unset-path.bugfix.rst b/docs/changelog/+batch-unset-path.bugfix.rst new file mode 100644 index 000000000..000e65e6a --- /dev/null +++ b/docs/changelog/+batch-unset-path.bugfix.rst @@ -0,0 +1,2 @@ +Undefine ``PATH`` again in ``deactivate.bat`` when it was undefined at activation, instead of leaving the environment's +``Scripts`` folder on it, and stop a second ``activate.bat`` from adding that folder twice. diff --git a/src/virtualenv/activation/batch/activate.bat b/src/virtualenv/activation/batch/activate.bat index 23bd6c81b..b050addfc 100644 --- a/src/virtualenv/activation/batch/activate.bat +++ b/src/virtualenv/activation/batch/activate.bat @@ -7,6 +7,7 @@ @REM Undo an activation that is still in effect, otherwise its values get saved as the ones to restore @if defined _OLD_VIRTUAL_PATH @call "__VIRTUAL_ENV_CALL__\__BIN_NAME__\deactivate.bat" +@if defined _OLD_VIRTUAL_PATH_UNSET @call "__VIRTUAL_ENV_CALL__\__BIN_NAME__\deactivate.bat" @set "VIRTUAL_ENV=__VIRTUAL_ENV__" @@ -52,6 +53,8 @@ @REM ) else ( @if defined _OLD_VIRTUAL_PATH @goto ENDIFVPATH2 @set "_OLD_VIRTUAL_PATH=%PATH%" + @REM An undefined PATH leaves _OLD_VIRTUAL_PATH undefined too, so record it for deactivate.bat + @if not defined PATH @set _OLD_VIRTUAL_PATH_UNSET=1 :ENDIFVPATH2 @set "PATH=%VIRTUAL_ENV%\__BIN_NAME__;%PATH%" diff --git a/src/virtualenv/activation/batch/deactivate.bat b/src/virtualenv/activation/batch/deactivate.bat index d486958ad..e7a566608 100644 --- a/src/virtualenv/activation/batch/deactivate.bat +++ b/src/virtualenv/activation/batch/deactivate.bat @@ -28,3 +28,5 @@ @set "PATH=%_OLD_VIRTUAL_PATH%" @set _OLD_VIRTUAL_PATH= :ENDIFVPATH +@if defined _OLD_VIRTUAL_PATH_UNSET @set PATH= +@set _OLD_VIRTUAL_PATH_UNSET= diff --git a/tests/unit/activation/test_batch.py b/tests/unit/activation/test_batch.py index b4da632c0..8c365f6b7 100644 --- a/tests/unit/activation/test_batch.py +++ b/tests/unit/activation/test_batch.py @@ -173,7 +173,18 @@ def test_batch_skips_changed_paths( pytest.param("@set PROMPT=", "$P$G", id="prompt-unset"), ], ) -def test_batch_path_round_trip(tmp_path: Path, name: str, delayed: str, set_prompt: str, prompt: str) -> None: +@pytest.mark.parametrize( + "path_source", + [ + pytest.param("PATH", id="path-set"), + # a build script that starts from a clean environment can run `set PATH=` before activating; an undefined + # variable expands to nothing in a batch file, so taking PATH from it leaves PATH undefined + pytest.param("VIRTUALENV_UNDEFINED_PATH", id="path-unset"), + ], +) +def test_batch_path_round_trip( # ruff:ignore[too-many-arguments] + tmp_path: Path, name: str, delayed: str, set_prompt: str, prompt: str, path_source: str +) -> None: creator: Final[Creator] = session_via_cli([str(tmp_path / name), "--no-seed"], setup_logging=False).creator creator.bin_dir.mkdir(parents=True) BatchActivator(VirtualEnvOptions(prompt="roundtrip$P")).generate(creator) @@ -182,8 +193,11 @@ def test_batch_path_round_trip(tmp_path: Path, name: str, delayed: str, set_prom "print(json.dumps({k:os.environ.get(k) for k in ('VIRTUAL_ENV','PATH','PROMPT')}))\"" ) (creator.bin_dir / "check.bat").write_text( - f"@echo off\n@set VIRTUAL_ENV=\n@set _OLD_VIRTUAL_PATH=\n@set _OLD_VIRTUAL_PROMPT=\n{set_prompt}\n" - f"@call activate.bat\n{snapshot}\n@call activate.bat\n{snapshot}\n@call deactivate.bat\n{snapshot}", + "@echo off\n@set VIRTUAL_ENV=\n@set _OLD_VIRTUAL_PATH=\n@set _OLD_VIRTUAL_PATH_UNSET=\n" + f'@set _OLD_VIRTUAL_PROMPT=\n{set_prompt}\n@set "PATH=%{path_source}%"\n' + f"@call activate.bat\n{snapshot}\n@call activate.bat\n{snapshot}\n@call deactivate.bat\n{snapshot}\n" + # a later activation from another PATH must save that PATH, not state that deactivate left behind + f'@set "PATH=%SystemRoot%"\n@call activate.bat\n{snapshot}', encoding="utf-8", ) result: Final[subprocess.CompletedProcess[str]] = subprocess.run( @@ -197,13 +211,14 @@ def test_batch_path_round_trip(tmp_path: Path, name: str, delayed: str, set_prom ) active: Final[dict[str, str]] = { "VIRTUAL_ENV": str(creator.dest), - "PATH": f"{creator.bin_dir};{os.environ['PATH']}", + "PATH": f"{creator.bin_dir};{os.environ.get(path_source, '')}", "PROMPT": f"(roundtrip$$P) {prompt}", } assert [json.loads(line) for line in result.stdout.splitlines()] == [ active, active, - {"VIRTUAL_ENV": None, "PATH": os.environ["PATH"], "PROMPT": prompt}, + {"VIRTUAL_ENV": None, "PATH": os.environ.get(path_source), "PROMPT": prompt}, + {**active, "PATH": f"{creator.bin_dir};{os.environ['SYSTEMROOT']}"}, ] assert not result.stderr