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

Add verdi code duplicate #1737

Merged
merged 21 commits into from
Sep 3, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
30 changes: 29 additions & 1 deletion aiida/backends/tests/cmdline/commands/test_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from click.testing import CliRunner

from aiida.backends.testbase import AiidaTestCase
from aiida.cmdline.commands.cmd_code import (setup_code, delete, hide, reveal, relabel, code_list, show)
from aiida.cmdline.commands.cmd_code import (setup_code, delete, hide, reveal, relabel, code_list, show, code_duplicate)
from aiida.common.exceptions import NotExistent


Expand Down Expand Up @@ -118,6 +118,7 @@ def setUp(self):
remote_computer_exec=[self.comp, '/remote/abs/path'],
)
code.label = 'code'
code.description = 'desc'
code.store()
self.code = code

Expand Down Expand Up @@ -198,3 +199,30 @@ def test_code_show(self):
result = self.runner.invoke(show, [str(self.code.pk)])
self.assertIsNone(result.exception)
self.assertTrue(str(self.code.pk) in result.output)

def test_code_duplicate_interactive(self):
os.environ['VISUAL'] = 'sleep 1; vim -cwq'
os.environ['EDITOR'] = 'sleep 1; vim -cwq'
label = 'code_duplicate_interactive'
user_input = label + '\n\n\n\n\n\n'
result = self.runner.invoke(code_duplicate, [str(self.code.pk)], input=user_input, catch_exceptions=False)
self.assertIsNone(result.exception, result.output)

from aiida.orm import Code
new_code = Code.get_from_string(label)
self.assertEquals(self.code.description, new_code.description)
self.assertEquals(self.code.get_prepend_text(), new_code.get_prepend_text())
self.assertEquals(self.code.get_append_text(), new_code.get_append_text())

def test_code_duplicate_non_interactive(self):
label = 'code_duplicate_noninteractive'
result = self.runner.invoke(code_duplicate, ['--non-interactive', '--label=' + label, str(self.code.pk)])
self.assertIsNone(result.exception)

from aiida.orm import Code
new_code = Code.get_from_string(label)
self.assertEquals(self.code.description, new_code.description)
self.assertEquals(self.code.get_prepend_text(), new_code.get_prepend_text())
self.assertEquals(self.code.get_append_text(), new_code.get_append_text())
self.assertEquals(self.code.get_input_plugin_name(), new_code.get_input_plugin_name())

