Source code for aiida.cmdline.groups.verdi

"""Subclass of :class:`click.Group` for the ``verdi`` CLI."""

from __future__ import annotations

import base64
import difflib
import gzip
import typing as t

import click

from aiida.common.exceptions import ConfigurationError
from aiida.common.extendeddicts import AttributeDict
from aiida.manage.configuration import get_config

from ..params import options

__all__ = ('VerdiCommandGroup',)

GIU = (

[docs] class LazyVerdiObjAttributeDict(AttributeDict): """Subclass of ``AttributeDict`` that lazily initializes the ``config`` and ``profile`` attributes. This class guarantees that the ``config`` and ``profile`` attributes never raise an ``AttributeError``. When the attributes are accessed when they are not set, ``config`` is initialized by the value returned by the method :meth:`aiida.manage.configuration.get_config`. The ``profile`` attribute is initialized to ``None``. """ _KEY_CONFIG = 'config' _KEY_PROFILE = 'profile'
[docs] def __init__(self, ctx: click.Context, dictionary: dict[str, t.Any] | None = None): super().__init__(dictionary) self.ctx = ctx
[docs] def __getattr__(self, attr: str) -> t.Any: """Override of ``AttributeDict.__getattr__`` to lazily initialize the ``config`` and ``profile`` attributes. :param attr: The attribute to return. :returns: The value of the attribute. :raises AttributeError: If the attribute does not correspond to an existing key. :raises click.exceptions.UsageError: If loading of the configuration fails. """ if attr == self._KEY_PROFILE: self.setdefault(self._KEY_PROFILE, None) elif attr == self._KEY_CONFIG and self._KEY_CONFIG not in self: try: self[self._KEY_CONFIG] = get_config(create=True) except ConfigurationError as exception: return super().__getattr__(attr)
[docs] class VerdiContext(click.Context): """Custom context implementation that defines the ``obj`` user object and adds the ``Config`` instance."""
[docs] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.obj is None: self.obj = LazyVerdiObjAttributeDict(self)
[docs] class VerdiCommandGroup(click.Group): """Subclass of :class:`click.Group` for the ``verdi`` CLI. The class automatically adds the verbosity option to all commands in the interface. It also adds some functionality to provide suggestions of commands in case the user provided command name does not exist. """ context_class = VerdiContext
[docs] @staticmethod def add_verbosity_option(cmd: click.Command) -> click.Command: """Apply the ``verbosity`` option to the command, which is common to all ``verdi`` commands.""" # Only apply the option if it hasn't been already added in a previous call. if 'verbosity' not in [ for param in cmd.params]: cmd = options.VERBOSITY()(cmd) return cmd
[docs] def fail_with_suggestions(self, ctx: click.Context, cmd_name: str) -> None: """Fail the command while trying to suggest commands to resemble the requested ``cmd_name``.""" # We might get better results with the Levenshtein distance or more advanced methods implemented in FuzzyWuzzy # or similar libs, but this is an easy win for now. matches = difflib.get_close_matches(cmd_name, self.list_commands(ctx), cutoff=0.5) if not matches: # Single letters are sometimes not matched so also try with a simple startswith matches = [c for c in sorted(self.list_commands(ctx)) if c.startswith(cmd_name)][:3] if matches: formatted = '\n'.join(f'\t{m}' for m in sorted(matches))'`{cmd_name}` is not a {} command.\n\nThe most similar commands are:\n{formatted}') else:'`{cmd_name}` is not a {} command.\n\nNo similar commands found.')
[docs] def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None: """Return the command that corresponds to the requested ``cmd_name``. This method is overridden from the base class in order to two functionalities: * If the command is found, automatically add the verbosity option. * If the command is not found, attempt to provide a list of suggestions with existing commands that resemble the requested command name. Note that if the command is not found and ``resilient_parsing`` is set to True on the context, then the latter feature is disabled because most likely we are operating in tab-completion mode. """ if int(cmd_name.lower().encode('utf-8').hex(), 16) == 0x6769757365707065: click.echo(gzip.decompress(base64.b85decode(GIU.encode('utf-8'))).decode('utf-8')) return None cmd = super().get_command(ctx, cmd_name) if cmd is not None: return self.add_verbosity_option(cmd) # If this command is called during tab-completion, we do not want to print an error message if the command can't # be found, but instead we want to simply return here. However, in a normal command execution, we do want to # execute the rest of this method to try and match commands that are similar in order to provide the user with # some hints. The problem is that there is no one canonical way to determine whether the invocation is due to a # normal command execution or a tab-complete operation. The `resilient_parsing` attribute of the `Context` is # designed to allow things like tab-completion, however, it is not the only purpose. For now this is our best # bet though to detect a tab-complete event. When `resilient_parsing` is switched on, we assume a tab-complete # and do nothing in case the command name does not match an actual command. if ctx.resilient_parsing: return None self.fail_with_suggestions(ctx, cmd_name) return None
[docs] def group(self, *args, **kwargs) -> click.Group: """Ensure that sub command groups use the same class but do not override an explicitly set value.""" kwargs.setdefault('cls', self.__class__) return super().group(*args, **kwargs)