diff --git a/aiida/cmdline/commands/cmd_plugin.py b/aiida/cmdline/commands/cmd_plugin.py index 3232441379..e28ae82df7 100644 --- a/aiida/cmdline/commands/cmd_plugin.py +++ b/aiida/cmdline/commands/cmd_plugin.py @@ -8,6 +8,7 @@ # For further information please visit http://www.aiida.net # ########################################################################### """Command for `verdi plugins`.""" +import inspect import click @@ -48,11 +49,11 @@ def plugin_list(entry_point_group, entry_point): echo.echo_critical(str(exception)) else: try: - if issubclass(plugin, Process): + if (inspect.isclass(plugin) and issubclass(plugin, Process)) or plugin.is_process_function: print_process_info(plugin) else: echo.echo(str(plugin.get_description())) - except (AttributeError, TypeError): + except AttributeError: echo.echo_error('No description available for {}'.format(entry_point)) else: entry_points = get_entry_point_names(entry_point_group) diff --git a/aiida/engine/processes/functions.py b/aiida/engine/processes/functions.py index f23a5cbdee..8c5ea9bcca 100644 --- a/aiida/engine/processes/functions.py +++ b/aiida/engine/processes/functions.py @@ -186,6 +186,7 @@ def decorated_function(*args, **kwargs): decorated_function.run_get_node = run_get_node decorated_function.is_process_function = True decorated_function.node_class = node_class + decorated_function.spec = process_class.spec return decorated_function diff --git a/tests/cmdline/commands/conftest.py b/tests/cmdline/commands/conftest.py new file mode 100644 index 0000000000..77e61ea530 --- /dev/null +++ b/tests/cmdline/commands/conftest.py @@ -0,0 +1,39 @@ +"""Pytest fixtures for command line interface tests.""" +import click +import pytest + + +@pytest.fixture +def run_cli_command(): + """Run a `click` command with the given options. + + The call will raise if the command triggered an exception or the exit code returned is non-zero. + """ + from click.testing import Result + + def _run_cli_command(command: click.Command, options: list = None, raises: bool = False) -> Result: + """Run the command and check the result. + + .. note:: the `output_lines` attribute is added to return value containing list of stripped output lines. + + :param options: the list of command line options to pass to the command invocation + :param raises: whether the command is expected to raise an exception + :return: test result + """ + import traceback + + runner = click.testing.CliRunner() + result = runner.invoke(command, options or []) + + if raises: + assert result.exception is not None, result.output + assert result.exit_code != 0 + else: + assert result.exception is None, ''.join(traceback.format_exception(*result.exc_info)) + assert result.exit_code == 0, result.output + + result.output_lines = [line.strip() for line in result.output.split('\n') if line.strip()] + + return result + + return _run_cli_command diff --git a/tests/cmdline/commands/test_plugin.py b/tests/cmdline/commands/test_plugin.py new file mode 100644 index 0000000000..ddc5e9d3df --- /dev/null +++ b/tests/cmdline/commands/test_plugin.py @@ -0,0 +1,49 @@ +"""Tests for the `verdi plugin list` command.""" +import pytest + +from aiida.cmdline.commands import cmd_plugin +from aiida.plugins import CalculationFactory, WorkflowFactory +from aiida.plugins.entry_point import ENTRY_POINT_GROUP_TO_MODULE_PATH_MAP + + +def test_plugin_list(run_cli_command): + """Test the `verdi plugin list` command. + + Call base command without parameters and check that all entry point groups are listed. + """ + result = run_cli_command(cmd_plugin.plugin_list, []) + for key in ENTRY_POINT_GROUP_TO_MODULE_PATH_MAP: + assert key in result.output + + +def test_plugin_list_group(run_cli_command): + """Test the `verdi plugin list` command for entry point group. + + Call for each entry point group and just check it doesn't except. + """ + for key in ENTRY_POINT_GROUP_TO_MODULE_PATH_MAP: + run_cli_command(cmd_plugin.plugin_list, [key]) + + +def test_plugin_list_non_existing(run_cli_command): + """Test the `verdi plugin list` command for a non-existing entry point.""" + run_cli_command(cmd_plugin.plugin_list, ['aiida.calculations', 'non_existing'], raises=True) + + +@pytest.mark.parametrize( + 'entry_point_string', ( + 'aiida.calculations:arithmetic.add', + 'aiida.workflows:arithmetic.multiply_add', + 'aiida.workflows:arithmetic.add_multiply', + ) +) +def test_plugin_list_detail(run_cli_command, entry_point_string): + """Test the `verdi plugin list` command for specific entry points.""" + from aiida.plugins.entry_point import parse_entry_point_string + + entry_point_group, entry_point_name = parse_entry_point_string(entry_point_string) + factory = CalculationFactory if entry_point_group == 'aiida.calculations' else WorkflowFactory + entry_point = factory(entry_point_name) + + result = run_cli_command(cmd_plugin.plugin_list, [entry_point_group, entry_point_name]) + assert entry_point.__doc__ in result.output diff --git a/tests/cmdline/utils/test_common.py b/tests/cmdline/utils/test_common.py index dea9f7ac1c..c755f34345 100644 --- a/tests/cmdline/utils/test_common.py +++ b/tests/cmdline/utils/test_common.py @@ -78,7 +78,7 @@ def test_print_process_info(self): """Test the `print_process_info` method.""" from aiida.cmdline.utils.common import print_process_info from aiida.common.utils import Capturing - from aiida.engine import Process + from aiida.engine import Process, calcfunction class TestProcessWithoutDocstring(Process): # pylint: disable=missing-docstring @@ -96,7 +96,17 @@ def define(cls, spec): super().define(spec) spec.input('some_input') + @calcfunction + def test_without_docstring(): + pass + + @calcfunction + def test_with_docstring(): + """Some docstring.""" + # We are just checking that the command does not except with Capturing(): print_process_info(TestProcessWithoutDocstring) print_process_info(TestProcessWithDocstring) + print_process_info(test_without_docstring) + print_process_info(test_with_docstring)