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: allow setting options for config without profiles #5544

Merged
merged 2 commits into from
May 30, 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
21 changes: 21 additions & 0 deletions aiida/cmdline/groups/verdi.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

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',)
Expand All @@ -28,13 +32,30 @@
)


class VerdiContext(click.Context):
"""Custom context implementation that defines the ``obj`` user object and adds the ``Config`` instance."""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

if self.obj is None:
self.obj = AttributeDict()

try:
self.obj.config = get_config(create=True)
except ConfigurationError as exception:
self.fail(str(exception))


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

@staticmethod
def add_verbosity_option(cmd):
"""Apply the ``verbosity`` option to the command, which is common to all ``verdi`` commands."""
Expand Down
23 changes: 15 additions & 8 deletions aiida/cmdline/params/types/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@


class ProfileParamType(LabelStringType):
"""The profile parameter type for click."""
"""The profile parameter type for click.

This parameter type requires the command that uses it to define the ``context_class`` class attribute to be the
:class:`aiida.cmdline.groups.verdi.VerdiContext` class, as that is responsible for creating the user defined object
``obj`` on the context and loads the instance config.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to validate this?
If this is straightforward (even if just indirect by checking the ctx.obj) a check + exception might be a more effective way of enforcing this than via documentation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can do, will add it.

"""

name = 'profile'

Expand All @@ -31,9 +36,16 @@ def deconvert_default(value):

def convert(self, value, param, ctx):
"""Attempt to match the given value to a valid profile."""
from aiida.common import extendeddicts
from aiida.common.exceptions import MissingConfigurationError, ProfileConfigurationError
from aiida.manage.configuration import Profile, get_config, load_profile
from aiida.manage.configuration import Profile, load_profile

try:
config = ctx.obj.config
except AttributeError:
raise RuntimeError(
'The context does not contain a user defined object with the loaded AiiDA configuration. '
'Is your click command setting `context_class` to :class:`aiida.cmdline.groups.verdi.VerdiContext`?'
)

# If the value is already of the expected return type, simply return it. This behavior is new in `click==8.0`:
# https://click.palletsprojects.com/en/8.0.x/parameters/#implementing-custom-types
Expand All @@ -43,7 +55,6 @@ def convert(self, value, param, ctx):
value = super().convert(value, param, ctx)

try:
config = get_config(create=True)
profile = config.get_profile(value)
except (MissingConfigurationError, ProfileConfigurationError) as exception:
if not self._cannot_exist:
Expand All @@ -58,10 +69,6 @@ def convert(self, value, param, ctx):
if self._load_profile:
load_profile(profile.name)

if ctx.obj is None:
ctx.obj = extendeddicts.AttributeDict()

ctx.obj.config = config
ctx.obj.profile = profile

return profile
Expand Down
3 changes: 2 additions & 1 deletion aiida/storage/psql_dos/alembic_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from sqlalchemy.util.compat import nullcontext

from aiida.cmdline import is_verbose
from aiida.cmdline.groups.verdi import VerdiCommandGroup
from aiida.cmdline.params import options
from aiida.storage.psql_dos.migrator import PsqlDostoreMigrator

Expand Down Expand Up @@ -44,7 +45,7 @@ def execute_alembic_command(self, command_name, connect=True, **kwargs):
pass_runner = click.make_pass_decorator(AlembicRunner, ensure=True)


@click.group()
@click.group(cls=VerdiCommandGroup)
@options.PROFILE(required=True)
@pass_runner
def alembic_cli(runner, profile):
Expand Down
223 changes: 121 additions & 102 deletions tests/cmdline/commands/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,143 +7,162 @@
# For further information on the license, see the LICENSE.txt file #
# For further information please visit http://www.aiida.net #
###########################################################################
# pylint: disable=no-self-use
"""Tests for `verdi config`."""
import pytest

"""Tests for ``verdi config``."""
from aiida import get_profile
from aiida.cmdline.commands import cmd_verdi
from aiida.manage.configuration import get_config


class TestVerdiConfig:
"""Tests for `verdi config`."""
def test_config_set_option_no_profile(run_cli_command, empty_config):
"""Test the `verdi config set` command when no profile is present in the config."""
config = empty_config

@pytest.fixture(autouse=True)
def setup_fixture(self, config_with_profile_factory):
config_with_profile_factory()
option_name = 'daemon.timeout'
option_value = str(10)

def test_config_set_option(self, run_cli_command):
"""Test the `verdi config set` command when setting an option."""
config = get_config()
options = ['config', 'set', option_name, str(option_value)]
run_cli_command(cmd_verdi.verdi, options)
assert str(config.get_option(option_name, scope=None)) == option_value

option_name = 'daemon.timeout'
option_values = [str(10), str(20)]

