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

Refactor the Code data plugin #5510

Merged
merged 7 commits into from
May 18, 2022
Merged
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
6 changes: 4 additions & 2 deletions .github/system_tests/pytest/test_memory_leaks.py
Original file line number Diff line number Diff line change
@@ -50,8 +50,10 @@ def test_leak_ssh_calcjob():

Note: This relies on the 'slurm-ssh' computer being set up.
"""
code = orm.Code(
input_plugin_name='core.arithmetic.add', remote_computer_exec=[orm.load_computer('slurm-ssh'), '/bin/bash']
code = orm.InstalledCode(
default_calc_job_plugin='core.arithmetic.add',
computer=orm.load_computer('slurm-ssh'),
filepath_executable='/bin/bash'
)
inputs = {'x': orm.Int(1), 'y': orm.Int(2), 'code': code}
run_finished_ok(ArithmeticAddCalculation, **inputs)
3 changes: 3 additions & 0 deletions aiida/cmdline/__init__.py
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@
# yapf: disable
# pylint: disable=wildcard-import

from .groups import *
from .params import *
from .utils import *

@@ -24,6 +25,7 @@
'ComputerParamType',
'ConfigOptionParamType',
'DataParamType',
'DynamicEntryPointCommandGroup',
'EmailType',
'EntryPointType',
'FileOrUrl',
@@ -42,6 +44,7 @@
'ProfileParamType',
'ShebangParamType',
'UserParamType',
'VerdiCommandGroup',
'WorkflowParamType',
'dbenv',
'echo_critical',
51 changes: 22 additions & 29 deletions aiida/cmdline/commands/cmd_code.py
Original file line number Diff line number Diff line change
@@ -14,10 +14,11 @@
import tabulate

from aiida.cmdline.commands.cmd_verdi import verdi
from aiida.cmdline.groups.dynamic import DynamicEntryPointCommandGroup
from aiida.cmdline.params import arguments, options
from aiida.cmdline.params.options.commands import code as options_code
from aiida.cmdline.utils import echo
from aiida.cmdline.utils.decorators import with_dbenv
from aiida.cmdline.utils.decorators import deprecated_command, with_dbenv
from aiida.common import exceptions


@@ -26,6 +27,11 @@ def verdi_code():
"""Setup and manage codes."""


@verdi_code.group('create', cls=DynamicEntryPointCommandGroup, entry_point_name_filter=r'core\.code\..*')
def code_create():
"""Create a new code."""


def get_default(key, ctx):
"""
Get the default argument using a user instance property
@@ -79,6 +85,7 @@ def set_code_builder(ctx, param, value):
@options.CONFIG_FILE()
@click.pass_context
@with_dbenv()
@deprecated_command('This command will be removed soon, use `verdi code create` instead.')
def setup_code(ctx, non_interactive, **kwargs):
"""Setup a new code."""
from aiida.orm.utils.builders.code import CodeBuilder
@@ -106,7 +113,6 @@ def setup_code(ctx, non_interactive, **kwargs):
except Exception as exception: # pylint: disable=broad-except
echo.echo_critical(f'Unable to store the Code: {exception}')

code.reveal()
echo.echo_success(f'Code<{code.pk}> {code.full_label} created')


@@ -121,9 +127,11 @@ def code_test(code):
* Whether the remote executable exists.

