Skip to content

Commit

Permalink
Devops: Update dependency requirement mypy==1.6.1
Browse files Browse the repository at this point in the history
The configuration is set to `strict` with the only exception of allowing
untyped calls of dependency code, since that is impossible to control in
this library itself.
  • Loading branch information
sphuber committed Nov 2, 2023
1 parent 7fc9ba5 commit 5ddb83e
Show file tree
Hide file tree
Showing 8 changed files with 68 additions and 45 deletions.
6 changes: 6 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,9 @@ repos:
language: python
args: [--config-file=pyproject.toml]
types: [python]
require_serial: true
pass_filenames: true
files: >-
(?x)^(
src/.*py|
)$
41 changes: 17 additions & 24 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ source = 'https://github.com/sphuber/aiida-shell'

[project.optional-dependencies]
dev = [
'mypy==0.981',
'mypy==1.6.1',
'pre-commit',
'pgtest~=1.3,>=1.3.1',
'pytest~=6.2',
Expand Down Expand Up @@ -81,17 +81,17 @@ fail-on-change = true
[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
'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`
Expand All @@ -102,18 +102,11 @@ ignore = [
quote-style = 'single'

[tool.mypy]
show_error_codes = true
check_untyped_defs = true
scripts_are_modules = true
warn_unused_ignores = true
warn_redundant_casts = true
no_warn_no_return = true
show_traceback = true

[[tool.mypy.overrides]]
module = 'aiida_shell.*'
follow_imports = 'skip'
check_untyped_defs = true
strict = true
disallow_untyped_calls = false
exclude = [
'^tests/',
]

[[tool.mypy.overrides]]
module = [
Expand Down
20 changes: 13 additions & 7 deletions src/aiida_shell/calculations/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class ShellJob(CalcJob):
DEFAULT_RETRIEVED_TEMPORARY: tuple[str, ...] = (FILENAME_STATUS, FILENAME_STDERR, FILENAME_STDOUT)

@classmethod
def define(cls, spec: CalcJobProcessSpec): # type: ignore[override]
def define(cls, spec: CalcJobProcessSpec) -> None: # type: ignore[override]
"""Define the process specification.
:param spec: The object to use to build up the process specification.
Expand Down Expand Up @@ -133,7 +133,7 @@ def serialize_parser(cls, value: t.Any) -> EntryPointData | PickledData:
raise TypeError(f'`value` should be a string or callable but got: {type(value)}')

@classmethod
def validate_parser(cls, value: t.Any, _) -> str | None:
def validate_parser(cls, value: t.Any, _: t.Any) -> str | None:
"""Validate the ``parser`` input."""
if not value:
return None
Expand All @@ -154,8 +154,10 @@ def validate_parser(cls, value: t.Any, _) -> str | None:
correct_signature = '(self, dirpath: pathlib.Path) -> dict[str, Data]:'
return f'The `parser` has an invalid function signature, it should be: {correct_signature}'

return None

@classmethod
def validate_nodes(cls, value: t.Mapping[str, Data], _) -> str | None:
def validate_nodes(cls, value: t.Mapping[str, Data], _: t.Any) -> str | None:
"""Validate the ``nodes`` input."""
for key, node in value.items():
if isinstance(node, (FolderData, RemoteData, SinglefileData)):
Expand All @@ -169,8 +171,10 @@ def validate_nodes(cls, value: t.Mapping[str, Data], _) -> str | None:
except Exception as exception:
return f'Casting `value` to `str` for `{key}` in `nodes` excepted: {exception}'

return None

@classmethod
def validate_arguments(cls, value: List, _) -> str | None:
def validate_arguments(cls, value: List, _: t.Any) -> str | None:
"""Validate the ``arguments`` input."""
if not value:
return None
Expand All @@ -187,8 +191,10 @@ def validate_arguments(cls, value: List, _) -> str | None:
if '>' in elements:
return 'the symbol `>` cannot be specified in the `arguments`; stdout is automatically redirected.'

return None

@classmethod
def validate_outputs(cls, value: List, _) -> str | None:
def validate_outputs(cls, value: List, _: t.Any) -> str | None:
"""Validate the ``outputs`` input."""
if not value:
return None
Expand Down Expand Up @@ -265,7 +271,7 @@ def prepare_for_submission(self, folder: Folder) -> CalcInfo:
return calc_info

@staticmethod
def handle_remote_data_nodes(inputs: dict[str, Data]) -> tuple[list, list]:
def handle_remote_data_nodes(inputs: dict[str, Data]) -> tuple[list[t.Any], list[t.Any]]:
"""Handle a ``RemoteData`` that was passed in the ``nodes`` input.
:param inputs: The inputs dictionary.
Expand Down Expand Up @@ -369,7 +375,7 @@ def write_single_file_data(dirpath: pathlib.Path, node: SinglefileData, key: str
:returns: The relative filename used to write the content to ``dirpath``.
"""
default_filename = node.filename if node.filename and node.filename != SinglefileData.DEFAULT_FILENAME else key
filename = filenames.get(key, default_filename)
filename: str = filenames.get(key, default_filename)
filepath = dirpath / filename

filepath.parent.mkdir(parents=True, exist_ok=True)
Expand Down
6 changes: 4 additions & 2 deletions src/aiida_shell/data/code.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Code that represents a shell command."""
from __future__ import annotations

import typing as t

from aiida.orm import InstalledCode

__all__ = ('ShellCode',)
Expand All @@ -14,7 +16,7 @@ class ShellCode(InstalledCode):
calculation job as well.
"""

def __init__(self, *args, default_calc_job_plugin: str = 'core.shell', **kwargs) -> None:
def __init__(self, *args: t.Any, default_calc_job_plugin: str = 'core.shell', **kwargs: t.Any) -> None:
"""Construct a new instance."""
self.validate_default_calc_job_plugin(default_calc_job_plugin)
super().__init__(*args, default_calc_job_plugin=default_calc_job_plugin, **kwargs)
Expand All @@ -31,7 +33,7 @@ def validate_default_calc_job_plugin(default_calc_job_plugin: str) -> None:
raise ValueError(f'`default_calc_job_plugin` has to be `core.shell`, but got: {default_calc_job_plugin}')

@classmethod
def _get_cli_options(cls) -> dict:
def _get_cli_options(cls) -> dict[str, t.Any]:
"""Return the CLI options that would allow to create an instance of this class."""
options = super()._get_cli_options()
options['default_calc_job_plugin']['default'] = 'core.shell'
Expand Down
7 changes: 6 additions & 1 deletion src/aiida_shell/data/entry_point.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@ class EntryPointData(Data):
"""Attribute key that stores the version of the package that provided the entry point, if available."""

def __init__(
self, *, entry_point: EntryPoint | None = None, name: str | None = None, group: str | None = None, **kwargs
self,
*,
entry_point: EntryPoint | None = None,
name: str | None = None,
group: str | None = None,
**kwargs: t.Any,
):
"""Construct a new instance.
Expand Down
2 changes: 1 addition & 1 deletion src/aiida_shell/data/pickled.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def get_unpickler(self) -> t.Callable[[bytes], t.Any]:
) from exception

try:
unpickler = getattr(unpickler_module, name)
unpickler: t.Callable[[bytes], t.Any] = getattr(unpickler_module, name)
except AttributeError as exception:
raise RuntimeError(
f'Could not load `{name}` from `{module} which should be able to unpickle this node.'
Expand Down
2 changes: 1 addition & 1 deletion src/aiida_shell/launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def prepare_code(command: str, computer: Computer | None = None) -> AbstractCode
f'failed to determine the absolute path of the command on the computer: {stderr}'
) from exception

code = ShellCode(
code = ShellCode( # type: ignore[assignment]
label=command, computer=computer, filepath_executable=executable, default_calc_job_plugin='core.shell'
).store()

Expand Down
29 changes: 20 additions & 9 deletions src/aiida_shell/parsers/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import pathlib
import re
import typing as t

from aiida.engine import ExitCode
from aiida.orm import Data, FolderData, SinglefileData
Expand All @@ -24,7 +25,7 @@
class ShellParser(Parser):
"""Parser for a :class:`aiida_shell.ShellJob` job."""

def parse(self, **kwargs):
def parse(self, **kwargs: t.Any) -> ExitCode:
"""Parse the contents of the output files stored in the ``retrieved`` output node."""
dirpath = pathlib.Path(kwargs['retrieved_temporary_folder'])

Expand All @@ -35,13 +36,23 @@ def parse(self, **kwargs):
try:
self.call_parser_hook(dirpath)
except Exception as exception:
return self.exit_codes.ERROR_PARSER_HOOK_EXCEPTED.format(exception=exception)
return self.exit_code('ERROR_PARSER_HOOK_EXCEPTED', exception=exception)

if missing_filepaths:
return self.exit_codes.ERROR_OUTPUT_FILEPATHS_MISSING.format(missing_filepaths=', '.join(missing_filepaths))
return self.exit_code('ERROR_OUTPUT_FILEPATHS_MISSING', missing_filepaths=', '.join(missing_filepaths))

return exit_code

def exit_code(self, key: str, **kwargs: t.Any) -> ExitCode:
"""Return the exit code corresponding to the given key.
:param key: The key under which the exit code is registered.
:param kwargs: Any keyword arguments to format the exit code message.
:returns: The formatted exit code.
"""
exit_code: ExitCode = getattr(self.exit_codes, key).format(**kwargs)
return exit_code

@staticmethod
def format_link_label(filename: str) -> str:
"""Format the link label from a given filename.
Expand Down Expand Up @@ -78,22 +89,22 @@ def parse_default_outputs(self, dirpath: pathlib.Path) -> ExitCode:
with (dirpath / filename_stdout).open(mode='rb') as handle:
node_stdout = SinglefileData(handle, filename=filename_stdout)
except FileNotFoundError:
return self.exit_codes.ERROR_OUTPUT_STDOUT_MISSING
return self.exit_code('ERROR_OUTPUT_STDOUT_MISSING')

self.out(self.format_link_label(filename_stdout), node_stdout)

try:
exit_status = int((dirpath / ShellJob.FILENAME_STATUS).read_text())
except FileNotFoundError:
return self.exit_codes.ERROR_OUTPUT_STATUS_MISSING
return self.exit_code('ERROR_OUTPUT_STATUS_MISSING')
except ValueError:
return self.exit_codes.ERROR_OUTPUT_STATUS_INVALID
return self.exit_code('ERROR_OUTPUT_STATUS_INVALID')

if exit_status != 0:
return self.exit_codes.ERROR_COMMAND_FAILED.format(status=exit_status, stderr=stderr)
return self.exit_code('ERROR_COMMAND_FAILED', status=exit_status, stderr=stderr)

if stderr:
return self.exit_codes.ERROR_STDERR_NOT_EMPTY
return self.exit_code('ERROR_STDERR_NOT_EMPTY')

return ExitCode()

Expand Down Expand Up @@ -121,7 +132,7 @@ def parse_custom_outputs(self, dirpath: pathlib.Path) -> list[str]:

return missing_filepaths

def call_parser_hook(self, dirpath: pathlib.Path):
def call_parser_hook(self, dirpath: pathlib.Path) -> None:
"""Execute the ``parser`` custom parser hook that was passed as input to the ``ShellJob``."""
unpickled_parser = self.node.inputs.parser.load()
results = unpickled_parser(self, dirpath) or {}
Expand Down

0 comments on commit 5ddb83e

Please sign in to comment.