158 changes: 137 additions & 21 deletions aiida/cmdline/commands/cmd_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@
This allows to setup and configure a code from command line.
"""
from __future__ import absolute_import
from functools import partial
import click
import tabulate

from aiida.cmdline.commands.cmd_verdi import verdi
from aiida.cmdline.params import options, arguments, types
from aiida.cmdline.params.options.interactive import InteractiveOption
from aiida.cmdline.params.options.overridable import OverridableOption
from aiida.cmdline.utils import echo
from aiida.cmdline.utils.decorators import with_dbenv, deprecated_command
from aiida.cmdline.utils.multi_line_input import ensure_scripts
Expand All @@ -37,47 +39,57 @@ def is_not_on_computer(ctx):
return bool(not is_on_computer(ctx))


@verdi_code.command('setup')
@options.LABEL(prompt='Label', cls=InteractiveOption, help='A label to refer to this code')
@options.DESCRIPTION(prompt='Description', cls=InteractiveOption, help='A human-readable description of this code')
@options.INPUT_PLUGIN(prompt='Default calculation input plugin', cls=InteractiveOption)
@click.option(
# Reusable options (code-specific)
ON_COMPUTER = OverridableOption(
'--on-computer/--store-in-db',
is_eager=False,
default=True,
prompt='Installed on target computer?',
cls=InteractiveOption)
@options.COMPUTER(
prompt='Computer',
cls=InteractiveOption,
required_fn=is_on_computer,
prompt_fn=is_on_computer,
help='Name of the computer, on which the code resides')
@click.option(
prompt='Installed on target computer?')

REMOTE_ABS_PATH = OverridableOption(
'--remote-abs-path',
prompt='Remote absolute path',
required_fn=is_on_computer,
prompt_fn=is_on_computer,
type=types.AbsolutePathParamType(dir_okay=False),
cls=InteractiveOption,
help=('[if --on-computer]: the absolute path to the executable on the remote machine'))
@click.option(

CODE_FOLDER = OverridableOption(
'--code-folder',
prompt='Local directory containing the code',
type=click.Path(file_okay=False, exists=True, readable=True),
required_fn=is_not_on_computer,
prompt_fn=is_not_on_computer,
type=click.Path(file_okay=False, exists=True, readable=True),
cls=InteractiveOption,
help=('[if --store-in-db]: directory the executable and all other files necessary for running it'))
@click.option(
help=('[if --store-in-db]: directory containing the executable and all other files necessary for running it'))

CODE_REL_PATH = OverridableOption(
'--code-rel-path',
prompt='Relative path of executable inside directory',
type=click.Path(dir_okay=False),
prompt='Relative path of executable inside code folder',
required_fn=is_not_on_computer,
prompt_fn=is_not_on_computer,
type=click.Path(dir_okay=False),
cls=InteractiveOption,
help=('[if --store-in-db]: relative path of the executable ' + \
'inside the code-folder'))


@verdi_code.command('setup')
@options.LABEL(prompt='Label', cls=InteractiveOption, help='A label to refer to this code')
@options.DESCRIPTION(prompt='Description', cls=InteractiveOption, help='A human-readable description of this code')
@options.INPUT_PLUGIN(prompt='Default calculation input plugin', cls=InteractiveOption)
@ON_COMPUTER()
@options.COMPUTER(
prompt='Computer',
cls=InteractiveOption,
required_fn=is_on_computer,
prompt_fn=is_on_computer,
help='Name of the computer, on which the code resides')
@REMOTE_ABS_PATH()
@CODE_FOLDER()
@CODE_REL_PATH()
@options.PREPEND_TEXT()
@options.APPEND_TEXT()
@options.NON_INTERACTIVE()
Expand All @@ -102,12 +114,115 @@ def setup_code(non_interactive, **kwargs):
code.store()
code.reveal() # newly setup code shall not be hidden
except ValidationError as err:
echo.echo_critical('unable to store the code: {}. Exiting...'.format(err))
echo.echo_critical('Unable to store the code: {}. Exiting...'.format(err))

echo.echo_success('code "{}" stored in DB.'.format(code.label))
echo.echo_info('pk: {}, uuid: {}'.format(code.pk, code.uuid))


def get_default(key, ctx):
"""
Get the default argument using a user instance property
:param value: The name of the property to use
:param ctx: The click context (which will be used to get the user)
:return: The default value, or None
"""
try:
value = getattr(ctx.code_builder, key)
if value == "":
value = None
except KeyError:
value = None

return value


def get_computer_name(ctx):
return getattr(ctx.code_builder, 'computer').name


def get_on_computer(ctx):
return not getattr(ctx.code_builder, 'is_local')()


#pylint: disable=unused-argument
def set_code_builder(ctx, param, value):
"""Set the code spec for defaults of following options."""
ctx.code_builder = CodeBuilder.from_code(value)
return value


@verdi_code.command('duplicate')
@arguments.CODE(callback=set_code_builder)
@options.LABEL(
prompt='Label', cls=InteractiveOption, help='Label for new code', contextual_default=partial(get_default, 'label'))
@options.DESCRIPTION(
prompt='Description',
cls=InteractiveOption,
help='A human-readable description of this code',
contextual_default=partial(get_default, 'description'))
@options.INPUT_PLUGIN(
prompt='Default calculation input plugin',
cls=InteractiveOption,
contextual_default=partial(get_default, 'input_plugin'))
@ON_COMPUTER(contextual_default=get_on_computer)
@options.COMPUTER(
prompt='Computer',
cls=InteractiveOption,
required_fn=is_on_computer,
prompt_fn=is_on_computer,
help='Name of the computer, on which the code resides',
contextual_default=get_computer_name)
@REMOTE_ABS_PATH(contextual_default=partial(get_default, 'remote_abs_path'))
@CODE_FOLDER(contextual_default=partial(get_default, 'code_folder'))
@CODE_REL_PATH(contextual_default=partial(get_default, 'code_rel_path'))
@click.option(
'--hide-original',
is_flag=True,
default=False,
help=('Hide the code being copied.'),
)
@options.PREPEND_TEXT()
@options.APPEND_TEXT()
@options.NON_INTERACTIVE()
@click.pass_context
@with_dbenv()
# pylint: disable=unused-argument
def code_duplicate(ctx, code, non_interactive, **kwargs):
"""
Create duplicate of existing code.
"""
from aiida.common.exceptions import ValidationError

if not non_interactive:
pre, post = ensure_scripts(kwargs.pop('prepend_text', ''), kwargs.pop('append_text', ''), kwargs)
kwargs['prepend_text'] = pre
kwargs['append_text'] = post

if kwargs.pop('on_computer'):
kwargs['code_type'] = CodeBuilder.CodeType.ON_COMPUTER
else:
kwargs['code_type'] = CodeBuilder.CodeType.STORE_AND_UPLOAD

if kwargs.pop('hide_original'):
code.hide()

code_builder = ctx.code_builder
for key, value in kwargs.items():
if value is not None:
setattr(code_builder, key, value)
new_code = code_builder.new()

try:
new_code.store()
new_code.reveal() # newly setup code shall not be hidden
except ValidationError as err:
echo.echo_critical('Unable to store the code: {}. Exiting...'.format(err))

echo.echo_success("Duplicated code '{}'.".format(code.full_label))
echo.echo_info('New ' + str(new_code))


@verdi_code.command()
@arguments.CODE()
@click.option('-v', '--verbose', is_flag=True, help='show additional verbose information')
Expand All @@ -134,11 +249,12 @@ def delete(codes):

for code in codes:
try:
code_str = str(code)
delete_code(code)
except InvalidOperation as exc:
echo.echo_critical(str(exc))

echo.echo_success("Code '{}' deleted.".format(code.pk))
echo.echo_success("{} deleted.".format(code_str))


@verdi_code.command()
Expand Down
8 changes: 4 additions & 4 deletions aiida/cmdline/commands/cmd_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,19 @@ def get_default(value, ctx):
prompt='First name',
type=str,
contextual_default=partial(get_default, 'first_name'),
cls=options.InteractiveOption)
cls=options.interactive.InteractiveOption)
@click.option(
'--last-name',
prompt='Last name',
type=str,
contextual_default=partial(get_default, 'last_name'),
cls=options.InteractiveOption)
cls=options.interactive.InteractiveOption)
@click.option(
'--institution',
prompt='Institution',
type=str,
contextual_default=partial(get_default, 'institution'),
cls=options.InteractiveOption)
cls=options.interactive.InteractiveOption)
@click.option(
'--password',
prompt='Password',
Expand All @@ -73,7 +73,7 @@ def get_default(value, ctx):
type=str,
default=PASSWORD_UNCHANGED,
confirmation_prompt=True,
cls=options.InteractiveOption)
cls=options.interactive.InteractiveOption)
@with_dbenv()
def configure(user, first_name, last_name, institution, password, non_interactive):
"""
Expand Down
1 change: 0 additions & 1 deletion aiida/cmdline/params/options/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from aiida.backends.profile import BACKEND_DJANGO, BACKEND_SQLA
from aiida.cmdline.params import types
from aiida.cmdline.params.options.conditional import ConditionalOption
from aiida.cmdline.params.options.interactive import InteractiveOption
from aiida.cmdline.params.options.multivalue import MultipleValueOption
from aiida.cmdline.params.options.overridable import OverridableOption

Expand Down
Loading