Source code for aiida.cmdline.params.types.plugin

# -*- coding: utf-8 -*-
###########################################################################
# Copyright (c), The AiiDA team. All rights reserved.                     #
# This file is part of the AiiDA code.                                    #
#                                                                         #
# The code is hosted on GitHub at https://github.com/aiidateam/aiida-core #
# For further information on the license, see the LICENSE.txt file        #
# For further information please visit http://www.aiida.net               #
###########################################################################
"""Click parameter type for AiiDA Plugins."""

import click

from aiida.cmdline.utils import decorators
from aiida.common import exceptions
from aiida.plugins.entry_point import ENTRY_POINT_STRING_SEPARATOR, ENTRY_POINT_GROUP_PREFIX, EntryPointFormat
from aiida.plugins.entry_point import format_entry_point_string, get_entry_point_string_format
from aiida.plugins.entry_point import get_entry_point, get_entry_points, get_entry_point_groups
from ..types import EntryPointType


[docs]class PluginParamType(EntryPointType): """ AiiDA Plugin name parameter type. :param group: string or tuple of strings, where each is a valid entry point group. Adding the `aiida.` prefix is optional. If it is not detected it will be prepended internally. :param load: when set to True, convert will not return the entry point, but the loaded entry point Usage:: click.option(... type=PluginParamType(group='aiida.calculations') or:: click.option(... type=PluginParamType(group=('calculations', 'data')) """ name = 'plugin'
[docs] def __init__(self, group=None, load=False, *args, **kwargs): """ Validate that group is either a string or a tuple of valid entry point groups, or if it is not specified use the tuple of all recognized entry point groups. """ # pylint: disable=keyword-arg-before-vararg valid_entry_point_groups = get_entry_point_groups() if group is None: self._groups = tuple(valid_entry_point_groups) else: if isinstance(group, str): invalidated_groups = tuple([group]) elif isinstance(group, tuple): invalidated_groups = group else: raise ValueError('invalid type for group') groups = [] for grp in invalidated_groups: if not grp.startswith(ENTRY_POINT_GROUP_PREFIX): grp = ENTRY_POINT_GROUP_PREFIX + grp if grp not in valid_entry_point_groups: raise ValueError('entry point group {} is not recognized'.format(grp)) groups.append(grp) self._groups = tuple(groups) self._init_entry_points() self.load = load super().__init__(*args, **kwargs)
[docs] def _init_entry_points(self): """ Populate entry point information that will be used later on. This should only be called once in the constructor after setting self.groups because the groups should not be changed after instantiation """ self._entry_points = [(group, entry_point) for group in self.groups for entry_point in get_entry_points(group)] self._entry_point_names = [entry_point.name for group in self.groups for entry_point in get_entry_points(group)]
@property def groups(self): return self._groups @property def has_potential_ambiguity(self): """ Returns whether the set of supported entry point groups can lead to ambiguity when only an entry point name is specified. This will happen if one ore more groups share an entry point with a common name """ return len(self._entry_point_names) != len(set(self._entry_point_names))
[docs] def get_valid_arguments(self): """ Return a list of all available plugins for the groups configured for this PluginParamType instance. If the entry point names are not unique, because there are multiple groups that contain an entry point that has an identical name, we need to prefix the names with the full group name :returns: list of valid entry point strings """ if self.has_potential_ambiguity: fmt = EntryPointFormat.FULL return sorted([format_entry_point_string(group, ep.name, fmt=fmt) for group, ep in self._entry_points]) return sorted(self._entry_point_names)
[docs] def get_possibilities(self, incomplete=''): """ Return a list of plugins starting with incomplete """ if incomplete == '': return self.get_valid_arguments() # If there is a chance of ambiguity we always return the entry point string in FULL format, otherwise # return the possibilities in the same format as the incomplete. Note that this may have some unexpected # effects. For example if incomplete equals `aiida.` or `calculations` it will be detected as the MINIMAL # format, even though they would also be the valid beginnings of a FULL or PARTIAL format, except that we # cannot know that for sure at this time if self.has_potential_ambiguity: possibilites = [eps for eps in self.get_valid_arguments() if eps.startswith(incomplete)] else: possibilites = [] fmt = get_entry_point_string_format(incomplete) for group, entry_point in self._entry_points: entry_point_string = format_entry_point_string(group, entry_point.name, fmt=fmt) if entry_point_string.startswith(incomplete): possibilites.append(entry_point_string) return possibilites
[docs] def complete(self, ctx, incomplete): # pylint: disable=unused-argument """ Return possible completions based on an incomplete value :returns: list of tuples of valid entry points (matching incomplete) and a description """ return [(p, '') for p in self.get_possibilities(incomplete=incomplete)]
[docs] def get_missing_message(self, param): return 'Possible arguments are:\n\n' + '\n'.join(self.get_valid_arguments())
[docs] def get_entry_point_from_string(self, entry_point_string): """ Validate a given entry point string, which means that it should have a valid entry point string format and that the entry point unambiguously corresponds to an entry point in the groups configured for this instance of PluginParameterType. :returns: the entry point if valid :raises: ValueError if the entry point string is invalid """ group = None name = None entry_point_format = get_entry_point_string_format(entry_point_string) if entry_point_format in (EntryPointFormat.FULL, EntryPointFormat.PARTIAL): group, name = entry_point_string.split(ENTRY_POINT_STRING_SEPARATOR) if entry_point_format == EntryPointFormat.PARTIAL: group = ENTRY_POINT_GROUP_PREFIX + group if group not in self.groups: raise ValueError('entry point group {} is not supported by this parameter') elif entry_point_format == EntryPointFormat.MINIMAL: name = entry_point_string matching_groups = [group for group, entry_point in self._entry_points if entry_point.name == name] if len(matching_groups) > 1: raise ValueError( "entry point '{}' matches more than one valid entry point group [{}], " 'please specify an explicit group prefix'.format(name, ' '.join(matching_groups)) ) elif not matching_groups: raise ValueError( "entry point '{}' is not valid for any of the allowed " 'entry point groups: {}'.format(name, ' '.join(self.groups)) ) else: group = matching_groups[0] else: ValueError('invalid entry point string format: {}'.format(entry_point_string)) try: entry_point = get_entry_point(group, name) except exceptions.EntryPointError as exception: raise ValueError(exception) return entry_point
[docs] @decorators.with_dbenv() def convert(self, value, param, ctx): """ Convert the string value to an entry point instance, if the value can be successfully parsed into an actual entry point. Will raise click.BadParameter if validation fails. """ value = super().convert(value, param, ctx) if not value: raise click.BadParameter('plugin name cannot be empty') try: entry_point = self.get_entry_point_from_string(value) except ValueError as exception: raise click.BadParameter(str(exception)) if self.load: try: return entry_point.load() except exceptions.LoadingEntryPointError as exception: raise click.BadParameter(str(exception)) else: return entry_point