diff --git a/aiida/cmdline/groups/verdi.py b/aiida/cmdline/groups/verdi.py index 2937389cd0..eb172a844c 100644 --- a/aiida/cmdline/groups/verdi.py +++ b/aiida/cmdline/groups/verdi.py @@ -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',) @@ -28,6 +32,21 @@ ) +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. @@ -35,6 +54,8 @@ class VerdiCommandGroup(click.Group): 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.""" diff --git a/aiida/cmdline/params/types/profile.py b/aiida/cmdline/params/types/profile.py index ed4307f88d..014ba8232b 100644 --- a/aiida/cmdline/params/types/profile.py +++ b/aiida/cmdline/params/types/profile.py @@ -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. + """ name = 'profile' @@ -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 @@ -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: @@ -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 diff --git a/aiida/storage/psql_dos/alembic_cli.py b/aiida/storage/psql_dos/alembic_cli.py index 1f03b2231c..d22d64863f 100755 --- a/aiida/storage/psql_dos/alembic_cli.py +++ b/aiida/storage/psql_dos/alembic_cli.py @@ -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 @@ -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): diff --git a/tests/cmdline/commands/test_config.py b/tests/cmdline/commands/test_config.py index f8f06df0d2..828f940287 100644 --- a/tests/cmdline/commands/test_config.py +++ b/tests/cmdline/commands/test_config.py @@ -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() diff --git a/tests/manage/configuration/test_config.py b/tests/manage/configuration/test_config.py index e807946c4a..47d9496679 100644 --- a/tests/manage/configuration/test_config.py +++ b/tests/manage/configuration/test_config.py @@ -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