Skip to content

Commit f4d39de

Browse files
authored
Added typehints and mypy test env in tox (#120)
* Changed help_internal function to return None and the way 'handle_internal_commands' checks whether its an internal command or not, to make every callback function for every internal command takes no arguments and return None * Added type hints, and made appropriate changes to all the files to satisfy mypy Using isinstance check with (Sequence, Generator, Iterator) rather than with (Mapping, Iterable) for aliases of an internal command: click_repl/utils.py * Added test env for mypy in tox Updated testing dependencies to include flake8 and mypy: setup.cfg
1 parent f08ba39 commit f4d39de

File tree

7 files changed

+164
-124
lines changed

7 files changed

+164
-124
lines changed

click_repl/_completer.py

Lines changed: 71 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
from __future__ import unicode_literals
1+
from __future__ import annotations
22

33
import os
4+
import typing as t
45
from glob import iglob
6+
from typing import Generator
57

68
import click
7-
from prompt_toolkit.completion import Completion, Completer
9+
from prompt_toolkit.completion import CompleteEvent, Completer, Completion
10+
from prompt_toolkit.document import Document
811

912
from .utils import _resolve_context, split_arg_string
1013

@@ -26,30 +29,32 @@
2629
AUTO_COMPLETION_PARAM = "autocompletion"
2730

2831

29-
def text_type(text):
30-
return "{}".format(text)
31-
32-
3332
class ClickCompleter(Completer):
3433
__slots__ = ("cli", "ctx", "parsed_args", "parsed_ctx", "ctx_command")
3534

36-
def __init__(self, cli, ctx, show_only_unused=False, shortest_only=False):
35+
def __init__(
36+
self,
37+
cli: click.MultiCommand,
38+
ctx: click.Context,
39+
show_only_unused: bool = False,
40+
shortest_only: bool = False,
41+
) -> None:
3742
self.cli = cli
3843
self.ctx = ctx
39-
self.parsed_args = []
44+
self.parsed_args: list[str] = []
4045
self.parsed_ctx = ctx
4146
self.ctx_command = ctx.command
4247
self.show_only_unused = show_only_unused
4348
self.shortest_only = shortest_only
4449

4550
def _get_completion_from_autocompletion_functions(
4651
self,
47-
param,
48-
autocomplete_ctx,
49-
args,
50-
incomplete,
51-
):
52-
param_choices = []
52+
param: click.Parameter,
53+
autocomplete_ctx: click.Context,
54+
args: list[str],
55+
incomplete: str,
56+
) -> list[Completion]:
57+
param_choices: list[Completion] = []
5358

