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 node repo dump' command. #3623

Merged
merged 22 commits into from
Dec 19, 2019
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f743114
Add 'verdi node repo cp' command.
Dec 9, 2019
44413d8
Change name and options in 'file copying' command.
Dec 10, 2019
6bd8b2a
Apply suggestions for simplified prompt wording.
greschd Dec 10, 2019
2c18ba0
Add 'verdi node repo cp' command.
Dec 9, 2019
27ed482
Change name and options in 'file copying' command.
Dec 10, 2019
b63281e
Apply suggestions for simplified prompt wording.
greschd Dec 10, 2019
e616d7e
Adapt test cases to modified interface of the dump command.
Dec 10, 2019
29ece24
Merge branch 'add_node_repo_cp_command' of github.com:greschd/aiida_c…
Dec 10, 2019
ddf3048
Merge branch 'develop' into add_node_repo_cp_command
greschd Dec 10, 2019
8829b46
Add check that stderr is empty in 'node repo dump' tests.
Dec 11, 2019
d9ff8ac
Fix and simplify '--force' handling, add test.
Dec 11, 2019
da7ab3f
Do not catch exceptions when running commands through click runner.
Dec 11, 2019
445880a
Fix compatibility with Python 3.5
Dec 11, 2019
023583b
Clean up implementation of _copy_tree.
Dec 12, 2019
f32d03c
Merge branch 'develop' into add_node_repo_cp_command
greschd Dec 12, 2019
290d5ac
Merge branch 'develop' into add_node_repo_cp_command
greschd Dec 12, 2019
340fa3e
Merge branch 'develop' into add_node_repo_cp_command
greschd Dec 17, 2019
39f0800
Merge branch 'develop' of https://github.com/aiidateam/aiida-core int…
Dec 17, 2019
00e0d63
Simplify 'verdi node repo dump' command.
Dec 17, 2019
10a9583
Merge branch 'develop' into add_node_repo_cp_command
greschd Dec 18, 2019
7e60feb
Merge branch 'develop' into add_node_repo_cp_command
ltalirz Dec 19, 2019
ef4a659
Improve docstring and abort message of 'verdi node repo dump'
Dec 19, 2019
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
67 changes: 67 additions & 0 deletions aiida/backends/tests/cmdline/commands/test_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
"""Tests for verdi node"""

import os
import io
import errno
import pathlib
import tempfile

from click.testing import CliRunner
Expand Down Expand Up @@ -52,6 +54,17 @@ def setUpClass(cls, *args, **kwargs):

cls.node = node

# Set up a FolderData for the node repo cp tests.
folder_node = orm.FolderData()
cls.content_file1 = 'nobody expects'
cls.content_file2 = 'the minister of silly walks'
cls.key_file1 = 'some/nested/folder/filename.txt'
cls.key_file2 = 'some_other_file.txt'
folder_node.put_object_from_filelike(io.StringIO(cls.content_file1), key=cls.key_file1)
folder_node.put_object_from_filelike(io.StringIO(cls.content_file2), key=cls.key_file2)
folder_node.store()
cls.folder_node = folder_node

def setUp(self):
self.cli_runner = CliRunner()

Expand Down Expand Up @@ -171,6 +184,60 @@ def test_node_extras(self):
result = self.cli_runner.invoke(cmd_node.extras, options)
self.assertIsNone(result.exception, result.output)

def test_node_repo_dump(self):
"""Test 'verdi node repo dump' command."""

with tempfile.TemporaryDirectory() as tmp_dir:
out_path = pathlib.Path(tmp_dir) / 'out_dir'
options = [str(self.folder_node.uuid), str(out_path)]
res = self.cli_runner.invoke(cmd_node.repo_dump, options, catch_exceptions=False)
self.assertFalse(res.stdout)

for file_key, content in [(self.key_file1, self.content_file1), (self.key_file2, self.content_file2)]:
curr_path = out_path
for key_part in file_key.split('/'):
curr_path /= key_part
self.assertTrue(curr_path.exists())
with curr_path.open('r') as res_file:
self.assertEqual(res_file.read(), content)

def test_node_repo_dump_to_nested_folder(self):
"""Test 'verdi node repo dump' command, with an output folder whose parent does not exist."""

