Skip to content

Commit 1553bf8

Browse files
committed
Enable mypy warn_unused_ignores and remove stale type: ignore comments
The combined "type: ignore[code, ty:rule]" comments made mypy's legacy parser flag every ty code as unused, which is why warn_unused_ignores was disabled. With those split into separate mypy and ty pragmas it can be turned back on. Verified against Python 3.11-3.15 on linux, darwin, and win32 so that ignores needed only on some platforms or versions are kept: - Remove 18 ignores that are unused in every configuration, keeping the "ty: ignore" half where ty still needs it. - Narrow the AnsiDecoder.decode monkey patch ignore to [method-assign]. - Mark the NoConsoleScreenBufferError fallback ignore as [no-redef, unused-ignore] since the redefinition only exists on win32. - Guard Cmd2HelpFormatter._set_color() with a positive version check instead of an early return so mypy skips it on Python < 3.14 rather than reporting the rest of the method as unreachable. - Annotate Cmd._initial_termios_settings as list[Any] | None and add an explicit platform check to the termios restore code. Previously mypy inferred the attribute as always None on win32 and only avoided checking that POSIX-only code because it looked unreachable.
1 parent e9cff73 commit 1553bf8

5 files changed

Lines changed: 33 additions & 35 deletions

File tree

‎cmd2/annotated.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -989,7 +989,7 @@ def _unwrap_optional(tp: Any) -> tuple[Any, bool]:
989989
there alone, so this helper never validates union members itself.
990990
"""
991991
origin = get_origin(tp)
992-
if origin is Union or origin is types.UnionType: # type: ignore[comparison-overlap]
992+
if origin is Union or origin is types.UnionType:
993993
all_args = get_args(tp)
994994
non_none = [a for a in all_args if a is not type(None)]
995995
has_none = len(non_none) < len(all_args)
@@ -1014,14 +1014,14 @@ def _normalize_annotation(annotation: Any) -> _NormalizedAnnotation:
10141014
tp, unwrapped = _unwrap_optional(tp)
10151015
if unwrapped:
10161016
is_optional = True
1017-
if get_origin(tp) is Annotated: # type: ignore[comparison-overlap]
1017+
if get_origin(tp) is Annotated:
10181018
inner_tp = get_args(tp)[0]
10191019
inner_origin = get_origin(inner_tp)
1020-
inner_is_union = inner_origin is Union or inner_origin is types.UnionType # type: ignore[comparison-overlap]
1020+
inner_is_union = inner_origin is Union or inner_origin is types.UnionType
10211021
if not (inner_is_union and type(None) in get_args(inner_tp)):
10221022
raise TypeError("Annotated[T, meta] | None is ambiguous. Use Annotated[T | None, meta] instead.")
10231023

1024-
if get_origin(tp) is Annotated: # type: ignore[comparison-overlap]
1024+
if get_origin(tp) is Annotated:
10251025
args = get_args(tp)
10261026
tp = args[0]
10271027
for meta in args[1:]:

‎cmd2/cmd2.py‎

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -183,13 +183,13 @@
183183

184184
try:
185185
if sys.platform == "win32":
186-
from prompt_toolkit.output.win32 import NoConsoleScreenBufferError # type: ignore[attr-defined]
186+
from prompt_toolkit.output.win32 import NoConsoleScreenBufferError
187187
else:
188188
# Trigger the except block for non-Windows platforms
189189
raise ImportError # noqa: TRY301
190190
except ImportError:
191191

192-
class NoConsoleScreenBufferError(Exception): # type: ignore[no-redef]
192+
class NoConsoleScreenBufferError(Exception): # type: ignore[no-redef, unused-ignore]
193193
"""Dummy exception to use when prompt_toolkit.output.win32.NoConsoleScreenBufferError is not available."""
194194

195195
def __init__(self, msg: str = "") -> None:
@@ -617,7 +617,7 @@ def __init__(
617617

618618
# Store initial termios settings to restore after each command.
619619
# This is a faster way of accomplishing what "stty sane" does.
620-
self._initial_termios_settings = None
620+
self._initial_termios_settings: list[Any] | None = None
621621
if not sys.platform.startswith("win") and self.stdin.isatty():
622622
try:
623623
import io
@@ -1042,7 +1042,7 @@ def _build_parser(
10421042

10431043
# Verify the factory returned the required type
10441044
if not isinstance(parser, Cmd2ArgumentParser):
1045-
builder_name = getattr(parser_source, "__name__", str(parser_source)) # type: ignore[unreachable]
1045+
builder_name = getattr(parser_source, "__name__", str(parser_source))
10461046
raise TypeError(
10471047
f"'{builder_name}' must return a 'Cmd2ArgumentParser' (or subclass). Received: '{type(parser).__name__}'."
10481048
)
@@ -1982,8 +1982,8 @@ def ppaged(
19821982

19831983
# If the pager was killed (e.g. SIGKILL), the terminal might be in a bad state.
19841984
# Attempt to restore terminal settings and foreground process group.
1985-
if self._initial_termios_settings is not None and self.stdin.isatty(): # type: ignore[unreachable]
1986-
try: # type: ignore[unreachable]
1985+
if not sys.platform.startswith("win") and self._initial_termios_settings is not None and self.stdin.isatty():
1986+
try:
19871987
import signal
19881988
import termios
19891989

@@ -3109,8 +3109,8 @@ def onecmd_plus_hooks(
31093109

31103110
def _run_cmdfinalization_hooks(self, stop: bool, statement: Statement | None) -> bool:
31113111
"""Run the command finalization hooks."""
3112-
if self._initial_termios_settings is not None and self.stdin.isatty(): # type: ignore[unreachable]
3113-
import io # type: ignore[unreachable]
3112+
if not sys.platform.startswith("win") and self._initial_termios_settings is not None and self.stdin.isatty():
3113+
import io
31143114
import termios
31153115

31163116
# Before the next command runs, fix any terminal problems like those
@@ -3875,7 +3875,7 @@ def _build_alias_parser() -> Cmd2ArgumentParser:
38753875
"An alias is a command that enables replacement of a word by another string.",
38763876
)
38773877
alias_parser = argparse_utils.DEFAULT_ARGUMENT_PARSER(description=alias_description)
3878-
alias_parser.epilog = TextGroup( # type: ignore[assignment] # ty: ignore[invalid-assignment]
3878+
alias_parser.epilog = TextGroup( # ty: ignore[invalid-assignment]
38793879
"See Also",
38803880
"macro",
38813881
)
@@ -3907,7 +3907,7 @@ def _build_alias_create_parser(cls) -> Cmd2ArgumentParser:
39073907
"for the actual command the alias resolves to."
39083908
),
39093909
)
3910-
alias_create_parser.epilog = TextGroup("Notes", alias_create_notes) # type: ignore[assignment] # ty: ignore[invalid-assignment]
3910+
alias_create_parser.epilog = TextGroup("Notes", alias_create_notes) # ty: ignore[invalid-assignment]
39113911

39123912
# Add arguments
39133913
alias_create_parser.add_argument("name", help="name of this alias")
@@ -4089,7 +4089,7 @@ def _build_macro_parser() -> Cmd2ArgumentParser:
40894089
"A macro is similar to an alias, but it can contain argument placeholders.",
40904090
)
40914091
macro_parser = argparse_utils.DEFAULT_ARGUMENT_PARSER(description=macro_description)
4092-
macro_parser.epilog = TextGroup( # type: ignore[assignment] # ty: ignore[invalid-assignment]
4092+
macro_parser.epilog = TextGroup( # ty: ignore[invalid-assignment]
40934093
"See Also",
40944094
"alias",
40954095
)
@@ -4152,7 +4152,7 @@ def _build_macro_create_parser(cls) -> Cmd2ArgumentParser:
41524152
"This default behavior changes if custom completion for macro arguments has been implemented."
41534153
),
41544154
)
4155-
macro_create_parser.epilog = TextGroup("Notes", macro_create_notes) # type: ignore[assignment] # ty: ignore[invalid-assignment]
4155+
macro_create_parser.epilog = TextGroup("Notes", macro_create_notes) # ty: ignore[invalid-assignment]
41564156

41574157
# Add arguments
41584158
macro_create_parser.add_argument("name", help="name of this macro")
@@ -4648,7 +4648,7 @@ def do_shortcuts(self, _: argparse.Namespace) -> None:
46484648
@staticmethod
46494649
def _build__eof_parser() -> Cmd2ArgumentParser:
46504650
_eof_parser = argparse_utils.DEFAULT_ARGUMENT_PARSER(description="Called when Ctrl-D is pressed.")
4651-
_eof_parser.epilog = TextGroup( # type: ignore[assignment] # ty: ignore[invalid-assignment]
4651+
_eof_parser.epilog = TextGroup( # ty: ignore[invalid-assignment]
46524652
"Note",
46534653
"This command is for internal use and is not intended to be called from the command line.",
46544654
)
@@ -4982,7 +4982,7 @@ def _set_up_py_shell_env(self, interp: InteractiveConsole) -> _SavedCmd2Env:
49824982
cmd2_env.completer = readline.get_completer()
49834983

49844984
# Set the completer to use the interpreter's locals
4985-
readline.set_completer(rlcompleter.Completer(interp.locals).complete) # type: ignore[arg-type]
4985+
readline.set_completer(rlcompleter.Completer(interp.locals).complete)
49864986

49874987
# Use the correct binding based on whether LibEdit or Readline is being used
49884988
if "libedit" in (readline.__doc__ or ""):
@@ -5259,7 +5259,7 @@ def do_ipy(self, _: argparse.Namespace) -> bool | None: # pragma: no cover
52595259
)
52605260

52615261
# Start IPython
5262-
start_ipython(config=config, argv=[], user_ns=local_vars) # type: ignore[no-untyped-call]
5262+
start_ipython(config=config, argv=[], user_ns=local_vars)
52635263
self.poutput("Now exiting IPython shell...")
52645264

52655265
# The IPython application is a singleton and won't be recreated next time
@@ -5547,7 +5547,7 @@ def _persist_history(self) -> None:
55475547
def _build_edit_parser(cls) -> Cmd2ArgumentParser:
55485548
edit_description = "Run a text editor and optionally open a file with it."
55495549
edit_parser = argparse_utils.DEFAULT_ARGUMENT_PARSER(description=edit_description)
5550-
edit_parser.epilog = TextGroup( # type: ignore[assignment] # ty: ignore[invalid-assignment]
5550+
edit_parser.epilog = TextGroup( # ty: ignore[invalid-assignment]
55515551
"Note",
55525552
Text.assemble(
55535553
"To set a new editor, run: ",
@@ -5669,7 +5669,7 @@ def _build__relative_run_script_parser(cls) -> Cmd2ArgumentParser:
56695669
_relative_run_script_parser = cls._build_base_run_script_parser()
56705670

56715671
# Append to existing description
5672-
_relative_run_script_parser.description = Group( # type: ignore[assignment] # ty: ignore[invalid-assignment]
5672+
_relative_run_script_parser.description = Group( # ty: ignore[invalid-assignment]
56735673
cast(Group, _relative_run_script_parser.description),
56745674
"\n",
56755675
(
@@ -5678,7 +5678,7 @@ def _build__relative_run_script_parser(cls) -> Cmd2ArgumentParser:
56785678
),
56795679
)
56805680

5681-
_relative_run_script_parser.epilog = TextGroup( # type: ignore[assignment] # ty: ignore[invalid-assignment]
5681+
_relative_run_script_parser.epilog = TextGroup( # ty: ignore[invalid-assignment]
56825682
"Note",
56835683
"This command is intended to be used from within a text script.",
56845684
)

‎cmd2/decorators.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ def arg_decorator(func: ArgparseCommandFunc[CmdOrSetT]) -> RawCommandFunc[CmdOrS
311311
:return: Function that takes raw input and converts to an argparse Namespace to passed to the wrapped function.
312312
"""
313313

