-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
CLI: update to be compatible with
aiida-core==2.1
Currently the CLI is broken with `aiida-core==2.1`. The reason is that we are using the `PROFILE` option, which relies on the command that it is attached to configure certain things, such as the context. In v2.1, the behavior was changed causing the commands to except because the `PROFILE` option cannot find certain attributes it expects in the context. For the `PROFILE` option to work, the root command should use the class `aiida.cmdline.groups.VerdiCommandGroup` as its `cls`. This class defines a custom context class that is necessary. It also automatically provides the verbosity option for all subcommands, so the explicit declaration can now be removed. The problem was not detected in the unit tests because they only show up when the root command is invoked. The unit tests call the subcommands directly though, and this circumvents the root command. This is documented behavior of `click` and there is no way around it. The only solution is to call the commands through the command line interface exactly as they would be called normally. This is now done in the `test_commands.py` file. The test parametrizes over all existing (sub)commands and directly calls it as a subprocess. This guarantees that the root command is called including its specific context that needs to be setup. The `VerdiCommandGroup` also automatically adds the `-v/--verbosity` option to all commands. This is useful as it automatically controls the logging configuration setup by `aiida-core`. However, the `-v` short flag overlaps with our `-v/--version` option that is used to designate pseudo potential family versions. To solve this the `VerdiCommandGroup` clas is subclassed to override the `add_verbosity_option` method so that we can change the `VERBOSITY` option for a custom one, that only defines the `--verbosity` long option.
- Loading branch information
Showing
4 changed files
with
83 additions
and
54 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,46 +1,29 @@ | ||
# -*- coding: utf-8 -*- | ||
"""Command line interface `aiida-pseudo`.""" | ||
from aiida.cmdline.params import options, types | ||
from aiida.cmdline.groups.verdi import VerdiCommandGroup | ||
import click | ||
|
||
from .params import options | ||
|
||
class VerbosityGroup(click.Group): | ||
"""Custom command group that automatically adds the ``VERBOSITY`` option to all subcommands.""" | ||
|
||
class CustomVerdiCommandGroup(VerdiCommandGroup): | ||
"""Subclass of :class:`aiida.cmdline.groups.verdi.VerdiCommandGroup` for the CLI. | ||
This subclass overrides the verbosity option to use a custom one that removes the ``-v`` short version of the option | ||
since that is used by other options in this CLI and so would clash. | ||
""" | ||
|
||
@staticmethod | ||
def add_verbosity_option(cmd): | ||
"""Apply the ``verbosity`` option to the command, which is common to all ``verdi`` commands.""" | ||
if 'verbosity' not in [param.name for param in cmd.params]: | ||
"""Apply the ``verbosity`` option to the command, which is common to all subcommands.""" | ||
if cmd is not None and 'verbosity' not in [param.name for param in cmd.params]: | ||
cmd = options.VERBOSITY()(cmd) | ||
|
||
return cmd | ||
|
||
def group(self, *args, **kwargs): | ||
"""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) | ||
|
||
def get_command(self, ctx, cmd_name): | ||
"""Return the command that corresponds to the requested ``cmd_name``. | ||
This method is overridden from the base class in order to automatically add the verbosity option. | ||
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. | ||
""" | ||
cmd = super().get_command(ctx, cmd_name) | ||
|
||
if cmd is not None: | ||
return self.add_verbosity_option(cmd) | ||
|
||
if ctx.resilient_parsing: | ||
return None | ||
|
||
return ctx.fail(f'`{cmd_name}` is not a {self.name} command.') | ||
|
||
|
||
@click.group('aiida-pseudo', context_settings={'help_option_names': ['-h', '--help']}) | ||
@options.PROFILE(type=types.ProfileParamType(load_profile=True), expose_value=False) | ||
@click.group('aiida-pseudo', cls=CustomVerdiCommandGroup, context_settings={'help_option_names': ['-h', '--help']}) | ||
@options.VERBOSITY() | ||
@options.PROFILE() | ||
def cmd_root(): | ||
"""CLI for the ``aiida-pseudo`` plugin.""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,47 @@ | ||
# -*- coding: utf-8 -*- | ||
"""Test the root command of the CLI.""" | ||
"""Tests for CLI commands.""" | ||
from __future__ import annotations | ||
|
||
import subprocess | ||
|
||
import click | ||
import pytest | ||
|
||
from aiida_pseudo.cli import cmd_root | ||
|
||
|
||
def test_root(run_cli_command): | ||
"""Test the root command for the CLI is callable.""" | ||
run_cli_command(cmd_root) | ||
def recurse_commands(command: click.Command, parents: list[str] = None): | ||
"""Recursively return all subcommands that are part of ``command``. | ||
:param command: The click command to start with. | ||
:param parents: A list of strings that represent the parent commands leading up to the current command. | ||
:returns: A list of strings denoting the full path to the current command. | ||
""" | ||
if isinstance(command, click.Group): | ||
for command_name in command.commands: | ||
subcommand = command.get_command(None, command_name) | ||
if parents is not None: | ||
subparents = parents + [command.name] | ||
else: | ||
subparents = [command.name] | ||
yield from recurse_commands(subcommand, subparents) | ||
|
||
if parents is not None: | ||
yield parents + [command.name] | ||
else: | ||
yield [command.name] | ||
|
||
|
||
@pytest.mark.parametrize('command', recurse_commands(cmd_root)) | ||
@pytest.mark.parametrize('help_option', ('--help', '-h')) | ||
def test_commands_help_option(command, help_option): | ||
"""Test the help options for all subcommands of the CLI. | ||
for option in ['-h', '--help']: | ||
result = run_cli_command(cmd_root, [option]) | ||
assert cmd_root.__doc__ in result.output | ||
The usage of ``subprocess.run`` is on purpose because using :meth:`click.Context.invoke`, which is used by the | ||
``run_cli_command`` fixture that should usually be used in testing CLI commands, does not behave exactly the same | ||
compared to a direct invocation on the command line. The invocation through ``invoke`` does not go through all the | ||
parent commands and so might not get all the necessary initializations. | ||
""" | ||
result = subprocess.run(command + [help_option], check=False, capture_output=True, text=True) | ||
assert result.returncode == 0, result.stderr | ||
assert 'Usage:' in result.stdout |