-
Notifications
You must be signed in to change notification settings - Fork 20
feat: shell completion for jmp, jmp-admin, and j #422
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
raballew
wants to merge
19
commits into
jumpstarter-dev:main
Choose a base branch
from
raballew:039-shell-completion
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
18293ab
feat: add shell completion for jmp-admin and shared completion factory
raballew cb5674e
fix: add unit tests for make_completion_command factory in cli-common
raballew 7364afd
feat: add shell completion for jmp, jmp-admin and j in jmp shell
raballew 724391d
fix: address shell completion review findings
raballew 3035ec5
fix: address remaining shell completion review findings
raballew fefea1f
fix: address CodeRabbit review findings on shell completion
raballew 1358556
fix: initialize zsh completion system before using compdef
raballew ce5026f
fix: address nitpick review findings on shell completion
raballew cb783ba
test: add shell integration tests for bash, zsh, and fish
raballew a02aa26
fix: address CodeRabbit review findings on shell completion
raballew 0e25f3f
fix: set shell prompt after user profile to prevent override
raballew 4fc31e8
fix: use absolute paths for completion commands in shell init
raballew 2a05ddb
fix: mock shutil.which in absolute path test to work in CI
raballew efc04ee
fix(tests): relocate get_completion_class mock test to match refactor…
raballew 907612d
fix: source zshrc before compinit to fix macOS compdef errors
raballew cae8918
fix(shell): source original zshenv when overriding ZDOTDIR
raballew 9664f0c
docs(shell): add docstrings to shell init and launch functions
raballew 43fef15
refactor(shell): move fish init file creation into _launch_fish
raballew c17cc99
test(shell): add fish launch integration test for init file lifecycle
raballew File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
10 changes: 10 additions & 0 deletions
10
python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/completion.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| from jumpstarter_cli_common.completion import make_completion_command | ||
|
|
||
|
|
||
| def _get_admin(): | ||
| from jumpstarter_cli_admin import admin | ||
|
|
||
| return admin | ||
|
|
||
|
|
||
| completion = make_completion_command(_get_admin, "jmp-admin", "_JMP_ADMIN_COMPLETE") |
43 changes: 43 additions & 0 deletions
43
python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/completion_test.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| from click.testing import CliRunner | ||
|
|
||
| from . import admin | ||
|
|
||
|
|
||
| def test_completion_bash_produces_script_with_jmp_admin(): | ||
| runner = CliRunner() | ||
| result = runner.invoke(admin, ["completion", "bash"]) | ||
| assert result.exit_code == 0 | ||
| assert len(result.output) > 0 | ||
| assert "complete" in result.output.lower() | ||
| assert "jmp-admin" in result.output.lower() | ||
|
|
||
|
|
||
| def test_completion_zsh_produces_compdef_for_jmp_admin(): | ||
| runner = CliRunner() | ||
| result = runner.invoke(admin, ["completion", "zsh"]) | ||
| assert result.exit_code == 0 | ||
| assert len(result.output) > 0 | ||
| assert "compdef" in result.output.lower() | ||
|
|
||
|
|
||
| def test_completion_fish_produces_complete_command_for_jmp_admin(): | ||
| runner = CliRunner() | ||
| result = runner.invoke(admin, ["completion", "fish"]) | ||
| assert result.exit_code == 0 | ||
| assert len(result.output) > 0 | ||
| assert "complete" in result.output.lower() | ||
| assert "--command jmp-admin" in result.output.lower() | ||
|
|
||
|
|
||
| def test_completion_missing_argument_exits_with_error(): | ||
| runner = CliRunner() | ||
| result = runner.invoke(admin, ["completion"]) | ||
| assert result.exit_code == 2 | ||
| assert "Missing argument" in result.output or "bash" in result.output | ||
|
|
||
|
|
||
| def test_completion_unsupported_shell_exits_with_error(): | ||
| runner = CliRunner() | ||
| result = runner.invoke(admin, ["completion", "powershell"]) | ||
| assert result.exit_code == 2 | ||
| assert "Invalid value" in result.output or "powershell" in result.output |
19 changes: 19 additions & 0 deletions
19
python/packages/jumpstarter-cli-common/jumpstarter_cli_common/completion.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| from typing import Callable | ||
|
|
||
| import click | ||
| from click.shell_completion import get_completion_class | ||
|
|
||
|
|
||
| def make_completion_command(cli_group_factory: Callable[[], click.Command], prog_name: str, complete_var: str): | ||
| @click.command("completion") | ||
| @click.argument("shell", type=click.Choice(["bash", "zsh", "fish"])) | ||
| def completion(shell: str): | ||
| """Generate shell completion script.""" | ||
| cli_group = cli_group_factory() | ||
| comp_cls = get_completion_class(shell) | ||
| if comp_cls is None: | ||
| raise click.ClickException(f"Unsupported shell: {shell}") | ||
| comp = comp_cls(cli_group, {}, prog_name, complete_var) | ||
| click.echo(comp.source()) | ||
|
|
||
| return completion | ||
75 changes: 75 additions & 0 deletions
75
python/packages/jumpstarter-cli-common/jumpstarter_cli_common/completion_test.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| from unittest.mock import patch | ||
|
|
||
| import click | ||
| from click.testing import CliRunner | ||
|
|
||
| from .completion import make_completion_command | ||
|
|
||
| PROG_NAME = "testcli" | ||
| COMPLETE_VAR = "_TESTCLI_COMPLETE" | ||
|
|
||
|
|
||
| def _make_test_group(): | ||
| @click.group() | ||
| def cli(): | ||
| pass | ||
|
|
||
| return cli | ||
|
|
||
|
|
||
| def _make_test_cli_with_completion(): | ||
| @click.group() | ||
| def cli(): | ||
| pass | ||
|
|
||
| cli.add_command(make_completion_command(_make_test_group, PROG_NAME, COMPLETE_VAR)) | ||
| return cli | ||
|
|
||
|
|
||
| def test_completion_bash_produces_completion_script(): | ||
| cli = _make_test_cli_with_completion() | ||
| runner = CliRunner() | ||
| result = runner.invoke(cli, ["completion", "bash"]) | ||
| assert result.exit_code == 0 | ||
| assert "complete" in result.output.lower() | ||
| assert PROG_NAME in result.output.lower() | ||
|
|
||
|
|
||
| def test_completion_zsh_produces_compdef(): | ||
| cli = _make_test_cli_with_completion() | ||
| runner = CliRunner() | ||
| result = runner.invoke(cli, ["completion", "zsh"]) | ||
| assert result.exit_code == 0 | ||
| assert "compdef" in result.output.lower() | ||
|
|
||
|
|
||
| def test_completion_fish_produces_complete_command(): | ||
| cli = _make_test_cli_with_completion() | ||
| runner = CliRunner() | ||
| result = runner.invoke(cli, ["completion", "fish"]) | ||
| assert result.exit_code == 0 | ||
| assert "complete" in result.output.lower() | ||
| assert f"--command {PROG_NAME}" in result.output.lower() | ||
|
|
||
|
|
||
| def test_completion_missing_argument_exits_with_error(): | ||
| cli = _make_test_cli_with_completion() | ||
| runner = CliRunner() | ||
| result = runner.invoke(cli, ["completion"]) | ||
| assert result.exit_code == 2 | ||
|
|
||
|
|
||
| def test_completion_unsupported_shell_exits_with_error(): | ||
| cli = _make_test_cli_with_completion() | ||
| runner = CliRunner() | ||
| result = runner.invoke(cli, ["completion", "powershell"]) | ||
| assert result.exit_code == 2 | ||
|
|
||
|
|
||
| def test_completion_raises_when_get_completion_class_returns_none(): | ||
| with patch("jumpstarter_cli_common.completion.get_completion_class", return_value=None): | ||
| cli = _make_test_cli_with_completion() | ||
| runner = CliRunner() | ||
| result = runner.invoke(cli, ["completion", "bash"]) | ||
| assert result.exit_code == 1 | ||
| assert "Unsupported shell" in result.output |
17 changes: 6 additions & 11 deletions
17
python/packages/jumpstarter-cli/jumpstarter_cli/completion.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,10 @@ | ||
| import click | ||
| from click.shell_completion import get_completion_class | ||
| from jumpstarter_cli_common.completion import make_completion_command | ||
|
|
||
|
|
||
| @click.command("completion") | ||
| @click.argument("shell", type=click.Choice(["bash", "zsh", "fish"])) | ||
| def completion(shell: str): | ||
| """Generate shell completion script.""" | ||
| def _get_jmp(): | ||
| from jumpstarter_cli.jmp import jmp | ||
|
|
||
| comp_cls = get_completion_class(shell) | ||
| if comp_cls is None: | ||
| raise click.ClickException(f"Unsupported shell: {shell}") | ||
| comp = comp_cls(jmp, {}, "jmp", "_JMP_COMPLETE") | ||
| click.echo(comp.source()) | ||
| return jmp | ||
|
|
||
|
|
||
| completion = make_completion_command(_get_jmp, "jmp", "_JMP_COMPLETE") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
73 changes: 73 additions & 0 deletions
73
python/packages/jumpstarter-cli/jumpstarter_cli/j_completion_test.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| from unittest.mock import AsyncMock, MagicMock, patch | ||
|
|
||
| import anyio | ||
| from anyio import run | ||
| from click.testing import CliRunner | ||
|
|
||
| from .j import _COMPLETION_TIMEOUT_SECONDS, _j_shell_complete, j_completion | ||
|
|
||
|
|
||
| def test_j_completion_bash_produces_script(): | ||
| runner = CliRunner() | ||
| result = runner.invoke(j_completion, ["bash"]) | ||
| assert result.exit_code == 0 | ||
| assert "complete" in result.output.lower() | ||
| assert "_J_COMPLETE" in result.output | ||
|
|
||
|
|
||
| def test_j_completion_zsh_produces_compdef(): | ||
| runner = CliRunner() | ||
| result = runner.invoke(j_completion, ["zsh"]) | ||
| assert result.exit_code == 0 | ||
| assert "compdef" in result.output.lower() | ||
|
|
||
|
|
||
| def test_j_completion_fish_produces_complete_command(): | ||
| runner = CliRunner() | ||
| result = runner.invoke(j_completion, ["fish"]) | ||
| assert result.exit_code == 0 | ||
| assert "complete" in result.output.lower() | ||
| assert "--command j" in result.output.lower() | ||
|
|
||
|
|
||
| def test_j_completion_no_args_exits_with_error(): | ||
| runner = CliRunner() | ||
| result = runner.invoke(j_completion, []) | ||
| assert result.exit_code == 2 | ||
|
|
||
|
|
||
| def test_j_completion_unsupported_shell_exits_with_error(): | ||
| runner = CliRunner() | ||
| result = runner.invoke(j_completion, ["powershell"]) | ||
| assert result.exit_code == 2 | ||
|
|
||
|
|
||
| def test_j_shell_complete_handles_system_exit_cleanly(): | ||
| mock_cli_group = MagicMock() | ||
| mock_cli_group.side_effect = SystemExit(0) | ||
| mock_client = MagicMock() | ||
| mock_client.cli.return_value = mock_cli_group | ||
|
|
||
| with patch("jumpstarter_cli.j.env_async") as mock_env: | ||
| mock_env.return_value.__aenter__ = AsyncMock(return_value=mock_client) | ||
| mock_env.return_value.__aexit__ = AsyncMock(return_value=False) | ||
| run(_j_shell_complete) | ||
| mock_client.cli.assert_called_once() | ||
|
raballew marked this conversation as resolved.
|
||
| mock_cli_group.assert_called_once() | ||
|
|
||
|
|
||
| def test_j_shell_complete_returns_empty_on_timeout(): | ||
| from contextlib import asynccontextmanager | ||
|
|
||
| @asynccontextmanager | ||
| async def slow_env(*args, **kwargs): | ||
| await anyio.sleep(_COMPLETION_TIMEOUT_SECONDS + 1) | ||
| yield MagicMock() | ||
|
|
||
| with patch("jumpstarter_cli.j.env_async", slow_env): | ||
| result = run(_j_shell_complete) | ||
| assert result is None | ||
|
|
||
|
|
||
| def test_completion_timeout_is_positive(): | ||
| assert _COMPLETION_TIMEOUT_SECONDS > 0 | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I recall this one, I also mentioned it in #35 as evaluate the usefulness of
jmp completesubcommand.. however upon further examination, the standard way for Click/Typer seems to be:_JMP_COMPLETE=<zsh/bash/fish>_source jmpwhich already exists without the implementation. thejmp completion <shell>format seems to be used by the Cobra library in tools written in go such as kubectl, and there are other patterns with other libraries and languages.so in this case, we're just adding another command exposing the same completion script for which a command already exists, and it's not the native way for python. there is still "some" benefit of familiarity, as we're working closely with k8s/openshift where the cli is built in Cobra so users may be familiar with it more, but that's a thin one.