diff --git a/tests/test_shell_completion_abort_exit.py b/tests/test_shell_completion_abort_exit.py new file mode 100644 index 0000000000..646c10e011 --- /dev/null +++ b/tests/test_shell_completion_abort_exit.py @@ -0,0 +1,65 @@ +import click +import pytest +from typer.completion import shell_complete + + +class _FakeCompletion: + def __init__(self, cli, ctx_args, prog_name, complete_var): + self.cli = cli + self.ctx_args = ctx_args + self.prog_name = prog_name + self.complete_var = complete_var + + def source(self): + # This will be overridden per-test via monkeypatch on the class + return "" + + def complete(self): + # This will be overridden per-test via monkeypatch on the class + return "" + + +@pytest.mark.parametrize( + "instruction", + ["complete_zsh", "source_zsh"], +) +@pytest.mark.parametrize( + "exc", + [click.exceptions.Abort, click.exceptions.Exit], +) +def test_shell_complete_handles_abort_and_exit(monkeypatch, capsys, instruction, exc): + monkeypatch.setattr( + click.shell_completion, + "get_completion_class", + lambda shell: _FakeCompletion, + ) + + if instruction.startswith("complete"): + monkeypatch.setattr( + _FakeCompletion, "complete", lambda self: (_ for _ in ()).throw(exc()) + ) + else: + monkeypatch.setattr( + _FakeCompletion, "source", lambda self: (_ for _ in ()).throw(exc()) + ) + + cli = click.Command("demo") + + code = shell_complete( + cli=cli, + ctx_args={}, + prog_name="demo", + complete_var="_DEMO_COMPLETE", + instruction=instruction, + ) + + out = capsys.readouterr() + assert code == 0 + assert out.out == "" + assert out.err == "" + + +def test_fake_completion_default_methods_are_covered(): + fc = _FakeCompletion(click.Command("demo"), {}, "demo", "_DEMO_COMPLETE") + assert fc.complete() == "" + assert fc.source() == "" diff --git a/typer/completion.py b/typer/completion.py index db87f83e3f..1ce7cdc51a 100644 --- a/typer/completion.py +++ b/typer/completion.py @@ -134,12 +134,22 @@ def shell_complete( comp = comp_cls(cli, ctx_args, prog_name, complete_var) if instruction == "source": - click.echo(comp.source()) + try: + click.echo(comp.source()) + except (click.exceptions.Abort, click.exceptions.Exit): + # During shell completion we should never show a traceback. + # If completion callbacks abort/exit, just return no output. + return 0 return 0 # Typer override to print the completion help msg with Rich if instruction == "complete": - click.echo(comp.complete()) + try: + click.echo(comp.complete()) + except (click.exceptions.Abort, click.exceptions.Exit): + # During shell completion we should never show a traceback. + # If completion callbacks abort/exit, just return no output. + return 0 return 0 # Typer override end