From e9cff73c5c0d681658566989d1e5dc4117f278d9 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Thu, 1 Oct 2026 23:39:39 -0400 Subject: [PATCH 1/2] Fix mypy 2.4 errors from combined mypy/ty ignore comments mypy 2.4 switched to its new Ruff-based native parser by default. That parser rejects third-party codes such as "ty:unresolved-attribute" inside "# type: ignore[...]" comments, reporting the whole comment as an invalid "type: ignore" [syntax] error and therefore no longer suppressing the original diagnostic either. Split each combined comment into separate mypy and ty pragmas: # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] The mypy pragma must come first for mypy to recognize it, and ty still needs its own rule code. This form is accepted by mypy's native and legacy parsers as well as by ty. Also bump the ruff pre-commit hook to v0.16.10. --- .pre-commit-config.yaml | 2 +- cmd2/annotated.py | 4 ++-- cmd2/argparse_completer.py | 12 ++++++------ cmd2/argparse_utils.py | 32 ++++++++++++++++---------------- cmd2/cmd2.py | 26 +++++++++++++------------- cmd2/decorators.py | 6 +++--- cmd2/rich_utils.py | 4 ++-- cmd2/utils.py | 2 +- 8 files changed, 44 insertions(+), 44 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 32e6c83ad..69e52d245 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.16.9" + rev: "v0.16.10" hooks: - id: ruff-format args: [--config=ruff.toml] diff --git a/cmd2/annotated.py b/cmd2/annotated.py index 2db4e7f21..ec58251c2 100644 --- a/cmd2/annotated.py +++ b/cmd2/annotated.py @@ -713,7 +713,7 @@ def _convert(value: str) -> enum.Enum: raise _invalid_choice(value, _value_map) _convert.__name__ = enum_class.__name__ - _convert._cmd2_enum_class = enum_class # type: ignore[attr-defined, ty:unresolved-attribute] + _convert._cmd2_enum_class = enum_class # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] return _convert @@ -1119,7 +1119,7 @@ def _convert(value: str) -> Any: _convert.__name__ = getattr(converter, "__name__", "preprocess") enum_class = getattr(converter, "_cmd2_enum_class", None) if enum_class is not None: - _convert._cmd2_enum_class = enum_class # type: ignore[attr-defined, ty:unresolved-attribute] + _convert._cmd2_enum_class = enum_class # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] return _convert diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index dc2a7d4a8..c6d9836bc 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -52,7 +52,7 @@ def _build_hint(parser: Cmd2ArgumentParser, arg_action: argparse.Action) -> str: """Build completion hint for a given argument.""" # Check if hinting is disabled for this argument - suppress_hint = arg_action.get_suppress_tab_hint() # type: ignore[attr-defined, ty:unresolved-attribute] + suppress_hint = arg_action.get_suppress_tab_hint() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] if suppress_hint or arg_action.help == argparse.SUPPRESS: return "" @@ -104,7 +104,7 @@ def __init__(self, arg_action: argparse.Action) -> None: self.is_remainder = self.action.nargs == argparse.REMAINDER # Check if nargs is a range - nargs_range: tuple[int, int | float] | None = self.action.get_nargs_range() # type: ignore[attr-defined, ty:unresolved-attribute] + nargs_range: tuple[int, int | float] | None = self.action.get_nargs_range() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] if nargs_range is not None: self.min = nargs_range[0] self.max = nargs_range[1] @@ -575,7 +575,7 @@ def _validate_table_data(arg_state: _ArgumentState, completions: Completions) -> :raises ValueError: if there is an error with the data. """ - table_columns = arg_state.action.get_table_columns() # type: ignore[attr-defined, ty:unresolved-attribute] + table_columns = arg_state.action.get_table_columns() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] has_table_data = any(item.table_data for item in completions) if table_columns is None: @@ -606,7 +606,7 @@ def _build_completion_table(self, arg_state: _ArgumentState, completions: Comple table_columns = cast( Sequence[str | Column] | None, - arg_state.action.get_table_columns(), # type: ignore[attr-defined, ty:unresolved-attribute] + arg_state.action.get_table_columns(), # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] ) # Skip table generation if results are outside thresholds or no columns are defined @@ -761,7 +761,7 @@ def _complete_arg( :raises CompletionError: if the completer or choices function this calls raises one """ # Check if the argument uses a completer - completer = arg_state.action.get_completer() # type: ignore[attr-defined, ty:unresolved-attribute] + completer = arg_state.action.get_completer() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] if completer is not None: args, kwargs = self._prepare_callable_params( completer, @@ -775,7 +775,7 @@ def _complete_arg( # Otherwise it uses a choices provider or choices list else: - choices_provider = arg_state.action.get_choices_provider() # type: ignore[attr-defined, ty:unresolved-attribute] + choices_provider = arg_state.action.get_choices_provider() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] if choices_provider is not None: args, kwargs = self._prepare_callable_params( choices_provider, diff --git a/cmd2/argparse_utils.py b/cmd2/argparse_utils.py index 3bad19c09..ec0b630c9 100644 --- a/cmd2/argparse_utils.py +++ b/cmd2/argparse_utils.py @@ -564,11 +564,11 @@ def _ActionsContainer_add_argument( # noqa: N802 new_arg = orig_actions_container_add_argument(self, *args, **kwargs) # Set the cmd2-specific attributes - new_arg.set_nargs_range(nargs_range) # type: ignore[attr-defined, ty:unresolved-attribute] - new_arg.set_choices_provider(choices_provider) # type: ignore[attr-defined, ty:unresolved-attribute] - new_arg.set_completer(completer) # type: ignore[attr-defined, ty:unresolved-attribute] - new_arg.set_suppress_tab_hint(suppress_tab_hint) # type: ignore[attr-defined, ty:unresolved-attribute] - new_arg.set_table_columns(table_columns) # type: ignore[attr-defined, ty:unresolved-attribute] + new_arg.set_nargs_range(nargs_range) # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] + new_arg.set_choices_provider(choices_provider) # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] + new_arg.set_completer(completer) # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] + new_arg.set_suppress_tab_hint(suppress_tab_hint) # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] + new_arg.set_table_columns(table_columns) # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] # Set other registered custom attributes for keyword, value in custom_attribs.items(): @@ -666,14 +666,14 @@ def _SubParsersAction_remove_all_parsers( # noqa: N802 # Get the next subcommand name. remove_parser() will remove # it and any associated aliases from _name_parser_map. name = next(iter(self._name_parser_map)) - record = self.remove_parser(name) # type: ignore[attr-defined, ty:unresolved-attribute] + record = self.remove_parser(name) # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] records.append(record) return records -argparse._SubParsersAction.remove_parser = _SubParsersAction_remove_parser # type: ignore[attr-defined, ty:unresolved-attribute] -argparse._SubParsersAction.remove_all_parsers = _SubParsersAction_remove_all_parsers # type: ignore[attr-defined, ty:unresolved-attribute] +argparse._SubParsersAction.remove_parser = _SubParsersAction_remove_parser # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] +argparse._SubParsersAction.remove_all_parsers = _SubParsersAction_remove_all_parsers # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] @dataclass @@ -747,8 +747,8 @@ def __init__( super().__init__( prog=prog, usage=usage, - description=description, # type: ignore[arg-type, ty:invalid-argument-type] - epilog=epilog, # type: ignore[arg-type, ty:invalid-argument-type] + description=description, # type: ignore[arg-type] # ty: ignore[invalid-argument-type] + epilog=epilog, # type: ignore[arg-type] # ty: ignore[invalid-argument-type] parents=parents, formatter_class=formatter_class, prefix_chars=prefix_chars, @@ -772,7 +772,7 @@ def __init__( self.description: HelpContent | None # type: ignore[assignment] self.epilog: HelpContent | None # type: ignore[assignment] - def print_usage(self, file: IO[str] | None = None) -> None: # type: ignore[override, ty:invalid-method-override] + def print_usage(self, file: IO[str] | None = None) -> None: # type: ignore[override] # ty: ignore[invalid-method-override] """Override to ensure the formatter is aware of the target file.""" if file is None: file = self._thread_locals.current_output_file @@ -780,7 +780,7 @@ def print_usage(self, file: IO[str] | None = None) -> None: # type: ignore[over with self.output_to(file): super().print_usage(file) - def print_help(self, file: IO[str] | None = None) -> None: # type: ignore[override, ty:invalid-method-override] + def print_help(self, file: IO[str] | None = None) -> None: # type: ignore[override] # ty: ignore[invalid-method-override] """Override to ensure the formatter is aware of the target file.""" if file is None: file = self._thread_locals.current_output_file @@ -984,7 +984,7 @@ def detach_subcommand(self, subcommand_path: Iterable[str], subcommand: str) -> try: record = cast( SubcommandRecord, - subparsers_action.remove_parser(subcommand), # type: ignore[attr-defined, ty:unresolved-attribute] + subparsers_action.remove_parser(subcommand), # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] ) except ValueError: raise ValueError(f"Subcommand '{subcommand}' does not exist for '{target_parser.prog}'") from None @@ -1006,7 +1006,7 @@ def detach_all_subcommands(self, subcommand_path: Iterable[str]) -> list[Subcomm records = cast( list[SubcommandRecord], - subparsers_action.remove_all_parsers(), # type: ignore[attr-defined, ty:unresolved-attribute] + subparsers_action.remove_all_parsers(), # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] ) # Update command for each detached subcommand for record in records: @@ -1046,7 +1046,7 @@ def format_help(self, *args: Any, **kwargs: Any) -> str: def _get_nargs_pattern(self, action: argparse.Action) -> str: """Override to support nargs ranges.""" - nargs_range = action.get_nargs_range() # type: ignore[attr-defined, ty:unresolved-attribute] + nargs_range = action.get_nargs_range() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] if nargs_range: range_max = "" if nargs_range[1] == constants.INFINITY else nargs_range[1] nargs_pattern = f"(-*A{{{nargs_range[0]},{range_max}}}-*)" @@ -1066,7 +1066,7 @@ def _match_argument(self, action: argparse.Action, arg_strings_pattern: str) -> # raise an exception if we weren't able to find a match if match is None: - nargs_range = action.get_nargs_range() # type: ignore[attr-defined, ty:unresolved-attribute] + nargs_range = action.get_nargs_range() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] if nargs_range is not None: raise ArgumentError(action, build_range_error(nargs_range[0], nargs_range[1])) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index fcd26b2e4..6d404b094 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -2610,11 +2610,11 @@ def _perform_completion( completer.complete, tokens=raw_tokens[1:] if spec.preserve_quotes else tokens[1:], cmd_set=cmd_set ) else: - completer_func = self.completedefault # type: ignore[assignment, ty:invalid-assignment] + completer_func = self.completedefault # type: ignore[assignment] # ty: ignore[invalid-assignment] # Not a recognized macro or command else: - completer_func = self.completedefault # type: ignore[assignment, ty:invalid-assignment] + completer_func = self.completedefault # type: ignore[assignment] # ty: ignore[invalid-assignment] # Otherwise we are completing the command token or performing custom completion else: @@ -3032,7 +3032,7 @@ def onecmd_plus_hooks( with self.sigint_protection: if py_bridge_call: # Start saving command's stdout at this point - self.stdout.pause_storage = False # type: ignore[attr-defined, ty:invalid-assignment] + self.stdout.pause_storage = False # type: ignore[attr-defined] # ty: ignore[invalid-assignment] redir_saved_state = self._redirect_output(statement) @@ -3071,7 +3071,7 @@ def onecmd_plus_hooks( if py_bridge_call: # Stop saving command's stdout before command finalization hooks run - self.stdout.pause_storage = True # type: ignore[attr-defined, ty:invalid-assignment] + self.stdout.pause_storage = True # type: ignore[attr-defined] # ty: ignore[invalid-assignment] except (SkipPostcommandHooks, EmptyStatement): # Don't do anything, but do allow command finalization hooks to run pass @@ -3875,7 +3875,7 @@ def _build_alias_parser() -> Cmd2ArgumentParser: "An alias is a command that enables replacement of a word by another string.", ) alias_parser = argparse_utils.DEFAULT_ARGUMENT_PARSER(description=alias_description) - alias_parser.epilog = TextGroup( # type: ignore[assignment, ty:invalid-assignment] + alias_parser.epilog = TextGroup( # type: ignore[assignment] # ty: ignore[invalid-assignment] "See Also", "macro", ) @@ -3907,7 +3907,7 @@ def _build_alias_create_parser(cls) -> Cmd2ArgumentParser: "for the actual command the alias resolves to." ), ) - alias_create_parser.epilog = TextGroup("Notes", alias_create_notes) # type: ignore[assignment, ty:invalid-assignment] + alias_create_parser.epilog = TextGroup("Notes", alias_create_notes) # type: ignore[assignment] # ty: ignore[invalid-assignment] # Add arguments alias_create_parser.add_argument("name", help="name of this alias") @@ -4089,7 +4089,7 @@ def _build_macro_parser() -> Cmd2ArgumentParser: "A macro is similar to an alias, but it can contain argument placeholders.", ) macro_parser = argparse_utils.DEFAULT_ARGUMENT_PARSER(description=macro_description) - macro_parser.epilog = TextGroup( # type: ignore[assignment, ty:invalid-assignment] + macro_parser.epilog = TextGroup( # type: ignore[assignment] # ty: ignore[invalid-assignment] "See Also", "alias", ) @@ -4152,7 +4152,7 @@ def _build_macro_create_parser(cls) -> Cmd2ArgumentParser: "This default behavior changes if custom completion for macro arguments has been implemented." ), ) - macro_create_parser.epilog = TextGroup("Notes", macro_create_notes) # type: ignore[assignment, ty:invalid-assignment] + macro_create_parser.epilog = TextGroup("Notes", macro_create_notes) # type: ignore[assignment] # ty: ignore[invalid-assignment] # Add arguments macro_create_parser.add_argument("name", help="name of this macro") @@ -4648,7 +4648,7 @@ def do_shortcuts(self, _: argparse.Namespace) -> None: @staticmethod def _build__eof_parser() -> Cmd2ArgumentParser: _eof_parser = argparse_utils.DEFAULT_ARGUMENT_PARSER(description="Called when Ctrl-D is pressed.") - _eof_parser.epilog = TextGroup( # type: ignore[assignment, ty:invalid-assignment] + _eof_parser.epilog = TextGroup( # type: ignore[assignment] # ty: ignore[invalid-assignment] "Note", "This command is for internal use and is not intended to be called from the command line.", ) @@ -5108,7 +5108,7 @@ def py_quit() -> None: # Check if we are running Python code if py_code_to_run: try: # noqa: SIM105 - interp.runcode(py_code_to_run) # type: ignore[arg-type, ty:invalid-argument-type] + interp.runcode(py_code_to_run) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] except BaseException: # noqa: BLE001, S110 # We don't care about any exception that happened in the Python code pass @@ -5547,7 +5547,7 @@ def _persist_history(self) -> None: def _build_edit_parser(cls) -> Cmd2ArgumentParser: edit_description = "Run a text editor and optionally open a file with it." edit_parser = argparse_utils.DEFAULT_ARGUMENT_PARSER(description=edit_description) - edit_parser.epilog = TextGroup( # type: ignore[assignment, ty:invalid-assignment] + edit_parser.epilog = TextGroup( # type: ignore[assignment] # ty: ignore[invalid-assignment] "Note", Text.assemble( "To set a new editor, run: ", @@ -5669,7 +5669,7 @@ def _build__relative_run_script_parser(cls) -> Cmd2ArgumentParser: _relative_run_script_parser = cls._build_base_run_script_parser() # Append to existing description - _relative_run_script_parser.description = Group( # type: ignore[assignment, ty:invalid-assignment] + _relative_run_script_parser.description = Group( # type: ignore[assignment] # ty: ignore[invalid-assignment] cast(Group, _relative_run_script_parser.description), "\n", ( @@ -5678,7 +5678,7 @@ def _build__relative_run_script_parser(cls) -> Cmd2ArgumentParser: ), ) - _relative_run_script_parser.epilog = TextGroup( # type: ignore[assignment, ty:invalid-assignment] + _relative_run_script_parser.epilog = TextGroup( # type: ignore[assignment] # ty: ignore[invalid-assignment] "Note", "This command is intended to be used from within a text script.", ) diff --git a/cmd2/decorators.py b/cmd2/decorators.py index 7bc56c90f..c62ab0e0f 100644 --- a/cmd2/decorators.py +++ b/cmd2/decorators.py @@ -311,7 +311,7 @@ def arg_decorator(func: ArgparseCommandFunc[CmdOrSetT]) -> RawCommandFunc[CmdOrS :return: Function that takes raw input and converts to an argparse Namespace to passed to the wrapped function. """ - @functools.wraps(func) # type: ignore[arg-type, ty:invalid-argument-type] + @functools.wraps(func) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def cmd_wrapper(*args: Any, **kwargs: Any) -> bool | None: """Command function wrapper which translates command line into argparse Namespace and call actual command function. @@ -346,9 +346,9 @@ def cmd_wrapper(*args: Any, **kwargs: Any) -> bool | None: parsing_results: tuple[argparse.Namespace] | tuple[argparse.Namespace, list[str]] with arg_parser.output_to(cmd_app.stdout): if with_unknown_args: - parsing_results = arg_parser.parse_known_args(command_arg_list, initial_namespace) # type: ignore[assignment, ty:invalid-assignment] + parsing_results = arg_parser.parse_known_args(command_arg_list, initial_namespace) # type: ignore[assignment] # ty: ignore[invalid-assignment] else: - parsing_results = (arg_parser.parse_args(command_arg_list, initial_namespace),) # type: ignore[assignment, ty:invalid-assignment] + parsing_results = (arg_parser.parse_args(command_arg_list, initial_namespace),) # type: ignore[assignment] # ty: ignore[invalid-assignment] except SystemExit as exc: raise Cmd2ArgparseError from exc diff --git a/cmd2/rich_utils.py b/cmd2/rich_utils.py index 011caf08e..355a485fd 100644 --- a/cmd2/rich_utils.py +++ b/cmd2/rich_utils.py @@ -202,7 +202,7 @@ def _format_args(self, action: argparse.Action, default_metavar: str) -> str: get_metavar = self._metavar_formatter(action, default_metavar) # Handle nargs specified as a range - nargs_range = action.get_nargs_range() # type: ignore[attr-defined, ty:unresolved-attribute] + nargs_range = action.get_nargs_range() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] if nargs_range is not None: arg_str = "%s" % get_metavar(1) # noqa: UP031 range_str = self._build_nargs_range_str(nargs_range) @@ -229,7 +229,7 @@ def _rich_metavar_parts( get_metavar = self._metavar_formatter(action, default_metavar) # Handle nargs specified as a range - nargs_range = action.get_nargs_range() # type: ignore[attr-defined, ty:unresolved-attribute] + nargs_range = action.get_nargs_range() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] if nargs_range is not None: yield "%s" % get_metavar(1), True # noqa: UP031 yield self._build_nargs_range_str(nargs_range), False diff --git a/cmd2/utils.py b/cmd2/utils.py index f88a46f20..ac0c16587 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -615,7 +615,7 @@ def _reader_thread_func(self, read_stdout: bool) -> None: # Run until process completes while self._proc.poll() is None: - available = read_stream.peek() # type: ignore[attr-defined, ty:unresolved-attribute] + available = read_stream.peek() # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] if available: read_stream.read(len(available)) self._write_bytes(write_stream, available) From 1553bf856fd045b99184ef907c8868834be6ddcd Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Thu, 1 Oct 2026 23:51:05 -0400 Subject: [PATCH 2/2] 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. --- cmd2/annotated.py | 8 ++++---- cmd2/cmd2.py | 36 ++++++++++++++++++------------------ cmd2/decorators.py | 6 +++--- cmd2/rich_utils.py | 16 +++++++--------- pyproject.toml | 2 +- 5 files changed, 33 insertions(+), 35 deletions(-) diff --git a/cmd2/annotated.py b/cmd2/annotated.py index ec58251c2..6dd52cb87 100644 --- a/cmd2/annotated.py +++ b/cmd2/annotated.py @@ -989,7 +989,7 @@ def _unwrap_optional(tp: Any) -> tuple[Any, bool]: there alone, so this helper never validates union members itself. """ origin = get_origin(tp) - if origin is Union or origin is types.UnionType: # type: ignore[comparison-overlap] + if origin is Union or origin is types.UnionType: all_args = get_args(tp) non_none = [a for a in all_args if a is not type(None)] has_none = len(non_none) < len(all_args) @@ -1014,14 +1014,14 @@ def _normalize_annotation(annotation: Any) -> _NormalizedAnnotation: tp, unwrapped = _unwrap_optional(tp) if unwrapped: is_optional = True - if get_origin(tp) is Annotated: # type: ignore[comparison-overlap] + if get_origin(tp) is Annotated: inner_tp = get_args(tp)[0] inner_origin = get_origin(inner_tp) - inner_is_union = inner_origin is Union or inner_origin is types.UnionType # type: ignore[comparison-overlap] + inner_is_union = inner_origin is Union or inner_origin is types.UnionType if not (inner_is_union and type(None) in get_args(inner_tp)): raise TypeError("Annotated[T, meta] | None is ambiguous. Use Annotated[T | None, meta] instead.") - if get_origin(tp) is Annotated: # type: ignore[comparison-overlap] + if get_origin(tp) is Annotated: args = get_args(tp) tp = args[0] for meta in args[1:]: diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 6d404b094..6a320f417 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -183,13 +183,13 @@ try: if sys.platform == "win32": - from prompt_toolkit.output.win32 import NoConsoleScreenBufferError # type: ignore[attr-defined] + from prompt_toolkit.output.win32 import NoConsoleScreenBufferError else: # Trigger the except block for non-Windows platforms raise ImportError # noqa: TRY301 except ImportError: - class NoConsoleScreenBufferError(Exception): # type: ignore[no-redef] + class NoConsoleScreenBufferError(Exception): # type: ignore[no-redef, unused-ignore] """Dummy exception to use when prompt_toolkit.output.win32.NoConsoleScreenBufferError is not available.""" def __init__(self, msg: str = "") -> None: @@ -617,7 +617,7 @@ def __init__( # Store initial termios settings to restore after each command. # This is a faster way of accomplishing what "stty sane" does. - self._initial_termios_settings = None + self._initial_termios_settings: list[Any] | None = None if not sys.platform.startswith("win") and self.stdin.isatty(): try: import io @@ -1042,7 +1042,7 @@ def _build_parser( # Verify the factory returned the required type if not isinstance(parser, Cmd2ArgumentParser): - builder_name = getattr(parser_source, "__name__", str(parser_source)) # type: ignore[unreachable] + builder_name = getattr(parser_source, "__name__", str(parser_source)) raise TypeError( f"'{builder_name}' must return a 'Cmd2ArgumentParser' (or subclass). Received: '{type(parser).__name__}'." ) @@ -1982,8 +1982,8 @@ def ppaged( # If the pager was killed (e.g. SIGKILL), the terminal might be in a bad state. # Attempt to restore terminal settings and foreground process group. - if self._initial_termios_settings is not None and self.stdin.isatty(): # type: ignore[unreachable] - try: # type: ignore[unreachable] + if not sys.platform.startswith("win") and self._initial_termios_settings is not None and self.stdin.isatty(): + try: import signal import termios @@ -3109,8 +3109,8 @@ def onecmd_plus_hooks( def _run_cmdfinalization_hooks(self, stop: bool, statement: Statement | None) -> bool: """Run the command finalization hooks.""" - if self._initial_termios_settings is not None and self.stdin.isatty(): # type: ignore[unreachable] - import io # type: ignore[unreachable] + if not sys.platform.startswith("win") and self._initial_termios_settings is not None and self.stdin.isatty(): + import io import termios # Before the next command runs, fix any terminal problems like those @@ -3875,7 +3875,7 @@ def _build_alias_parser() -> Cmd2ArgumentParser: "An alias is a command that enables replacement of a word by another string.", ) alias_parser = argparse_utils.DEFAULT_ARGUMENT_PARSER(description=alias_description) - alias_parser.epilog = TextGroup( # type: ignore[assignment] # ty: ignore[invalid-assignment] + alias_parser.epilog = TextGroup( # ty: ignore[invalid-assignment] "See Also", "macro", ) @@ -3907,7 +3907,7 @@ def _build_alias_create_parser(cls) -> Cmd2ArgumentParser: "for the actual command the alias resolves to." ), ) - alias_create_parser.epilog = TextGroup("Notes", alias_create_notes) # type: ignore[assignment] # ty: ignore[invalid-assignment] + alias_create_parser.epilog = TextGroup("Notes", alias_create_notes) # ty: ignore[invalid-assignment] # Add arguments alias_create_parser.add_argument("name", help="name of this alias") @@ -4089,7 +4089,7 @@ def _build_macro_parser() -> Cmd2ArgumentParser: "A macro is similar to an alias, but it can contain argument placeholders.", ) macro_parser = argparse_utils.DEFAULT_ARGUMENT_PARSER(description=macro_description) - macro_parser.epilog = TextGroup( # type: ignore[assignment] # ty: ignore[invalid-assignment] + macro_parser.epilog = TextGroup( # ty: ignore[invalid-assignment] "See Also", "alias", ) @@ -4152,7 +4152,7 @@ def _build_macro_create_parser(cls) -> Cmd2ArgumentParser: "This default behavior changes if custom completion for macro arguments has been implemented." ), ) - macro_create_parser.epilog = TextGroup("Notes", macro_create_notes) # type: ignore[assignment] # ty: ignore[invalid-assignment] + macro_create_parser.epilog = TextGroup("Notes", macro_create_notes) # ty: ignore[invalid-assignment] # Add arguments macro_create_parser.add_argument("name", help="name of this macro") @@ -4648,7 +4648,7 @@ def do_shortcuts(self, _: argparse.Namespace) -> None: @staticmethod def _build__eof_parser() -> Cmd2ArgumentParser: _eof_parser = argparse_utils.DEFAULT_ARGUMENT_PARSER(description="Called when Ctrl-D is pressed.") - _eof_parser.epilog = TextGroup( # type: ignore[assignment] # ty: ignore[invalid-assignment] + _eof_parser.epilog = TextGroup( # ty: ignore[invalid-assignment] "Note", "This command is for internal use and is not intended to be called from the command line.", ) @@ -4982,7 +4982,7 @@ def _set_up_py_shell_env(self, interp: InteractiveConsole) -> _SavedCmd2Env: cmd2_env.completer = readline.get_completer() # Set the completer to use the interpreter's locals - readline.set_completer(rlcompleter.Completer(interp.locals).complete) # type: ignore[arg-type] + readline.set_completer(rlcompleter.Completer(interp.locals).complete) # Use the correct binding based on whether LibEdit or Readline is being used if "libedit" in (readline.__doc__ or ""): @@ -5259,7 +5259,7 @@ def do_ipy(self, _: argparse.Namespace) -> bool | None: # pragma: no cover ) # Start IPython - start_ipython(config=config, argv=[], user_ns=local_vars) # type: ignore[no-untyped-call] + start_ipython(config=config, argv=[], user_ns=local_vars) self.poutput("Now exiting IPython shell...") # The IPython application is a singleton and won't be recreated next time @@ -5547,7 +5547,7 @@ def _persist_history(self) -> None: def _build_edit_parser(cls) -> Cmd2ArgumentParser: edit_description = "Run a text editor and optionally open a file with it." edit_parser = argparse_utils.DEFAULT_ARGUMENT_PARSER(description=edit_description) - edit_parser.epilog = TextGroup( # type: ignore[assignment] # ty: ignore[invalid-assignment] + edit_parser.epilog = TextGroup( # ty: ignore[invalid-assignment] "Note", Text.assemble( "To set a new editor, run: ", @@ -5669,7 +5669,7 @@ def _build__relative_run_script_parser(cls) -> Cmd2ArgumentParser: _relative_run_script_parser = cls._build_base_run_script_parser() # Append to existing description - _relative_run_script_parser.description = Group( # type: ignore[assignment] # ty: ignore[invalid-assignment] + _relative_run_script_parser.description = Group( # ty: ignore[invalid-assignment] cast(Group, _relative_run_script_parser.description), "\n", ( @@ -5678,7 +5678,7 @@ def _build__relative_run_script_parser(cls) -> Cmd2ArgumentParser: ), ) - _relative_run_script_parser.epilog = TextGroup( # type: ignore[assignment] # ty: ignore[invalid-assignment] + _relative_run_script_parser.epilog = TextGroup( # ty: ignore[invalid-assignment] "Note", "This command is intended to be used from within a text script.", ) diff --git a/cmd2/decorators.py b/cmd2/decorators.py index c62ab0e0f..b3167e499 100644 --- a/cmd2/decorators.py +++ b/cmd2/decorators.py @@ -311,7 +311,7 @@ def arg_decorator(func: ArgparseCommandFunc[CmdOrSetT]) -> RawCommandFunc[CmdOrS :return: Function that takes raw input and converts to an argparse Namespace to passed to the wrapped function. """ - @functools.wraps(func) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] + @functools.wraps(func) # ty: ignore[invalid-argument-type] def cmd_wrapper(*args: Any, **kwargs: Any) -> bool | None: """Command function wrapper which translates command line into argparse Namespace and call actual command function. @@ -346,9 +346,9 @@ def cmd_wrapper(*args: Any, **kwargs: Any) -> bool | None: parsing_results: tuple[argparse.Namespace] | tuple[argparse.Namespace, list[str]] with arg_parser.output_to(cmd_app.stdout): if with_unknown_args: - parsing_results = arg_parser.parse_known_args(command_arg_list, initial_namespace) # type: ignore[assignment] # ty: ignore[invalid-assignment] + parsing_results = arg_parser.parse_known_args(command_arg_list, initial_namespace) # ty: ignore[invalid-assignment] else: - parsing_results = (arg_parser.parse_args(command_arg_list, initial_namespace),) # type: ignore[assignment] # ty: ignore[invalid-assignment] + parsing_results = (arg_parser.parse_args(command_arg_list, initial_namespace),) # ty: ignore[invalid-assignment] except SystemExit as exc: raise Cmd2ArgparseError from exc diff --git a/cmd2/rich_utils.py b/cmd2/rich_utils.py index 355a485fd..8e5988cef 100644 --- a/cmd2/rich_utils.py +++ b/cmd2/rich_utils.py @@ -174,14 +174,12 @@ def _set_color(self, color: bool, **kwargs: Any) -> None: to _set_color() which some versions of RichHelpFormatter don't support. """ # Argparse didn't add color support until 3.14 - if sys.version_info < (3, 14): - return - - try: # type: ignore[unreachable] - super()._set_color(color, **kwargs) - except TypeError: - # Fallback for older versions of RichHelpFormatter that don't support keyword arguments - super()._set_color(color) + if sys.version_info >= (3, 14): + try: + super()._set_color(color, **kwargs) + except TypeError: + # Fallback for older versions of RichHelpFormatter that don't support keyword arguments + super()._set_color(color) def _build_nargs_range_str(self, nargs_range: tuple[int, int | float]) -> str: """Build nargs range string for help text.""" @@ -710,4 +708,4 @@ def _decode_has_linebreak_bug() -> bool: # Only apply the monkey patch if the bug is present if _decode_has_linebreak_bug(): - AnsiDecoder.decode = _AnsiDecoder_decode # type: ignore[assignment] + AnsiDecoder.decode = _AnsiDecoder_decode # type: ignore[method-assign] diff --git a/pyproject.toml b/pyproject.toml index ec98c9ef6..a3abb38e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,7 +98,7 @@ strict = true warn_redundant_casts = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = false +warn_unused_ignores = true [tool.pytest.ini_options] testpaths = ["tests"]