feat: auto-complete `plugins` arguments

Support auto-completion of plugin name and path arguments in the `tutor
plugins` commands.
This commit is contained in:
Régis Behmo 2022-11-17 12:18:59 +01:00 committed by Régis Behmo
parent 6069f91cce
commit ee09612326
3 changed files with 49 additions and 5 deletions

View File

@ -0,0 +1 @@
- [Improvement] Auto-completion of `plugins` arguments: `plugins enable/disable NAME` and `install PATH`. (by @regisb)

View File

@ -2,6 +2,7 @@ import unittest
from unittest.mock import Mock, patch
from tutor import plugins
from tutor.commands import plugins as plugins_commands
from .base import TestCommandMixin
@ -40,3 +41,17 @@ class PluginsTests(unittest.TestCase, TestCommandMixin):
result = self.invoke(["plugins", "disable", "notFound"])
self.assertEqual(0, result.exit_code)
self.assertFalse(result.exception)
@patch.object(
plugins,
"iter_info",
return_value=[("aacd", None), ("abcd", None), ("abef", None), ("alba", None)],
)
def test_plugins_name_auto_complete(self, _iter_info: Mock) -> None:
self.assertEqual([], plugins_commands.PluginName().get_names("z"))
self.assertEqual(
["abcd", "abef"], plugins_commands.PluginName().get_names("ab")
)
self.assertEqual(
["all", "alba"], plugins_commands.PluginName(allow_all=True).get_names("al")
)

View File

@ -3,6 +3,7 @@ import typing as t
import urllib.request
import click
import click.shell_completion
from tutor import config as tutor_config
from tutor import exceptions, fmt, hooks, plugins
@ -11,6 +12,31 @@ from tutor.plugins.base import PLUGINS_ROOT, PLUGINS_ROOT_ENV_VAR_NAME
from .context import Context
class PluginName(click.ParamType):
"""
Convenient param type that supports plugin name autocompletion.
"""
def __init__(self, allow_all: bool = False):
self.allow_all = allow_all
def shell_complete(
self, ctx: click.Context, param: click.Parameter, incomplete: str
) -> t.List[click.shell_completion.CompletionItem]:
return [
click.shell_completion.CompletionItem(name)
for name in self.get_names(incomplete)
]
def get_names(self, incomplete: str) -> t.List[str]:
candidates = []
if self.allow_all:
candidates.append("all")
candidates += [name for name, _ in plugins.iter_info()]
return [name for name in candidates if name.startswith(incomplete)]
@click.group(
name="plugins",
short_help="Manage Tutor plugins",
@ -34,12 +60,12 @@ def list_command() -> None:
lines.append((plugin, status, plugin_info))
first_column_width = max([first_column_width, len(plugin) + 2])
for line in lines:
print("{:{width}}\t{:10}\t{}".format(*line, width=first_column_width))
for plugin, status, plugin_info in lines:
print(f"{plugin:{first_column_width}}\t{status:10}\t{plugin_info}")
@click.command(help="Enable a plugin")
@click.argument("plugin_names", metavar="plugin", nargs=-1)
@click.argument("plugin_names", metavar="plugin", nargs=-1, type=PluginName())
@click.pass_obj
def enable(context: Context, plugin_names: t.List[str]) -> None:
config = tutor_config.load_minimal(context.root)
@ -57,7 +83,9 @@ def enable(context: Context, plugin_names: t.List[str]) -> None:
short_help="Disable a plugin",
help="Disable one or more plugins. Specify 'all' to disable all enabled plugins at once.",
)
@click.argument("plugin_names", metavar="plugin", nargs=-1)
@click.argument(
"plugin_names", metavar="plugin", nargs=-1, type=PluginName(allow_all=True)
)
@click.pass_obj
def disable(context: Context, plugin_names: t.List[str]) -> None:
config = tutor_config.load_minimal(context.root)
@ -90,7 +118,7 @@ def printroot() -> None:
help=f"""Install a plugin, either from a local Python/YAML file or a remote, web-hosted
location. The plugin will be installed to {PLUGINS_ROOT_ENV_VAR_NAME}.""",
)
@click.argument("location")
@click.argument("location", type=click.Path(dir_okay=False))
def install(location: str) -> None:
basename = os.path.basename(location)
if not basename.endswith(".yml") and not basename.endswith(".py"):