Skip to content

Commit

Permalink
CLI: Enable install --download-only without available profile (#166)
Browse files Browse the repository at this point in the history
The `aiida-pseudo install sssp/pseudo-dojo` provide the `--download-only`
option to only download the archive. This is useful in case a family
needs to be installed on a machine without a (stable) internet
connection. With this option, the archive can be downloaded on another
machine first before transferring the file and installing it directly
from the archive.

Currently, however, the install command always requires an AiiDA profile
to be configured, even for the `--download-only` option in which case
the command never needs access to the profile. Here, this requirement is
removed by removing the `with_dbenv` decorator from the command.
Instead, the profile is loaded manually inside the command at the point
where it is really necessary.
  • Loading branch information
sphuber authored Oct 25, 2023
1 parent fdb5cd9 commit ef146e7
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 24 deletions.
20 changes: 18 additions & 2 deletions src/aiida_pseudo/cli/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,14 +198,15 @@ def install_sssp(
@options.DOWNLOAD_ONLY()
@options.FROM_DOWNLOAD()
@options.TRACEBACK()
@decorators.with_dbenv()
def cmd_install_sssp(version, functional, protocol, download_only, from_download, traceback):
"""Install an SSSP configuration.
The SSSP configuration will be automatically downloaded from the Materials Cloud Archive entry to create a new
`SsspFamily`.
"""
# pylint: disable=too-many-locals, too-many-statements
from aiida import load_profile
from aiida.common.exceptions import ConfigurationError
from aiida.common.files import md5_file
from aiida.orm import QueryBuilder

Expand Down Expand Up @@ -271,6 +272,13 @@ def cmd_install_sssp(version, functional, protocol, download_only, from_download
if configuration not in SsspFamily.valid_configurations:
echo.echo_critical(f'{version} {functional} {protocol} is not a valid SSSP configuration')

try:
load_profile()
except ConfigurationError:
echo.echo_critical(
'Could not load a valid AiiDA profile. Check `verdi profile list` to make sure you have one defined.'
)

if QueryBuilder().append(SsspFamily, filters={'label': label}).first():
echo.echo_report(f'{SsspFamily.__name__}<{label}> is already installed')
sys.exit(1)
Expand Down Expand Up @@ -411,7 +419,6 @@ def install_pseudo_dojo(
@options.DOWNLOAD_ONLY()
@options.FROM_DOWNLOAD()
@options.TRACEBACK()
@decorators.with_dbenv()
def cmd_install_pseudo_dojo(
version, functional, relativistic, protocol, pseudo_format, default_stringency, download_only, from_download,
traceback
Expand All @@ -422,6 +429,8 @@ def cmd_install_pseudo_dojo(
`PseudoDojoFamily` subclass instance based on the specified pseudopotential format.
"""
# pylint: disable=too-many-locals,too-many-arguments,too-many-branches,too-many-statements
from aiida import load_profile
from aiida.common.exceptions import ConfigurationError
from aiida.common.files import md5_file
from aiida.orm import QueryBuilder

Expand Down Expand Up @@ -491,6 +500,13 @@ def cmd_install_pseudo_dojo(
if configuration not in PseudoDojoFamily.valid_configurations:
echo.echo_critical(f'{configuration} is not a valid configuration')

try:
load_profile()
except ConfigurationError:
echo.echo_critical(
'Could not load a valid AiiDA profile. Check `verdi profile list` to make sure you have one defined.'
)

if QueryBuilder().append(PseudoDojoFamily, filters={'label': label}).first():
echo.echo_report(f'{PseudoDojoFamily.__name__}<{label}> is already installed')
sys.exit(1)
Expand Down
106 changes: 84 additions & 22 deletions tests/cli/test_install.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# -*- coding: utf-8 -*-
# pylint: disable=redefined-outer-name
"""Tests for `aiida-pseudo install`."""
import contextlib
import json
import pathlib

from aiida.manage.configuration import Config
from aiida.orm import QueryBuilder
import pytest

Expand All @@ -14,8 +16,54 @@
from aiida_pseudo.groups.family.sssp import SsspConfiguration, SsspFamily


@contextlib.contextmanager
def empty_config() -> Config:
"""Provide a temporary empty configuration.
This creates a temporary directory with a clean `.aiida` folder and basic configuration file. The currently loaded
configuration and profile are stored in memory and are automatically restored at the end of this context manager.
:return: a new empty config instance.
"""
import os
import tempfile

from aiida.manage import configuration, get_manager
from aiida.manage.configuration import settings

current_config = configuration.CONFIG
current_config_path = current_config.dirpath
current_profile = configuration.get_profile()
current_path_variable = os.environ.get(settings.DEFAULT_AIIDA_PATH_VARIABLE, None)

manager = get_manager()
manager.unload_profile()

with tempfile.TemporaryDirectory() as dirpath:
dirpath_config = pathlib.Path(dirpath) / 'config'
os.environ[settings.DEFAULT_AIIDA_PATH_VARIABLE] = str(dirpath_config)
settings.AIIDA_CONFIG_FOLDER = str(dirpath_config)
settings.set_configuration_directory()
configuration.CONFIG = configuration.load_config(create=True)

try:
yield configuration.CONFIG
finally:
if current_path_variable is None:
os.environ.pop(settings.DEFAULT_AIIDA_PATH_VARIABLE, None)
else:
os.environ[settings.DEFAULT_AIIDA_PATH_VARIABLE] = current_path_variable

settings.AIIDA_CONFIG_FOLDER = current_config_path
settings.set_configuration_directory()
configuration.CONFIG = current_config

if current_profile:
get_manager().load_profile(current_profile.name, allow_switch=True)


@pytest.fixture
def run_monkeypatched_install_sssp(run_cli_command, get_pseudo_potential_data, monkeypatch, tmp_path):
def run_monkeypatched_install_sssp(run_cli_command, filepath_pseudos, monkeypatch, tmp_path):
"""Fixture to monkeypatch the ``aiida_pseudo.cli.install.download_sssp`` method and call the install cmd."""

def download_sssp(
Expand All @@ -36,13 +84,10 @@ def download_sssp(
import shutil

element = 'Ar'
pseudo = get_pseudo_potential_data(element)
filepath = tmp_path / pseudo.filename

with pseudo.open(mode='rb') as handle:
md5 = hashlib.md5(handle.read()).hexdigest()
handle.seek(0)
filepath.write_bytes(handle.read())
entry_point = 'upf'
filepath_pseudo = filepath_pseudos(entry_point) / f'{element}.{entry_point}'
(tmp_path / filepath_pseudo.name).write_bytes(filepath_pseudo.read_bytes())
md5 = hashlib.md5(filepath_pseudo.read_bytes()).hexdigest()

filename_archive = shutil.make_archive('temparchive', 'gztar', root_dir=tmp_path, base_dir='.')
shutil.move(pathlib.Path.cwd() / filename_archive, filepath_archive)
Expand All @@ -60,7 +105,7 @@ def _run_monkeypatched_install_sssp(options=None, raises=None):


@pytest.fixture
def run_monkeypatched_install_pseudo_dojo(run_cli_command, get_pseudo_potential_data, monkeypatch, tmp_path):
def run_monkeypatched_install_pseudo_dojo(run_cli_command, filepath_pseudos, monkeypatch, tmp_path):
"""Fixture to monkeypatch the ``aiida_pseudo.cli.install.download_pseudo_dojo`` method and call the install cmd."""

def download_pseudo_dojo(
Expand All @@ -81,13 +126,10 @@ def download_pseudo_dojo(
import shutil

element = 'Ar'
pseudo = get_pseudo_potential_data(element, entry_point='jthxml')
filepath = tmp_path / pseudo.filename

with pseudo.open(mode='rb') as handle:
md5 = hashlib.md5(handle.read()).hexdigest()
handle.seek(0)
filepath.write_bytes(handle.read())
entry_point = 'jthxml'
filepath_pseudo = filepath_pseudos(entry_point) / f'{element}.{entry_point}'
(tmp_path / filepath_pseudo.name).write_bytes(filepath_pseudo.read_bytes())
md5 = hashlib.md5(filepath_pseudo.read_bytes()).hexdigest()

filename_archive = shutil.make_archive('temparchive', 'gztar', root_dir=tmp_path, base_dir='.')
shutil.move(pathlib.Path.cwd() / filename_archive, filepath_archive)
Expand Down Expand Up @@ -272,9 +314,19 @@ def test_install_pseudo_dojo_monkeypatched(run_monkeypatched_install_pseudo_dojo

@pytest.mark.usefixtures('clear_db', 'chdir_tmp_path')
def test_install_sssp_download_only(run_monkeypatched_install_sssp):
"""Test the ``aiida-pseudo install sssp`` command with the ``--download-only`` option."""
options = ['--download-only']
result = run_monkeypatched_install_sssp(options=options)
"""Test the ``aiida-pseudo install sssp`` command with the ``--download-only`` option.
The command should be callable with the ``--download-only`` option even if no profiles are defined for the current
AiiDA instance. To test this, the ``empty_config`` context manager temporarily unloads the profile that is loaded
for the test suite and replaces the config with an empty one. To verify that the ``empty_config`` context does its
job the command is first called without the ``--download-only`` option which should then raise. Then the command
is called again, this time with the option.
"""
with empty_config():
result = run_monkeypatched_install_sssp(raises=True)

with empty_config():
result = run_monkeypatched_install_sssp(options=['--download-only'])

assert SsspFamily.collection.count() == 0
assert 'Success: Pseudopotential archive written to:' in result.output
Expand Down Expand Up @@ -321,9 +373,19 @@ def test_install_sssp_from_download(run_monkeypatched_install_sssp, configuratio

@pytest.mark.usefixtures('clear_db', 'chdir_tmp_path')
def test_install_pseudo_dojo_download_only(run_monkeypatched_install_pseudo_dojo):
"""Test the ``aiida-pseudo install pseudo-dojo`` command with the ``--download-only`` option."""
options = ['--download-only']
result = run_monkeypatched_install_pseudo_dojo(options=options)
"""Test the ``aiida-pseudo install pseudo-dojo`` command with the ``--download-only`` option.
The command should be callable with the ``--download-only`` option even if no profiles are defined for the current
AiiDA instance. To test this, the ``empty_config`` context manager temporarily unloads the profile that is loaded
for the test suite and replaces the config with an empty one. To verify that the ``empty_config`` context does its
job the command is first called without the ``--download-only`` option which should then raise. Then the command
is called again, this time with the option.
"""
with empty_config():
result = run_monkeypatched_install_pseudo_dojo(raises=True)

with empty_config():
result = run_monkeypatched_install_pseudo_dojo(options=['--download-only'])

assert PseudoDojoFamily.collection.count() == 0
assert 'Success: Pseudopotential archive written to:' in result.output
Expand Down

0 comments on commit ef146e7

Please sign in to comment.