"""
if not code.is_local():
from aiida.orm import InstalledCode

if isinstance(code, InstalledCode):
try:
code.validate_remote_exec_path()
code.validate_filepath_executable()
except exceptions.ValidationError as exception:
echo.echo_critical(f'validation failed: {exception}')

@@ -163,10 +171,11 @@ def code_duplicate(ctx, code, non_interactive, **kwargs):
kwargs['code_type'] = CodeBuilder.CodeType.STORE_AND_UPLOAD

if kwargs.pop('hide_original'):
code.hide()
code.is_hidden = True

# Convert entry point to its name
kwargs['input_plugin'] = kwargs['input_plugin'].name
if kwargs['input_plugin']:
kwargs['input_plugin'] = kwargs['input_plugin'].name

code_builder = ctx.code_builder
for key, value in kwargs.items():
@@ -175,7 +184,7 @@ def code_duplicate(ctx, code, non_interactive, **kwargs):

try:
new_code.store()
new_code.reveal()
new_code.is_hidden = False
except ValidationError as exception:
echo.echo_critical(f'Unable to store the Code: {exception}')

@@ -188,31 +197,15 @@ def code_duplicate(ctx, code, non_interactive, **kwargs):
def show(code):
"""Display detailed information for a code."""
from aiida.cmdline import is_verbose
from aiida.repository import FileType

table = []
table.append(['PK', code.pk])
table.append(['UUID', code.uuid])
table.append(['Label', code.label])
table.append(['Description', code.description])
table.append(['Default plugin', code.get_input_plugin_name()])

if code.is_local():
table.append(['Type', 'local'])
table.append(['Exec name', code.get_execname()])
table.append(['List of files/folders:', ''])
for obj in code.list_objects():
if obj.file_type == FileType.DIRECTORY:
table.append(['directory', obj.name])
else:
table.append(['file', obj.name])
else:
table.append(['Type', 'remote'])
table.append(['Remote machine', code.get_remote_computer().label])
table.append(['Remote absolute path', code.get_remote_exec_path()])

table.append(['Prepend text', code.get_prepend_text()])
table.append(['Append text', code.get_append_text()])
table.append(['Default plugin', code.default_calc_job_plugin])
Copy link
Member

Choose a reason for hiding this comment

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

Is this means less code information is shown by verdi code show? Can we dynamically show depending on code type or from cli option?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I removed it for now because it was hard-coding a bunch of stuff. But you are right, I think we might be able to get this behavior dynamically using the cli option definitions. There are some caveats though. As long as the cli options are stored as simple attributes, the automatic introspection may fail if there are more complex data types that are being returned, or they don't have a direct property to access it.

table.append(['Prepend text', code.prepend_text])
table.append(['Append text', code.append_text])

if is_verbose():
table.append(['Calculations', len(code.base.links.get_outgoing().all())])
@@ -252,7 +245,7 @@ def _dry_run_callback(pks):
def hide(codes):
"""Hide one or more codes from `verdi code list`."""
for code in codes:
code.hide()
code.is_hidden = True
echo.echo_success(f'Code<{code.pk}> {code.full_label} hidden')


@@ -262,7 +255,7 @@ def hide(codes):
def reveal(codes):
"""Reveal one or more hidden codes in `verdi code list`."""
for code in codes:
code.reveal()
code.is_hidden = False
echo.echo_success(f'Code<{code.pk}> {code.full_label} revealed')


@@ -275,7 +268,7 @@ def relabel(code, label):
old_label = code.full_label

try:
code.relabel(label)
code.label = label
except (TypeError, ValueError) as exception:
echo.echo_critical(f'invalid code label: {exception}')
else:
91 changes: 2 additions & 89 deletions aiida/cmdline/commands/cmd_verdi.py
Original file line number Diff line number Diff line change
@@ -8,99 +8,12 @@
# For further information please visit http://www.aiida.net #
###########################################################################
"""The main `verdi` click group."""
import difflib

import click

from aiida import __version__
from aiida.cmdline.params import options, types

GIU = (
'ABzY8%U8Kw0{@klyK?I~3`Ki?#qHQ&IIM|J;6yB`9_+{&w)p(JK}vokj-11jhve8xcx?dZ>+9nwrEF!x*S>9A+EWYrR?6GA-u?jFa+et65GF@1+D{%'
'8{C~xjt%>uVM4RTSS?j2M)XH%T#>M{K$lE2XGD`<aYaOFUit|Wh*Q?-n)Bs}n0}hc2!oAkoBQyEOaSkqfd=oq;sTs(x@(H&qOKnXDFA52~gq4CvNbk'
'%p9&pE+KH!2lm^MThvE$xC2x*>RS0T67213wbAs!SZmn+;(-m!>f(T@e%@oxd`yRBp9nu+9N`4xv8AS@O$CaQ;7FXzM=ug^$?3ta2551EDL`wK4|Cm'
'%RnJdS#0UF<by)B<XQ+8vZ}j$b$@4Q1#kj{G|g6{1#bLd+w)SZ16_wTnfo<QXMd-s4ImikcvSj#7~2Yq-1l@K)y|aS%KqzrV$m)VgR$xs8bGr7%$lJ'
'_ObpObgS_mhZ7M97P}ATen9Zil^7;P;<I08Mv_;!$Eb2P)F|&t+wT?>wVwe<Fez}g$`Zi>DkcfdNjtUv1N^iSQui#TL(q!FmIeKb!yW4|L`@!@-4x6'
'B6I^ptRdH+4o0ODM;1_f^}4@LMe@#_YHz0wQdq@d)@n)uYNtAb2OLo&fpBkct5{~3kbRag^_5QG%qrTksHMXAYAQoz1#2wtHCy0}h?CJtzv&@Q?^9r'
'd&02;isB7NJMMr7F@>$!ELj(sbwzIR4)rnch=oVZrG;8)%R6}FUk*fv2O&!#ZA)$HloK9!es&4Eb+h=OIyWFha(8PPy9u?NqfkuPYg;GO1RVzBLX)7'
'ORMM>1hEM`-96mGjJ+A!e-_}4X{M|4CkKE~uF4j+LW#6IsFa*_da_mLqzr)E<`%ikthkMO2<vdLNlWMLBrceLb&%p<SlbT6NAh+Tz~72^U2(&}V&4H'
'd8r#{;M;(*9swiCZ2RIx2&yLd0wV^AQs5$V63{q89vFwvV_?Vk!HuDTPr83yG~Wm}YG_*0gIL7B~{>>65cNMtpDE*VejqZV^MyewPJJAS*VM6jY;QY'
'#g7gOKgPbFg{@;YDL6Gbxxr|2T&BQunB?PBetq?X<bp0b)qASa|!TspwLp%9CE7Y#XI@}Wa3E6#mZC62W0F#k>>jW1hFF7&>EaYkKYqIa_ld(Z@AJT'
'+lJ(Pd;+?<&&M>A0agti19^z3n4Z6_WG}c~_+XHyJI_iau7+V$#YA$pJ~H)yHEVy1D?5^Sw`tb@{nnNNo=eSMZLf0>m^A@7f{y$nb_HJWgLRtZ?<Vq'
'Glx*4Tkeba)%-_D&vG1uUL0(DEK)&;4yGQ@C_0d4x%#)?G?XfN*{PN40X3QeC3@;*9uMwABesu!8?(0#>x2?*>SwM?JoQ>p|-1ZRU0#+{^UhK22+~o'
'R9k7rh<eeM8~?e5)U+OloQYk-ebZsXE8KFmR5;uE)C;wlw}@dIbx-{${l+HbC|PrK*6{Q(q6jMpni4Be``)PJJRU>(GH9y|jm){jY9_xAI4N_EfU#4'
'taTUXFY4a4l$v=N-+f+w&wuH;Z(6p6#=n8XwlZ;*L&-rcL~T_vEm@#-Xi8&g06!MO+R(<NRBkE$(0Y_~^2C_k<o~_a3*HAHukZKXCs8^y%UZZVvze'
)


class VerdiCommandGroup(click.Group):
"""Custom class for ``verdi`` top-level command group."""

@staticmethod
def add_verbosity_option(cmd):
"""Apply the ``verbosity`` option to the command, which is common to all ``verdi`` commands."""
# Only apply the option if it hasn't been already added in a previous call.
if cmd is not None and 'verbosity' not in [param.name for param in cmd.params]:
cmd = options.VERBOSITY()(cmd)

return cmd

def fail_with_suggestions(self, ctx, cmd_name):
"""Fail the command while trying to suggest commands to resemble the requested ``cmd_name``."""
# We might get better results with the Levenshtein distance or more advanced methods implemented in FuzzyWuzzy
# or similar libs, but this is an easy win for now.
matches = difflib.get_close_matches(cmd_name, self.list_commands(ctx), cutoff=0.5)

if not matches:
# Single letters are sometimes not matched so also try with a simple startswith
matches = [c for c in sorted(self.list_commands(ctx)) if c.startswith(cmd_name)][:3]

if matches:
formatted = '\n'.join(f'\t{m}' for m in sorted(matches))
ctx.fail(f'`{cmd_name}` is not a {self.name} command.\n\nThe most similar commands are:\n{formatted}')
else:
ctx.fail(f'`{cmd_name}` is not a {self.name} command.\n\nNo similar commands found.')

def get_command(self, ctx, cmd_name):
"""Return the command that corresponds to the requested ``cmd_name``.

