Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI: update to be compatible with aiida-core==2.1 #136

Merged
merged 1 commit into from
Nov 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ classifiers = [
keywords = ['aiida', 'pseudopotentials']
requires-python = '>=3.8'
dependencies = [
'aiida-core~=2.0',
'aiida-core~=2.1',
'click~=8.0',
'pint~=0.16.1',
'requests~=2.20',
Expand Down
43 changes: 27 additions & 16 deletions src/aiida_pseudo/cli/params/options.py
Original file line number Diff line number Diff line change
@@ -1,66 +1,77 @@
# -*- coding: utf-8 -*-
"""Reusable options for CLI commands."""
import functools
import shutil

from aiida.cmdline.params.options import OverridableOption
from aiida.cmdline.params import options as core_options
from aiida.cmdline.params import types as core_types
import click

from .types import PseudoPotentialFamilyTypeParam, PseudoPotentialTypeParam, UnitParamType

__all__ = (
'VERSION', 'FUNCTIONAL', 'RELATIVISTIC', 'PROTOCOL', 'PSEUDO_FORMAT', 'STRINGENCY', 'DEFAULT_STRINGENCY',
'TRACEBACK', 'FAMILY_TYPE', 'ARCHIVE_FORMAT', 'UNIT', 'DOWNLOAD_ONLY'
'PROFILE', 'VERBOSITY', 'VERSION', 'FUNCTIONAL', 'RELATIVISTIC', 'PROTOCOL', 'PSEUDO_FORMAT', 'STRINGENCY',
'DEFAULT_STRINGENCY', 'TRACEBACK', 'FAMILY_TYPE', 'ARCHIVE_FORMAT', 'UNIT', 'DOWNLOAD_ONLY'
)

VERSION = OverridableOption(
PROFILE = functools.partial(
core_options.PROFILE, type=core_types.ProfileParamType(load_profile=True), expose_value=False
)

# Clone the ``VERBOSITY`` option from ``aiida-core`` so the ``-v`` short flag can be removed, since that overlaps with
# the flag of the ``VERSION`` option of this CLI.
VERBOSITY = core_options.VERBOSITY.clone()
VERBOSITY.args = ('--verbosity',)

VERSION = core_options.OverridableOption(
'-v', '--version', type=click.STRING, required=False, help='Select the version of the installed configuration.'
)

FUNCTIONAL = OverridableOption(
FUNCTIONAL = core_options.OverridableOption(
'-x',
'--functional',
type=click.STRING,
required=False,
help='Select the functional of the installed configuration.'
)

RELATIVISTIC = OverridableOption(
RELATIVISTIC = core_options.OverridableOption(
'-r',
'--relativistic',
type=click.STRING,
required=False,
help='Select the type of relativistic effects included in the installed configuration.'
)

PROTOCOL = OverridableOption(
PROTOCOL = core_options.OverridableOption(
'-p', '--protocol', type=click.STRING, required=False, help='Select the protocol of the installed configuration.'
)

PSEUDO_FORMAT = OverridableOption(
PSEUDO_FORMAT = core_options.OverridableOption(
'-f',
'--pseudo-format',
type=click.STRING,
required=True,
help='Select the pseudopotential file format of the installed configuration.'
)

STRINGENCY = OverridableOption(
STRINGENCY = core_options.OverridableOption(
'-s', '--stringency', type=click.STRING, required=False, help='Stringency level for the recommended cutoffs.'
)

DEFAULT_STRINGENCY = OverridableOption(
DEFAULT_STRINGENCY = core_options.OverridableOption(
'-s',
'--default-stringency',
type=click.STRING,
required=False,
help='Select the default stringency level for the installed configuration.'
)

TRACEBACK = OverridableOption(
TRACEBACK = core_options.OverridableOption(
'-t', '--traceback', is_flag=True, help='Include the stacktrace if an exception is encountered.'
)

FAMILY_TYPE = OverridableOption(
FAMILY_TYPE = core_options.OverridableOption(
'-F',
'--family-type',
type=PseudoPotentialFamilyTypeParam(),
Expand All @@ -69,7 +80,7 @@
help='Choose the type of pseudo potential family to create.'
)

PSEUDO_TYPE = OverridableOption(
PSEUDO_TYPE = core_options.OverridableOption(
'-P',
'--pseudo-type',
type=PseudoPotentialTypeParam(),
Expand All @@ -81,11 +92,11 @@
)
)

ARCHIVE_FORMAT = OverridableOption(
ARCHIVE_FORMAT = core_options.OverridableOption(
'-f', '--archive-format', type=click.Choice([fmt[0] for fmt in shutil.get_archive_formats()])
)

UNIT = OverridableOption(
UNIT = core_options.OverridableOption(
'-u',
'--unit',
type=UnitParamType(quantity='energy'),
Expand All @@ -95,7 +106,7 @@
help='Specify the energy unit of the cutoffs. Must be recognized by the ``UnitRegistry`` of the ``pint`` library.'
)

DOWNLOAD_ONLY = OverridableOption(
DOWNLOAD_ONLY = core_options.OverridableOption(
'--download-only',
is_flag=True,
help=(
Expand Down
43 changes: 13 additions & 30 deletions src/aiida_pseudo/cli/root.py
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."""
49 changes: 42 additions & 7 deletions tests/cli/test_root.py
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