Skip to content

Commit cfb7fd1

Browse files
authored
Add typing to aiida.cmdline.params module (#6952)
* Add typing to src/aiida/cmdline/params/options/multivalue.py * Add typing to src/aiida/cmdline/params/types/group.py * Type check src/aiida/cmdline/params/options/interactive.py * Typecheck src/aiida/cmdline/params/options/main.py * Typecheck src/aiida/cmdline/params/options/commands/setup.py * Type AiiDAConfigDir.get and get_config * Fix type error in src/aiida/cmdline/utils/multi_line_input.py * Add typing to src/aiida/cmdline/utils/shell.py * Don't allow sub_classes=None in cmdline/params/types/group.py
1 parent 32d515a commit cfb7fd1

File tree

14 files changed

+75
-52
lines changed

14 files changed

+75
-52
lines changed

.pre-commit-config.yaml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,6 @@ repos:
107107
src/aiida/cmdline/commands/cmd_node.py|
108108
src/aiida/cmdline/commands/cmd_shell.py|
109109
src/aiida/cmdline/commands/cmd_storage.py|
110-
src/aiida/cmdline/params/options/commands/setup.py|
111-
src/aiida/cmdline/params/options/interactive.py|
112-
src/aiida/cmdline/params/options/main.py|
113-
src/aiida/cmdline/params/options/multivalue.py|
114-
src/aiida/cmdline/params/types/group.py|
115110
src/aiida/cmdline/utils/ascii_vis.py|
116111
src/aiida/cmdline/utils/common.py|
117112
src/aiida/cmdline/utils/echo.py|

docs/source/nitpick-exceptions

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ py:class click.types.Choice
135135
py:class click.types.Path
136136
py:class click.types.File
137137
py:class click.types.StringParamType
138+
py:class click.parser.OptionParser
138139
py:func click.shell_completion._start_of_option
139140
py:meth click.Option.get_default
140141
py:meth fail

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

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,23 @@
88
###########################################################################
99
"""Reusable command line interface options for the setup commands."""
1010

11+
from __future__ import annotations
12+
1113
import functools
1214
import getpass
15+
import typing as t
1316

1417
import click
1518

1619
from aiida.brokers.rabbitmq.defaults import BROKER_DEFAULTS
1720
from aiida.cmdline.params import options, types
1821
from aiida.manage.configuration import Profile, get_config, get_config_option
19-
from aiida.manage.external.postgres import DEFAULT_DBINFO
22+
from aiida.manage.external.postgres import DEFAULT_DBINFO # type: ignore[attr-defined]
2023

2124
PASSWORD_UNCHANGED = '***'
2225

2326

24-
def validate_profile_parameter(ctx):
27+
def validate_profile_parameter(ctx: click.Context) -> None:
2528
"""Validate that the context contains the option `profile` and it contains a `Profile` instance.
2629
2730
:param ctx: click context which should contain the selected profile
@@ -32,10 +35,10 @@ def validate_profile_parameter(ctx):
3235
raise click.BadParameter('specifying the name of the profile is required', param_hint=f'"--{option}"')
3336

3437

35-
def get_profile_attribute_default(attribute_tuple, ctx):
38+
def get_profile_attribute_default(attribute_tuple: tuple[str, t.Any], ctx: click.Context) -> t.Any:
3639
"""Return the default value for the given attribute of the profile passed in the context.
3740
38-
:param attribute: attribute for which to get the current value
41+
:param attribute_tuple: attribute for which to get the current value, and its default
3942
:param ctx: click context which should contain the selected profile
4043
:return: profile attribute default value if set, or None
4144
"""
@@ -58,7 +61,7 @@ def get_profile_attribute_default(attribute_tuple, ctx):
5861
return default
5962

6063

61-
def get_repository_uri_default(ctx):
64+
def get_repository_uri_default(ctx: click.Context) -> str:
6265
"""Return the default value for the repository URI for the current profile in the click context.
6366
6467
:param ctx: click context which should contain the selected profile
@@ -74,7 +77,7 @@ def get_repository_uri_default(ctx):
7477
return os.path.join(configure_directory, 'repository', ctx.params['profile'].name)
7578

7679

77-
def get_quicksetup_repository_uri(ctx, param, value):
80+
def get_quicksetup_repository_uri(ctx: click.Context, _param: click.Parameter, _value: t.Any) -> str:
7881
"""Return the repository URI to be used as default in `verdi quicksetup`
7982
8083
:param ctx: click context which should contain the contextual parameters
@@ -83,7 +86,7 @@ def get_quicksetup_repository_uri(ctx, param, value):
8386
return get_repository_uri_default(ctx)
8487

8588

86-
def get_quicksetup_database_name(ctx, param, value):
89+
def get_quicksetup_database_name(ctx: click.Context, _param: click.Parameter, value: str | None) -> str:
8790
"""Determine the database name to be used as default for the Postgres connection in `verdi quicksetup`
8891
8992
If a value is explicitly passed, that value is returned unchanged.
@@ -109,7 +112,7 @@ def get_quicksetup_database_name(ctx, param, value):
109112
return database_name
110113

111114

112-
def get_quicksetup_username(ctx, param, value):
115+
def get_quicksetup_username(ctx: click.Context, param: click.Parameter, value: str | None) -> str:
113116
"""Determine the username to be used as default for the Postgres connection in `verdi quicksetup`
114117
115118
If a value is explicitly passed, that value is returned. If there is no value, the name will be based on the
@@ -130,7 +133,7 @@ def get_quicksetup_username(ctx, param, value):
130133
return username
131134

132135

133-
def get_quicksetup_password(ctx, param, value):
136+
def get_quicksetup_password(ctx: click.Context, param: click.Parameter, value: str | None) -> str:
134137
"""Determine the password to be used as default for the Postgres connection in `verdi quicksetup`
135138
136139
If a value is explicitly passed, that value is returned. If there is no value, the current username in the context

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@
88
###########################################################################
99
"""Tools and an option class for interactive parameter entry with additional features such as help lookup."""
1010

11+
from __future__ import annotations
12+
1113
import typing as t
1214

1315
import click
16+
from click.shell_completion import CompletionItem
1417

1518
from aiida.cmdline.utils import echo
1619

@@ -64,7 +67,7 @@ def prompt(self):
6467
return click.style(self._prompt, fg=self.PROMPT_COLOR)
6568

6669
@prompt.setter
67-
def prompt(self, value):
70+
def prompt(self, value: str | None) -> None:
6871
"""Set the prompt text."""
6972
self._prompt = value
7073

@@ -86,7 +89,7 @@ def prompt_for_value(self, ctx: click.Context) -> t.Any:
8689
if not hasattr(ctx, 'prompt_loop_info_printed'):
8790
echo.echo_report(f'enter {self.CHARACTER_PROMPT_HELP} for help.')
8891
echo.echo_report(f'enter {self.CHARACTER_IGNORE_DEFAULT} to ignore the default and set no value.')
89-
ctx.prompt_loop_info_printed = True
92+
ctx.prompt_loop_info_printed = True # type: ignore[attr-defined]
9093

9194
return super().prompt_for_value(ctx)
9295

@@ -101,6 +104,9 @@ def process_value(self, ctx: click.Context, value: t.Any) -> t.Any:
101104
specified the ``click.Context.get_parameter_source`` method is used. The ``click.Parameter.handle_parse_result``
102105
method will set this after ``Parameter.consume_value``` is called but before ``Parameter.process_value`` is.
103106
"""
107+
if self.name is None:
108+
return value
109+
104110
source = ctx.get_parameter_source(self.name)
105111

106112
if source is None:
@@ -127,11 +133,12 @@ def process_value(self, ctx: click.Context, value: t.Any) -> t.Any:
127133
return self.prompt_for_value(ctx)
128134
raise
129135

130-
def get_help_message(self):
136+
def get_help_message(self) -> str:
131137
"""Return a message to be displayed for in-prompt help."""
132138
message = self.help or f'Expecting {self.type.name}'
133139

134-
choices = getattr(self.type, 'shell_complete', lambda x, y, z: [])(self.type, None, '')
140+
shell_complete: t.Callable = getattr(self.type, 'shell_complete', lambda x, y, z: [])
141+
choices: list[CompletionItem] = shell_complete(self.type, None, '')
135142
choices_string = []
136143

137144
for choice in choices:
@@ -146,7 +153,7 @@ def get_help_message(self):
146153

147154
return message
148155

149-
def get_default(self, ctx: click.Context, call: bool = True) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]:
156+
def get_default(self, ctx: click.Context, call: bool = True) -> t.Callable[[], t.Any] | None:
150157
"""Provides the functionality of :meth:`click.Option.get_default`"""
151158
if ctx.resilient_parsing:
152159
return None
@@ -157,7 +164,7 @@ def get_default(self, ctx: click.Context, call: bool = True) -> t.Optional[t.Uni
157164
default = super().get_default(ctx, call=call)
158165

159166
try:
160-
default = self.type.deconvert_default(default)
167+
default = self.type.deconvert_default(default) # type: ignore[attr-defined]
161168
except AttributeError:
162169
pass
163170

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

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@
88
###########################################################################
99
"""Module with pre-defined reusable commandline options that can be used as `click` decorators."""
1010

11+
from __future__ import annotations
12+
1113
import pathlib
14+
import typing as t
1215

1316
import click
1417

1518
from aiida.brokers.rabbitmq.defaults import BROKER_DEFAULTS
1619
from aiida.common.log import LOG_LEVELS, configure_logging
17-
from aiida.manage.external.postgres import DEFAULT_DBINFO
20+
from aiida.manage.external.postgres import DEFAULT_DBINFO # type: ignore[attr-defined]
1821

1922
from ...utils import defaults, echo
2023
from .. import types
@@ -149,21 +152,21 @@
149152
}
150153

151154

152-
def valid_process_states():
155+
def valid_process_states() -> tuple[str, ...]:
153156
"""Return a list of valid values for the ProcessState enum."""
154157
from plumpy import ProcessState
155158

156159
return tuple(state.value for state in ProcessState)
157160

158161

159-
def valid_calc_job_states():
162+
def valid_calc_job_states() -> tuple[str, ...]:
160163
"""Return a list of valid values for the CalcState enum."""
161164
from aiida.common.datastructures import CalcJobState
162165

163166
return tuple(state.value for state in CalcJobState)
164167

165168

166-
def active_process_states():
169+
def active_process_states() -> list[str]:
167170
"""Return a list of process states that are considered active."""
168171
from plumpy import ProcessState
169172

@@ -174,7 +177,7 @@ def active_process_states():
174177
]
175178

176179

177-
def graph_traversal_rules(rules):
180+
def graph_traversal_rules(rules: dict) -> t.Callable:
178181
"""Apply the graph traversal rule options to the command."""
179182

180183
def decorator(command):
@@ -191,7 +194,7 @@ def decorator(command):
191194
return decorator
192195

193196

194-
def set_log_level(ctx, _param, value):
197+
def set_log_level(ctx: click.Context, _param: click.Parameter, value: t.Any) -> t.Any:
195198
"""Configure the logging for the CLI command being executed.
196199
197200
Note that we cannot use the most obvious approach of directly setting the level on the various loggers. The reason

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
__all__ = ('MultipleValueOption',)
1616

1717

18-
def collect_usage_pieces(self, ctx):
18+
def collect_usage_pieces(self: click.Command, ctx: click.Context) -> list[str]:
1919
"""Returns all the pieces that go into the usage line and returns it as a list of strings."""
2020
result = [self.options_metavar]
2121

@@ -27,11 +27,11 @@ def collect_usage_pieces(self, ctx):
2727
for param in self.get_params(ctx):
2828
result.extend(param.get_usage_pieces(ctx))
2929

30-
return result
30+
return result # type: ignore[return-value]
3131

3232

3333
# Override the `collect_usage_pieces` method of the `click.Command` class to automatically affect all commands
34-
click.Command.collect_usage_pieces = collect_usage_pieces
34+
click.Command.collect_usage_pieces = collect_usage_pieces # type: ignore[method-assign]
3535

3636

3737
class MultipleValueOption(click.Option):
@@ -57,7 +57,8 @@ def __init__(self, *args, **kwargs):
5757
self._previous_parser_process = None
5858
self._eat_all_parser = None
5959

60-
def add_to_parser(self, parser, ctx):
60+
# TODO: add_to_parser has been deprecated in 8.2.0
61+
def add_to_parser(self, parser: click.parser.OptionParser, ctx: click.Context) -> None:
6162
"""Override built in click method that allows us to specify a custom parser
6263
to eat up parameters until the following flag or 'endopt' (i.e. --)
6364
"""
@@ -75,20 +76,20 @@ def parser_process(value, state):
7576

7677
# Grab everything up to the next option or endopts symbol
7778
while state.rargs and not done:
78-
for prefix in self._eat_all_parser.prefixes:
79+
for prefix in self._eat_all_parser.prefixes: # type: ignore[union-attr]
7980
if state.rargs[0].startswith(prefix) or state.rargs[0] == ENDOPTS:
8081
done = True
8182
if not done:
8283
value.append(state.rargs.pop(0))
8384

8485
value = tuple(value)
8586

86-
self._previous_parser_process(value, state)
87+
self._previous_parser_process(value, state) # type: ignore[misc]
8788

8889
for name in self.opts:
8990
our_parser = parser._long_opt.get(name) or parser._short_opt.get(name)
9091
if our_parser:
9192
self._eat_all_parser = our_parser
9293
self._previous_parser_process = our_parser.process
93-
our_parser.process = parser_process
94+
our_parser.process = parser_process # type: ignore[method-assign]
9495
break

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

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

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

1315
__all__ = ('OverridableOption',)
@@ -48,7 +50,7 @@ def __init__(self, *args, **kwargs):
4850
self.args = args
4951
self.kwargs = kwargs
5052

51-
def __call__(self, **kwargs):
53+
def __call__(self, **kwargs) -> t.Callable[[t.Any], t.Any]:
5254
"""Override the stored kwargs, (ignoring args as we do not allow option name changes) and return the option.
5355
5456
:param kwargs: keyword arguments that will override those set in the construction
@@ -58,7 +60,7 @@ def __call__(self, **kwargs):
5860
kw_copy.update(kwargs)
5961
return click.option(*self.args, **kw_copy)
6062

61-
def clone(self, **kwargs):
63+
def clone(self, **kwargs) -> 'OverridableOption':
6264
"""Create a new instance of by cloning the current instance and updating the stored kwargs with those passed.
6365
6466
This can be useful when an already predefined OverridableOption needs to be further specified and reused

src/aiida/cmdline/params/types/group.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
###########################################################################
99
"""Module for custom click param type group."""
1010

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

1317
from aiida.cmdline.utils import decorators
@@ -23,7 +27,7 @@ class GroupParamType(IdentifierParamType):
2327

2428
name = 'Group'
2529

26-
def __init__(self, create_if_not_exist=False, sub_classes=('aiida.groups:core',)):
30+
def __init__(self, create_if_not_exist: bool = False, sub_classes: tuple[str] = ('aiida.groups:core',)):
2731
"""Construct the parameter type.
2832
2933
The `sub_classes` argument can be used to narrow the set of subclasses of `Group` that should be matched. By
@@ -60,7 +64,9 @@ def orm_class_loader(self):
6064
return GroupEntityLoader
6165

6266
@decorators.with_dbenv()
63-
def shell_complete(self, ctx, param, incomplete):
67+
def shell_complete(
68+
self, ctx: click.Context, param: click.Parameter, incomplete: str
69+
) -> list[click.shell_completion.CompletionItem]:
6470
"""Return possible completions based on an incomplete value.
6571
6672
:returns: list of tuples of valid entry points (matching incomplete) and a description
@@ -71,13 +77,13 @@ def shell_complete(self, ctx, param, incomplete):
7177
]
7278

7379
@decorators.with_dbenv()
74-
def convert(self, value, param, ctx):
80+
def convert(self, value: t.Any, param: click.Parameter, ctx: click.Context) -> t.Any:
7581
try:
7682
group = super().convert(value, param, ctx)
7783
except click.BadParameter:
7884
if self._create_if_not_exist:
7985
# The particular subclass to load will be stored in `_sub_classes` as loaded by `convert` of the super.
80-
cls = self._sub_classes[0]
86+
cls = self._sub_classes[0] # type: ignore[index]
8187
group = cls(label=value).store()
8288
else:
8389
raise

src/aiida/cmdline/params/types/identifier.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,15 @@ def _entry_points(self) -> list[EntryPoint]:
7676

7777
@property
7878
@abstractmethod
79-
@with_dbenv() # type: ignore[misc]
79+
@with_dbenv()
8080
def orm_class_loader(self) -> OrmEntityLoader:
8181
"""Return the orm entity loader class, which should be a subclass of OrmEntityLoader. This class is supposed
8282
to be used to load the entity for a given identifier
8383
8484
:return: the orm entity loader class for this ParamType
8585
"""
8686

87-
@with_dbenv() # type: ignore[misc]
87+
@with_dbenv()
8888
def convert(self, value: t.Any, param: click.Parameter | None, ctx: click.Context) -> t.Any:
8989
"""Attempt to convert the given value to an instance of the orm class using the orm class loader.
9090

0 commit comments

Comments
 (0)