This method is overridden from the base class in order to two functionalities:

* If the command is found, automatically add the verbosity option.
* If the command is not found, attempt to provide a list of suggestions with existing commands that resemble
the requested command name.

Note that if the command is not found and ``resilient_parsing`` is set to True on the context, then the latter
feature is disabled because most likely we are operating in tab-completion mode.
"""
if int(cmd_name.lower().encode('utf-8').hex(), 16) == 0x6769757365707065:
import base64
import gzip
click.echo(gzip.decompress(base64.b85decode(GIU.encode('utf-8'))).decode('utf-8'))
return None

cmd = super().get_command(ctx, cmd_name)

if cmd is not None:
return self.add_verbosity_option(cmd)

# If this command is called during tab-completion, we do not want to print an error message if the command can't
# be found, but instead we want to simply return here. However, in a normal command execution, we do want to
# execute the rest of this method to try and match commands that are similar in order to provide the user with
# some hints. The problem is that there is no one canonical way to determine whether the invocation is due to a
# normal command execution or a tab-complete operation. The `resilient_parsing` attribute of the `Context` is
# designed to allow things like tab-completion, however, it is not the only purpose. For now this is our best
# bet though to detect a tab-complete event. When `resilient_parsing` is switched on, we assume a tab-complete
# and do nothing in case the command name does not match an actual command.
if ctx.resilient_parsing:
return

self.fail_with_suggestions(ctx, cmd_name)

def group(self, *args, **kwargs):
"""Ensure that sub command groups use the same class but do not override an explicitly set value."""
kwargs.setdefault('cls', self.__class__)
return super().group(*args, **kwargs)
from ..groups import VerdiCommandGroup
from ..params import options, types


# Pass the version explicitly to ``version_option`` otherwise editable installs can show the wrong version number
17 changes: 17 additions & 0 deletions aiida/cmdline/groups/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
"""Module with custom implementations of :class:`click.Group`."""

# AUTO-GENERATED

# yapf: disable
# pylint: disable=wildcard-import

from .dynamic import *
from .verdi import *

__all__ = (
'DynamicEntryPointCommandGroup',
'VerdiCommandGroup',
)

# yapf: enable
Loading