with tempfile.TemporaryDirectory() as tmp_dir:
out_path = pathlib.Path(tmp_dir) / 'out_dir' / 'nested' / 'path'
options = [str(self.folder_node.uuid), str(out_path)]
res = self.cli_runner.invoke(cmd_node.repo_dump, options, catch_exceptions=False)
self.assertFalse(res.stdout)

for file_key, content in [(self.key_file1, self.content_file1), (self.key_file2, self.content_file2)]:
curr_path = out_path
for key_part in file_key.split('/'):
curr_path /= key_part
self.assertTrue(curr_path.exists())
with curr_path.open('r') as res_file:
self.assertEqual(res_file.read(), content)

def test_node_repo_existing_out_dir(self):
"""Test 'verdi node repo dump' command, check that an existing output directory is not overwritten."""

with tempfile.TemporaryDirectory() as tmp_dir:
out_path = pathlib.Path(tmp_dir) / 'out_dir'
# Create the directory and put a file in it
out_path.mkdir()
some_file = out_path / 'file_name'
some_file_content = 'ni!'
with some_file.open('w') as file_handle:
file_handle.write(some_file_content)
options = [str(self.folder_node.uuid), str(out_path)]
res = self.cli_runner.invoke(cmd_node.repo_dump, options, catch_exceptions=False)
self.assertIn('exists', res.stdout)
self.assertIn('Aborted!', res.stdout)

# Make sure the directory content is still there
with some_file.open('r') as file_handle:
assert file_handle.read() == some_file_content


def delete_temporary_file(filepath):
"""
Expand Down
49 changes: 49 additions & 0 deletions aiida/cmdline/commands/cmd_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
###########################################################################
"""`verdi node` command."""

import shutil
import pathlib

import click
import tabulate

Expand Down Expand Up @@ -61,6 +64,52 @@ def repo_ls(node, relative_path, color):
echo.echo_critical(exception)


@verdi_node_repo.command('dump')
@arguments.NODE()
@click.argument(
'output_directory',
type=click.Path(),
required=True,
)
@with_dbenv()
def repo_dump(node, output_directory):
"""Copy the repository files of a node to an output directory."""
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you write a note saying that the output directory should not exist

Copy link
Member Author

Choose a reason for hiding this comment

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

Done! I did notice though that this doesn't appear anywhere in the documentation, only the top-level command help is shown. That should probably be improved, we could show the help for all command groups.

from aiida.orm.utils.repository import FileType

output_directory = pathlib.Path(output_directory)

try:
output_directory.mkdir(parents=True, exist_ok=False)
except FileExistsError:
click.echo('Error: Invalid value for "OUTPUT_DIRECTORY": Path "{}" exists.'.format(output_directory))
Copy link
Contributor

Choose a reason for hiding this comment

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

can you please use aiida.cmdline.utils.echo.echo_critical here

Copy link
Member Author

Choose a reason for hiding this comment

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

Done!

raise click.Abort

def _copy_tree(key, output_dir): # pylint: disable=too-many-branches
ltalirz marked this conversation as resolved.
Show resolved Hide resolved
"""
Recursively copy the content at the ``key`` path in the given node to
the ``output_dir``.
"""
for file in node.list_objects(key=key):
# Not using os.path.join here, because this is the "path"
# in the AiiDA node, not an actual OS - level path.
file_key = file.name if not key else key + '/' + file.name
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a good point and I now start to wonder whether we actually handle this correctly and consistently everywhere in the repositiory interface. Anyhow, this is good for now.

Copy link
Member Author

Choose a reason for hiding this comment

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

Well, that's only going to be a problem if os.sep is a backslash.. not the trickiest part for Windows support, I suppose 😄

if file.type == FileType.DIRECTORY:
new_out_dir = output_dir / file.name
assert not new_out_dir.exists()
new_out_dir.mkdir()
_copy_tree(key=file_key, output_dir=new_out_dir)

else:
assert file.type == FileType.FILE
out_file_path = output_dir / file.name
assert not out_file_path.exists()
with node.open(file_key, 'rb') as in_file:
with out_file_path.open('wb') as out_file:
shutil.copyfileobj(in_file, out_file)

_copy_tree(key='', output_dir=output_directory)


@verdi_node.command('label')
@arguments.NODES()
@options.LABEL(help='Set LABEL as the new label for all NODES')
Expand Down