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

✨ NEW: Add verdi group move-nodes command #4428

Merged
merged 5 commits into from
Jan 26, 2022
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
64 changes: 63 additions & 1 deletion aiida/cmdline/commands/cmd_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import click

from aiida.cmdline.commands.cmd_verdi import verdi
from aiida.cmdline.params import arguments, options
from aiida.cmdline.params import arguments, options, types
from aiida.cmdline.utils import echo
from aiida.cmdline.utils.decorators import with_dbenv
from aiida.common.exceptions import UniquenessError
Expand Down Expand Up @@ -84,6 +84,68 @@ def group_remove_nodes(group, nodes, clear, force):
group.remove_nodes(nodes)


@verdi_group.command('move-nodes')
@arguments.NODES()
@click.option('-s', '--source-group', type=types.GroupParamType(), required=True, help='The group whose nodes to move.')
@click.option(
'-t', '--target-group', type=types.GroupParamType(), required=True, help='The group to which the nodes are moved.'
)
@options.FORCE(help='Do not ask for confirmation and skip all checks.')
@options.ALL(help='Move all nodes from the source to the target group.')
@with_dbenv()
def group_move_nodes(source_group, target_group, force, nodes, all_entries):
"""Move the specified NODES from one group to another."""
from aiida.orm import Group, Node, QueryBuilder

if source_group.pk == target_group.pk:
echo.echo_critical(f'Source and target group are the same: {source_group}.')

if not nodes:
if all_entries:
nodes = list(source_group.nodes)
else:
echo.echo_critical('Neither NODES or the `-a, --all` option was specified.')

node_pks = [node.pk for node in nodes]

if not all_entries:
query = QueryBuilder()
query.append(Group, filters={'id': source_group.pk}, tag='group')
query.append(Node, with_group='group', filters={'id': {'in': node_pks}}, project='id')

source_group_node_pks = query.all(flat=True)

if not source_group_node_pks:
echo.echo_critical(f'None of the specified nodes are in {source_group}.')

if len(node_pks) > len(source_group_node_pks):
absent_node_pks = set(node_pks).difference(set(source_group_node_pks))
echo.echo_warning(f'{len(absent_node_pks)} nodes with PK {absent_node_pks} are not in {source_group}.')
nodes = [node for node in nodes if node.pk in source_group_node_pks]
node_pks = set(node_pks).difference(absent_node_pks)

query = QueryBuilder()
query.append(Group, filters={'id': target_group.pk}, tag='group')
query.append(Node, with_group='group', filters={'id': {'in': node_pks}}, project='id')

target_group_node_pks = query.all(flat=True)

if target_group_node_pks:
echo.echo_warning(
f'{len(target_group_node_pks)} nodes with PK {set(target_group_node_pks)} are already in '
f'{target_group}. These will still be removed from {source_group}.'
)

if not force:
click.confirm(
f'Are you sure you want to move {len(nodes)} nodes from {source_group} '
f'to {target_group}?', abort=True
)

source_group.remove_nodes(nodes)
target_group.add_nodes(nodes)


@verdi_group.command('delete')
@arguments.GROUP()
@options.FORCE()
Expand Down
1 change: 1 addition & 0 deletions docs/source/reference/command_line.rst
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ Below is a list with all available subcommands.
delete Delete a group and (optionally) the nodes it contains.
description Change the description of a group.
list Show a list of existing groups.
move-nodes Move the specified NODES from one group to another.
path Inspect groups of nodes, with delimited label paths.
relabel Change the label of a group.
remove-nodes Remove nodes from a group.
Expand Down
69 changes: 69 additions & 0 deletions tests/cmdline/commands/test_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,75 @@ def test_add_remove_nodes(self):
self.assertIn('Aborted', result.output)
self.assertEqual(group.count(), 1)

def test_move_nodes(self):
"""Test `verdi group move-nodes` command."""
node_01 = orm.CalculationNode().store()
node_02 = orm.Int(1).store()
node_03 = orm.Bool(True).store()

group1 = orm.load_group('dummygroup1')
group2 = orm.load_group('dummygroup2')

group1.add_nodes([node_01, node_02])

# Moving the nodes to the same group
result = self.cli_runner.invoke(
cmd_group.group_move_nodes, ['-s', 'dummygroup1', '-t', 'dummygroup1', node_01.uuid, node_02.uuid]
)
self.assertIn('Source and target group are the same:', result.output)

# Not specifying NODES or `--all`
result = self.cli_runner.invoke(cmd_group.group_move_nodes, ['-s', 'dummygroup1', '-t', 'dummygroup2'])
self.assertIn('Neither NODES or the `-a, --all` option was specified.', result.output)

# Moving the nodes from the empty group
result = self.cli_runner.invoke(
cmd_group.group_move_nodes, ['-s', 'dummygroup2', '-t', 'dummygroup1', node_01.uuid, node_02.uuid]
)
self.assertIn('None of the specified nodes are in', result.output)

# Move two nodes to the second dummy group, but specify a missing uuid
result = self.cli_runner.invoke(
cmd_group.group_move_nodes, ['-s', 'dummygroup1', '-t', 'dummygroup2', node_01.uuid, node_03.uuid]
)
self.assertIn(f'1 nodes with PK {{{node_03.pk}}} are not in', result.output)
# Check that the node that is present is actually moved
result = self.cli_runner.invoke(
cmd_group.group_move_nodes,
['-f', '-s', 'dummygroup1', '-t', 'dummygroup2', node_01.uuid, node_03.uuid],
)
assert node_01 not in group1.nodes
assert node_01 in group2.nodes

# Add the first node back to the first group, and try to move it from the second one
group1.add_nodes(node_01)
result = self.cli_runner.invoke(
cmd_group.group_move_nodes, ['-s', 'dummygroup2', '-t', 'dummygroup1', node_01.uuid]
)
self.assertIn(f'1 nodes with PK {{{node_01.pk}}} are already', result.output)
# Check that it is still removed from the second group
result = self.cli_runner.invoke(
cmd_group.group_move_nodes,
['-f', '-s', 'dummygroup2', '-t', 'dummygroup1', node_01.uuid],
)
assert node_01 not in group2.nodes

# Force move the two nodes to the second dummy group
result = self.cli_runner.invoke(
cmd_group.group_move_nodes, ['-f', '-s', 'dummygroup1', '-t', 'dummygroup2', node_01.uuid, node_02.uuid]
)
assert node_01 in group2.nodes
assert node_02 in group2.nodes

# Force move all nodes back to the first dummy group
result = self.cli_runner.invoke(
cmd_group.group_move_nodes, ['-f', '-s', 'dummygroup2', '-t', 'dummygroup1', '--all']
)
assert node_01 not in group2.nodes
assert node_02 not in group2.nodes
assert node_01 in group1.nodes
assert node_02 in group1.nodes

def test_copy_existing_group(self):
"""Test user is prompted to continue if destination group exists and is not empty"""
source_label = 'source_copy_existing_group'
Expand Down