314-
@functools.wraps(func) # type: ignore[arg-type] # ty: ignore[invalid-argument-type]
314+
@functools.wraps(func) # ty: ignore[invalid-argument-type]
315315
def cmd_wrapper(*args: Any, **kwargs: Any) -> bool | None:
316316
"""Command function wrapper which translates command line into argparse Namespace and call actual command function.
317317
@@ -346,9 +346,9 @@ def cmd_wrapper(*args: Any, **kwargs: Any) -> bool | None:
346346
parsing_results: tuple[argparse.Namespace] | tuple[argparse.Namespace, list[str]]
347347
with arg_parser.output_to(cmd_app.stdout):
348348
if with_unknown_args:
349-
parsing_results = arg_parser.parse_known_args(command_arg_list, initial_namespace) # type: ignore[assignment] # ty: ignore[invalid-assignment]
349+
parsing_results = arg_parser.parse_known_args(command_arg_list, initial_namespace) # ty: ignore[invalid-assignment]
350350
else:
351-
parsing_results = (arg_parser.parse_args(command_arg_list, initial_namespace),) # type: ignore[assignment] # ty: ignore[invalid-assignment]
351+
parsing_results = (arg_parser.parse_args(command_arg_list, initial_namespace),) # ty: ignore[invalid-assignment]
352352
except SystemExit as exc:
353353
raise Cmd2ArgparseError from exc
354354

‎cmd2/rich_utils.py‎

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -174,14 +174,12 @@ def _set_color(self, color: bool, **kwargs: Any) -> None:
174174
to _set_color() which some versions of RichHelpFormatter don't support.
175175
"""
176176
# Argparse didn't add color support until 3.14
177-
if sys.version_info < (3, 14):
178-
return
179-
180-
try: # type: ignore[unreachable]
181-
super()._set_color(color, **kwargs)
182-
except TypeError:
183-
# Fallback for older versions of RichHelpFormatter that don't support keyword arguments
184-
super()._set_color(color)
177+
if sys.version_info >= (3, 14):
178+
try:
179+
super()._set_color(color, **kwargs)
180+
except TypeError:
181+
# Fallback for older versions of RichHelpFormatter that don't support keyword arguments
182+
super()._set_color(color)
185183

186184
def _build_nargs_range_str(self, nargs_range: tuple[int, int | float]) -> str:
187185
"""Build nargs range string for help text."""
@@ -710,4 +708,4 @@ def _decode_has_linebreak_bug() -> bool:
710708

711709
# Only apply the monkey patch if the bug is present
712710
if _decode_has_linebreak_bug():
713-
AnsiDecoder.decode = _AnsiDecoder_decode # type: ignore[assignment]
711+
AnsiDecoder.decode = _AnsiDecoder_decode # type: ignore[method-assign]

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ strict = true
9898
warn_redundant_casts = true
9999
warn_return_any = true
100100
warn_unreachable = true
101-
warn_unused_ignores = false
101+
warn_unused_ignores = true
102102

103103
[tool.pytest.ini_options]
104104
testpaths = ["tests"]

0 commit comments

Comments
 (0)