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

verdi code setup: validate the uniqueness of the full label #5205

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
23 changes: 18 additions & 5 deletions aiida/cmdline/commands/cmd_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,24 +58,31 @@ def set_code_builder(ctx, param, value):
return value


# Defining the ``COMPUTER`` option first guarantees that the user is prompted for the computer first. This is necessary
# because the ``LABEL`` option has a callback that relies on the computer being already set. Execution order is
# guaranteed only for the interactive case, however. For the non-interactive case, the callback is called explicitly
# once more in the command body to cover the case when the label is specified before the computer.
@verdi_code.command('setup')
@options_code.ON_COMPUTER()
@options_code.COMPUTER()
@options_code.LABEL()
@options_code.DESCRIPTION()
@options_code.INPUT_PLUGIN()
@options_code.ON_COMPUTER()
@options_code.COMPUTER()
@options_code.REMOTE_ABS_PATH()
@options_code.FOLDER()
@options_code.REL_PATH()
@options_code.PREPEND_TEXT()
@options_code.APPEND_TEXT()
@options.NON_INTERACTIVE()
@options.CONFIG_FILE()
@click.pass_context
@with_dbenv()
def setup_code(non_interactive, **kwargs):
def setup_code(ctx, non_interactive, **kwargs):
"""Setup a new code."""
from aiida.orm.utils.builders.code import CodeBuilder

options_code.validate_label_uniqueness(ctx, None, kwargs['label'])

if kwargs.pop('on_computer'):
kwargs['code_type'] = CodeBuilder.CodeType.ON_COMPUTER
else:
Expand All @@ -97,13 +104,17 @@ def setup_code(non_interactive, **kwargs):
echo.echo_success(f'Code<{code.pk}> {code.full_label} created')


# Defining the ``COMPUTER`` option first guarantees that the user is prompted for the computer first. This is necessary
# because the ``LABEL`` option has a callback that relies on the computer being already set. Execution order is
# guaranteed only for the interactive case, however. For the non-interactive case, the callback is called explicitly
# once more in the command body to cover the case when the label is specified before the computer.
@verdi_code.command('duplicate')
@arguments.CODE(callback=set_code_builder)
@options_code.ON_COMPUTER(contextual_default=get_on_computer)
@options_code.COMPUTER(contextual_default=get_computer_name)
@options_code.LABEL(contextual_default=partial(get_default, 'label'))
@options_code.DESCRIPTION(contextual_default=partial(get_default, 'description'))
@options_code.INPUT_PLUGIN(contextual_default=partial(get_default, 'input_plugin'))
@options_code.ON_COMPUTER(contextual_default=get_on_computer)
@options_code.COMPUTER(contextual_default=get_computer_name)
@options_code.REMOTE_ABS_PATH(contextual_default=partial(get_default, 'remote_abs_path'))
@options_code.FOLDER(contextual_default=partial(get_default, 'code_folder'))
@options_code.REL_PATH(contextual_default=partial(get_default, 'code_rel_path'))
Expand All @@ -118,6 +129,8 @@ def code_duplicate(ctx, code, non_interactive, **kwargs):
from aiida.common.exceptions import ValidationError
from aiida.orm.utils.builders.code import CodeBuilder

options_code.validate_label_uniqueness(ctx, None, kwargs['label'])

if kwargs.pop('on_computer'):
kwargs['code_type'] = CodeBuilder.CodeType.ON_COMPUTER
else:
Expand Down
28 changes: 28 additions & 0 deletions aiida/cmdline/params/options/commands/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,33 @@ def is_not_on_computer(ctx):
return bool(not is_on_computer(ctx))


