diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 70b59b910..0ea7df298 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,3 +47,40 @@ repos: args: [--fix] # Run the formatter. - id: ruff-format + + - repo: local + + hooks: + + - id: mypy + name: mypy + entry: mypy + args: [--config-file=setup.cfg] + language: python + types: [python] + require_serial: true + pass_filenames: true + exclude: >- + (?x)^( + .github/.*| + docker/.*| + docs/.*| + + tests/.*| + tests_integration/.*| + + # Exclude the following files temporarily. + src/aiidalab_qe/app/submission/__init__.py| + src/aiidalab_qe/app/parameters/__init__.py| + src/aiidalab_qe/__main__.py| + src/aiidalab_qe/common/widgets.py| + src/aiidalab_qe/common/setup_pseudos.py| + src/aiidalab_qe/common/process.py| + src/aiidalab_qe/common/setup_codes.py| + + # Exclude the following files temporarily for old plugins. + src/aiidalab_qe/plugins/bands/.*.py| + src/aiidalab_qe/plugins/pdos/.*.py| + src/aiidalab_qe/plugins/electronic_structure/.*.py| + + )$ diff --git a/qe.ipynb b/qe.ipynb index dbb706798..bf7a8f319 100644 --- a/qe.ipynb +++ b/qe.ipynb @@ -39,13 +39,6 @@ "from aiidalab_qe.version import __version__" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, { "cell_type": "code", "execution_count": null, diff --git a/setup.cfg b/setup.cfg index ee7175886..74dea780c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -41,10 +41,13 @@ where = src dev = bumpver~=2023.1124 pre-commit~=3.2 + mypy~=1.8.0 pytest~=6.2 pytest-regressions~=2.2 pgtest==1.3.1 pytest-cov~=4.0 + beautifulsoup4~=4.10 + types-beautifulsoup4~=4.10 [options.package_data] aiidalab_qe.app.parameters = qeapp.yaml @@ -57,6 +60,45 @@ aiidalab_qe.properties = pdos = aiidalab_qe.plugins.pdos:pdos electronic_structure = aiidalab_qe.plugins.electronic_structure:electronic_structure +[mypy] +disallow_any_generics = false +disallow_incomplete_defs = false +disallow_subclassing_any = false +disallow_untyped_calls = false +# these options reduce the strictness and should eventually be removed +disallow_untyped_defs = false +show_error_codes = true +show_traceback = true +# strictness +strict = true +warn_return_any = false +warn_unreachable = true +no_namespace_packages = true + +[mypy-ipywidgets.*] +ignore_missing_imports = true + +[mypy-aiidalab_widgets_base.*] +ignore_missing_imports = true + +[mypy-aiida_quantumespresso.*] +ignore_missing_imports = true + +[mypy-IPython.*] +ignore_missing_imports = true + +[mypy-setuptools.*] +ignore_missing_imports = true + +[mypy-aiida_pseudo.*] +ignore_missing_imports = true + +[mypy-ase.*] +ignore_missing_imports = true + +[mypy-nglview.*] +ignore_missing_imports = true + [aiidalab] title = Quantum ESPRESSO description = Perform Quantum ESPRESSO calculations diff --git a/src/aiidalab_qe/app/configuration/advanced.py b/src/aiidalab_qe/app/configuration/advanced.py index 18950fcd6..cb16dfc17 100644 --- a/src/aiidalab_qe/app/configuration/advanced.py +++ b/src/aiidalab_qe/app/configuration/advanced.py @@ -7,6 +7,7 @@ import ipywidgets as ipw import traitlets as tl +import typing as t from aiida import orm from aiida_quantumespresso.calculations.functions.create_kpoints_from_distance import ( create_kpoints_from_distance, @@ -222,7 +223,7 @@ def update_settings(self, **kwargs): def get_panel_value(self): # create the the initial_magnetic_moments as None (Default) # XXX: start from parameters = {} and then bundle the settings by purposes (e.g. pw, bands, etc.) - parameters = { + parameters: dict[str, t.Any] = { "initial_magnetic_moments": None, "pw": { "parameters": { diff --git a/src/aiidalab_qe/app/configuration/pseudos.py b/src/aiidalab_qe/app/configuration/pseudos.py index 7815c8600..16306459f 100644 --- a/src/aiidalab_qe/app/configuration/pseudos.py +++ b/src/aiidalab_qe/app/configuration/pseudos.py @@ -3,6 +3,7 @@ import io import re +import typing as t import ipywidgets as ipw import traitlets as tl @@ -19,6 +20,9 @@ PseudoFamily, ) +if t.TYPE_CHECKING: + from aiida_pseudo.data.pseudo.upf import UpfData as TypeUpfData + UpfData = DataFactory("pseudo.upf") SsspFamily = GroupFactory("pseudo.family.sssp") PseudoDojoFamily = GroupFactory("pseudo.family.pseudo_dojo") @@ -174,9 +178,8 @@ def set_text_color(self, change): self.dft_functional_help, self.dft_functional_prompt, ): - old_opacity = re.match( - r"[\s\S]+opacity:([\S]+);[\S\s]+", html.value - ).groups()[0] + matched = re.match(r"[\s\S]+opacity:([\S]+);[\S\s]+", html.value) + old_opacity = matched.group(1) if matched else 1.0 html.value = html.value.replace( f"opacity:{old_opacity};", f"opacity:{opacity};" ) @@ -344,7 +347,7 @@ def _reset(self): return try: - pseudos = pseudo_family.get_pseudos(structure=self.structure) + pseudos = pseudo_family.get_pseudos(structure=self.structure) # type: ignore # get cutoffs dict of all elements cutoffs = self._get_cutoffs(pseudo_family) except ValueError as exception: @@ -356,23 +359,23 @@ def _reset(self): self.pseudos = {kind: pseudo.uuid for kind, pseudo in pseudos.items()} self.set_pseudos(self.pseudos, cutoffs) - def _get_pseudos_family(self, pseudo_family: str) -> orm.Group: + def _get_pseudos_family(self, pseudo_family: str) -> t.Optional[orm.Group]: """Get the pseudo family from the database.""" try: pseudo_set = (PseudoDojoFamily, SsspFamily, CutoffsPseudoPotentialFamily) - pseudo_family = ( + pseudo_family_node = ( orm.QueryBuilder() .append(pseudo_set, filters={"label": pseudo_family}) .one()[0] ) + return pseudo_family_node + except exceptions.NotExistent as exception: raise exceptions.NotExistent( f"required pseudo family `{pseudo_family}` is not installed. Please use `aiida-pseudo install` to" "install it." ) from exception - return pseudo_family - def _get_cutoffs(self, pseudo_family): """Get the cutoffs from the pseudo family.""" from aiida_pseudo.common.units import U @@ -462,7 +465,7 @@ class PseudoUploadWidget(ipw.HBox): def __init__( self, kind: str = "", - pseudo: UpfData | None = None, + pseudo: TypeUpfData | None = None, cutoffs: dict | None = None, **kwargs, ): diff --git a/src/aiidalab_qe/app/result/__init__.py b/src/aiidalab_qe/app/result/__init__.py index 125c5c738..4ccb53221 100644 --- a/src/aiidalab_qe/app/result/__init__.py +++ b/src/aiidalab_qe/app/result/__init__.py @@ -103,7 +103,7 @@ def _on_click_kill_button(self, _=None): First kill the process, then update the kill button layout. """ workchain = [orm.load_node(self.process)] - control.kill_processes(workchain) + control.kill_processes(workchain) # type: ignore # update the kill button layout self._update_kill_button_layout() diff --git a/src/aiidalab_qe/app/result/workchain_viewer.py b/src/aiidalab_qe/app/result/workchain_viewer.py index b90d76586..ca6f79367 100644 --- a/src/aiidalab_qe/app/result/workchain_viewer.py +++ b/src/aiidalab_qe/app/result/workchain_viewer.py @@ -167,11 +167,10 @@ def __init__(self, node, export_dir=None, **kwargs): self._download_archive_button.on_click(self._download_archive) self._download_button_container = ipw.Box([self._download_archive_button]) - if node.exit_status != 0: + if node.exit_status != 0 and (final_calcjob := self._get_final_calcjob(node)): title = ipw.HTML( f"