Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/virtualenv/activation/nushell/activate.nu
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
48 changes: 48 additions & 0 deletions tests/unit/activation/test_nushell.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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):
Expand All @@ -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
Loading