Skip to content

Commit e4a4e45

Browse files
committed
More mypy fixes
1 parent 5b9e999 commit e4a4e45

File tree

5 files changed

+81
-51
lines changed

5 files changed

+81
-51
lines changed

cmd2/argparse_completer.py

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
deque,
1414
)
1515
from typing import (
16+
Any,
17+
cast,
1618
Dict,
1719
List,
1820
Optional,
@@ -106,8 +108,8 @@ class _ArgumentState:
106108

107109
def __init__(self, arg_action: argparse.Action) -> None:
108110
self.action = arg_action
109-
self.min = None
110-
self.max = None
111+
self.min: Union[int, str]
112+
self.max: Union[float, int, str]
111113
self.count = 0
112114
self.is_remainder = self.action.nargs == argparse.REMAINDER
113115

@@ -144,7 +146,7 @@ def __init__(self, flag_arg_state: _ArgumentState) -> None:
144146
"""
145147
error = "Error: argument {}: {} ({} entered)".format(
146148
argparse._get_action_name(flag_arg_state.action),
147-
generate_range_error(flag_arg_state.min, flag_arg_state.max),
149+
generate_range_error(cast(int, flag_arg_state.min), cast(Union[int, float], flag_arg_state.max)),
148150
flag_arg_state.count,
149151
)
150152
super().__init__(error)
@@ -234,19 +236,19 @@ def complete(
234236
skip_remaining_flags = False
235237

236238
# _ArgumentState of the current positional
237-
pos_arg_state = None
239+
pos_arg_state: Optional[_ArgumentState] = None
238240

239241
# _ArgumentState of the current flag
240-
flag_arg_state = None
242+
flag_arg_state: Optional[_ArgumentState] = None
241243

242244
# Non-reusable flags that we've parsed
243-
matched_flags = []
245+
matched_flags: List[str] = []
244246

245247
# Keeps track of arguments we've seen and any tokens they consumed
246-
consumed_arg_values = dict() # dict(arg_name -> List[tokens])
248+
consumed_arg_values: Dict[str, List[str]] = dict() # dict(arg_name -> List[tokens])
247249

248250
# Completed mutually exclusive groups
249-
completed_mutex_groups = dict() # dict(argparse._MutuallyExclusiveGroup -> Action which completed group)
251+
completed_mutex_groups: Dict[argparse._MutuallyExclusiveGroup, argparse.Action] = dict()
250252

251253
def consume_argument(arg_state: _ArgumentState) -> None:
252254
"""Consuming token as an argument"""
@@ -315,7 +317,9 @@ def update_mutex_groups(arg_action: argparse.Action) -> None:
315317
# Handle '--' which tells argparse all remaining arguments are non-flags
316318
elif token == '--' and not skip_remaining_flags:
317319
# Check if there is an unfinished flag
318-
if flag_arg_state is not None and flag_arg_state.count < flag_arg_state.min:
320+
if flag_arg_state is not None \
321+
and isinstance(flag_arg_state.min, int) \
322+
and flag_arg_state.count < flag_arg_state.min:
319323
raise _UnfinishedFlagError(flag_arg_state)
320324

321325
# Otherwise end the current flag
@@ -328,7 +332,9 @@ def update_mutex_groups(arg_action: argparse.Action) -> None:
328332
if _looks_like_flag(token, self._parser) and not skip_remaining_flags:
329333

330334
# Check if there is an unfinished flag
331-
if flag_arg_state is not None and flag_arg_state.count < flag_arg_state.min:
335+
if flag_arg_state is not None \
336+
and isinstance(flag_arg_state.min, int) \
337+
and flag_arg_state.count < flag_arg_state.min:
332338
raise _UnfinishedFlagError(flag_arg_state)
333339

334340
# Reset flag arg state but not positional tracking because flags can be
@@ -361,7 +367,7 @@ def update_mutex_groups(arg_action: argparse.Action) -> None:
361367
new_arg_state = _ArgumentState(action)
362368

363369
# Keep track of this flag if it can receive arguments
364-
if new_arg_state.max > 0:
370+
if new_arg_state.max > 0: # type: ignore[operator]
365371
flag_arg_state = new_arg_state
366372
skip_remaining_flags = flag_arg_state.is_remainder
367373

@@ -370,7 +376,7 @@ def update_mutex_groups(arg_action: argparse.Action) -> None:
370376
consume_argument(flag_arg_state)
371377

372378
# Check if we have finished with this flag
373-
if flag_arg_state.count >= flag_arg_state.max:
379+
if isinstance(flag_arg_state.max, (float, int)) and flag_arg_state.count >= flag_arg_state.max:
374380
flag_arg_state = None
375381

376382
# Otherwise treat as a positional argument
@@ -415,7 +421,7 @@ def update_mutex_groups(arg_action: argparse.Action) -> None:
415421
skip_remaining_flags = True
416422

417423
# Check if we have finished with this positional
418-
elif pos_arg_state.count >= pos_arg_state.max:
424+
elif isinstance(pos_arg_state.max, (float, int)) and pos_arg_state.count >= pos_arg_state.max:
419425
pos_arg_state = None
420426

421427
# Check if the next positional has nargs set to argparse.REMAINDER.
@@ -432,7 +438,9 @@ def update_mutex_groups(arg_action: argparse.Action) -> None:
432438
# the current argument. We will handle the completion of flags that start with only one prefix
433439
# character (-f) at the end.
434440
if _looks_like_flag(text, self._parser) and not skip_remaining_flags:
435-
if flag_arg_state is not None and flag_arg_state.count < flag_arg_state.min:
441+
if flag_arg_state is not None \
442+
and isinstance(flag_arg_state.min, int) \
443+
and flag_arg_state.count < flag_arg_state.min:
436444
raise _UnfinishedFlagError(flag_arg_state)
437445
return self._complete_flags(text, line, begidx, endidx, matched_flags)
438446

@@ -453,7 +461,7 @@ def update_mutex_groups(arg_action: argparse.Action) -> None:
453461

454462
# Otherwise, print a hint if the flag isn't finished or text isn't possibly the start of a flag
455463
elif (
456-
flag_arg_state.count < flag_arg_state.min
464+
(isinstance(flag_arg_state.min, int) and flag_arg_state.count < flag_arg_state.min)
457465
or not _single_prefix_char(text, self._parser)
458466
or skip_remaining_flags
459467
):
@@ -523,14 +531,15 @@ def _complete_flags(self, text: str, line: str, begidx: int, endidx: int, matche
523531

524532
return matches
525533

526-
def _format_completions(self, arg_state: _ArgumentState, completions: List[Union[str, CompletionItem]]) -> List[str]:
534+
def _format_completions(self, arg_state: _ArgumentState, completions: Union[List[str], List[CompletionItem]]) -> List[str]:
527535
# Check if the results are CompletionItems and that there aren't too many to display
528536
if 1 < len(completions) <= self._cmd2_app.max_completion_items and isinstance(completions[0], CompletionItem):
537+
completion_items = cast(List[CompletionItem], completions)
529538
four_spaces = 4 * ' '
530539

531540
# If the user has not already sorted the CompletionItems, then sort them before appending the descriptions
532541
if not self._cmd2_app.matches_sorted:
533-
completions.sort(key=self._cmd2_app.default_sort_key)
542+
completion_items.sort(key=self._cmd2_app.default_sort_key)
534543
self._cmd2_app.matches_sorted = True
535544

536545
# If a metavar was defined, use that instead of the dest field
@@ -556,7 +565,7 @@ def _format_completions(self, arg_state: _ArgumentState, completions: List[Union
556565
token_width = ansi.style_aware_wcswidth(destination)
557566
desc_width = ansi.widest_line(desc_header)
558567

559-
for item in completions:
568+
for item in completion_items:
560569
token_width = max(ansi.style_aware_wcswidth(item), token_width)
561570

562571
# Replace tabs with 4 spaces so we can calculate width
@@ -568,10 +577,10 @@ def _format_completions(self, arg_state: _ArgumentState, completions: List[Union
568577
cols.append(Column(desc_header, width=desc_width))
569578

570579
hint_table = SimpleTable(cols, divider_char=None)
571-
table_data = [[item, item.description] for item in completions]
580+
table_data = [[item, item.description] for item in completion_items]
572581
self._cmd2_app.formatted_completions = hint_table.generate_table(table_data, row_spacing=0)
573582

574-
return completions
583+
return cast(List[str], completions)
575584

576585
def complete_subcommand_help(self, text: str, line: str, begidx: int, endidx: int, tokens: List[str]) -> List[str]:
577586
"""
@@ -623,14 +632,15 @@ def _complete_arg(
623632
arg_state: _ArgumentState,
624633
consumed_arg_values: Dict[str, List[str]],
625634
*,
626-
cmd_set: Optional[CommandSet] = None
635+
cmd_set: Optional[CommandSet] = None,
627636
) -> List[str]:
628637
"""
629638
Tab completion routine for an argparse argument
630639
:return: list of completions
631640
:raises: CompletionError if the completer or choices function this calls raises one
632641
"""
633642
# Check if the arg provides choices to the user
643+
arg_choices: Union[List[str], ChoicesCallable]
634644
if arg_state.action.choices is not None:
635645
arg_choices = list(arg_state.action.choices)
636646
if not arg_choices:
@@ -645,11 +655,12 @@ def _complete_arg(
645655
for index, choice in enumerate(arg_choices):
646656
# Prevent converting anything that is already a str (i.e. CompletionItem)
647657
if not isinstance(choice, str):
648-
arg_choices[index] = str(choice)
658+
arg_choices[index] = str(choice) # type: ignore[unreachable]
649659
else:
650-
arg_choices = getattr(arg_state.action, ATTR_CHOICES_CALLABLE, None)
651-
if arg_choices is None:
660+
choices_attr = getattr(arg_state.action, ATTR_CHOICES_CALLABLE, None)
661+
if choices_attr is None:
652662
return []
663+
arg_choices = choices_attr
653664

654665
# If we are going to call a completer/choices function, then set up the common arguments
655666
args = []
@@ -681,20 +692,26 @@ def _complete_arg(
681692
# Check if the argument uses a specific tab completion function to provide its choices
682693
if isinstance(arg_choices, ChoicesCallable) and arg_choices.is_completer:
683694
args.extend([text, line, begidx, endidx])
684-
results = arg_choices.to_call(*args, **kwargs)
695+
results = arg_choices.to_call(*args, **kwargs) # type: ignore[arg-type]
685696

686697
# Otherwise use basic_complete on the choices
687698
else:
688699
# Check if the choices come from a function
689-
if isinstance(arg_choices, ChoicesCallable) and not arg_choices.is_completer:
690-
arg_choices = arg_choices.to_call(*args, **kwargs)
700+
completion_items: List[str]
701+
if isinstance(arg_choices, ChoicesCallable):
702+
if not arg_choices.is_completer:
703+
completion_items = arg_choices.to_call(*args, **kwargs) # type: ignore[arg-type]
704+
else:
705+
completion_items = []
706+
else:
707+
completion_items = arg_choices
691708

692709
# Filter out arguments we already used
693710
used_values = consumed_arg_values.get(arg_state.action.dest, [])
694-
arg_choices = [choice for choice in arg_choices if choice not in used_values]
711+
completion_items = [choice for choice in completion_items if choice not in used_values]
695712

696713
# Do tab completion on the choices
697-
results = self._cmd2_app.basic_complete(text, line, begidx, endidx, arg_choices)
714+
results = self._cmd2_app.basic_complete(text, line, begidx, endidx, completion_items)
698715

699716
if not results:
700717
# Reset the value for matches_sorted. This is because completion of flag names

cmd2/argparse_custom.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ def my_completer(self, text, line, begidx, endidx, arg_tokens)
214214
Sequence,
215215
Tuple,
216216
Type,
217-
Union,
217+
Union, runtime_checkable,
218218
)
219219

220220
from . import (
@@ -299,7 +299,8 @@ def __init__(self, value: object, desc: str = '', *args: Any) -> None:
299299
############################################################################################################
300300

301301

302-
class ChoicesProviderFunc(Protocol):
302+
@runtime_checkable
303+
class ChoicesProviderFuncBase(Protocol):
303304
"""
304305
Function that returns a list of choices in support of tab completion
305306
"""
@@ -308,16 +309,21 @@ def __call__(self) -> List[str]:
308309
... # pragma: no cover
309310

310311

312+
@runtime_checkable
311313
class ChoicesProviderFuncWithTokens(Protocol):
312314
"""
313315
Function that returns a list of choices in support of tab completion and accepts a dictionary of prior arguments.
314316
"""
315317

316-
def __call__(self, *, arg_tokens: Dict[str, List[str]]) -> List[str]:
318+
def __call__(self, *, arg_tokens: Dict[str, List[str]] = {}) -> List[str]:
317319
... # pragma: no cover
318320

319321

320-
class CompleterFunc(Protocol):
322+
ChoicesProviderFunc = Union[ChoicesProviderFuncBase, ChoicesProviderFuncWithTokens]
323+
324+
325+
@runtime_checkable
326+
class CompleterFuncBase(Protocol):
321327
"""
322328
Function to support tab completion with the provided state of the user prompt
323329
"""
@@ -332,6 +338,7 @@ def __call__(
332338
... # pragma: no cover
333339

334340

341+
@runtime_checkable
335342
class CompleterFuncWithTokens(Protocol):
336343
"""
337344
Function to support tab completion with the provided state of the user prompt and accepts a dictionary of prior
@@ -345,11 +352,13 @@ def __call__(
345352
begidx: int,
346353
endidx: int,
347354
*,
348-
arg_tokens: Dict[str, List[str]],
355+
arg_tokens: Dict[str, List[str]] = {},
349356
) -> List[str]:
350357
... # pragma: no cover
351358

352359

360+
CompleterFunc = Union[CompleterFuncBase, CompleterFuncWithTokens]
361+
353362
class ChoicesCallable:
354363
"""
355364
Enables using a callable as the choices provider for an argparse argument.
@@ -359,7 +368,7 @@ class ChoicesCallable:
359368
def __init__(
360369
self,
361370
is_completer: bool,
362-
to_call: Union[CompleterFunc, CompleterFuncWithTokens, ChoicesProviderFunc, ChoicesProviderFuncWithTokens],
371+
to_call: Union[CompleterFunc, ChoicesProviderFunc],
363372
) -> None:
364373
"""
365374
Initializer
@@ -368,6 +377,8 @@ def __init__(
368377
:param to_call: the callable object that will be called to provide choices for the argument
369378
"""
370379
self.is_completer = is_completer
380+
if not isinstance(to_call, (CompleterFuncBase, CompleterFuncWithTokens)):
381+
raise ValueError('With is_completer set to true, to_call must be either CompleterFunc, CompleterFuncWithTokens')
371382
self.to_call = to_call
372383

373384

@@ -394,15 +405,15 @@ def _set_choices_callable(action: argparse.Action, choices_callable: ChoicesCall
394405

395406
def set_choices_provider(
396407
action: argparse.Action,
397-
choices_provider: Union[ChoicesProviderFunc, ChoicesProviderFuncWithTokens],
408+
choices_provider: ChoicesProviderFunc,
398409
) -> None:
399410
"""Set choices_provider on an argparse action"""
400411
_set_choices_callable(action, ChoicesCallable(is_completer=False, to_call=choices_provider))
401412

402413

403414
def set_completer(
404415
action: argparse.Action,
405-
completer: Union[CompleterFunc, CompleterFuncWithTokens],
416+
completer: CompleterFunc,
406417
) -> None:
407418
"""Set completer on an argparse action"""
408419
_set_choices_callable(action, ChoicesCallable(is_completer=True, to_call=completer))
@@ -421,8 +432,8 @@ def _add_argument_wrapper(
421432
self: argparse._ActionsContainer,
422433
*args: Any,
423434
nargs: Union[int, str, Tuple[int], Tuple[int, int], Tuple[int, float], None] = None,
424-
choices_provider: Optional[Union[ChoicesProviderFunc, ChoicesProviderFuncWithTokens]] = None,
425-
completer: Optional[Union[CompleterFunc, CompleterFuncWithTokens]] = None,
435+
choices_provider: Optional[ChoicesProviderFunc] = None,
436+
completer: Optional[CompleterFunc] = None,
426437
suppress_tab_hint: bool = False,
427438
descriptive_header: Optional[str] = None,
428439
**kwargs: Any,

cmd2/cmd2.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@
7979
from .argparse_custom import (
8080
DEFAULT_ARGUMENT_PARSER,
8181
ChoicesProviderFunc,
82-
ChoicesProviderFuncWithTokens,
8382
CompleterFunc,
8483
CompleterFuncWithTokens,
8584
CompletionItem,
@@ -672,7 +671,7 @@ def _install_command_function(self, command: str, command_wrapper: Callable[...,
672671

673672
setattr(self, cmd_func_name, command_wrapper)
674673

675-
def _install_completer_function(self, cmd_name: str, cmd_completer: Union[CompleterFunc, CompleterFuncWithTokens]) -> None:
674+
def _install_completer_function(self, cmd_name: str, cmd_completer: CompleterFunc) -> None:
676675
completer_func_name = COMPLETER_FUNC_PREFIX + cmd_name
677676

678677
if hasattr(self, completer_func_name):
@@ -2809,8 +2808,8 @@ def read_input(
28092808
completion_mode: utils.CompletionMode = utils.CompletionMode.NONE,
28102809
preserve_quotes: bool = False,
28112810
choices: Optional[Iterable[Any]] = None,
2812-
choices_provider: Optional[Union[ChoicesProviderFunc, ChoicesProviderFuncWithTokens]] = None,
2813-
completer: Optional[Union[CompleterFunc, CompleterFuncWithTokens]] = None,
2811+
choices_provider: Optional[ChoicesProviderFunc] = None,
2812+
completer: Optional[CompleterFunc] = None,
28142813
parser: Optional[argparse.ArgumentParser] = None,
28152814
) -> str:
28162815
"""
@@ -2845,7 +2844,7 @@ def read_input(
28452844
:raises: any exceptions raised by input() and stdin.readline()
28462845
"""
28472846
readline_configured = False
2848-
saved_completer: Optional[Union[CompleterFunc, CompleterFuncWithTokens]] = None
2847+
saved_completer: Optional[CompleterFunc] = None
28492848
saved_history: Optional[List[str]] = None
28502849

28512850
def configure_readline() -> None:
@@ -5210,7 +5209,10 @@ def register_cmdfinalization_hook(
52105209
self._validate_cmdfinalization_callable(func)
52115210
self._cmdfinalization_hooks.append(func)
52125211

5213-
def _resolve_func_self(self, cmd_support_func: Callable[..., Any], cmd_self: Union[CommandSet, 'Cmd']) -> Optional[object]:
5212+
def _resolve_func_self(self,
5213+
cmd_support_func: Callable[..., Any],
5214+
cmd_self: Union[CommandSet, 'Cmd', None],
5215+
) -> Optional[object]:
52145216
"""
52155217
Attempt to resolve a candidate instance to pass as 'self' for an unbound class method that was
52165218
used when defining command's argparse object. Since we restrict registration to only a single CommandSet

0 commit comments

Comments
 (0)