5459
if HAS_CLICK_V8:
5560
autocompletions = param.shell_complete(autocomplete_ctx, incomplete)
@@ -62,7 +67,7 @@ def _get_completion_from_autocompletion_functions(
6267
if isinstance(autocomplete, tuple):
6368
param_choices.append(
6469
Completion(
65-
text_type(autocomplete[0]),
70+
str(autocomplete[0]),
6671
-len(incomplete),
6772
display_meta=autocomplete[1],
6873
)
@@ -71,46 +76,48 @@ def _get_completion_from_autocompletion_functions(
7176
elif HAS_CLICK_V8 and isinstance(
7277
autocomplete, click.shell_completion.CompletionItem
7378
):
74-
param_choices.append(
75-
Completion(text_type(autocomplete.value), -len(incomplete))
76-
)
79+
param_choices.append(Completion(autocomplete.value, -len(incomplete)))
7780

7881
else:
79-
param_choices.append(
80-
Completion(text_type(autocomplete), -len(incomplete))
81-
)
82+
param_choices.append(Completion(str(autocomplete), -len(incomplete)))
8283

8384
return param_choices
8485

85-
def _get_completion_from_choices_click_le_7(self, param, incomplete):
86+
def _get_completion_from_choices_click_le_7(
87+
self, param: click.Parameter, incomplete: str
88+
) -> list[Completion]:
89+
param_type = t.cast(click.Choice, param.type)
90+
8691
if not getattr(param.type, "case_sensitive", True):
8792
incomplete = incomplete.lower()
8893
return [
8994
Completion(
90-
text_type(choice),
95+
choice,
9196
-len(incomplete),
92-
display=text_type(repr(choice) if " " in choice else choice),
97+
display=repr(choice) if " " in choice else choice,
9398
)
94-
for choice in param.type.choices # type: ignore[attr-defined]
99+
for choice in param_type.choices # type: ignore[attr-defined]
95100
if choice.lower().startswith(incomplete)
96101
]
97102

98103
else:
99104
return [
100105
Completion(
101-
text_type(choice),
106+
choice,
102107
-len(incomplete),
103-
display=text_type(repr(choice) if " " in choice else choice),
108+
display=repr(choice) if " " in choice else choice,
104109
)
105-
for choice in param.type.choices # type: ignore[attr-defined]
110+
for choice in param_type.choices # type: ignore[attr-defined]
106111
if choice.startswith(incomplete)
107112
]
108113

109-
def _get_completion_for_Path_types(self, param, args, incomplete):
114+
def _get_completion_for_Path_types(
115+
self, param: click.Parameter, args: list[str], incomplete: str
116+
) -> list[Completion]:
110117
if "*" in incomplete:
111118
return []
112119

113-
choices = []
120+
choices: list[Completion] = []
114121
_incomplete = os.path.expandvars(incomplete)
115122
search_pattern = _incomplete.strip("'\"\t\n\r\v ").replace("\\\\", "\\") + "*"
116123
quote = ""
@@ -134,29 +141,36 @@ def _get_completion_for_Path_types(self, param, args, incomplete):
134141

135142
choices.append(
136143
Completion(
137-
text_type(path),
144+
path,
138145
-len(incomplete),
139-
display=text_type(os.path.basename(path.strip("'\""))),
146+
display=os.path.basename(path.strip("'\"")),
140147
)
141148
)
142149

143150
return choices
144151

145-
def _get_completion_for_Boolean_type(self, param, incomplete):
152+
def _get_completion_for_Boolean_type(
153+
self, param: click.Parameter, incomplete: str
154+
) -> list[Completion]:
155+
boolean_mapping: dict[str, tuple[str, ...]] = {
156+
"true": ("1", "true", "t", "yes", "y", "on"),
157+
"false": ("0", "false", "f", "no", "n", "off"),
158+
}
159+
146160
return [
147-
Completion(
148-
text_type(k), -len(incomplete), display_meta=text_type("/".join(v))
149-
)
150-
for k, v in {
151-
"true": ("1", "true", "t", "yes", "y", "on"),
152-
"false": ("0", "false", "f", "no", "n", "off"),
153-
}.items()
161+
Completion(k, -len(incomplete), display_meta="/".join(v))
162+
for k, v in boolean_mapping.items()
154163
if any(i.startswith(incomplete) for i in v)
155164
]
156165

157-
def _get_completion_from_params(self, autocomplete_ctx, args, param, incomplete):
158-
159-
choices = []
166+
def _get_completion_from_params(
167+
self,
168+
autocomplete_ctx: click.Context,
169+
args: list[str],
170+
param: click.Parameter,
171+
incomplete: str,
172+
) -> list[Completion]:
173+
choices: list[Completion] = []
160174
param_type = param.type
161175

162176
# shell_complete method for click.Choice is intorduced in click-v8
@@ -185,12 +199,12 @@ def _get_completion_from_params(self, autocomplete_ctx, args, param, incomplete)
185199

186200
def _get_completion_for_cmd_args(
187201
self,
188-
ctx_command,
189-
incomplete,
190-
autocomplete_ctx,
191-
args,
192-
):
193-
choices = []
202+
ctx_command: click.Command,
203+
incomplete: str,
204+
autocomplete_ctx: click.Context,
205+
args: list[str],
206+
) -> list[Completion]:
207+
choices: list[Completion] = []
194208
param_called = False
195209

196210
for param in ctx_command.params:
@@ -229,9 +243,9 @@ def _get_completion_for_cmd_args(
229243
elif option.startswith(incomplete) and not hide:
230244
choices.append(
231245
Completion(
232-
text_type(option),
246+
option,
233247
-len(incomplete),
234-
display_meta=text_type(param.help or ""),
248+
display_meta=param.help or "",
235249
)
236250
)
237251

@@ -250,12 +264,14 @@ def _get_completion_for_cmd_args(
250264

251265
return choices
252266

253-
def get_completions(self, document, complete_event=None):
267+
def get_completions(
268+
self, document: Document, complete_event: CompleteEvent | None = None
269+
) -> Generator[Completion, None, None]:
254270
# Code analogous to click._bashcomplete.do_complete
255271

256272
args = split_arg_string(document.text_before_cursor, posix=False)
257273

258-
choices = []
274+
choices: list[Completion] = []
259275
cursor_within_command = (
260276
document.text_before_cursor.rstrip() == document.text_before_cursor
261277
)
@@ -277,7 +293,7 @@ def get_completions(self, document, complete_event=None):
277293
try:
278294
self.parsed_ctx = _resolve_context(args, self.ctx)
279295
except Exception:
280-
return [] # autocompletion for nonexistent cmd can throw here
296+
return # autocompletion for nonexistent cmd can throw here
281297
self.ctx_command = self.parsed_ctx.command
282298

283299
if getattr(self.ctx_command, "hidden", False):
@@ -301,7 +317,7 @@ def get_completions(self, document, complete_event=None):
301317
elif name.lower().startswith(incomplete_lower):
302318
choices.append(
303319
Completion(
304-
text_type(name),
320+
name,
305321
-len(incomplete),
306322
display_meta=getattr(command, "short_help", ""),
307323
)
@@ -310,10 +326,5 @@ def get_completions(self, document, complete_event=None):
310326
except Exception as e:
311327
click.echo("{}: {}".format(type(e).__name__, str(e)))
312328

313-
# If we are inside a parameter that was called, we want to show only
314-
# relevant choices
315-
# if param_called:
316-
# choices = param_choices
317-
318329
for item in choices:
319330
yield item

click_repl/_repl.py

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,30 @@
1-
from __future__ import with_statement
1+
from __future__ import annotations
22

3-
import click
43
import sys
4+
from typing import Any, MutableMapping, cast
5+
6+
import click
57
from prompt_toolkit.history import InMemoryHistory
68

79
from ._completer import ClickCompleter
10+
from .core import ReplContext
811
from .exceptions import ClickExit # type: ignore[attr-defined]
912
from .exceptions import CommandLineParserError, ExitReplException, InvalidGroupFormat
10-
from .utils import _execute_internal_and_sys_cmds
11-
from .core import ReplContext
1213
from .globals_ import ISATTY, get_current_repl_ctx
13-
14+
from .utils import _execute_internal_and_sys_cmds
1415

1516
__all__ = ["bootstrap_prompt", "register_repl", "repl"]
1617

1718

1819
def bootstrap_prompt(
19-
group,
20-
prompt_kwargs,
21-
ctx=None,
22-
):
20+
group: click.MultiCommand,
21+
prompt_kwargs: dict[str, Any],
22+
ctx: click.Context,
23+
) -> dict[str, Any]:
2324
"""
2425
Bootstrap prompt_toolkit kwargs or use user defined values.
2526
26-
:param group: click Group
27+
:param group: click.MultiCommand object
2728
:param prompt_kwargs: The user specified prompt kwargs.
2829
"""
2930

@@ -38,8 +39,11 @@ def bootstrap_prompt(
3839

3940

4041
def repl(
41-
old_ctx, prompt_kwargs={}, allow_system_commands=True, allow_internal_commands=True
42-
):
42+
old_ctx: click.Context,
43+
prompt_kwargs: dict[str, Any] = {},
44+
allow_system_commands: bool = True,
45+
allow_internal_commands: bool = True,
46+
) -> None:
4347
"""
4448
Start an interactive shell. All subcommands are available in it.
4549
@@ -54,10 +58,12 @@ def repl(
5458
group_ctx = old_ctx
5559
# Switching to the parent context that has a Group as its command
5660
# as a Group acts as a CLI for all of its subcommands
57-
if old_ctx.parent is not None and not isinstance(old_ctx.command, click.Group):
61+
if old_ctx.parent is not None and not isinstance(
62+
old_ctx.command, click.MultiCommand
63+
):
5864
group_ctx = old_ctx.parent
5965

60-
group = group_ctx.command
66+
group = cast(click.MultiCommand, group_ctx.command)
6167

6268
# An Optional click.Argument in the CLI Group, that has no value
6369
# will consume the first word from the REPL input, causing issues in
@@ -66,7 +72,7 @@ def repl(
6672
for param in group.params:
6773
if (
6874
isinstance(param, click.Argument)
69-
and group_ctx.params[param.name] is None
75+
and group_ctx.params[param.name] is None # type: ignore[index]
7076
and not param.required
7177
):
7278
raise InvalidGroupFormat(
@@ -78,16 +84,20 @@ def repl(
7884
# nesting REPLs (note: pass `None` to `pop` as we don't want to error if
7985
# REPL command already not present for some reason).
8086
repl_command_name = old_ctx.command.name
81-
if isinstance(group_ctx.command, click.CommandCollection):
87+
88+
available_commands: MutableMapping[str, click.Command] = {}
89+
90+
if isinstance(group, click.CommandCollection):
8291
available_commands = {
83-
cmd_name: cmd_obj
84-
for source in group_ctx.command.sources
85-
for cmd_name, cmd_obj in source.commands.items()
92+
cmd_name: source.get_command(group_ctx, cmd_name) # type: ignore[misc]
93+
for source in group.sources
94+
for cmd_name in source.list_commands(group_ctx)
8695
}
87-
else:
88-
available_commands = group_ctx.command.commands
8996

90-
original_command = available_commands.pop(repl_command_name, None)
97+
elif isinstance(group, click.Group):
98+
available_commands = group.commands
99+
100+
original_command = available_commands.pop(repl_command_name, None) # type: ignore
91101

92102
repl_ctx = ReplContext(
93103
group_ctx,
@@ -152,9 +162,9 @@ def get_command() -> str:
152162
break
153163

154164
if original_command is not None:
155-
available_commands[repl_command_name] = original_command
165+
available_commands[repl_command_name] = original_command # type: ignore[index]
156166

157167

158-
def register_repl(group, name="repl"):
168+
def register_repl(group: click.Group, name="repl") -> None:
159169
"""Register :func:`repl()` as sub-command *name* of *group*."""
160170
group.command(name=name)(click.pass_context(repl))

0 commit comments

Comments
 (0)