for option_value in option_values:
options = ['config', 'set', option_name, str(option_value)]
run_cli_command(cmd_verdi.verdi, options)
assert str(config.get_option(option_name, scope=get_profile().name)) == option_value
def test_config_set_option(run_cli_command, config_with_profile_factory):
"""Test the `verdi config set` command when setting an option."""
config = config_with_profile_factory()

def test_config_append_option(self, run_cli_command):
"""Test the `verdi config set --append` command when appending an option value."""
config = get_config()
option_name = 'caching.enabled_for'
for value in ['x', 'y']:
options = ['config', 'set', '--append', option_name, value]
run_cli_command(cmd_verdi.verdi, options)
assert config.get_option(option_name, scope=get_profile().name) == ['x', 'y']
option_name = 'daemon.timeout'
option_values = [str(10), str(20)]

def test_config_remove_option(self, run_cli_command):
"""Test the `verdi config set --remove` command when removing an option value."""
config = get_config()
for option_value in option_values:
options = ['config', 'set', option_name, option_value]
run_cli_command(cmd_verdi.verdi, options)
assert str(config.get_option(option_name, scope=get_profile().name)) == option_value

option_name = 'caching.disabled_for'
config.set_option(option_name, ['x', 'y'], scope=get_profile().name)

options = ['config', 'set', '--remove', option_name, 'x']
def test_config_append_option(run_cli_command, config_with_profile_factory):
"""Test the `verdi config set --append` command when appending an option value."""
config = config_with_profile_factory()
option_name = 'caching.enabled_for'
for value in ['x', 'y']:
options = ['config', 'set', '--append', option_name, value]
run_cli_command(cmd_verdi.verdi, options)
assert config.get_option(option_name, scope=get_profile().name) == ['y']
assert config.get_option(option_name, scope=get_profile().name) == ['x', 'y']

def test_config_get_option(self, run_cli_command):
"""Test the `verdi config show` command when getting an option."""
option_name = 'daemon.timeout'
option_value = str(30)

options = ['config', 'set', option_name, option_value]
result = run_cli_command(cmd_verdi.verdi, options)
def test_config_remove_option(run_cli_command, config_with_profile_factory):
"""Test the `verdi config set --remove` command when removing an option value."""
config = config_with_profile_factory()

options = ['config', 'get', option_name]
result = run_cli_command(cmd_verdi.verdi, options)
assert option_value in result.output.strip()
option_name = 'caching.disabled_for'
config.set_option(option_name, ['x', 'y'], scope=get_profile().name)

def test_config_unset_option(self, run_cli_command):
"""Test the `verdi config` command when unsetting an option."""
option_name = 'daemon.timeout'
option_value = str(30)
options = ['config', 'set', '--remove', option_name, 'x']
run_cli_command(cmd_verdi.verdi, options)
assert config.get_option(option_name, scope=get_profile().name) == ['y']

options = ['config', 'set', option_name, str(option_value)]
result = run_cli_command(cmd_verdi.verdi, options)

options = ['config', 'get', option_name]
result = run_cli_command(cmd_verdi.verdi, options)
assert option_value in result.output.strip()
def test_config_get_option(run_cli_command, config_with_profile_factory):
"""Test the `verdi config show` command when getting an option."""
config_with_profile_factory()
option_name = 'daemon.timeout'
option_value = str(30)

options = ['config', 'unset', option_name]
result = run_cli_command(cmd_verdi.verdi, options)
assert f"'{option_name}' unset" in result.output.strip()
options = ['config', 'set', option_name, option_value]
result = run_cli_command(cmd_verdi.verdi, options)

options = ['config', 'get', option_name]
result = run_cli_command(cmd_verdi.verdi, options)
assert result.output.strip() == str(20) # back to the default
options = ['config', 'get', option_name]
result = run_cli_command(cmd_verdi.verdi, options)
assert option_value in result.output.strip()

def test_config_set_option_global_only(self, run_cli_command):
"""Test that `global_only` options are only set globally even if the `--global` flag is not set."""
option_name = 'autofill.user.email'
option_value = 'some@email.com'

options = ['config', 'set', option_name, str(option_value)]
result = run_cli_command(cmd_verdi.verdi, options)
def test_config_unset_option(run_cli_command, config_with_profile_factory):
"""Test the `verdi config` command when unsetting an option."""
config_with_profile_factory()
option_name = 'daemon.timeout'
option_value = str(30)

options = ['config', 'get', option_name]
result = run_cli_command(cmd_verdi.verdi, options)
options = ['config', 'set', option_name, str(option_value)]
result = run_cli_command(cmd_verdi.verdi, options)

options = ['config', 'get', option_name]
result = run_cli_command(cmd_verdi.verdi, options)
assert option_value in result.output.strip()

options = ['config', 'unset', option_name]
result = run_cli_command(cmd_verdi.verdi, options)
assert f"'{option_name}' unset" in result.output.strip()

# Check that the current profile name is not in the output
assert option_value in result.output.strip()
assert get_profile().name not in result.output.strip()
options = ['config', 'get', option_name]
result = run_cli_command(cmd_verdi.verdi, options)
assert result.output.strip() == str(20) # back to the default

