diff --git a/src/_nebari/subcommands/plugin.py b/src/_nebari/subcommands/plugin.py new file mode 100644 index 000000000..97893ebca --- /dev/null +++ b/src/_nebari/subcommands/plugin.py @@ -0,0 +1,42 @@ +import rich +import typer +from rich.table import Table + +from importlib.metadata import version + +from _nebari.version import __version__ +from nebari.hookspecs import hookimpl + +@hookimpl +def nebari_subcommand(cli: typer.Typer): + plugin_cmd = typer.Typer( + add_completion=False, + no_args_is_help=True, + rich_markup_mode="rich", + context_settings={"help_option_names": ["-h", "--help"]}, + ) + + cli.add_typer( + plugin_cmd, + name="plugin", + help="Interact with nebari plugins", + rich_help_panel="Additional Commands" + ) + + @plugin_cmd.command() + def list(ctx: typer.Context): + """ + List installed plugins + """ + from nebari.plugins import nebari_plugin_manager + + external_plugins = nebari_plugin_manager.get_external_plugins() + + table = Table(title="Plugins") + table.add_column("name", justify="left", no_wrap=True) + table.add_column("version", justify="left", no_wrap=True) + + for plugin in external_plugins: + table.add_row(plugin, version(plugin)) + + rich.print(table) diff --git a/src/nebari/plugins.py b/src/nebari/plugins.py index 9eac0c1b5..a6cb1aa68 100644 --- a/src/nebari/plugins.py +++ b/src/nebari/plugins.py @@ -19,6 +19,7 @@ "_nebari.subcommands.deploy", "_nebari.subcommands.destroy", "_nebari.subcommands.keycloak", + "_nebari.subcommands.plugin", "_nebari.subcommands.render", "_nebari.subcommands.support", "_nebari.subcommands.upgrade", diff --git a/tests/tests_unit/test_cli_plugin.py b/tests/tests_unit/test_cli_plugin.py new file mode 100644 index 000000000..91db3c05c --- /dev/null +++ b/tests/tests_unit/test_cli_plugin.py @@ -0,0 +1,66 @@ +import pytest +from typing import List +from typer.testing import CliRunner +from unittest.mock import Mock, patch + +from _nebari.cli import create_cli + +runner = CliRunner() + +@pytest.mark.parametrize( + "args, exit_code, content", + [ + # --help + ([], 0, ["Usage:"]), + (["--help"], 0, ["Usage:"]), + (["-h"], 0, ["Usage:"]), + (["list", "--help"], 0, ["Usage:"]), + (["list", "-h"], 0, ["Usage:"]), + (["list"], 0, ["Plugins"]), + ], +) +def test_cli_plugin_stdout(args: List[str], exit_code: int, content: List[str]): + app = create_cli() + result = runner.invoke(app, ["plugin"] + args) + assert result.exit_code == exit_code + for c in content: + assert c in result.stdout + + +def mock_get_plugins(): + mytestexternalplugin = Mock() + mytestexternalplugin.__name__ = "mytestexternalplugin" + + otherplugin = Mock() + otherplugin.__name__ = "otherplugin" + + return [mytestexternalplugin, otherplugin] + + +def mock_version(pkg): + pkg_version_map = { + "mytestexternalplugin": "0.4.4", + "otherplugin": "1.1.1", + } + return pkg_version_map.get(pkg) + + +@patch( + "nebari.plugins.NebariPluginManager.plugin_manager.get_plugins", + mock_get_plugins +) +@patch( + "_nebari.subcommands.plugin.version", + mock_version +) +def test_cli_plugin_list_external_plugins(): + app = create_cli() + result = runner.invoke(app, ["plugin", "list"]) + assert result.exit_code == 0 + expected_ouput = [ + "Plugins", + "mytestexternalplugin │ 0.4.4", + "otherplugin │ 1.1.1" + ] + for c in expected_ouput: + assert c in result.stdout