Skip to content

Commit

Permalink
Add support for process functions in verdi plugin list
Browse files Browse the repository at this point in the history
So far, only actual `Process` subclasses were supported, however,
process functions can be turned into a `Process` subclass just as well.
The wrapped function gets a new attribute `spec`, which when called
returns the `ProcessSpec` that is dynamically built based on the
function signature of the decorated function.
  • Loading branch information
sphuber committed Jun 3, 2020
1 parent e9a3e95 commit 57a7bed
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 3 deletions.
5 changes: 3 additions & 2 deletions aiida/cmdline/commands/cmd_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
# For further information please visit http://www.aiida.net #
###########################################################################
"""Command for `verdi plugins`."""
import inspect

import click

Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions aiida/engine/processes/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
35 changes: 35 additions & 0 deletions tests/cmdline/commands/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Pytest fixtures for command line interface tests."""
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
"""

def _run_cli_command(command, options=None, raises=None):
"""Run the command and check the result.
:param options: the list of command line options to pass to the command invocation
:param raises: optionally an exception class that is expected to be raised
"""
import traceback
from click.testing import CliRunner

runner = CliRunner()
result = runner.invoke(command, options or [])

if raises is not None:
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
43 changes: 43 additions & 0 deletions tests/cmdline/commands/test_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Tests for the `verdi plugin list` command."""
import pytest

from aiida.cmdline.commands import cmd_plugin
from aiida.plugins import CalculationFactory, WorkflowFactory


def test_plugin_list(run_cli_command):
"""Test the `verdi plugin list` command."""
from aiida.plugins.entry_point import ENTRY_POINT_GROUP_TO_MODULE_PATH_MAP

# 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."""
from aiida.plugins.entry_point import ENTRY_POINT_GROUP_TO_MODULE_PATH_MAP

# 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])


@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
12 changes: 11 additions & 1 deletion tests/cmdline/utils/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

0 comments on commit 57a7bed

Please sign in to comment.