def test_config_list(self, run_cli_command):
"""Test `verdi config list`"""
options = ['config', 'list']

def test_config_set_option_global_only(run_cli_command, config_with_profile_factory):
"""Test that `global_only` options are only set globally even if the `--global` flag is not set."""
config_with_profile_factory()
option_name = 'autofill.user.email'
option_value = 'some@email.com'

options = ['config', 'set', option_name, str(option_value)]
result = run_cli_command(cmd_verdi.verdi, options)

options = ['config', 'get', option_name]
result = run_cli_command(cmd_verdi.verdi, options)

# Check that the current profile name is not in the output
assert option_value in result.output.strip()
assert get_profile().name not in result.output.strip()


def test_config_list(run_cli_command, config_with_profile_factory):
"""Test `verdi config list`"""
config_with_profile_factory()
options = ['config', 'list']
result = run_cli_command(cmd_verdi.verdi, options)

assert 'daemon.timeout' in result.output
assert 'Timeout in seconds' not in result.output


def test_config_list_description(run_cli_command, config_with_profile_factory):
"""Test `verdi config list --description`"""
config_with_profile_factory()
for flag in ['-d', '--description']:
options = ['config', 'list', flag]
result = run_cli_command(cmd_verdi.verdi, options)

assert 'daemon.timeout' in result.output
assert 'Timeout in seconds' not in result.output
assert 'Timeout in seconds' in result.output

def test_config_list_description(self, run_cli_command):
"""Test `verdi config list --description`"""
for flag in ['-d', '--description']:
options = ['config', 'list', flag]
result = run_cli_command(cmd_verdi.verdi, options)

assert 'daemon.timeout' in result.output
assert 'Timeout in seconds' in result.output
def test_config_show(run_cli_command, config_with_profile_factory):
"""Test `verdi config show`"""
config_with_profile_factory()
options = ['config', 'show', 'daemon.timeout']
result = run_cli_command(cmd_verdi.verdi, options)
assert 'schema' in result.output

def test_config_show(self, run_cli_command):
"""Test `verdi config show`"""
options = ['config', 'show', 'daemon.timeout']
result = run_cli_command(cmd_verdi.verdi, options)
assert 'schema' in result.output

def test_config_caching(self, run_cli_command):
"""Test `verdi config caching`"""
result = run_cli_command(cmd_verdi.verdi, ['config', 'caching'])
assert result.output.strip() == ''
def test_config_caching(run_cli_command, config_with_profile_factory):
"""Test `verdi config caching`"""
config = config_with_profile_factory()

result = run_cli_command(cmd_verdi.verdi, ['config', 'caching', '--disabled'])
assert 'core.arithmetic.add' in result.output.strip()
result = run_cli_command(cmd_verdi.verdi, ['config', 'caching'])
assert result.output.strip() == ''

config = get_config()
config.set_option('caching.default_enabled', True, scope=get_profile().name)
result = run_cli_command(cmd_verdi.verdi, ['config', 'caching', '--disabled'])
assert 'core.arithmetic.add' in result.output.strip()

result = run_cli_command(cmd_verdi.verdi, ['config', 'caching'])
assert 'core.arithmetic.add' in result.output.strip()
config.set_option('caching.default_enabled', True, scope=get_profile().name)

result = run_cli_command(cmd_verdi.verdi, ['config', 'caching', '--disabled'])
assert result.output.strip() == ''
result = run_cli_command(cmd_verdi.verdi, ['config', 'caching'])
assert 'core.arithmetic.add' in result.output.strip()

def test_config_downgrade(self, run_cli_command):
"""Test `verdi config downgrade`"""
options = ['config', 'downgrade', '1']
result = run_cli_command(cmd_verdi.verdi, options)
assert 'Success: Downgraded' in result.output.strip()
result = run_cli_command(cmd_verdi.verdi, ['config', 'caching', '--disabled'])
assert result.output.strip() == ''


def test_config_downgrade(run_cli_command, config_with_profile_factory):
"""Test `verdi config downgrade`"""
config_with_profile_factory()
options = ['config', 'downgrade', '1']
result = run_cli_command(cmd_verdi.verdi, options)
assert 'Success: Downgraded' in result.output.strip()
10 changes: 10 additions & 0 deletions tests/manage/configuration/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,16 @@ def test_set_option_override(config_with_profile):
assert config.get_option(option_name, scope=None, default=False) == option_value_two


def test_option_empty_config(empty_config):
"""Test setting an option on a config without any profiles."""
config = empty_config
option_name = 'autofill.user.email'
option_value = 'first@email.com'

config.set_option(option_name, option_value)
assert config.get_option(option_name, scope=None, default=False) == option_value


def test_store(config_with_profile):
"""Test that the store method writes the configuration properly to disk."""
config = config_with_profile
Expand Down