From f85d0291ace35852bbba1e84af400275bf97e07a Mon Sep 17 00:00:00 2001 From: Sebastiaan Huber Date: Tue, 2 Nov 2021 22:57:52 +0100 Subject: [PATCH] `verdi code setup`: validate the uniqueness of label for local codes In commit d25339dd7e8f8bed40145392dd5166817f3afef5 `verdi code setup` was improved to have a callback for the `label` option to check for its uniqueness. However, it only implemented this for "remote" computers, which have an associated `Computer` and so the uniqueness criterion is on the "full label", which is the `label@computer.label`. However, there are also "local" codes, which don't have an associated computer and so only have the label as the identifier that should be unique. The callback `validate_label_uniqueness` is now updated to distinguish between these two cases. --- aiida/cmdline/params/options/commands/code.py | 30 ++++++-- tests/cmdline/commands/test_code.py | 73 +++++++++++++++++-- 2 files changed, 92 insertions(+), 11 deletions(-) diff --git a/aiida/cmdline/params/options/commands/code.py b/aiida/cmdline/params/options/commands/code.py index 8e96e2854b..69539cf836 100644 --- a/aiida/cmdline/params/options/commands/code.py +++ b/aiida/cmdline/params/options/commands/code.py @@ -24,18 +24,34 @@ def is_not_on_computer(ctx): def validate_label_uniqueness(ctx, _, value): - """Validate the uniqueness of the full label of the code, i.e., `label@computer.label`. + """Validate the uniqueness of the label of the code. + + The exact uniqueness criterion depends on the type of the code, whether it is "local" or "remote". For the former, + the `label` itself should be unique, whereas for the latter it is the full label, i.e., `label@computer.label`. + + .. note:: For this to work in the case of the remote code, the computer parameter already needs to have been parsed + In interactive mode, this means that the computer parameter needs to be defined after the label parameter in the + command definition. For non-interactive mode, the parsing order will always be determined by the order the + parameters are specified by the caller and so this validator may get called before the computer is parsed. For + that reason, this validator should also be called in the command itself, to ensure it has both the label and + computer parameter available. - .. note:: For this to work, the computer parameter already needs to have been parsed. In interactive mode, this - means that the computer parameter needs to be defined after the label parameter in the command definition. For - non-interactive mode, the parsing order will always be determined by the order the parameters are specified by - the caller and so this validator may get called before the computer is parsed. For that reason, this validator - should also be called in the command itself, to ensure it has both the label and computer parameter available. """ from aiida.common import exceptions from aiida.orm import load_code computer = ctx.params.get('computer', None) + on_computer = ctx.params.get('on_computer', None) + + if on_computer is False: + try: + load_code(value) + except exceptions.NotExistent: + pass + except exceptions.MultipleObjectsError: + raise click.BadParameter(f'multiple copies of the remote code `{value}` already exist.') + else: + raise click.BadParameter(f'the code `{value}` already exists.') if computer is not None: full_label = f'{value}@{computer.label}' @@ -44,6 +60,8 @@ def validate_label_uniqueness(ctx, _, value): load_code(full_label) except exceptions.NotExistent: pass + except exceptions.MultipleObjectsError: + raise click.BadParameter(f'multiple copies of the local code `{full_label}` already exist.') else: raise click.BadParameter(f'the code `{full_label}` already exists.') diff --git a/tests/cmdline/commands/test_code.py b/tests/cmdline/commands/test_code.py index 25ee8617d9..d399db3770 100644 --- a/tests/cmdline/commands/test_code.py +++ b/tests/cmdline/commands/test_code.py @@ -13,10 +13,12 @@ import tempfile import textwrap +import click import pytest from aiida.cmdline.commands import cmd_code -from aiida.common.exceptions import NotExistent +from aiida.cmdline.params.options.commands.code import validate_label_uniqueness +from aiida.common.exceptions import MultipleObjectsError, NotExistent from aiida.orm import Code, load_code @@ -281,10 +283,10 @@ def test_from_config_url(non_interactive_editor, run_cli_command, aiida_localhos @pytest.mark.usefixtures('clear_database_before_test') @pytest.mark.parametrize('non_interactive_editor', ('sleep 1; vim -cwq',), indirect=True) -def test_code_setup_duplicate_full_label_interactive( +def test_code_setup_remote_duplicate_full_label_interactive( run_cli_command, aiida_local_code_factory, aiida_localhost, non_interactive_editor ): - """Test ``verdi code setup`` in interactive mode when specifying a full label that already exists.""" + """Test ``verdi code setup`` for a remote code in interactive mode specifying an existing full label.""" label = 'some-label' aiida_local_code_factory('core.arithmetic.add', '/bin/cat', computer=aiida_localhost, label=label) assert isinstance(load_code(label), Code) @@ -297,10 +299,10 @@ def test_code_setup_duplicate_full_label_interactive( @pytest.mark.usefixtures('clear_database_before_test') @pytest.mark.parametrize('label_first', (True, False)) -def test_code_setup_duplicate_full_label_non_interactive( +def test_code_setup_remote_duplicate_full_label_non_interactive( run_cli_command, aiida_local_code_factory, aiida_localhost, label_first ): - """Test ``verdi code setup`` in non-interactive mode when specifying a full label that already exists.""" + """Test ``verdi code setup`` for a remote code in non-interactive mode specifying an existing full label.""" label = 'some-label' aiida_local_code_factory('core.arithmetic.add', '/bin/cat', computer=aiida_localhost, label=label) assert isinstance(load_code(label), Code) @@ -314,3 +316,64 @@ def test_code_setup_duplicate_full_label_non_interactive( result = run_cli_command(cmd_code.setup_code, options, raises=True) assert f'the code `{label}@{aiida_localhost.label}` already exists.' in result.output + + +@pytest.mark.usefixtures('clear_database_before_test') +@pytest.mark.parametrize('non_interactive_editor', ('sleep 1; vim -cwq',), indirect=True) +def test_code_setup_local_duplicate_full_label_interactive( + run_cli_command, aiida_local_code_factory, aiida_localhost, non_interactive_editor +): + """Test ``verdi code setup`` for a local code in interactive mode specifying an existing full label.""" + label = 'some-label' + code = Code(local_executable='bash', files=['/bin/bash']) + code.label = label + code.store() + assert isinstance(load_code(label), Code) + + label_unique = 'label-unique' + user_input = '\n'.join(['no', label, label_unique, 'd', 'core.arithmetic.add', '/bin', 'bash']) + run_cli_command(cmd_code.setup_code, user_input=user_input) + assert isinstance(load_code(label_unique), Code) + + +@pytest.mark.usefixtures('clear_database_before_test') +def test_code_setup_local_duplicate_full_label_non_interactive( + run_cli_command, aiida_local_code_factory, aiida_localhost +): + """Test ``verdi code setup`` for a local code in non-interactive mode specifying an existing full label.""" + label = 'some-label' + code = Code(local_executable='bash', files=['/bin/bash']) + code.label = label + code.store() + assert isinstance(load_code(label), Code) + + options = [ + '-n', '-D', 'd', '-P', 'core.arithmetic.add', '--store-in-db', '--code-folder=/bin', '--code-rel-path=bash', + '--label', label + ] + + result = run_cli_command(cmd_code.setup_code, options, raises=True) + assert f'the code `{label}` already exists.' in result.output + + +@pytest.mark.usefixtures('clear_database_before_test') +def test_validate_label_uniqueness(monkeypatch, aiida_localhost): + """Test the ``validate_label_uniqueness`` validator.""" + from aiida import orm + + def load_code(*args, **kwargs): + raise MultipleObjectsError() + + monkeypatch.setattr(orm, 'load_code', load_code) + + ctx = click.Context(cmd_code.setup_code) + ctx.params = {'on_computer': False} + + with pytest.raises(click.BadParameter, match=r'multiple copies of the remote code `.*` already exist.'): + validate_label_uniqueness(ctx, None, 'some-code') + + ctx = click.Context(cmd_code.setup_code) + ctx.params = {'on_computer': None, 'computer': aiida_localhost} + + with pytest.raises(click.BadParameter, match=r'multiple copies of the local code `.*` already exist.'): + validate_label_uniqueness(ctx, None, 'some-code')