Skip to content

Commit 0d5c4d4

Browse files
authored
More typing for cmdline.params module (#7009)
* More typing for cmdline.params.{arguments,types} modules * More precise Callable type * Better types for cmdline.params.types.strings module * More accurate return types of convert methods * Strict typing for cmdline.params.options * Don't ignore errors from plumpy * Enable strict settings for aiida.cmdline.params * Upgrade mypy to 1.18.1 * Strict typing for cmdline.groups module
1 parent 7255f01 commit 0d5c4d4

36 files changed

+341
-197
lines changed

docs/source/nitpick-exceptions

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,10 @@ py:func aiida.orm.implementation.BackendQueryBuilder
117117
py:obj aiida.engine.processes.functions.P
118118
py:obj aiida.engine.processes.functions.N
119119
py:obj aiida.engine.processes.functions.R_co
120+
py:class FC
120121
py:class P
121122
py:class N
123+
py:class aiida.common.lang.T
122124
py:class aiida.engine.processes.functions.N
123125
py:class aiida.engine.processes.functions.R_co
124126

pyproject.toml

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ notebook = [
231231
]
232232
pre-commit = [
233233
'aiida-core[atomic_tools,rest,tests,tui]',
234-
'mypy~=1.17.0',
234+
'mypy~=1.18.0',
235235
'packaging~=23.0',
236236
'pre-commit~=3.5',
237237
'sqlalchemy[mypy]~=2.0',
@@ -324,6 +324,18 @@ module = 'aiida'
324324
disallow_untyped_decorators = false
325325
module = 'aiida.cmdline.commands.*'
326326

327+
[[tool.mypy.overrides]]
328+
disallow_any_generics = true
329+
disallow_incomplete_defs = true
330+
disallow_subclassing_any = true
331+
disallow_untyped_calls = true
332+
disallow_untyped_defs = true
333+
module = [
334+
'aiida.cmdline.params.*',
335+
'aiida.cmdline.groups.*'
336+
]
337+
warn_return_any = true
338+
327339
[[tool.mypy.overrides]]
328340
check_untyped_defs = false
329341
module = 'tests.*'
@@ -374,10 +386,6 @@ module = [
374386
'upf_to_json.*'
375387
]
376388

377-
[[tool.mypy.overrides]]
378-
ignore_errors = true
379-
module = 'plumpy'
380-
381389
[tool.pytest.ini_options]
382390
addopts = '--benchmark-skip --durations=5 --durations-min=1 --strict-config --strict-markers -ra --cov-report xml --cov-append '
383391
filterwarnings = [

src/aiida/cmdline/groups/dynamic.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
from ..params.options.interactive import InteractiveOption
1717
from .verdi import VerdiCommandGroup
1818

19+
if t.TYPE_CHECKING:
20+
from click.decorators import FC
21+
1922
__all__ = ('DynamicEntryPointCommandGroup',)
2023

2124

@@ -44,11 +47,11 @@ def cmd_create():
4447

4548
def __init__(
4649
self,
47-
command: t.Callable,
50+
command: click.Command,
4851
entry_point_group: str,
4952
entry_point_name_filter: str = r'.*',
50-
shared_options: list[t.Callable[[t.Any], t.Any]] | None = None,
51-
**kwargs,
53+
shared_options: list[FC] | None = None,
54+
**kwargs: t.Any,
5255
):
5356
super().__init__(**kwargs)
5457
self._command = command
@@ -88,7 +91,7 @@ def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None
8891
command = super().get_command(ctx, cmd_name)
8992
return command
9093

91-
def call_command(self, ctx, cls, non_interactive, **kwargs):
94+
def call_command(self, ctx: click.Context, cls: t.Any, non_interactive: bool, **kwargs: t.Any) -> t.Any:
9295
"""Call the ``command`` after validating the provided inputs."""
9396
from pydantic import ValidationError
9497

@@ -116,13 +119,13 @@ def create_command(self, ctx: click.Context, entry_point: str) -> click.Command:
116119
command.__doc__ = cls.__doc__
117120
return click.command(entry_point)(self.create_options(entry_point)(command))
118121

119-
def create_options(self, entry_point: str) -> t.Callable:
122+
def create_options(self, entry_point: str) -> t.Callable[[FC], FC]:
120123
"""Create the option decorators for the command function for the given entry point.
121124
122125
:param entry_point: The entry point.
123126
"""
124127

125-
def apply_options(func):
128+
def apply_options(func: FC) -> FC:
126129
"""Decorate the command function with the appropriate options for the given entry point."""
127130
func = options.NON_INTERACTIVE()(func)
128131
func = options.CONFIG_FILE()(func)
@@ -143,7 +146,7 @@ def apply_options(func):
143146

144147
return apply_options
145148

146-
def list_options(self, entry_point: str) -> list[t.Callable[[t.Any], t.Any]]:
149+
def list_options(self, entry_point: str) -> list[t.Callable[[FC], FC]]:
147150
"""Return the list of options that should be applied to the command for the given entry point.
148151
149152
:param entry_point: The entry point.
@@ -207,7 +210,7 @@ def list_options(self, entry_point: str) -> list[t.Callable[[t.Any], t.Any]]:
207210
return options_ordered
208211

209212
@staticmethod
210-
def create_option(name, spec: dict) -> t.Callable[[t.Any], t.Any]:
213+
def create_option(name: str, spec: dict[str, t.Any]) -> t.Callable[[FC], FC]:
211214
"""Create a click option from a name and a specification."""
212215
is_flag = spec.pop('is_flag', False)
213216
name_dashed = name.replace('_', '-')

src/aiida/cmdline/groups/verdi.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class LazyVerdiObjAttributeDict(AttributeDict):
4848
_KEY_PROFILE = 'profile'
4949

5050
def __init__(self, ctx: click.Context, dictionary: dict[str, t.Any] | None = None):
51-
super().__init__(dictionary)
51+
super().__init__(dictionary) # type: ignore[no-untyped-call]
5252
self.ctx = ctx
5353

5454
def __getattr__(self, attr: str) -> t.Any:
@@ -68,13 +68,13 @@ def __getattr__(self, attr: str) -> t.Any:
6868
except ConfigurationError as exception:
6969
self.ctx.fail(str(exception))
7070

71-
return super().__getattr__(attr)
71+
return super().__getattr__(attr) # type: ignore[no-untyped-call]
7272

7373

7474
class VerdiContext(click.Context):
7575
"""Custom context implementation that defines the ``obj`` user object and adds the ``Config`` instance."""
7676

77-
def __init__(self, *args, **kwargs):
77+
def __init__(self, *args: t.Any, **kwargs: t.Any):
7878
super().__init__(*args, **kwargs)
7979
if self.obj is None:
8080
self.obj = LazyVerdiObjAttributeDict(self)
@@ -190,7 +190,7 @@ def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None
190190

191191
return None
192192

193-
def group(self, *args, **kwargs) -> click.Group:
193+
def group(self, *args: t.Any, **kwargs: t.Any) -> click.Group:
194194
"""Ensure that sub command groups use the same class but do not override an explicitly set value."""
195195
kwargs.setdefault('cls', self.__class__)
196-
return super().group(*args, **kwargs)
196+
return super().group(*args, **kwargs) # type: ignore[no-any-return]

src/aiida/cmdline/params/arguments/overridable.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,15 @@
88
###########################################################################
99
"""Convenience class which can be used to defined a set of commonly used arguments that can be easily reused."""
1010

11+
from __future__ import annotations
12+
13+
import typing as t
14+
1115
import click
1216

17+
if t.TYPE_CHECKING:
18+
from click.decorators import FC
19+
1320
__all__ = ('OverridableArgument',)
1421

1522

@@ -35,12 +42,12 @@ def print_code_pks(codes):
3542
the function argument name is determined, can be overridden.
3643
"""
3744

38-
def __init__(self, *args, **kwargs):
45+
def __init__(self, *args: t.Any, **kwargs: t.Any):
3946
"""Store the default args and kwargs"""
4047
self.args = args
4148
self.kwargs = kwargs
4249

43-
def __call__(self, *args, **kwargs):
50+
def __call__(self, *args: t.Any, **kwargs: t.Any) -> t.Callable[[FC], FC]:
4451
"""Override the stored kwargs with the passed kwargs and return the argument.
4552
4653
The stored args are used only if they are not provided. This allows the user to override the variable name,

src/aiida/cmdline/params/options/commands/code.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@
1515
from aiida.cmdline.params.options.overridable import OverridableOption
1616

1717

18-
def is_on_computer(ctx):
18+
def is_on_computer(ctx: click.Context) -> bool:
1919
return bool(ctx.params.get('on_computer'))
2020

2121

22-
def is_not_on_computer(ctx):
22+
def is_not_on_computer(ctx: click.Context) -> bool:
2323
return bool(not is_on_computer(ctx))
2424

2525

26-
def validate_label_uniqueness(ctx, _, value):
26+
def validate_label_uniqueness(ctx: click.Context, _: None, value: str) -> str:
2727
"""Validate the uniqueness of the label of the code.
2828
2929
The exact uniqueness criterion depends on the type of the code, whether it is "local" or "remote". For the former,

src/aiida/cmdline/params/options/commands/computer.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,27 @@
88
###########################################################################
99
"""Reusable command line interface options for Computer commands."""
1010

11+
import typing as t
12+
1113
import click
1214

1315
from aiida.cmdline.params import options, types
1416
from aiida.cmdline.params.options.interactive import InteractiveOption, TemplateInteractiveOption
1517
from aiida.cmdline.params.options.overridable import OverridableOption
1618

19+
if t.TYPE_CHECKING:
20+
from aiida.schedulers.datastructures import JobResource
21+
1722

18-
def get_job_resource_cls(ctx):
23+
def get_job_resource_cls(ctx: click.Context) -> 'type[JobResource]':
1924
"""Return job resource cls from ctx."""
2025
from aiida.common.exceptions import ValidationError
26+
from aiida.schedulers import Scheduler
2127

2228
scheduler_ep = ctx.params['scheduler']
2329
if scheduler_ep is not None:
2430
try:
25-
scheduler_cls = scheduler_ep.load()
31+
scheduler_cls = t.cast(Scheduler, scheduler_ep.load())
2632
except ImportError:
2733
raise ImportError(f"Unable to load the '{scheduler_ep.name}' scheduler")
2834
else:
@@ -33,22 +39,21 @@ def get_job_resource_cls(ctx):
3339
return scheduler_cls.job_resource_class
3440

3541

36-
def should_call_default_mpiprocs_per_machine(ctx):
42+
def should_call_default_mpiprocs_per_machine(ctx: click.Context) -> bool:
3743
"""Return whether the selected scheduler type accepts `default_mpiprocs_per_machine`.
3844
3945
:return: `True` if the scheduler type accepts `default_mpiprocs_per_machine`, `False`
4046
otherwise. If the scheduler class could not be loaded `False` is returned by default.
4147
"""
4248
job_resource_cls = get_job_resource_cls(ctx)
43-
4449
if job_resource_cls is None:
4550
# Odd situation...
46-
return False
51+
return False # type: ignore[unreachable]
4752

4853
return job_resource_cls.accepts_default_mpiprocs_per_machine()
4954

5055

51-
def should_call_default_memory_per_machine(ctx):
56+
def should_call_default_memory_per_machine(ctx: click.Context) -> bool:
5257
"""Return whether the selected scheduler type accepts `default_memory_per_machine`.
5358
5459
:return: `True` if the scheduler type accepts `default_memory_per_machine`, `False`
@@ -58,7 +63,7 @@ def should_call_default_memory_per_machine(ctx):
5863

5964
if job_resource_cls is None:
6065
# Odd situation...
61-
return False
66+
return False # type: ignore[unreachable]
6267

6368
return job_resource_cls.accepts_default_memory_per_machine()
6469

src/aiida/cmdline/params/options/conditional.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,15 @@
88
###########################################################################
99
"""Option whose requiredness is determined by a callback function."""
1010

11+
from __future__ import annotations
12+
13+
import typing as t
14+
1115
import click
1216

17+
if t.TYPE_CHECKING:
18+
from collections.abc import Sequence
19+
1320

1421
class ConditionalOption(click.Option):
1522
"""Option whose requiredness is determined by a callback function.
@@ -24,7 +31,12 @@ class ConditionalOption(click.Option):
2431
is typically used when the condition depends on other parameters specified on the command line.
2532
"""
2633

27-
def __init__(self, param_decls=None, required_fn=None, **kwargs):
34+
def __init__(
35+
self,
36+
param_decls: Sequence[str] | None = None,
37+
required_fn: t.Callable[[click.Context], bool] | None = None,
38+
**kwargs: t.Any,
39+
):
2840
self.required_fn = required_fn
2941

3042
# If there is not callback to determine requiredness, assume the option is not required.
@@ -33,7 +45,7 @@ def __init__(self, param_decls=None, required_fn=None, **kwargs):
3345

3446
super().__init__(param_decls=param_decls, **kwargs)
3547

36-
def process_value(self, ctx, value):
48+
def process_value(self, ctx: click.Context, value: t.Any) -> t.Any:
3749
try:
3850
value = super().process_value(ctx, value)
3951
except click.MissingParameter:
@@ -45,7 +57,7 @@ def process_value(self, ctx, value):
4557

4658
return value
4759

48-
def is_required(self, ctx):
60+
def is_required(self, ctx: click.Context) -> bool:
4961
"""Runs the given check on the context to determine requiredness"""
5062
if self.required_fn:
5163
return self.required_fn(ctx)

src/aiida/cmdline/params/options/config.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@
2525

2626
from .overridable import OverridableOption
2727

28+
if t.TYPE_CHECKING:
29+
from click.decorators import FC
30+
2831
__all__ = ('ConfigFileOption',)
2932

3033

31-
def yaml_config_file_provider(handle, cmd_name):
34+
def yaml_config_file_provider(handle: t.Any, _cmd_name: t.Any) -> t.Any:
3235
"""Read yaml config file from file handle."""
3336
import yaml
3437

@@ -45,7 +48,7 @@ def configuration_callback(
4548
ctx: click.Context,
4649
param: click.Parameter,
4750
value: t.Any,
48-
):
51+
) -> t.Any:
4952
"""Callback for reading the config file.
5053
5154
Also takes care of calling user specified custom callback afterwards.
@@ -89,7 +92,7 @@ def configuration_callback(
8992
return saved_callback(ctx, param, value) if saved_callback else value
9093

9194

92-
def configuration_option(*param_decls, **attrs):
95+
def configuration_option(*param_decls: t.Any, **attrs: t.Any) -> t.Callable[[FC], FC]:
9396
"""Adds configuration file support to a click application.
9497
9598
This will create an option of type ``click.File`` expecting the path to a configuration file. When specified, it
@@ -114,7 +117,7 @@ def configuration_option(*param_decls, **attrs):
114117
param_decls = param_decls or ('--config',)
115118
option_name = param_decls[0]
116119

117-
def decorator(func):
120+
def decorator(func: FC) -> FC:
118121
attrs.setdefault('is_eager', True)
119122
attrs.setdefault('help', 'Read configuration from FILE.')
120123
attrs.setdefault('expose_value', False)
@@ -164,7 +167,7 @@ def computer_setup(computer_name):
164167
165168
"""
166169

167-
def __init__(self, *args, **kwargs):
170+
def __init__(self, *args: t.Any, **kwargs: t.Any):
168171
"""Store the default args and kwargs.
169172
170173
:param args: default arguments to be used for the option
@@ -173,7 +176,7 @@ def __init__(self, *args, **kwargs):
173176
kwargs.update({'provider': yaml_config_file_provider, 'implicit': False})
174177
super().__init__(*args, **kwargs)
175178

176-
def __call__(self, **kwargs):
179+
def __call__(self, **kwargs: t.Any) -> t.Any:
177180
"""Override the stored kwargs, (ignoring args as we do not allow option name changes) and return the option.
178181
179182
:param kwargs: keyword arguments that will override those set in the construction

0 commit comments

Comments
 (0)