diff --git a/src/virtualenv/activation/nushell/activate.nu b/src/virtualenv/activation/nushell/activate.nu index 7046c5880..559872d23 100644 --- a/src/virtualenv/activation/nushell/activate.nu +++ b/src/virtualenv/activation/nushell/activate.nu @@ -2,8 +2,9 @@ # - Activate with `overlay use activate.nu` # - Deactivate with `deactivate`, as usual # -# To customize the overlay name, you can call `overlay use activate.nu as foo`, but then simply `deactivate` won't work -# because it is just an alias to hide the "activate" overlay. You'd need to call `overlay hide foo` manually. +# To customize the overlay name, call `overlay use activate.nu as foo`, but then `deactivate` won't work — +# you'd need to run `overlay hide foo` manually. Do not activate with `use activate.nu *`; that does not +# create an overlay and deactivation will fail. module warning { export-env { @@ -86,4 +87,5 @@ export-env { } export alias pydoc = python -m pydoc +# If deactivate errors "not an active overlay": activate with `overlay use activate.nu` not `use activate.nu *`; for custom names use `overlay hide NAME` export alias deactivate = overlay hide activate diff --git a/tests/unit/activation/test_nushell.py b/tests/unit/activation/test_nushell.py index 284de52fe..e7cb63028 100644 --- a/tests/unit/activation/test_nushell.py +++ b/tests/unit/activation/test_nushell.py @@ -1,10 +1,18 @@ from __future__ import annotations +import subprocess from argparse import Namespace from shutil import which +from typing import TYPE_CHECKING + +import pytest from virtualenv.activation import NushellActivator from virtualenv.info import IS_WIN +from virtualenv.run import cli_run + +if TYPE_CHECKING: + from pathlib import Path def test_nushell_tkinter_generation(tmp_path) -> None: @@ -47,6 +55,16 @@ def __init__(self, dest) -> None: assert expected_tcl in content assert expected_tk in content + # overlay hide is a parser keyword: a def body would fail at parse time because the overlay doesn't exist yet + # when the def is compiled. The alias defers that check to call time, when the overlay is active. + assert "export alias deactivate = overlay hide activate" in content + # nushell shows one line of context before the error site, so placing the hint comment directly above the alias + # makes it appear in the error output users see when they activate via `use *` or a custom name (gh-3103). + lines = content.splitlines() + alias_idx = next(i for i, line in enumerate(lines) if "export alias deactivate" in line) + assert alias_idx > 0 + assert "overlay use activate.nu" in lines[alias_idx - 1] + def test_nushell(activation_tester_class, activation_tester) -> None: class Nushell(activation_tester_class): @@ -66,3 +84,33 @@ def activate_call(self, script): return f"{cmd} {scr}".strip() activation_tester(Nushell) + + +def test_nushell_deactivate_errors(tmp_path: Path) -> None: + """Regression for gh-3103: both misuse patterns give actionable inline errors. + + `^nu | complete` captures stderr from child invocations without aborting the outer script, so both cases run in a + single subprocess call. + + """ + nu = which("nu") + if nu is None: + pytest.skip("nu not installed") + + activate_nu = cli_run(["--without-pip", str(tmp_path / "venv")]).creator.bin_dir / "activate.nu" + quoted = NushellActivator.quote(str(activate_nu)) + + # `to nuon` re-quotes the path so it remains valid nushell syntax after string interpolation. + script = f"""\ +let path = {quoted} +let r1 = (^nu --commands $"use ($path | to nuon) *; deactivate" | complete) +if ($r1.exit_code == 0) {{ error make {{ msg: "expected deactivate to fail for use-star" }} }} +if not ("not an active overlay" in $r1.stderr) {{ error make {{ msg: "overlay error missing" }} }} +if not ("overlay use activate.nu" in $r1.stderr) {{ error make {{ msg: "hint missing for use-star" }} }} +let r2 = (^nu --commands $"overlay use ($path | to nuon) as myenv; deactivate" | complete) +if ($r2.exit_code == 0) {{ error make {{ msg: "expected deactivate to fail for custom name" }} }} +if not ("not an active overlay" in $r2.stderr) {{ error make {{ msg: "overlay error missing" }} }} +if not ("overlay hide NAME" in $r2.stderr) {{ error make {{ msg: "hint missing for custom name" }} }} +""" + result = subprocess.run([nu, "--commands", script], capture_output=True, text=True, timeout=60, check=False) + assert result.returncode == 0, result.stderr