diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 7eda16cf..72811846 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -35,7 +35,7 @@ jobs: - name: Install Python uses: actions/setup-python@v4 with: - python-version: '3.9' + python-version: '3.11' cache: pip cache-dependency-path: pyproject.toml @@ -43,7 +43,7 @@ jobs: run: pip install -U pip wheel - name: Install Python package and dependencies - run: pip install -e .[docs,pre-commit,tests] + run: pip install -e .[all_plugins,docs,pre-commit,tests] - name: Run pre-commit run: pre-commit run --all-files || ( git status --short ; git diff ; exit 1 ) @@ -78,15 +78,41 @@ jobs: run: pip install -U pip wheel - name: Install Python package and dependencies + run: pip install -e .[all_plugins,tests] + + - name: Run pytest + run: pytest -sv -m "not minimal_install" tests + + tests-minimal-install: + + runs-on: ubuntu-latest + timeout-minutes: 30 + + strategy: + matrix: + python-version: ['3.9'] + + steps: + - uses: actions/checkout@v2 + + - name: Install Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: pip + cache-dependency-path: pyproject.toml + + - name: Install Python package without any extras run: pip install -e .[tests] - name: Run pytest - run: pytest -sv tests + run: pytest -sv tests -m minimal_install + publish: name: Publish to PyPI - needs: [pre-commit, tests] + needs: [pre-commit, tests, tests-minimal-install] runs-on: ubuntu-latest steps: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f897aec9..4059cec5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: - name: Install Python uses: actions/setup-python@v4 with: - python-version: '3.9' + python-version: '3.11' cache: pip cache-dependency-path: pyproject.toml @@ -22,7 +22,7 @@ jobs: run: pip install -U pip wheel - name: Install Python package and dependencies - run: pip install -e .[docs,pre-commit,tests] + run: pip install -e .[all_plugins,docs,pre-commit,tests] - name: Run pre-commit run: pre-commit run --all-files || ( git status --short ; git diff ; exit 1 ) @@ -56,9 +56,34 @@ jobs: run: pip install -U pip wheel - name: Install Python package and dependencies - run: pip install -e .[tests] + run: pip install -e .[all_plugins,tests] - name: Run pytest env: AIIDA_WARN_v3: true - run: pytest -sv tests + run: pytest -sv -m "not minimal_install" tests + + tests-minimal-install: + + runs-on: ubuntu-latest + timeout-minutes: 30 + + strategy: + matrix: + python-version: ['3.9'] + + steps: + - uses: actions/checkout@v2 + + - name: Install Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: pip + cache-dependency-path: pyproject.toml + + - name: Install Python package without any extras + run: pip install -e .[tests] + + - name: Run pytest + run: pytest -sv tests -m minimal_install diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fb7ee95b..7086ac0a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,3 +32,11 @@ repos: - id: pretty-format-yaml args: [--autofix] exclude: src/aiida_common_workflows/workflows/relax/cp2k/protocol.yml + +- repo: local + hooks: + - id: optional-dependencies + name: validate optional dependencies + entry: python ./dev/validate_optional_dependencies.py + language: system + files: pyproject.toml| diff --git a/.readthedocs.yml b/.readthedocs.yml index 973aa8d8..998bd09a 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -12,6 +12,7 @@ python: - method: pip path: . extra_requirements: + - all_plugins - docs sphinx: diff --git a/dev/validate_optional_dependencies.py b/dev/validate_optional_dependencies.py new file mode 100755 index 00000000..d6bfbeb0 --- /dev/null +++ b/dev/validate_optional_dependencies.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +"""Script to validate the optional dependencies in the `pyproject.toml`.""" + + +def main(): + """Validate the optional dependencies.""" + import pathlib + import sys + + import tomllib + + filepath_pyproject_toml = pathlib.Path(__file__).parent.parent / 'pyproject.toml' + + with filepath_pyproject_toml.open('rb') as handle: + pyproject = tomllib.load(handle) + + exclude = ['all_plugins', 'docs', 'pre-commit', 'tests'] + dependencies_all_plugins = pyproject['project']['optional-dependencies']['all_plugins'] + dependencies_separate = [] + + for key, dependencies in pyproject['project']['optional-dependencies'].items(): + if key in exclude: + continue + dependencies_separate.extend(dependencies) + + missing_all_plugins = set(dependencies_separate).difference(set(dependencies_all_plugins)) + excess_all_plugins = set(dependencies_all_plugins).difference(set(dependencies_separate)) + + if missing_all_plugins: + print( + 'ERROR: the `all_plugins` extras are inconsistent. The following plugin dependencies are missing: ' + f'{", ".join(missing_all_plugins)}', + file=sys.stderr, + ) + sys.exit(1) + + if excess_all_plugins: + print( + 'ERROR: the `all_plugins` extras are inconsistent. The following dependencies are not declared by any ' + f'plugin extras: {", ".join(excess_all_plugins)}', + file=sys.stderr, + ) + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/docs/source/index.rst b/docs/source/index.rst index a169ff55..82d4c207 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -62,6 +62,49 @@ The common workflows can be subdivided into two categories: +.. _installation: + +************ +Installation +************ + +The Python package can be installed from the Python Package index (PyPI) or directly from the source: +The recommended method of installation is to use the Python package manager ``pip``: + +.. code-block:: console + + $ pip install aiida-common-workflows + +This will install the latest stable version that was released to PyPI. +Note that this will not install any of the plugin packages that are required to run any of the common workflow implementations. +To install all plugin packages that implement a common workflow, run the install with the ``all_plugins`` extra: + +.. code-block:: console + + $ pip install aiida-common-workflows[all_plugins] + +Alternatively, you can choose a specific plugin to prevent having to install all plugin packages, for example: + +.. code-block:: console + + $ pip install aiida-common-workflows[quantum_espresso] + +will install the package plus the dependencies that are required to run the implementation for Quantum ESPRESSO. +The available extras are ``abinit``, ``bigdft``, ``castep``, ``cp2k``, ``fleur``, ``gaussian``, ``gpaw``, ``nwchem``, ``orca``, ``quantum_espresso``, ``siesta``, ``vasp`` and ``wien2k``. + +To install the package from source, first clone the repository and then install using ``pip``: + +.. code-block:: console + + $ git clone https://github.com/aiidateam/aiida-common-workflows + $ pip install -e aiida-common-workflows + +The ``-e`` flag will install the package in editable mode, meaning that changes to the source code will be automatically picked up. + +To work with ``aiida-common-workflows``, a configured AiiDA profile is required. +Please refer to `AiiDA's documentation `_ for detailed instructions. + + .. _how-to-submit: ******************************* @@ -94,7 +137,7 @@ If more flexibility is required, it is advised to write a custom launch script, .. code:: python from aiida.engine import submit - from aiida.plugins import WorkflowFactory + from aiida_common_workflows.plugins import WorkflowFactory RelaxWorkChain = WorkflowFactory('common_workflows.relax.quantum_espresso') # Load the relax workflow implementation of choice. diff --git a/docs/source/workflows/base/relax/index.rst b/docs/source/workflows/base/relax/index.rst index 9d11e526..909f4c96 100644 --- a/docs/source/workflows/base/relax/index.rst +++ b/docs/source/workflows/base/relax/index.rst @@ -36,7 +36,7 @@ A typical script for the submission of common relax workflow could look somethin .. code:: python from aiida.engine import submit - from aiida.plugins import WorkflowFactory + from aiida_common_workflows.plugins import WorkflowFactory RelaxWorkChain = WorkflowFactory('common_workflows.relax.') # Load the relax workflow implementation of choice. diff --git a/docs/source/workflows/composite/dc.rst b/docs/source/workflows/composite/dc.rst index 3719da39..a2edc117 100644 --- a/docs/source/workflows/composite/dc.rst +++ b/docs/source/workflows/composite/dc.rst @@ -17,7 +17,7 @@ A typical script for the submission of common DC workflow could look something l from aiida.orm import List, Dict from aiida.engine import submit - from aiida.plugins import WorkflowFactory + from aiida_common_workflows.plugins import WorkflowFactory cls = WorkflowFactory('common_workflows.dissociation_curve') diff --git a/docs/source/workflows/composite/eos.rst b/docs/source/workflows/composite/eos.rst index c0089cc6..7e27f1d7 100644 --- a/docs/source/workflows/composite/eos.rst +++ b/docs/source/workflows/composite/eos.rst @@ -15,7 +15,7 @@ A typical script for the submission of common EoS workflow could look something from aiida.orm import List, Dict from aiida.engine import submit - from aiida.plugins import WorkflowFactory + from aiida_common_workflows.plugins import WorkflowFactory cls = WorkflowFactory('common_workflows.eos') diff --git a/pyproject.toml b/pyproject.toml index aa61b355..d6cdb4fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,25 +17,9 @@ classifiers = [ 'Programming Language :: Python :: 3.12' ] dependencies = [ - 'abipy==0.9.6', - 'aiida-ase~=3.0', - 'aiida-abinit~=0.5.0', - 'aiida-bigdft~=0.3.0', - 'aiida-castep~=2.0', 'aiida-core[atomic_tools]~=2.1', - 'aiida-cp2k~=2.0', - 'aiida-fleur~=2.0', - 'aiida-gaussian~=2.0', - 'aiida-nwchem~=3.0', - 'aiida-orca~=0.6.0', - 'aiida-pseudo~=1.0', - 'aiida-quantumespresso~=4.4', - 'aiida-siesta~=2.0', - 'aiida-vasp~=3.1', - 'aiida-wien2k~=0.2.0', 'ase!=3.20.*', 'click~=8.0', - 'masci-tools~=0.9', 'pint~=0.16', 'pymatgen>=2022.1.20' ] @@ -65,6 +49,36 @@ requires-python = '>=3.9' 'common_workflows.relax.wien2k' = 'aiida_common_workflows.workflows.relax.wien2k.workchain:Wien2kCommonRelaxWorkChain' [project.optional-dependencies] +abinit = [ + 'abipy==0.9.6', + 'aiida-abinit~=0.5.0' +] +all_plugins = [ + 'abipy==0.9.6', + 'aiida-abinit~=0.5.0', + 'aiida-ase~=3.0', + 'aiida-bigdft~=0.3.0', + 'aiida-castep~=2.0', + 'aiida-cp2k~=2.0', + 'aiida-fleur~=2.0', + 'aiida-gaussian~=2.0', + 'aiida-nwchem~=3.0', + 'aiida-orca~=0.6.0', + 'aiida-quantumespresso~=4.4', + 'aiida-siesta~=2.0', + 'aiida-vasp~=3.1', + 'aiida-wien2k~=0.2.0', + 'masci-tools~=0.9' +] +bigdft = [ + 'aiida-bigdft~=0.3.0' +] +castep = [ + 'aiida-castep~=2.0' +] +cp2k = [ + 'aiida-cp2k~=2.0' +] docs = [ 'pydata-sphinx-theme~=0.14.3', 'sphinx~=7.2', @@ -72,14 +86,42 @@ docs = [ 'sphinx-design~=0.5.0', 'sphinxcontrib-details-directive~=0.1.0' ] +fleur = [ + 'aiida-fleur~=2.0', + 'masci-tools~=0.9' +] +gaussian = [ + 'aiida-gaussian~=2.0' +] +gpaw = [ + 'aiida-ase~=3.0' +] +nwchem = [ + 'aiida-nwchem~=3.0' +] +orca = [ + 'aiida-orca~=0.6.0' +] pre-commit = [ 'pre-commit~=3.6' ] +quantum_espresso = [ + 'aiida-quantumespresso~=4.4' +] +siesta = [ + 'aiida-siesta~=2.0' +] tests = [ 'pytest~=7.2', 'pgtest~=1.3,>=1.3.1', 'pytest-regressions~=1.0' ] +vasp = [ + 'aiida-vasp~=3.1' +] +wien2k = [ + 'aiida-wien2k~=0.2.0' +] [project.scripts] acwf = 'aiida_common_workflows.cli:cmd_root' @@ -120,6 +162,9 @@ filterwarnings = [ 'ignore:Creating AiiDA configuration folder.*:UserWarning', 'ignore:Object of type .* not in session, .* operation along .* will not proceed:sqlalchemy.exc.SAWarning' ] +markers = [ + 'minimal_install: mark test as relevant for minimal install.' +] testpaths = [ 'tests' ] diff --git a/src/aiida_common_workflows/plugins/__init__.py b/src/aiida_common_workflows/plugins/__init__.py index 2936a136..567bb19c 100644 --- a/src/aiida_common_workflows/plugins/__init__.py +++ b/src/aiida_common_workflows/plugins/__init__.py @@ -1,4 +1,10 @@ """Module with utilities for working with the plugins provided by this plugin package.""" from .entry_point import get_entry_point_name_from_class, get_workflow_entry_point_names, load_workflow_entry_point +from .factories import WorkflowFactory -__all__ = ('get_workflow_entry_point_names', 'get_entry_point_name_from_class', 'load_workflow_entry_point') +__all__ = ( + 'WorkflowFactory', + 'get_workflow_entry_point_names', + 'get_entry_point_name_from_class', + 'load_workflow_entry_point', +) diff --git a/src/aiida_common_workflows/plugins/entry_point.py b/src/aiida_common_workflows/plugins/entry_point.py index 56337038..4dd4f8dc 100644 --- a/src/aiida_common_workflows/plugins/entry_point.py +++ b/src/aiida_common_workflows/plugins/entry_point.py @@ -3,6 +3,8 @@ from aiida.plugins import entry_point +from .factories import WorkflowFactory + PACKAGE_PREFIX = 'common_workflows' __all__ = ('get_workflow_entry_point_names', 'get_entry_point_name_from_class', 'load_workflow_entry_point') @@ -38,5 +40,5 @@ def load_workflow_entry_point(workflow: str, plugin_name: str): :param plugin_name: name of the plugin implementation. :return: the workchain class of the plugin implementation of the common workflow. """ - prefix = f'{PACKAGE_PREFIX}.{workflow}.{plugin_name}' - return entry_point.load_entry_point('aiida.workflows', prefix) + entry_point_name = f'{PACKAGE_PREFIX}.{workflow}.{plugin_name}' + return WorkflowFactory(entry_point_name) diff --git a/src/aiida_common_workflows/plugins/factories.py b/src/aiida_common_workflows/plugins/factories.py new file mode 100644 index 00000000..b8a48435 --- /dev/null +++ b/src/aiida_common_workflows/plugins/factories.py @@ -0,0 +1,47 @@ +"""Factories to load entry points.""" +import typing as t + +from aiida import plugins +from aiida.common import exceptions + +if t.TYPE_CHECKING: + from aiida.engine import WorkChain + from importlib_metadata import EntryPoint + +__all__ = ('WorkflowFactory',) + + +@t.overload +def WorkflowFactory(entry_point_name: str, load: t.Literal[True] = True) -> t.Union[t.Type['WorkChain'], t.Callable]: + ... + + +@t.overload +def WorkflowFactory(entry_point_name: str, load: t.Literal[False]) -> 'EntryPoint': + ... + + +def WorkflowFactory(entry_point_name: str, load: bool = True) -> t.Union['EntryPoint', t.Type['WorkChain'], t.Callable]: # noqa: N802 + """Return the `WorkChain` sub class registered under the given entry point. + + :param entry_point_name: the entry point name. + :param load: if True, load the matched entry point and return the loaded resource instead of the entry point itself. + :return: sub class of :py:class:`~aiida.engine.processes.workchains.workchain.WorkChain` or a `workfunction` + :raises aiida.common.MissingEntryPointError: entry point was not registered + :raises aiida.common.MultipleEntryPointError: entry point could not be uniquely resolved + :raises aiida.common.LoadingEntryPointError: entry point could not be loaded + :raises aiida.common.InvalidEntryPointTypeError: if the type of the loaded entry point is invalid. + """ + common_workflow_prefixes = ('common_workflows.relax.', 'common_workflows.bands.') + try: + return plugins.WorkflowFactory(entry_point_name, load) + except exceptions.MissingEntryPointError as exception: + for prefix in common_workflow_prefixes: + if entry_point_name.startswith(prefix): + plugin_name = entry_point_name.removeprefix(prefix) + raise exceptions.MissingEntryPointError( + f'Could not load the entry point `{entry_point_name}`, probably because the plugin package is not ' + f'installed. Please install it with `pip install aiida-common-workflows[{plugin_name}]`.' + ) from exception + else: # noqa: PLW0120 + raise diff --git a/src/aiida_common_workflows/workflows/relax/abinit/workchain.py b/src/aiida_common_workflows/workflows/relax/abinit/workchain.py index 52083421..64b0a79b 100644 --- a/src/aiida_common_workflows/workflows/relax/abinit/workchain.py +++ b/src/aiida_common_workflows/workflows/relax/abinit/workchain.py @@ -3,7 +3,7 @@ from aiida import orm from aiida.common import exceptions from aiida.engine import calcfunction -from aiida_abinit.workflows.base import AbinitBaseWorkChain +from aiida.plugins import WorkflowFactory from ..workchain import CommonRelaxWorkChain from .generator import AbinitCommonRelaxInputGenerator @@ -44,7 +44,7 @@ def get_total_magnetization(parameters): class AbinitCommonRelaxWorkChain(CommonRelaxWorkChain): """Implementation of `aiida_common_workflows.common.relax.workchain.CommonRelaxWorkChain` for Abinit.""" - _process_class = AbinitBaseWorkChain + _process_class = WorkflowFactory('abinit.base') _generator_class = AbinitCommonRelaxInputGenerator def convert_outputs(self): diff --git a/src/aiida_common_workflows/workflows/relax/castep/generator.py b/src/aiida_common_workflows/workflows/relax/castep/generator.py index 1f380356..4c6e1cbd 100644 --- a/src/aiida_common_workflows/workflows/relax/castep/generator.py +++ b/src/aiida_common_workflows/workflows/relax/castep/generator.py @@ -8,14 +8,15 @@ import yaml from aiida import engine, orm, plugins from aiida.common import exceptions -from aiida_castep.data import get_pseudos_from_structure -from aiida_castep.data.otfg import OTFGGroup from aiida_common_workflows.common import ElectronicType, RelaxType, SpinType from aiida_common_workflows.generators import ChoiceType, CodeType from ..generator import CommonRelaxInputGenerator +if t.TYPE_CHECKING: + from aiida_castep.data.otfg import OTFGGroup + KNOWN_BUILTIN_FAMILIES = ('C19', 'NCP19', 'QC5', 'C17', 'C9') __all__ = ('CastepCommonRelaxInputGenerator',) @@ -247,8 +248,8 @@ def generate_inputs( :param override: a dictionary to override specific inputs :return: input dictionary """ - from aiida.common.lang import type_check + from aiida_castep.data.otfg import OTFGGroup family_name = protocol['relax']['base']['pseudos_family'] if isinstance(family_name, orm.Str): @@ -285,7 +286,7 @@ def generate_inputs_relax( protocol: t.Dict, code: orm.Code, structure: orm.StructureData, - otfg_family: OTFGGroup, + otfg_family: 'OTFGGroup', override: t.Optional[t.Dict[str, t.Any]] = None, ) -> t.Dict[str, t.Any]: """Generate the inputs for the `CastepCommonRelaxWorkChain` for a given code, structure and pseudo potential family. @@ -321,7 +322,7 @@ def generate_inputs_base( protocol: t.Dict, code: orm.Code, structure: orm.StructureData, - otfg_family: OTFGGroup, + otfg_family: 'OTFGGroup', override: t.Optional[t.Dict[str, t.Any]] = None, ) -> t.Dict[str, t.Any]: """Generate the inputs for the `CastepBaseWorkChain` for a given code, structure and pseudo potential family. @@ -359,7 +360,7 @@ def generate_inputs_calculation( protocol: t.Dict, code: orm.Code, structure: orm.StructureData, - otfg_family: OTFGGroup, + otfg_family: 'OTFGGroup', override: t.Optional[t.Dict[str, t.Any]] = None, ) -> t.Dict[str, t.Any]: """Generate the inputs for the `CastepCalculation` for a given code, structure and pseudo potential family. @@ -372,6 +373,7 @@ def generate_inputs_calculation( :return: the fully defined input dictionary. """ from aiida_castep.calculations.helper import CastepHelper + from aiida_castep.data import get_pseudos_from_structure override = {} if not override else override.get('calc', {}) # This merge perserves the merged `parameters` in the override @@ -415,9 +417,8 @@ def ensure_otfg_family(family_name, force_update=False): NOTE: CASTEP also supports UPF families, but it is not enabled here, since no UPS based protocol has been implemented. """ - from aiida.common import NotExistent - from aiida_castep.data.otfg import upload_otfg_family + from aiida_castep.data.otfg import OTFGGroup, upload_otfg_family # Ensure family name is a str if isinstance(family_name, orm.Str): diff --git a/src/aiida_common_workflows/workflows/relax/quantum_espresso/generator.py b/src/aiida_common_workflows/workflows/relax/quantum_espresso/generator.py index 3d1cdfdf..64ab66bf 100644 --- a/src/aiida_common_workflows/workflows/relax/quantum_espresso/generator.py +++ b/src/aiida_common_workflows/workflows/relax/quantum_espresso/generator.py @@ -3,7 +3,6 @@ import yaml from aiida import engine, orm, plugins -from aiida_quantumespresso.workflows.protocols.utils import recursive_merge from aiida_common_workflows.common import ElectronicType, RelaxType, SpinType from aiida_common_workflows.generators import ChoiceType, CodeType @@ -108,8 +107,8 @@ def _construct_builder(self, **kwargs) -> engine.ProcessBuilder: # noqa: PLR091 The keyword arguments will have been validated against the input generator specification. """ - from aiida_quantumespresso.common import types + from aiida_quantumespresso.workflows.protocols.utils import recursive_merge from qe_tools import CONSTANTS structure = kwargs['structure'] diff --git a/tests/cli/test_root.py b/tests/cli/test_root.py index cd194bb2..619e173c 100644 --- a/tests/cli/test_root.py +++ b/tests/cli/test_root.py @@ -32,6 +32,7 @@ def recurse_commands(command: click.Command, parents: list[str] | None = None): @pytest.mark.parametrize('command', recurse_commands(cmd_root)) @pytest.mark.parametrize('help_option', ('--help', '-h')) +@pytest.mark.minimal_install def test_commands_help_option(command, help_option): """Test the help options for all subcommands of the CLI. diff --git a/tests/test_minimal_install.py b/tests/test_minimal_install.py new file mode 100644 index 00000000..e1c682cb --- /dev/null +++ b/tests/test_minimal_install.py @@ -0,0 +1,48 @@ +"""Tests for a minimal install of the package without any extra dependencies. + +The unit tests in this module should be run against a minimal install of the package without any extra dependencies +installed. This guarantees that most of the code can be imported without any plugin packages being installed. +""" +import pytest +from aiida.common import exceptions +from aiida_common_workflows.plugins import WorkflowFactory, get_workflow_entry_point_names + + +@pytest.mark.minimal_install +def test_imports(): + """The following modules should be importable without any plugin packages installed.""" + import aiida_common_workflows.cli + import aiida_common_workflows.common + import aiida_common_workflows.generators + import aiida_common_workflows.plugins + import aiida_common_workflows.protocol + import aiida_common_workflows.utils + import aiida_common_workflows.workflows + import aiida_common_workflows.workflows.dissociation + import aiida_common_workflows.workflows.eos # noqa: F401 + + +@pytest.mark.minimal_install +@pytest.mark.parametrize('entry_point_name', get_workflow_entry_point_names('relax')) +def test_workflow_factory_relax(entry_point_name): + """Test that trying to load common relax workflow implementations will raise if not installed. + + The exception message should provide the pip command to install the require plugin package. + """ + plugin_name = entry_point_name.removeprefix('common_workflows.relax.') + match = rf'.*plugin package is not installed.*`pip install aiida-common-workflows\[{plugin_name}\]`.*' + with pytest.raises(exceptions.MissingEntryPointError, match=match): + WorkflowFactory(entry_point_name) + + +@pytest.mark.minimal_install +@pytest.mark.parametrize('entry_point_name', get_workflow_entry_point_names('bands')) +def test_workflow_factory_bands(entry_point_name): + """Test that trying to load common bands workflow implementations will raise if not installed. + + The exception message should provide the pip command to install the require plugin package. + """ + plugin_name = entry_point_name.removeprefix('common_workflows.bands.') + match = rf'.*plugin package is not installed.*`pip install aiida-common-workflows\[{plugin_name}\]`.*' + with pytest.raises(exceptions.MissingEntryPointError, match=match): + WorkflowFactory(entry_point_name) diff --git a/tests/workflows/relax/test_abinit.py b/tests/workflows/relax/test_abinit.py index f1fb154a..132c7911 100644 --- a/tests/workflows/relax/test_abinit.py +++ b/tests/workflows/relax/test_abinit.py @@ -1,10 +1,11 @@ """Tests for the :mod:`aiida_common_workflows.workflows.relax.abinit` module.""" - import pytest from aiida import engine, plugins -WORKCHAIN = plugins.WorkflowFactory('common_workflows.relax.abinit') -GENERATOR = WORKCHAIN.get_input_generator() + +@pytest.fixture +def generator(): + return plugins.WorkflowFactory('common_workflows.relax.abinit').get_input_generator() @pytest.fixture @@ -22,52 +23,52 @@ def default_builder_inputs(generate_code, generate_structure): @pytest.mark.usefixtures('pseudo_dojo_jthxml_family') -def test_get_builder(default_builder_inputs): +def test_get_builder(generator, default_builder_inputs): """Test the ``get_builder`` with default arguments.""" - builder = GENERATOR.get_builder(**default_builder_inputs) + builder = generator.get_builder(**default_builder_inputs) assert isinstance(builder, engine.ProcessBuilder) @pytest.mark.usefixtures('pseudo_dojo_jthxml_family') @pytest.mark.skip('Running this test will fail with an `UnroutableError` in `kiwipy`.') -def test_submit(default_builder_inputs): +def test_submit(generator, default_builder_inputs): """Test submitting the builder returned by ``get_builder`` called with default arguments. This will actually create the ``WorkChain`` instance, so if it doesn't raise, that means the input spec was valid. """ - builder = GENERATOR.get_builder(**default_builder_inputs) + builder = generator.get_builder(**default_builder_inputs) engine.submit(builder) @pytest.mark.usefixtures('pseudo_dojo_jthxml_family') -def test_supported_electronic_types(default_builder_inputs): +def test_supported_electronic_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``electronic_types``.""" inputs = default_builder_inputs - for electronic_type in GENERATOR.spec().inputs['electronic_type'].choices: + for electronic_type in generator.spec().inputs['electronic_type'].choices: inputs['electronic_type'] = electronic_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) @pytest.mark.usefixtures('pseudo_dojo_jthxml_family') -def test_supported_relax_types(default_builder_inputs): +def test_supported_relax_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``relax_types``.""" inputs = default_builder_inputs - for relax_type in GENERATOR.spec().inputs['relax_type'].choices: + for relax_type in generator.spec().inputs['relax_type'].choices: inputs['relax_type'] = relax_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) @pytest.mark.usefixtures('pseudo_dojo_jthxml_family') @pytest.mark.filterwarnings('ignore: input magnetization per site was None') -def test_supported_spin_types(default_builder_inputs): +def test_supported_spin_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``spin_types``.""" inputs = default_builder_inputs - for spin_type in GENERATOR.spec().inputs['spin_type'].choices: + for spin_type in generator.spec().inputs['spin_type'].choices: inputs['spin_type'] = spin_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) diff --git a/tests/workflows/relax/test_bigdft.py b/tests/workflows/relax/test_bigdft.py index de0f9b50..1363ef95 100644 --- a/tests/workflows/relax/test_bigdft.py +++ b/tests/workflows/relax/test_bigdft.py @@ -1,10 +1,11 @@ """Tests for the :mod:`aiida_common_workflows.workflows.relax.bigdft` module.""" - import pytest from aiida import engine, plugins -WORKCHAIN = plugins.WorkflowFactory('common_workflows.relax.bigdft') -GENERATOR = WORKCHAIN.get_input_generator() + +@pytest.fixture +def generator(): + return plugins.WorkflowFactory('common_workflows.relax.bigdft').get_input_generator() @pytest.fixture @@ -21,47 +22,47 @@ def default_builder_inputs(generate_code, generate_structure): } -def test_get_builder(default_builder_inputs): +def test_get_builder(generator, default_builder_inputs): """Test the ``get_builder`` with default arguments.""" - builder = GENERATOR.get_builder(**default_builder_inputs) + builder = generator.get_builder(**default_builder_inputs) assert isinstance(builder, engine.ProcessBuilder) @pytest.mark.skip('Running this test will fail with an `UnroutableError` in `kiwipy`.') -def test_submit(default_builder_inputs): +def test_submit(generator, default_builder_inputs): """Test submitting the builder returned by ``get_builder`` called with default arguments. This will actually create the ``WorkChain`` instance, so if it doesn't raise, that means the input spec was valid. """ - builder = GENERATOR.get_builder(**default_builder_inputs) + builder = generator.get_builder(**default_builder_inputs) engine.submit(builder) -def test_supported_electronic_types(default_builder_inputs): +def test_supported_electronic_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``electronic_types``.""" inputs = default_builder_inputs - for electronic_type in GENERATOR.spec().inputs['electronic_type'].choices: + for electronic_type in generator.spec().inputs['electronic_type'].choices: inputs['electronic_type'] = electronic_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) -def test_supported_relax_types(default_builder_inputs): +def test_supported_relax_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``relax_types``.""" inputs = default_builder_inputs - for relax_type in GENERATOR.spec().inputs['relax_type'].choices: + for relax_type in generator.spec().inputs['relax_type'].choices: inputs['relax_type'] = relax_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) -def test_supported_spin_types(default_builder_inputs): +def test_supported_spin_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``spin_types``.""" inputs = default_builder_inputs - for spin_type in GENERATOR.spec().inputs['spin_type'].choices: + for spin_type in generator.spec().inputs['spin_type'].choices: inputs['spin_type'] = spin_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) diff --git a/tests/workflows/relax/test_castep.py b/tests/workflows/relax/test_castep.py index bf0f1c39..bb9cdf04 100644 --- a/tests/workflows/relax/test_castep.py +++ b/tests/workflows/relax/test_castep.py @@ -1,28 +1,17 @@ """Tests for the :mod:`aiida_common_workflows.workflows.relax.castep` module.""" - import copy import pytest from aiida import engine, plugins from aiida.orm import StructureData from aiida.plugins import WorkflowFactory -from aiida_castep.data.otfg import OTFGGroup -from aiida_common_workflows.workflows.relax.castep.generator import ( - CastepCommonRelaxInputGenerator, - ElectronicType, - RelaxType, - SpinType, - ensure_otfg_family, - generate_inputs, - generate_inputs_base, - generate_inputs_calculation, - generate_inputs_relax, -) -from aiida_common_workflows.workflows.relax.castep.workchain import CastepCommonRelaxWorkChain +from aiida_common_workflows.common import ElectronicType, RelaxType, SpinType from ase.build.bulk import bulk -WORKCHAIN = plugins.WorkflowFactory('common_workflows.relax.castep') -GENERATOR = WORKCHAIN.get_input_generator() + +@pytest.fixture +def generator(): + return plugins.WorkflowFactory('common_workflows.relax.castep').get_input_generator() @pytest.fixture @@ -50,6 +39,8 @@ def castep_code(generate_code): @pytest.fixture def with_otfg(with_database): """Ensure has OTFG""" + from aiida_common_workflows.workflows.relax.castep.generator import ensure_otfg_family + ensure_otfg_family('C19') @@ -64,54 +55,57 @@ def default_builder_inputs(generate_code, generate_structure, castep_code): } -def test_get_builder(default_builder_inputs): +def test_get_builder(generator, default_builder_inputs): """Test the ``get_builder`` with default arguments.""" - builder = GENERATOR.get_builder(**default_builder_inputs) + builder = generator.get_builder(**default_builder_inputs) assert isinstance(builder, engine.ProcessBuilder) @pytest.mark.skip('Running this test will fail with an `UnroutableError` in `kiwipy`.') -def test_submit(default_builder_inputs): +def test_submit(generator, default_builder_inputs): """Test submitting the builder returned by ``get_builder`` called with default arguments. This will actually create the ``WorkChain`` instance, so if it doesn't raise, that means the input spec was valid. """ - builder = GENERATOR.get_builder(**default_builder_inputs) + builder = generator.get_builder(**default_builder_inputs) engine.submit(builder) -def test_supported_electronic_types(default_builder_inputs): +def test_supported_electronic_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``electronic_types``.""" inputs = default_builder_inputs - for electronic_type in GENERATOR.spec().inputs['electronic_type'].choices: + for electronic_type in generator.spec().inputs['electronic_type'].choices: inputs['electronic_type'] = electronic_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) -def test_supported_relax_types(default_builder_inputs): +def test_supported_relax_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``relax_types``.""" inputs = default_builder_inputs - for relax_type in GENERATOR.spec().inputs['relax_type'].choices: + for relax_type in generator.spec().inputs['relax_type'].choices: inputs['relax_type'] = relax_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) -def test_supported_spin_types(default_builder_inputs): +def test_supported_spin_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``spin_types``.""" inputs = default_builder_inputs - for spin_type in GENERATOR.spec().inputs['spin_type'].choices: + for spin_type in generator.spec().inputs['spin_type'].choices: inputs['spin_type'] = spin_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) def test_calc_generator(nacl, castep_code, with_otfg): """Test the functionality of the calculation generator""" + from aiida_castep.data.otfg import OTFGGroup + from aiida_common_workflows.workflows.relax.castep.generator import generate_inputs_calculation + protcol = { 'kpoints_spacing': 0.05, 'calc': {'parameters': {'task': 'geometryoptimisation', 'basis_precision': 'medium'}}, @@ -128,6 +122,9 @@ def test_calc_generator(nacl, castep_code, with_otfg): def test_base_generator(castep_code, nacl, with_otfg): """Test for generating the Base namespace""" + from aiida_castep.data.otfg import OTFGGroup + from aiida_common_workflows.workflows.relax.castep.generator import generate_inputs_base + protcol = { 'kpoints_spacing': 0.05, 'max_iterations': 5, @@ -145,12 +142,12 @@ def test_base_generator(castep_code, nacl, with_otfg): assert generated['calc']['metadata']['label'] == 'test' -def test_relax_generator(castep_code, nacl, with_otfg): +def test_relax_generator(generator, castep_code, nacl, with_otfg): """Test for generating the relax namespace""" - CastepCommonRelaxWorkChain = WorkflowFactory('castep.relax') # noqa: N806 - protocol = CastepCommonRelaxInputGenerator(process_class=CastepCommonRelaxWorkChain).get_protocol('moderate')[ - 'relax' - ] + from aiida_castep.data.otfg import OTFGGroup + from aiida_common_workflows.workflows.relax.castep.generator import generate_inputs_relax + + protocol = generator.get_protocol('moderate')['relax'] override = { 'base': { 'metadata': {'label': 'test'}, @@ -170,11 +167,13 @@ def test_relax_generator(castep_code, nacl, with_otfg): assert generated['base']['metadata']['label'] == 'test' -def test_generate_inputs(castep_code, nacl, si): +def test_generate_inputs(generator, castep_code, nacl, si): """ Test for the generator """ - protocol = CastepCommonRelaxInputGenerator(process_class=CastepCommonRelaxWorkChain).get_protocol('moderate') + from aiida_common_workflows.workflows.relax.castep.generator import generate_inputs + + protocol = generator.get_protocol('moderate') override = {'base': {'metadata': {'label': 'test'}, 'calc': {}}} output = generate_inputs(WorkflowFactory('castep.relax'), copy.deepcopy(protocol), castep_code, si, override) @@ -186,28 +185,31 @@ def test_generate_inputs(castep_code, nacl, si): assert 'structure' in output['calc'] -def test_input_generator(castep_code, nacl, si): +def test_input_generator(generator, castep_code, nacl, si): """Test for the input generator""" - gen = CastepCommonRelaxInputGenerator(process_class=CastepCommonRelaxWorkChain) engines = {'relax': {'code': castep_code, 'options': {}}} - builder = gen.get_builder(structure=si, engines=engines, protocol='moderate') + builder = generator.get_builder(structure=si, engines=engines, protocol='moderate') param = builder.calc.parameters.get_dict() assert param['cut_off_energy'] == 326 assert builder.base.kpoints_spacing == pytest.approx(0.023873, abs=1e-6) - builder = gen.get_builder(structure=si, engines=engines, protocol='moderate', relax_type=RelaxType.POSITIONS) + builder = generator.get_builder(structure=si, engines=engines, protocol='moderate', relax_type=RelaxType.POSITIONS) assert 'fix_all_cell' in builder.calc.parameters.get_dict() - builder = gen.get_builder(structure=si, engines=engines, protocol='moderate', relax_type=RelaxType.POSITIONS_SHAPE) + builder = generator.get_builder( + structure=si, engines=engines, protocol='moderate', relax_type=RelaxType.POSITIONS_SHAPE + ) assert 'fix_vol' in builder.calc.parameters.get_dict() - builder = gen.get_builder(structure=si, engines=engines, protocol='moderate', spin_type=SpinType.COLLINEAR) + builder = generator.get_builder(structure=si, engines=engines, protocol='moderate', spin_type=SpinType.COLLINEAR) assert 'SPINS' in builder.calc.settings.get_dict() - builder = gen.get_builder(structure=si, engines=engines, protocol='moderate', spin_type=SpinType.NON_COLLINEAR) + builder = generator.get_builder( + structure=si, engines=engines, protocol='moderate', spin_type=SpinType.NON_COLLINEAR + ) assert builder.calc.settings['SPINS'][0] == [1.0, 1.0, 1.0] - builder = gen.get_builder( + builder = generator.get_builder( structure=si, engines=engines, protocol='moderate', electronic_type=ElectronicType.INSULATOR ) assert builder.calc.settings is None @@ -218,6 +220,8 @@ def test_otfg_upload(with_otfg): """ Test uploading customized OTFG family """ + from aiida_castep.data.otfg import OTFGGroup + from aiida_common_workflows.workflows.relax.castep.generator import ensure_otfg_family # Initial upload ensure_otfg_family('C19V2') diff --git a/tests/workflows/relax/test_cp2k.py b/tests/workflows/relax/test_cp2k.py index 40de2478..18fa8468 100644 --- a/tests/workflows/relax/test_cp2k.py +++ b/tests/workflows/relax/test_cp2k.py @@ -1,10 +1,11 @@ """Tests for the :mod:`aiida_common_workflows.workflows.relax.cp2k` module.""" - import pytest from aiida import engine, plugins -WORKCHAIN = plugins.WorkflowFactory('common_workflows.relax.cp2k') -GENERATOR = WORKCHAIN.get_input_generator() + +@pytest.fixture +def generator(): + return plugins.WorkflowFactory('common_workflows.relax.cp2k').get_input_generator() @pytest.fixture @@ -21,47 +22,47 @@ def default_builder_inputs(generate_code, generate_structure): } -def test_get_builder(default_builder_inputs): +def test_get_builder(generator, default_builder_inputs): """Test the ``get_builder`` with default arguments.""" - builder = GENERATOR.get_builder(**default_builder_inputs) + builder = generator.get_builder(**default_builder_inputs) assert isinstance(builder, engine.ProcessBuilder) @pytest.mark.skip('Running this test will fail with an `UnroutableError` in `kiwipy`.') -def test_submit(default_builder_inputs): +def test_submit(generator, default_builder_inputs): """Test submitting the builder returned by ``get_builder`` called with default arguments. This will actually create the ``WorkChain`` instance, so if it doesn't raise, that means the input spec was valid. """ - builder = GENERATOR.get_builder(**default_builder_inputs) + builder = generator.get_builder(**default_builder_inputs) engine.submit(builder) -def test_supported_electronic_types(default_builder_inputs): +def test_supported_electronic_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``electronic_types``.""" inputs = default_builder_inputs - for electronic_type in GENERATOR.spec().inputs['electronic_type'].choices: + for electronic_type in generator.spec().inputs['electronic_type'].choices: inputs['electronic_type'] = electronic_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) -def test_supported_relax_types(default_builder_inputs): +def test_supported_relax_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``relax_types``.""" inputs = default_builder_inputs - for relax_type in GENERATOR.spec().inputs['relax_type'].choices: + for relax_type in generator.spec().inputs['relax_type'].choices: inputs['relax_type'] = relax_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) -def test_supported_spin_types(default_builder_inputs): +def test_supported_spin_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``spin_types``.""" inputs = default_builder_inputs - for spin_type in GENERATOR.spec().inputs['spin_type'].choices: + for spin_type in generator.spec().inputs['spin_type'].choices: inputs['spin_type'] = spin_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) diff --git a/tests/workflows/relax/test_fleur.py b/tests/workflows/relax/test_fleur.py index cd72d625..49a3508c 100644 --- a/tests/workflows/relax/test_fleur.py +++ b/tests/workflows/relax/test_fleur.py @@ -1,10 +1,11 @@ """Tests for the :mod:`aiida_common_workflows.workflows.relax.fleur` module.""" - import pytest from aiida import engine, plugins -WORKCHAIN = plugins.WorkflowFactory('common_workflows.relax.fleur') -GENERATOR = WORKCHAIN.get_input_generator() + +@pytest.fixture +def generator(): + return plugins.WorkflowFactory('common_workflows.relax.fleur').get_input_generator() @pytest.fixture @@ -25,47 +26,47 @@ def default_builder_inputs(generate_code, generate_structure): } -def test_get_builder(default_builder_inputs): +def test_get_builder(generator, default_builder_inputs): """Test the ``get_builder`` with default arguments.""" - builder = GENERATOR.get_builder(**default_builder_inputs) + builder = generator.get_builder(**default_builder_inputs) assert isinstance(builder, engine.ProcessBuilder) @pytest.mark.skip('Running this test will fail with an `UnroutableError` in `kiwipy`.') -def test_submit(default_builder_inputs): +def test_submit(generator, default_builder_inputs): """Test submitting the builder returned by ``get_builder`` called with default arguments. This will actually create the ``WorkChain`` instance, so if it doesn't raise, that means the input spec was valid. """ - builder = GENERATOR.get_builder(**default_builder_inputs) + builder = generator.get_builder(**default_builder_inputs) engine.submit(builder) -def test_supported_electronic_types(default_builder_inputs): +def test_supported_electronic_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``electronic_types``.""" inputs = default_builder_inputs - for electronic_type in GENERATOR.spec().inputs['electronic_type'].choices: + for electronic_type in generator.spec().inputs['electronic_type'].choices: inputs['electronic_type'] = electronic_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) -def test_supported_relax_types(default_builder_inputs): +def test_supported_relax_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``relax_types``.""" inputs = default_builder_inputs - for relax_type in GENERATOR.spec().inputs['relax_type'].choices: + for relax_type in generator.spec().inputs['relax_type'].choices: inputs['relax_type'] = relax_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) -def test_supported_spin_types(default_builder_inputs): +def test_supported_spin_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``spin_types``.""" inputs = default_builder_inputs - for spin_type in GENERATOR.spec().inputs['spin_type'].choices: + for spin_type in generator.spec().inputs['spin_type'].choices: inputs['spin_type'] = spin_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) diff --git a/tests/workflows/relax/test_gaussian.py b/tests/workflows/relax/test_gaussian.py index 67ced3a0..7dd8572c 100644 --- a/tests/workflows/relax/test_gaussian.py +++ b/tests/workflows/relax/test_gaussian.py @@ -1,10 +1,11 @@ """Tests for the :mod:`aiida_common_workflows.workflows.relax.gaussian` module.""" - import pytest from aiida import engine, plugins -WORKCHAIN = plugins.WorkflowFactory('common_workflows.relax.gaussian') -GENERATOR = WORKCHAIN.get_input_generator() + +@pytest.fixture +def generator(): + return plugins.WorkflowFactory('common_workflows.relax.gaussian').get_input_generator() @pytest.fixture @@ -21,47 +22,47 @@ def default_builder_inputs(generate_code, generate_structure): } -def test_get_builder(default_builder_inputs): +def test_get_builder(generator, default_builder_inputs): """Test the ``get_builder`` with default arguments.""" - builder = GENERATOR.get_builder(**default_builder_inputs) + builder = generator.get_builder(**default_builder_inputs) assert isinstance(builder, engine.ProcessBuilder) @pytest.mark.skip('Running this test will fail with an `UnroutableError` in `kiwipy`.') -def test_submit(default_builder_inputs): +def test_submit(generator, default_builder_inputs): """Test submitting the builder returned by ``get_builder`` called with default arguments. This will actually create the ``WorkChain`` instance, so if it doesn't raise, that means the input spec was valid. """ - builder = GENERATOR.get_builder(**default_builder_inputs) + builder = generator.get_builder(**default_builder_inputs) engine.submit(builder) -def test_supported_electronic_types(default_builder_inputs): +def test_supported_electronic_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``electronic_types``.""" inputs = default_builder_inputs - for electronic_type in GENERATOR.spec().inputs['electronic_type'].choices: + for electronic_type in generator.spec().inputs['electronic_type'].choices: inputs['electronic_type'] = electronic_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) -def test_supported_relax_types(default_builder_inputs): +def test_supported_relax_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``relax_types``.""" inputs = default_builder_inputs - for relax_type in GENERATOR.spec().inputs['relax_type'].choices: + for relax_type in generator.spec().inputs['relax_type'].choices: inputs['relax_type'] = relax_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) -def test_supported_spin_types(default_builder_inputs): +def test_supported_spin_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``spin_types``.""" inputs = default_builder_inputs - for spin_type in GENERATOR.spec().inputs['spin_type'].choices: + for spin_type in generator.spec().inputs['spin_type'].choices: inputs['spin_type'] = spin_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) diff --git a/tests/workflows/relax/test_nwchem.py b/tests/workflows/relax/test_nwchem.py index 710c8a36..1f99b386 100644 --- a/tests/workflows/relax/test_nwchem.py +++ b/tests/workflows/relax/test_nwchem.py @@ -1,10 +1,11 @@ """Tests for the :mod:`aiida_common_workflows.workflows.relax.nwchem` module.""" - import pytest from aiida import engine, plugins -WORKCHAIN = plugins.WorkflowFactory('common_workflows.relax.nwchem') -GENERATOR = WORKCHAIN.get_input_generator() + +@pytest.fixture +def generator(): + return plugins.WorkflowFactory('common_workflows.relax.nwchem').get_input_generator() @pytest.fixture @@ -21,47 +22,47 @@ def default_builder_inputs(generate_code, generate_structure): } -def test_get_builder(default_builder_inputs): +def test_get_builder(generator, default_builder_inputs): """Test the ``get_builder`` with default arguments.""" - builder = GENERATOR.get_builder(**default_builder_inputs) + builder = generator.get_builder(**default_builder_inputs) assert isinstance(builder, engine.ProcessBuilder) @pytest.mark.skip('Running this test will fail with an `UnroutableError` in `kiwipy`.') -def test_submit(default_builder_inputs): +def test_submit(generator, default_builder_inputs): """Test submitting the builder returned by ``get_builder`` called with default arguments. This will actually create the ``WorkChain`` instance, so if it doesn't raise, that means the input spec was valid. """ - builder = GENERATOR.get_builder(**default_builder_inputs) + builder = generator.get_builder(**default_builder_inputs) engine.submit(builder) -def test_supported_electronic_types(default_builder_inputs): +def test_supported_electronic_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``electronic_types``.""" inputs = default_builder_inputs - for electronic_type in GENERATOR.spec().inputs['electronic_type'].choices: + for electronic_type in generator.spec().inputs['electronic_type'].choices: inputs['electronic_type'] = electronic_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) -def test_supported_relax_types(default_builder_inputs): +def test_supported_relax_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``relax_types``.""" inputs = default_builder_inputs - for relax_type in GENERATOR.spec().inputs['relax_type'].choices: + for relax_type in generator.spec().inputs['relax_type'].choices: inputs['relax_type'] = relax_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) -def test_supported_spin_types(default_builder_inputs): +def test_supported_spin_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``spin_types``.""" inputs = default_builder_inputs - for spin_type in GENERATOR.spec().inputs['spin_type'].choices: + for spin_type in generator.spec().inputs['spin_type'].choices: inputs['spin_type'] = spin_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) diff --git a/tests/workflows/relax/test_orca.py b/tests/workflows/relax/test_orca.py index 18167b7f..59d77793 100644 --- a/tests/workflows/relax/test_orca.py +++ b/tests/workflows/relax/test_orca.py @@ -1,10 +1,11 @@ """Tests for the :mod:`aiida_common_workflows.workflows.relax.orca` module.""" - import pytest from aiida import engine, plugins -WORKCHAIN = plugins.WorkflowFactory('common_workflows.relax.orca') -GENERATOR = WORKCHAIN.get_input_generator() + +@pytest.fixture +def generator(): + return plugins.WorkflowFactory('common_workflows.relax.orca').get_input_generator() @pytest.fixture @@ -22,51 +23,51 @@ def default_builder_inputs(generate_code, generate_structure): @pytest.mark.filterwarnings('ignore: PBC detected ') -def test_get_builder(default_builder_inputs): +def test_get_builder(generator, default_builder_inputs): """Test the ``get_builder`` with default arguments.""" - builder = GENERATOR.get_builder(**default_builder_inputs) + builder = generator.get_builder(**default_builder_inputs) assert isinstance(builder, engine.ProcessBuilder) @pytest.mark.filterwarnings('ignore: PBC detected ') @pytest.mark.skip('Running this test will fail with an `UnroutableError` in `kiwipy`.') -def test_submit(default_builder_inputs): +def test_submit(generator, default_builder_inputs): """Test submitting the builder returned by ``get_builder`` called with default arguments. This will actually create the ``WorkChain`` instance, so if it doesn't raise, that means the input spec was valid. """ - builder = GENERATOR.get_builder(**default_builder_inputs) + builder = generator.get_builder(**default_builder_inputs) engine.submit(builder) @pytest.mark.filterwarnings('ignore: PBC detected ') -def test_supported_electronic_types(default_builder_inputs): +def test_supported_electronic_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``electronic_types``.""" inputs = default_builder_inputs - for electronic_type in GENERATOR.spec().inputs['electronic_type'].choices: + for electronic_type in generator.spec().inputs['electronic_type'].choices: inputs['electronic_type'] = electronic_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) @pytest.mark.filterwarnings('ignore: PBC detected ') -def test_supported_relax_types(default_builder_inputs): +def test_supported_relax_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``relax_types``.""" inputs = default_builder_inputs - for relax_type in GENERATOR.spec().inputs['relax_type'].choices: + for relax_type in generator.spec().inputs['relax_type'].choices: inputs['relax_type'] = relax_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) @pytest.mark.filterwarnings('ignore: PBC detected ') -def test_supported_spin_types(default_builder_inputs): +def test_supported_spin_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``spin_types``.""" inputs = default_builder_inputs - for spin_type in GENERATOR.spec().inputs['spin_type'].choices: + for spin_type in generator.spec().inputs['spin_type'].choices: inputs['spin_type'] = spin_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) diff --git a/tests/workflows/relax/test_quantum_espresso.py b/tests/workflows/relax/test_quantum_espresso.py index d9a95e6f..bf862086 100644 --- a/tests/workflows/relax/test_quantum_espresso.py +++ b/tests/workflows/relax/test_quantum_espresso.py @@ -1,12 +1,12 @@ """Tests for the :mod:`aiida_common_workflows.workflows.relax.quantum_espresso` module.""" - import pytest from aiida import engine, plugins from aiida_common_workflows.workflows.relax.generator import ElectronicType, RelaxType, SpinType -from qe_tools import CONSTANTS -WORKCHAIN = plugins.WorkflowFactory('common_workflows.relax.quantum_espresso') -GENERATOR = WORKCHAIN.get_input_generator() + +@pytest.fixture +def generator(): + return plugins.WorkflowFactory('common_workflows.relax.quantum_espresso').get_input_generator() @pytest.fixture @@ -24,72 +24,71 @@ def default_builder_inputs(generate_code, generate_structure): @pytest.mark.usefixtures('sssp') -def test_get_builder(default_builder_inputs): +def test_get_builder(generator, default_builder_inputs): """Test the ``get_builder`` with default arguments.""" - builder = GENERATOR.get_builder(**default_builder_inputs) + builder = generator.get_builder(**default_builder_inputs) assert isinstance(builder, engine.ProcessBuilder) @pytest.mark.usefixtures('sssp') @pytest.mark.skip('Running this test will fail with an `UnroutableError` in `kiwipy`.') -def test_submit(default_builder_inputs): +def test_submit(generator, default_builder_inputs): """Test submitting the builder returned by ``get_builder`` called with default arguments. This will actually create the ``WorkChain`` instance, so if it doesn't raise, that means the input spec was valid. """ - builder = GENERATOR.get_builder(**default_builder_inputs) + builder = generator.get_builder(**default_builder_inputs) engine.submit(builder) @pytest.mark.usefixtures('sssp') -def test_options(default_builder_inputs): +def test_options(generator, default_builder_inputs): """Test that the ``options`` of the ``engines`` argument are added to all namespaces.""" inputs = default_builder_inputs - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) assert builder.base.pw.metadata.options.resources == inputs['engines']['relax']['options']['resources'] assert builder.base_final_scf.pw.metadata.options.resources == inputs['engines']['relax']['options']['resources'] @pytest.mark.usefixtures('sssp') -def test_supported_electronic_types(default_builder_inputs): +def test_supported_electronic_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``electronic_types``.""" inputs = default_builder_inputs - for electronic_type in GENERATOR.spec().inputs['electronic_type'].choices: + for electronic_type in generator.spec().inputs['electronic_type'].choices: inputs['electronic_type'] = electronic_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) @pytest.mark.usefixtures('sssp') -def test_supported_relax_types(default_builder_inputs): +def test_supported_relax_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``relax_types``.""" inputs = default_builder_inputs - for relax_type in GENERATOR.spec().inputs['relax_type'].choices: + for relax_type in generator.spec().inputs['relax_type'].choices: inputs['relax_type'] = relax_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) @pytest.mark.usefixtures('sssp') -def test_supported_spin_types(default_builder_inputs): +def test_supported_spin_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``spin_types``.""" inputs = default_builder_inputs - for spin_type in GENERATOR.spec().inputs['spin_type'].choices: + for spin_type in generator.spec().inputs['spin_type'].choices: inputs['spin_type'] = spin_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) @pytest.mark.usefixtures('sssp') -def test_relax_type(generate_code, generate_structure): +def test_relax_type(generator, generate_code, generate_structure): """Test the ``relax_type`` keyword argument.""" code = generate_code('quantumespresso.pw') structure = generate_structure(symbols=('Si',)) - generator = WORKCHAIN.get_input_generator() engines = {'relax': {'code': code, 'options': {}}} builder = generator.get_builder(structure=structure, engines=engines, relax_type=RelaxType.NONE) @@ -124,11 +123,10 @@ def test_relax_type(generate_code, generate_structure): @pytest.mark.usefixtures('sssp') -def test_spin_type(generate_code, generate_structure): +def test_spin_type(generator, generate_code, generate_structure): """Test the ``spin_type`` keyword argument.""" code = generate_code('quantumespresso.pw') structure = generate_structure(symbols=('Si',)) - generator = WORKCHAIN.get_input_generator() engines = {'relax': {'code': code, 'options': {}}} builder = generator.get_builder(structure=structure, engines=engines, spin_type=SpinType.NONE) @@ -140,11 +138,10 @@ def test_spin_type(generate_code, generate_structure): @pytest.mark.usefixtures('sssp') -def test_electronic_type(generate_code, generate_structure): +def test_electronic_type(generator, generate_code, generate_structure): """Test the ``electronic_type`` keyword argument.""" code = generate_code('quantumespresso.pw') structure = generate_structure(symbols=('Si',)) - generator = WORKCHAIN.get_input_generator() engines = {'relax': {'code': code, 'options': {}}} builder = generator.get_builder(structure=structure, engines=engines, electronic_type=ElectronicType.METAL) @@ -158,11 +155,12 @@ def test_electronic_type(generate_code, generate_structure): @pytest.mark.usefixtures('sssp') -def test_threshold_forces(generate_code, generate_structure): +def test_threshold_forces(generator, generate_code, generate_structure): """Test the ``threshold_forces`` keyword argument.""" + from qe_tools import CONSTANTS + code = generate_code('quantumespresso.pw') structure = generate_structure(symbols=('Si',)) - generator = WORKCHAIN.get_input_generator() engines = {'relax': {'code': code, 'options': {}}} threshold_forces = 0.1 @@ -172,11 +170,12 @@ def test_threshold_forces(generate_code, generate_structure): @pytest.mark.usefixtures('sssp') -def test_threshold_stress(generate_code, generate_structure): +def test_threshold_stress(generator, generate_code, generate_structure): """Test the ``threshold_stress`` keyword argument.""" + from qe_tools import CONSTANTS + code = generate_code('quantumespresso.pw') structure = generate_structure(symbols=('Si',)) - generator = WORKCHAIN.get_input_generator() engines = {'relax': {'code': code, 'options': {}}} threshold_stress = 0.1 @@ -186,11 +185,10 @@ def test_threshold_stress(generate_code, generate_structure): @pytest.mark.usefixtures('sssp') -def test_magnetization_per_site(generate_code, generate_structure): +def test_magnetization_per_site(generator, generate_code, generate_structure): """Test the ``magnetization_per_site`` keyword argument.""" code = generate_code('quantumespresso.pw') structure = generate_structure(symbols=('Si', 'Ge')) - generator = WORKCHAIN.get_input_generator() engines = {'relax': {'code': code, 'options': {}}} magnetization_per_site = [0.0, 0.1, 0.2] diff --git a/tests/workflows/relax/test_siesta.py b/tests/workflows/relax/test_siesta.py index 9959c4f7..a9501343 100644 --- a/tests/workflows/relax/test_siesta.py +++ b/tests/workflows/relax/test_siesta.py @@ -1,10 +1,11 @@ """Tests for the :mod:`aiida_common_workflows.workflows.relax.siesta` module.""" - import pytest from aiida import engine, plugins -WORKCHAIN = plugins.WorkflowFactory('common_workflows.relax.siesta') -GENERATOR = WORKCHAIN.get_input_generator() + +@pytest.fixture +def generator(): + return plugins.WorkflowFactory('common_workflows.relax.siesta').get_input_generator() @pytest.fixture @@ -22,51 +23,51 @@ def default_builder_inputs(generate_code, generate_structure): @pytest.mark.usefixtures('psml_family') -def test_get_builder(default_builder_inputs): +def test_get_builder(generator, default_builder_inputs): """Test the ``get_builder`` with default arguments.""" - builder = GENERATOR.get_builder(**default_builder_inputs) + builder = generator.get_builder(**default_builder_inputs) assert isinstance(builder, engine.ProcessBuilder) @pytest.mark.usefixtures('psml_family') @pytest.mark.skip('Running this test will fail with an `UnroutableError` in `kiwipy`.') -def test_submit(default_builder_inputs): +def test_submit(generator, default_builder_inputs): """Test submitting the builder returned by ``get_builder`` called with default arguments. This will actually create the ``WorkChain`` instance, so if it doesn't raise, that means the input spec was valid. """ - builder = GENERATOR.get_builder(**default_builder_inputs) + builder = generator.get_builder(**default_builder_inputs) engine.submit(builder) @pytest.mark.usefixtures('psml_family') -def test_supported_electronic_types(default_builder_inputs): +def test_supported_electronic_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``electronic_types``.""" inputs = default_builder_inputs - for electronic_type in GENERATOR.spec().inputs['electronic_type'].choices: + for electronic_type in generator.spec().inputs['electronic_type'].choices: inputs['electronic_type'] = electronic_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) @pytest.mark.usefixtures('psml_family') -def test_supported_relax_types(default_builder_inputs): +def test_supported_relax_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``relax_types``.""" inputs = default_builder_inputs - for relax_type in GENERATOR.spec().inputs['relax_type'].choices: + for relax_type in generator.spec().inputs['relax_type'].choices: inputs['relax_type'] = relax_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) @pytest.mark.usefixtures('psml_family') -def test_supported_spin_types(default_builder_inputs): +def test_supported_spin_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``spin_types``.""" inputs = default_builder_inputs - for spin_type in GENERATOR.spec().inputs['spin_type'].choices: + for spin_type in generator.spec().inputs['spin_type'].choices: inputs['spin_type'] = spin_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) diff --git a/tests/workflows/relax/test_vasp.py b/tests/workflows/relax/test_vasp.py index 59ca8799..b5eca1ca 100644 --- a/tests/workflows/relax/test_vasp.py +++ b/tests/workflows/relax/test_vasp.py @@ -1,10 +1,11 @@ """Tests for the :mod:`aiida_common_workflows.workflows.relax.vasp` module.""" - import pytest from aiida import engine, plugins -WORKCHAIN = plugins.WorkflowFactory('common_workflows.relax.vasp') -GENERATOR = WORKCHAIN.get_input_generator() + +@pytest.fixture +def generator(): + return plugins.WorkflowFactory('common_workflows.relax.vasp').get_input_generator() @pytest.fixture @@ -21,47 +22,47 @@ def default_builder_inputs(generate_code, generate_structure): } -def test_get_builder(default_builder_inputs): +def test_get_builder(generator, default_builder_inputs): """Test the ``get_builder`` with default arguments.""" - builder = GENERATOR.get_builder(**default_builder_inputs) + builder = generator.get_builder(**default_builder_inputs) assert isinstance(builder, engine.ProcessBuilder) @pytest.mark.skip('Running this test will fail with an `UnroutableError` in `kiwipy`.') -def test_submit(default_builder_inputs): +def test_submit(generator, default_builder_inputs): """Test submitting the builder returned by ``get_builder`` called with default arguments. This will actually create the ``WorkChain`` instance, so if it doesn't raise, that means the input spec was valid. """ - builder = GENERATOR.get_builder(**default_builder_inputs) + builder = generator.get_builder(**default_builder_inputs) engine.submit(builder) -def test_supported_electronic_types(default_builder_inputs): +def test_supported_electronic_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``electronic_types``.""" inputs = default_builder_inputs - for electronic_type in GENERATOR.spec().inputs['electronic_type'].choices: + for electronic_type in generator.spec().inputs['electronic_type'].choices: inputs['electronic_type'] = electronic_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) -def test_supported_relax_types(default_builder_inputs): +def test_supported_relax_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``relax_types``.""" inputs = default_builder_inputs - for relax_type in GENERATOR.spec().inputs['relax_type'].choices: + for relax_type in generator.spec().inputs['relax_type'].choices: inputs['relax_type'] = relax_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder) -def test_supported_spin_types(default_builder_inputs): +def test_supported_spin_types(generator, default_builder_inputs): """Test calling ``get_builder`` for the supported ``spin_types``.""" inputs = default_builder_inputs - for spin_type in GENERATOR.spec().inputs['spin_type'].choices: + for spin_type in generator.spec().inputs['spin_type'].choices: inputs['spin_type'] = spin_type - builder = GENERATOR.get_builder(**inputs) + builder = generator.get_builder(**inputs) assert isinstance(builder, engine.ProcessBuilder)