diff --git a/.github/workflows/validate_release_tag.py b/.github/workflows/validate_release_tag.py index f800ebd..d53b43a 100644 --- a/.github/workflows/validate_release_tag.py +++ b/.github/workflows/validate_release_tag.py @@ -17,8 +17,11 @@ def get_version_from_module(content: str) -> str: try: return next( - ast.literal_eval(statement.value) for statement in module.body if isinstance(statement, ast.Assign) - for target in statement.targets if isinstance(target, ast.Name) and target.id == '__version__' + ast.literal_eval(statement.value) + for statement in module.body + if isinstance(statement, ast.Assign) + for target in statement.targets + if isinstance(target, ast.Name) and target.id == '__version__' ) except StopIteration as exception: raise IOError('Unable to find the `__version__` attribute in the module.') from exception diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8e73cf6..aa282b1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,25 +13,12 @@ repos: hooks: - id: flynt -- repo: https://github.com/pycqa/isort - rev: '5.12.0' +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: 'v0.1.6' hooks: - - id: isort - -- repo: https://github.com/pre-commit/mirrors-yapf - rev: 'v0.32.0' - hooks: - - id: yapf - name: yapf - types: [python] - args: ['-i'] - additional_dependencies: ['toml'] - -- repo: https://github.com/PyCQA/pydocstyle - rev: '6.1.1' - hooks: - - id: pydocstyle - additional_dependencies: ['toml'] + - id: ruff-format + - id: ruff + args: [--fix, --exit-non-zero-on-fix, --show-fixes] - repo: local hooks: @@ -47,9 +34,3 @@ repos: (?x)^( src/.*py| )$ - - - id: pylint - name: pylint - entry: pylint - types: [python] - language: system diff --git a/pyproject.toml b/pyproject.toml index fbe0fab..b847cd4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,8 +51,6 @@ Source = 'https://github.com/microsoft/aiida-pyscf' pre-commit = [ 'mypy==1.3.0', 'pre-commit~=2.17', - 'pylint~=2.16.0', - 'pylint-aiida~=0.1.1', ] tests = [ 'packaging', @@ -75,11 +73,29 @@ exclude = [ line-length = 120 fail-on-change = true -[tool.isort] -force_sort_within_sections = true -include_trailing_comma = true -line_length = 120 -multi_line_output = 3 +[tool.ruff] +line-length = 120 +select = [ + 'E', # pydocstyle + 'W', # pydocstyle + 'F', # pyflakes + 'I', # isort + 'N', # pep8-naming + 'D', # pydocstyle + 'PLC', # pylint-convention + 'PLE', # pylint-error + 'PLR', # pylint-refactor + 'PLW', # pylint-warning + 'RUF', # ruff +] +ignore = [ + 'D203', # Incompatible with D211 `no-blank-line-before-class` + 'D213', # Incompatible with D212 `multi-line-summary-second-line` + 'PLR2004', # Magic value used in comparison +] + +[tool.ruff.format] +quote-style = 'single' [tool.mypy] show_error_codes = true @@ -105,37 +121,7 @@ module = [ ] ignore_missing_imports = true -[tool.pydocstyle] -ignore = [ - 'D104', - 'D203', - 'D213' -] - -[tool.pylint.master] -load-plugins = ['pylint_aiida'] - -[tool.pylint.format] -max-line-length = 120 - -[tool.pylint.messages_control] -disable = [ - 'duplicate-code', - 'import-outside-toplevel', - 'inconsistent-return-statements', - 'too-many-ancestors', -] - [tool.pytest.ini_options] filterwarnings = [ 'ignore:Creating AiiDA configuration folder.*:UserWarning' ] - -[tool.yapf] -align_closing_bracket_with_visual_indent = true -based_on_style = 'google' -coalesce_brackets = true -column_limit = 120 -dedent_closing_brackets = true -indent_dictionary_value = false -split_arguments_when_comma_terminated = true diff --git a/src/aiida_pyscf/calculations/base.py b/src/aiida_pyscf/calculations/base.py index a3e2176..462c385 100644 --- a/src/aiida_pyscf/calculations/base.py +++ b/src/aiida_pyscf/calculations/base.py @@ -8,6 +8,7 @@ import pathlib import typing as t +import numpy as np from aiida.common.datastructures import CalcInfo, CodeInfo from aiida.common.folders import Folder from aiida.engine import CalcJob, CalcJobProcessSpec @@ -15,7 +16,6 @@ from aiida_shell.data import PickledData from ase.io.xyz import write_xyz from jinja2 import Environment, PackageLoader, PrefixLoader -import numpy as np from plumpy.utils import AttributesFrozendict __all__ = ('PyscfCalculation',) @@ -69,7 +69,7 @@ def define(cls, spec: CalcJobProcessSpec): # type: ignore[override] 'parameters', valid_type=Dict, required=False, - help='Various computed properties parsed from the `FILENAME_RESULTS` output file.' + help='Various computed properties parsed from the `FILENAME_RESULTS` output file.', ) spec.output( 'structure', @@ -106,7 +106,7 @@ def define(cls, spec: CalcJobProcessSpec): # type: ignore[override] 'cubegen.mep', valid_type=SinglefileData, required=False, - help='The molecular electrostatic potential (MEP) in `.cube` format.' + help='The molecular electrostatic potential (MEP) in `.cube` format.', ) spec.output( 'hessian', @@ -120,16 +120,16 @@ def define(cls, spec: CalcJobProcessSpec): # type: ignore[override] spec.exit_code( 410, 'ERROR_ELECTRONIC_CONVERGENCE_NOT_REACHED', - message='The electronic minimization cycle did not reach self-consistency.' + message='The electronic minimization cycle did not reach self-consistency.', ) spec.exit_code( 500, 'ERROR_IONIC_CONVERGENCE_NOT_REACHED', - message='The ionic minimization cycle did not converge for the given thresholds.' + message='The ionic minimization cycle did not converge for the given thresholds.', ) @classmethod - def validate_parameters(cls, value: Dict | None, _) -> str | None: # pylint: disable=too-many-return-statements,too-many-branches,too-many-locals + def validate_parameters(cls, value: Dict | None, _) -> str | None: # noqa: PLR0911, PLR0912 """Validate the parameters input.""" if not value: return None diff --git a/src/aiida_pyscf/parsers/__init__.py b/src/aiida_pyscf/parsers/__init__.py index e69de29..98a759d 100644 --- a/src/aiida_pyscf/parsers/__init__.py +++ b/src/aiida_pyscf/parsers/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Module for :mod:`aiida_pyscf.parsers`.""" diff --git a/src/aiida_pyscf/parsers/base.py b/src/aiida_pyscf/parsers/base.py index bed6799..c3459e3 100644 --- a/src/aiida_pyscf/parsers/base.py +++ b/src/aiida_pyscf/parsers/base.py @@ -5,13 +5,13 @@ import json import pathlib +import dill +import numpy from aiida.engine import ExitCode from aiida.orm import ArrayData, Dict, SinglefileData, TrajectoryData from aiida.parsers.parser import Parser from aiida_shell.data import PickledData from ase.io.extxyz import read_extxyz -import dill -import numpy from pint import UnitRegistry from aiida_pyscf.calculations.base import PyscfCalculation @@ -27,7 +27,7 @@ def __init__(self, *args, **kwargs): self.dirpath_temporary: pathlib.Path | None = None super().__init__(*args, **kwargs) - def parse(self, retrieved_temporary_folder: str | None = None, **kwargs): # pylint: disable=arguments-differ,too-many-locals,too-many-branches,too-many-statements + def parse(self, retrieved_temporary_folder: str | None = None, **kwargs): # noqa: PLR0912, PLR0915 """Parse the contents of the output files stored in the ``retrieved`` output node. :returns: An exit code if the job failed. @@ -41,7 +41,7 @@ def parse(self, retrieved_temporary_folder: str | None = None, **kwargs): # pyl try: with self.retrieved.base.repository.open(PyscfCalculation.FILENAME_STDOUT, 'r') as handle: - stdout = handle.read() # pylint: disable=unused-variable + handle.read() except FileNotFoundError: return self.handle_failure('ERROR_OUTPUT_STDOUT_MISSING') @@ -133,7 +133,7 @@ def build_output_trajectory(self, filepath_trajectory: pathlib.Path) -> Trajecto def batch(iterable, batch_size): """Split an iterable into a list of elements which size ``batch_size.""" - return [iterable[i:i + batch_size] for i in range(0, len(iterable), batch_size)] + return [iterable[i : i + batch_size] for i in range(0, len(iterable), batch_size)] with filepath_trajectory.open() as handle: for atoms in read_extxyz(handle, index=slice(None, None)): @@ -155,8 +155,8 @@ def batch(iterable, batch_size): symbols=[site.kind_name for site in self.node.inputs.structure.sites], positions=numpy.array(positions), ) - energies = (numpy.array(energies) * ureg.hartree).to(ureg.electron_volt).magnitude # type: ignore[attr-defined] - trajectory.set_array('energies', energies) + energies = (numpy.array(energies) * ureg.hartree).to(ureg.electron_volt).magnitude + trajectory.set_array('energies', energies) # type: ignore[arg-type] return trajectory diff --git a/src/aiida_pyscf/workflows/__init__.py b/src/aiida_pyscf/workflows/__init__.py index e69de29..79b8767 100644 --- a/src/aiida_pyscf/workflows/__init__.py +++ b/src/aiida_pyscf/workflows/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Module for :mod:`aiida_pyscf.workflows`.""" diff --git a/src/aiida_pyscf/workflows/base.py b/src/aiida_pyscf/workflows/base.py index de9e7d7..88b7c89 100644 --- a/src/aiida_pyscf/workflows/base.py +++ b/src/aiida_pyscf/workflows/base.py @@ -32,7 +32,7 @@ def define(cls, spec): spec.exit_code( 310, 'ERROR_NO_CHECKPOINT_TO_RESTART', - message='The calculation failed and did not retrieve a checkpoint file from which can be restarted.' + message='The calculation failed and did not retrieve a checkpoint file from which can be restarted.', ) def setup(self): @@ -68,7 +68,7 @@ def handle_unrecoverable_failure(self, node): priority=500, exit_codes=[ PyscfCalculation.exit_codes.ERROR_IONIC_CONVERGENCE_NOT_REACHED, # type: ignore[union-attr] - ] + ], ) def handle_ionic_convergence_not_reached(self, node): """Handle ``ERROR_IONIC_CONVERGENCE_NOT_REACHED`` error. @@ -95,7 +95,7 @@ def handle_ionic_convergence_not_reached(self, node): priority=410, exit_codes=[ PyscfCalculation.exit_codes.ERROR_ELECTRONIC_CONVERGENCE_NOT_REACHED, # type: ignore[union-attr] - ] + ], ) def handle_electronic_convergence_not_reached(self, node): """Handle ``ERROR_ELECTRONIC_CONVERGENCE_NOT_REACHED`` error. @@ -110,7 +110,7 @@ def handle_electronic_convergence_not_reached(self, node): priority=110, exit_codes=[ PyscfCalculation.exit_codes.ERROR_SCHEDULER_NODE_FAILURE, # type: ignore[union-attr] - ] + ], ) def handle_scheduler_node_failure(self, node): """Handle ``ERROR_SCHEDULER_NODE_FAILURE`` error. @@ -128,7 +128,7 @@ def handle_scheduler_node_failure(self, node): priority=100, exit_codes=[ PyscfCalculation.exit_codes.ERROR_SCHEDULER_OUT_OF_WALLTIME, # type: ignore[union-attr] - ] + ], ) def handle_out_of_walltime(self, node): """Handle ``ERROR_SCHEDULER_OUT_OF_WALLTIME`` error. diff --git a/tests/calculations/__init__.py b/tests/calculations/__init__.py index e69de29..30e02d4 100644 --- a/tests/calculations/__init__.py +++ b/tests/calculations/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Tests for :mod:`aiida_pyscf.calculations`.""" diff --git a/tests/calculations/test_base.py b/tests/calculations/test_base.py index 05f4b4b..81838f1 100644 --- a/tests/calculations/test_base.py +++ b/tests/calculations/test_base.py @@ -1,15 +1,13 @@ # -*- coding: utf-8 -*- """Tests for the :mod:`aiida_pyscf.calculations.base` module.""" -# pylint: disable=redefined-outer-name import io import textwrap +import pytest from aiida.manage.tests.pytest_fixtures import recursive_merge from aiida.orm import Dict, SinglefileData -from jinja2 import BaseLoader, Environment -import pytest - from aiida_pyscf.calculations.base import PyscfCalculation +from jinja2 import BaseLoader, Environment @pytest.fixture @@ -24,13 +22,7 @@ def factory(**kwargs): 'code': aiida_local_code_factory('pyscf.base', 'python'), 'structure': generate_structure(), 'parameters': Dict(parameters), - 'metadata': { - 'options': { - 'resources': { - 'num_machines': 1 - } - } - }, + 'metadata': {'options': {'resources': {'num_machines': 1}}}, } inputs.update(**kwargs) return inputs @@ -43,15 +35,19 @@ def test_default(generate_calc_job, generate_inputs_pyscf, file_regression): inputs = generate_inputs_pyscf() tmp_path, calc_info = generate_calc_job(PyscfCalculation, inputs=inputs) - assert sorted(calc_info.retrieve_list) == sorted([ - PyscfCalculation.FILENAME_RESULTS, - PyscfCalculation.FILENAME_MODEL, - PyscfCalculation.FILENAME_STDOUT, - ]) + assert sorted(calc_info.retrieve_list) == sorted( + [ + PyscfCalculation.FILENAME_RESULTS, + PyscfCalculation.FILENAME_MODEL, + PyscfCalculation.FILENAME_STDOUT, + ] + ) - assert sorted(calc_info.retrieve_temporary_list) == sorted([ - PyscfCalculation.FILENAME_CHECKPOINT, - ]) + assert sorted(calc_info.retrieve_temporary_list) == sorted( + [ + PyscfCalculation.FILENAME_CHECKPOINT, + ] + ) content_input_file = (tmp_path / PyscfCalculation.FILENAME_SCRIPT).read_text() file_regression.check(content_input_file, encoding='utf-8', extension='.pyr') @@ -61,10 +57,7 @@ def test_parameters_structure(generate_calc_job, generate_inputs_pyscf, file_reg """Test the ``structure`` key of the ``parameters`` input.""" parameters = { 'structure': { - 'basis': { - 'O': 'sto-3g', - 'H': 'cc-pvdz' - }, + 'basis': {'O': 'sto-3g', 'H': 'cc-pvdz'}, 'cart': True, 'charge': 1, 'spin': 2, @@ -83,9 +76,7 @@ def test_parameters_mean_field(generate_calc_job, generate_inputs_pyscf, file_re 'mean_field': { 'diis_start_cycle': 2, 'method': 'RHF', - 'grids': { - 'level': 3 - }, + 'grids': {'level': 3}, 'xc': 'PBE', }, } diff --git a/tests/conftest.py b/tests/conftest.py index 99b104e..1e63423 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- -# pylint: disable=redefined-outer-name """Module with test fixtures.""" from __future__ import annotations import collections import pathlib +import pytest from aiida.common.folders import Folder from aiida.common.links import LinkType from aiida.engine.utils import instantiate_process @@ -14,9 +14,8 @@ from aiida.plugins import ParserFactory, WorkflowFactory from ase.build import molecule from plumpy import ProcessState -import pytest -pytest_plugins = ['aiida.manage.tests.pytest_fixtures'] # pylint: disable=invalid-name +pytest_plugins = ['aiida.manage.tests.pytest_fixtures'] @pytest.fixture @@ -83,7 +82,7 @@ def flatten_inputs(inputs, prefix=''): return flat_inputs def factory( - entry_point: str, test_name: str, inputs: dict = None, retrieve_temporary_list: list[str] | None = None + entry_point: str, test_name: str, inputs: dict | None = None, retrieve_temporary_list: list[str] | None = None ): """Create and return a :class:`aiida.orm.CalcJobNode` instance.""" node = CalcJobNode(computer=aiida_computer_local(), process_type=f'aiida.calculations:{entry_point}') diff --git a/tests/parsers/__init__.py b/tests/parsers/__init__.py index e69de29..9f2e69d 100644 --- a/tests/parsers/__init__.py +++ b/tests/parsers/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Tests for :mod:`aiida_pyscf.parsers`.""" diff --git a/tests/parsers/test_base.py b/tests/parsers/test_base.py index 3f4bae6..8850603 100644 --- a/tests/parsers/test_base.py +++ b/tests/parsers/test_base.py @@ -1,12 +1,10 @@ # -*- coding: utf-8 -*- """Tests for the :mod:`aiida_pyscf.parsers.base` module.""" -# pylint: disable=redefined-outer-name from aiida.orm import SinglefileData +from aiida_pyscf.calculations.base import PyscfCalculation from aiida_shell.data import PickledData from pyscf.scf.hf import RHF -from aiida_pyscf.calculations.base import PyscfCalculation - def test_default(generate_calc_job_node, generate_parser, data_regression): """Test parsing a default output case.""" @@ -29,13 +27,15 @@ def test_relax(generate_calc_job_node, generate_parser, generate_structure, data assert calcfunction.is_finished, calcfunction.exception assert calcfunction.is_finished_ok, calcfunction.exit_message assert 'structure' in results - data_regression.check({ - 'parameters': results['parameters'].get_dict(), - 'structure': results['structure'].base.attributes.all, - 'trajectory': results['trajectory'].base.attributes.all, - 'energies': results['trajectory'].get_array('energies').flatten().tolist(), - 'positions': results['trajectory'].get_array('positions').flatten().tolist(), - }) + data_regression.check( + { + 'parameters': results['parameters'].get_dict(), + 'structure': results['structure'].base.attributes.all, + 'trajectory': results['trajectory'].base.attributes.all, + 'energies': results['trajectory'].get_array('energies').flatten().tolist(), + 'positions': results['trajectory'].get_array('positions').flatten().tolist(), + } + ) def test_failed_out_of_walltime(generate_calc_job_node, generate_parser): diff --git a/tests/test_integration.py b/tests/test_integration.py index 9e1db78..2aa921d 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- """Module with integration tests.""" -from aiida import engine, orm import numpy - +from aiida import engine, orm from aiida_pyscf.calculations.base import PyscfCalculation from aiida_pyscf.workflows.base import PyscfBaseWorkChain @@ -33,10 +32,7 @@ def test_pyscf_base_mean_field(aiida_local_code_factory, generate_structure, dat 'total_energy': parameters['mean_field'].pop('total_energy'), 'mo_energies': parameters['mean_field']['molecular_orbitals'].pop('energies'), }, - default_tolerance={ - 'atol': 1e-4, - 'rtol': 1e-18 - }, + default_tolerance={'atol': 1e-4, 'rtol': 1e-18}, ) data_regression.check(parameters) @@ -48,14 +44,14 @@ def test_pyscf_base_geometry_optimization( code = aiida_local_code_factory('pyscf.base', 'python') builder = code.get_builder() builder.structure = generate_structure() - builder.parameters = orm.Dict({ - 'mean_field': { - 'method': 'RHF' - }, - 'optimizer': { - 'solver': 'geomeTRIC', - }, - }) + builder.parameters = orm.Dict( + { + 'mean_field': {'method': 'RHF'}, + 'optimizer': { + 'solver': 'geomeTRIC', + }, + } + ) results, node = engine.run_get_node(builder) assert node.is_finished_ok @@ -77,10 +73,7 @@ def test_pyscf_base_geometry_optimization( 'total_energy': parameters['mean_field'].pop('total_energy'), 'mo_energies': parameters['mean_field']['molecular_orbitals'].pop('energies'), }, - default_tolerance={ - 'atol': 1e-4, - 'rtol': 1e-18 - }, + default_tolerance={'atol': 1e-4, 'rtol': 1e-18}, ) data_regression.check(parameters) @@ -103,18 +96,18 @@ def test_pyscf_base_cubegen(aiida_local_code_factory, generate_structure): code = aiida_local_code_factory('pyscf.base', 'python') builder = code.get_builder() builder.structure = generate_structure(formula='N2') - builder.parameters = orm.Dict({ - 'mean_field': { - 'method': 'RHF' - }, - 'cubegen': { - 'orbitals': { - 'indices': [5, 6], + builder.parameters = orm.Dict( + { + 'mean_field': {'method': 'RHF'}, + 'cubegen': { + 'orbitals': { + 'indices': [5, 6], + }, + 'density': {}, + 'mep': {}, }, - 'density': {}, - 'mep': {}, } - }) + ) results, node = engine.run_get_node(builder) assert node.is_finished_ok @@ -129,15 +122,9 @@ def test_pyscf_base_fcidump(aiida_local_code_factory, generate_structure): code = aiida_local_code_factory('pyscf.base', 'python') builder = code.get_builder() builder.structure = generate_structure(formula='N2') - builder.parameters = orm.Dict({ - 'mean_field': { - 'method': 'RHF' - }, - 'fcidump': { - 'active_spaces': [[5, 6, 8, 9]], - 'occupations': [[1, 1, 1, 1]] - } - }) + builder.parameters = orm.Dict( + {'mean_field': {'method': 'RHF'}, 'fcidump': {'active_spaces': [[5, 6, 8, 9]], 'occupations': [[1, 1, 1, 1]]}} + ) results, node = engine.run_get_node(builder) assert node.is_finished_ok @@ -148,9 +135,9 @@ def test_pyscf_base_fcidump(aiida_local_code_factory, generate_structure): def test_pyscf_base_work_chain(aiida_local_code_factory, generate_structure): """Test a ``PyscfBaseWorkChain``: test the automatic restart after SCF fails to converge.""" builder = PyscfBaseWorkChain.get_builder() - builder.pyscf.code = aiida_local_code_factory('pyscf.base', 'python') # pylint: disable=no-member - builder.pyscf.structure = generate_structure(formula='H2O') # pylint: disable=no-member - builder.pyscf.parameters = orm.Dict( # pylint: disable=no-member + builder.pyscf.code = aiida_local_code_factory('pyscf.base', 'python') + builder.pyscf.structure = generate_structure(formula='H2O') + builder.pyscf.parameters = orm.Dict( { 'mean_field': { 'method': 'RHF', @@ -169,17 +156,19 @@ def test_failed_electronic_convergence(aiida_local_code_factory, generate_struct code = aiida_local_code_factory('pyscf.base', 'python') builder = code.get_builder() builder.structure = generate_structure(formula='NO') - builder.parameters = orm.Dict({ - 'mean_field': { - 'method': 'UKS', - 'xc': 'LDA', - }, - 'structure': { - 'symmetry': True, - 'basis': '6-31G', - 'spin': 1, + builder.parameters = orm.Dict( + { + 'mean_field': { + 'method': 'UKS', + 'xc': 'LDA', + }, + 'structure': { + 'symmetry': True, + 'basis': '6-31G', + 'spin': 1, + }, } - }) + ) results, node = engine.run_get_node(builder) assert node.is_failed assert node.exit_status == PyscfCalculation.exit_codes.ERROR_ELECTRONIC_CONVERGENCE_NOT_REACHED.status diff --git a/tests/test_version.py b/tests/test_version.py index 08bd5e0..eceb026 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- """Tests for the :mod:`aiida_pyscf` module.""" -from packaging.version import Version, parse - import aiida_pyscf +from packaging.version import Version, parse def test_version(): diff --git a/tests/workflows/test_base.py b/tests/workflows/test_base.py index c81b5fb..6781673 100644 --- a/tests/workflows/test_base.py +++ b/tests/workflows/test_base.py @@ -4,7 +4,6 @@ from aiida.engine import ProcessHandlerReport from aiida.orm import Log, SinglefileData - from aiida_pyscf.calculations.base import PyscfCalculation from aiida_pyscf.workflows.base import PyscfBaseWorkChain