def validate_label_uniqueness(ctx, _, value):
"""Validate the uniqueness of the full label of the code, i.e., `label@computer.label`.

.. 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)

if computer is not None:
full_label = f'{value}@{computer.label}'

try:
load_code(full_label)
except exceptions.NotExistent:
pass
else:
raise click.BadParameter(f'the code `{full_label}` already exists.')

return value


ON_COMPUTER = OverridableOption(
'--on-computer/--store-in-db',
is_eager=False,
Expand Down Expand Up @@ -66,6 +93,7 @@ def is_not_on_computer(ctx):

LABEL = options.LABEL.clone(
prompt='Label',
callback=validate_label_uniqueness,
cls=InteractiveOption,
help="This label can be used to identify the code (using 'label@computerlabel'), as long as labels are unique per "
'computer.'
Expand Down
49 changes: 42 additions & 7 deletions tests/cmdline/commands/test_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,7 @@ def test_noninteractive_upload(run_cli_command, non_interactive_editor):
def test_interactive_remote(run_cli_command, aiida_localhost, non_interactive_editor):
"""Test interactive remote code setup."""
label = 'interactive_remote'
user_input = '\n'.join([
label, 'description', 'core.arithmetic.add', 'yes', aiida_localhost.label, '/remote/abs/path'
])
user_input = '\n'.join(['yes', aiida_localhost.label, label, 'desc', 'core.arithmetic.add', '/remote/abs/path'])
run_cli_command(cmd_code.setup_code, user_input=user_input)
assert isinstance(load_code(label), Code)

Expand All @@ -187,7 +185,7 @@ def test_interactive_upload(run_cli_command, non_interactive_editor):
label = 'interactive_upload'
dirname = os.path.dirname(__file__)
basename = os.path.basename(__file__)
user_input = '\n'.join([label, 'description', 'core.arithmetic.add', 'no', dirname, basename])
user_input = '\n'.join(['no', label, 'description', 'core.arithmetic.add', dirname, basename])
run_cli_command(cmd_code.setup_code, user_input=user_input)
assert isinstance(load_code(label), Code)

Expand All @@ -198,7 +196,7 @@ def test_mixed(run_cli_command, aiida_localhost, non_interactive_editor):
"""Test mixed (interactive/from config) code setup."""
label = 'mixed_remote'
options = ['--description=description', '--on-computer', '--remote-abs-path=/remote/abs/path']
user_input = '\n'.join([label, 'core.arithmetic.add', aiida_localhost.label])
user_input = '\n'.join([aiida_localhost.label, label, 'core.arithmetic.add'])
run_cli_command(cmd_code.setup_code, options, user_input=user_input)
assert isinstance(load_code(label), Code)

Expand All @@ -208,7 +206,7 @@ def test_mixed(run_cli_command, aiida_localhost, non_interactive_editor):
def test_code_duplicate_interactive(run_cli_command, aiida_local_code_factory, non_interactive_editor):
"""Test code duplication interactive."""
label = 'code_duplicate_interactive'
user_input = f'{label}\n\n\n\n\n\n'
user_input = f'\n\n{label}\n\n\n\n'
code = aiida_local_code_factory('core.arithmetic.add', '/bin/cat', label='code')
run_cli_command(cmd_code.code_duplicate, [str(code.pk)], user_input=user_input)

Expand All @@ -226,7 +224,7 @@ def test_code_duplicate_ignore(run_cli_command, aiida_local_code_factory, non_in
Regression test for: https://github.com/aiidateam/aiida-core/issues/3770
"""
label = 'code_duplicate_interactive'
user_input = f'{label}\n!\n\n\n\n\n'
user_input = f'\n\n{label}\n!\n\n\n'
code = aiida_local_code_factory('core.arithmetic.add', '/bin/cat', label='code')
run_cli_command(cmd_code.code_duplicate, [str(code.pk)], user_input=user_input)

Expand Down Expand Up @@ -279,3 +277,40 @@ def test_from_config_url(non_interactive_editor, run_cli_command, aiida_localhos
fake_url = 'https://my.url.com'
run_cli_command(cmd_code.setup_code, ['--non-interactive', '--config', fake_url])
assert isinstance(load_code(label), Code)


@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(
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."""
label = 'some-label'
aiida_local_code_factory('core.arithmetic.add', '/bin/cat', computer=aiida_localhost, label=label)
assert isinstance(load_code(label), Code)

label_unique = 'label-unique'
user_input = '\n'.join(['yes', aiida_localhost.label, 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')
@pytest.mark.parametrize('label_first', (True, False))
def test_code_setup_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."""
label = 'some-label'
aiida_local_code_factory('core.arithmetic.add', '/bin/cat', computer=aiida_localhost, label=label)
assert isinstance(load_code(label), Code)

options = ['-n', '-D', 'd', '-P', 'core.arithmetic.add', '--on-computer', '--remote-abs-path=/remote/abs/path']

if label_first:
options.extend(['--label', label, '--computer', aiida_localhost.label])
else:
options.extend(['--computer', aiida_localhost.label, '--label', label])

result = run_cli_command(cmd_code.setup_code, options, raises=True)
assert f'the code `{label}@{aiida_localhost.label}` already exists.' in result.output