Skip to content

Commit

Permalink
Add ability to list user installed plugins from the CLI (#2891)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelovilla authored Jan 7, 2025
2 parents ff66c22 + 3cecf54 commit a095b25
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 2 deletions.
15 changes: 13 additions & 2 deletions src/_nebari/subcommands/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@

@hookimpl
def nebari_subcommand(cli: typer.Typer):
EXTERNAL_PLUGIN_STYLE = "cyan"

@cli.command()
def info(ctx: typer.Context):
from nebari.plugins import nebari_plugin_manager

rich.print(f"Nebari version: {__version__}")

external_plugins = nebari_plugin_manager.get_external_plugins()

hooks = collections.defaultdict(list)
for plugin in nebari_plugin_manager.plugin_manager.get_plugins():
for hook in nebari_plugin_manager.plugin_manager.get_hookcallers(plugin):
Expand All @@ -27,7 +31,8 @@ def info(ctx: typer.Context):

for hook_name, modules in hooks.items():
for module in modules:
table.add_row(hook_name, module)
style = EXTERNAL_PLUGIN_STYLE if module in external_plugins else None
table.add_row(hook_name, module, style=style)

rich.print(table)

Expand All @@ -36,8 +41,14 @@ def info(ctx: typer.Context):
table.add_column("priority")
table.add_column("module")
for stage in nebari_plugin_manager.ordered_stages:
style = (
EXTERNAL_PLUGIN_STYLE if stage.__module__ in external_plugins else None
)
table.add_row(
stage.name, str(stage.priority), f"{stage.__module__}.{stage.__name__}"
stage.name,
str(stage.priority),
f"{stage.__module__}.{stage.__name__}",
style=style,
)

rich.print(table)
42 changes: 42 additions & 0 deletions src/_nebari/subcommands/plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from importlib.metadata import version

import rich
import typer
from rich.table import Table

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)
9 changes: 9 additions & 0 deletions src/nebari/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -121,6 +122,14 @@ def read_config(self, config_path: typing.Union[str, Path], **kwargs):

return read_configuration(config_path, self.config_schema, **kwargs)

def get_external_plugins(self):
external_plugins = []
all_plugins = DEFAULT_SUBCOMMAND_PLUGINS + DEFAULT_STAGES_PLUGINS
for plugin in self.plugin_manager.get_plugins():
if plugin.__name__ not in all_plugins:
external_plugins.append(plugin.__name__)
return external_plugins

@property
def ordered_stages(self):
return self.get_available_stages()
Expand Down
64 changes: 64 additions & 0 deletions tests/tests_unit/test_cli_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from typing import List
from unittest.mock import Mock, patch

import pytest
from typer.testing import CliRunner

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_output = [
"Plugins",
"mytestexternalplugin │ 0.4.4",
"otherplugin │ 1.1.1",
]
for c in expected_output:
assert c in result.stdout

0 comments on commit a095b25

Please sign in to comment.