Repository: python-cmd2/cmd2
Pull request #1089 (2021-04-03 22:42:38)

Mypy continued
Zero errors on mypy 0.812.

codecov[bot]: # [Codecov](https://codecov.io/gh/python-cmd2/cmd2/pull/1089?src=pr&el=h1) Report
> Merging [#1089](https://codecov.io/gh/python-cmd2/cmd2/pull/1089?src=pr&el=desc) (dfa061f) into [master](https://codecov.io/gh/python-cmd2/cmd2/commit/5b9e99979e9b5c4b04d674bad3a4d156cec62c31?el=desc) (5b9e999) will **decrease** coverage by `98.02%`.
> The diff coverage is `0.00%`.

[![Impacted file tree graph](https://codecov.io/gh/python-cmd2/cmd2/pull/1089/graphs/tree.svg?width=650&height=150&src=pr&token=eks2PUwa6A)](https://codecov.io/gh/python-cmd2/cmd2/pull/1089?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master   #1089       +/-   ##
==========================================
- Coverage   98.34%   0.31%   -98.03%     
==========================================
  Files          22      22               
  Lines        4704    4722       +18     
==========================================
- Hits         4626      15     -4611     
- Misses         78    4707     +4629     
```


| [Impacted Files](https://codecov.io/gh/python-cmd2/cmd2/pull/1089?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [cmd2/argparse\_completer.py](https://codecov.io/gh/python-cmd2/cmd2/pull/1089/diff?src=pr&el=tree#diff-Y21kMi9hcmdwYXJzZV9jb21wbGV0ZXIucHk=) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [cmd2/argparse\_custom.py](https://codecov.io/gh/python-cmd2/cmd2/pull/1089/diff?src=pr&el=tree#diff-Y21kMi9hcmdwYXJzZV9jdXN0b20ucHk=) | `0.00% <0.00%> (-94.97%)` | :arrow_down: |
| [cmd2/cmd2.py](https://codecov.io/gh/python-cmd2/cmd2/pull/1089/diff?src=pr&el=tree#diff-Y21kMi9jbWQyLnB5) | `0.00% <0.00%> (-98.11%)` | :arrow_down: |
| [cmd2/decorators.py](https://codecov.io/gh/python-cmd2/cmd2/pull/1089/diff?src=pr&el=tree#diff-Y21kMi9kZWNvcmF0b3JzLnB5) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [cmd2/history.py](https://codecov.io/gh/python-cmd2/cmd2/pull/1089/diff?src=pr&el=tree#diff-Y21kMi9oaXN0b3J5LnB5) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [cmd2/parsing.py](https://codecov.io/gh/python-cmd2/cmd2/pull/1089/diff?src=pr&el=tree#diff-Y21kMi9wYXJzaW5nLnB5) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [cmd2/py\_bridge.py](https://codecov.io/gh/python-cmd2/cmd2/pull/1089/diff?src=pr&el=tree#diff-Y21kMi9weV9icmlkZ2UucHk=) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [cmd2/table\_creator.py](https://codecov.io/gh/python-cmd2/cmd2/pull/1089/diff?src=pr&el=tree#diff-Y21kMi90YWJsZV9jcmVhdG9yLnB5) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [cmd2/transcript.py](https://codecov.io/gh/python-cmd2/cmd2/pull/1089/diff?src=pr&el=tree#diff-Y21kMi90cmFuc2NyaXB0LnB5) | `0.00% <0.00%> (-92.99%)` | :arrow_down: |
| [cmd2/utils.py](https://codecov.io/gh/python-cmd2/cmd2/pull/1089/diff?src=pr&el=tree#diff-Y21kMi91dGlscy5weQ==) | `0.00% <ø> (-97.93%)` | :arrow_down: |
| ... and [23 more](https://codecov.io/gh/python-cmd2/cmd2/pull/1089/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/python-cmd2/cmd2/pull/1089?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/python-cmd2/cmd2/pull/1089?src=pr&el=footer). Last update [5b9e999...dfa061f](https://codecov.io/gh/python-cmd2/cmd2/pull/1089?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml
new file mode 100644
index 00000000..aa5d4b2b
--- /dev/null
+++ b/.github/workflows/mypy.yml
@@ -0,0 +1,28 @@
+# For documentation on GitHub Actions Workflows, see:
+# https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
+name: MyPy
+
+on: [push, pull_request]
+
+jobs:
+  mypy:
+    strategy:
+      matrix:
+        os: [ubuntu-latest]
+        python-version: [3.9]
+      fail-fast: false
+    runs-on: ${{ matrix.os }}
+    steps:
+      - uses: actions/checkout@v2  # https://github.com/actions/checkout
+        with:
+          # Only a single commit is fetched by default, for the ref/SHA that triggered the workflow.
+          # Set fetch-depth: 0 to fetch all history for all branches and tags.
+          fetch-depth: 0 # Needed for setuptools_scm to work correctly
+      - name: Set up Python
+        uses: actions/setup-python@v2  # https://github.com/actions/setup-python
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: Install python prerequisites
+        run: pip install -U --user pip setuptools setuptools-scm mypy
+      - name: MyPy
+        run: python -m mypy cmd2
diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py
index ef35eabc..8a6fdea7 100644
--- a/cmd2/argparse_completer.py
+++ b/cmd2/argparse_completer.py
@@ -13,10 +13,12 @@ from collections import (
     deque,
 )
 from typing import (
+    Any,
     Dict,
     List,
     Optional,
     Union,
+    cast,
 )
 
 from . import (
@@ -30,6 +32,8 @@ from .argparse_custom import (
     ATTR_NARGS_RANGE,
     ATTR_SUPPRESS_TAB_HINT,
     ChoicesCallable,
+    ChoicesProviderFuncBase,
+    ChoicesProviderFuncWithTokens,
     CompletionItem,
     generate_range_error,
 )
@@ -106,8 +110,8 @@ class _ArgumentState:
 
     def __init__(self, arg_action: argparse.Action) -> None:
         self.action = arg_action
-        self.min = None
-        self.max = None
+        self.min: Union[int, str]
+        self.max: Union[float, int, str]
         self.count = 0
         self.is_remainder = self.action.nargs == argparse.REMAINDER
 
@@ -144,7 +148,7 @@ class _UnfinishedFlagError(CompletionError):
         """
         error = "Error: argument {}: {} ({} entered)".format(
             argparse._get_action_name(flag_arg_state.action),
-            generate_range_error(flag_arg_state.min, flag_arg_state.max),
+            generate_range_error(cast(int, flag_arg_state.min), cast(Union[int, float], flag_arg_state.max)),
             flag_arg_state.count,
         )
         super().__init__(error)
@@ -234,19 +238,19 @@ class ArgparseCompleter:
         skip_remaining_flags = False
 
         # _ArgumentState of the current positional
-        pos_arg_state = None
+        pos_arg_state: Optional[_ArgumentState] = None
 
         # _ArgumentState of the current flag
-        flag_arg_state = None
+        flag_arg_state: Optional[_ArgumentState] = None
 
         # Non-reusable flags that we've parsed
-        matched_flags = []
+        matched_flags: List[str] = []
 
         # Keeps track of arguments we've seen and any tokens they consumed
-        consumed_arg_values = dict()  # dict(arg_name -> List[tokens])
+        consumed_arg_values: Dict[str, List[str]] = dict()  # dict(arg_name -> List[tokens])
 
         # Completed mutually exclusive groups
-        completed_mutex_groups = dict()  # dict(argparse._MutuallyExclusiveGroup -> Action which completed group)
+        completed_mutex_groups: Dict[argparse._MutuallyExclusiveGroup, argparse.Action] = dict()
 
         def consume_argument(arg_state: _ArgumentState) -> None:
             """Consuming token as an argument"""
@@ -315,7 +319,11 @@ class ArgparseCompleter:
             # Handle '--' which tells argparse all remaining arguments are non-flags
             elif token == '--' and not skip_remaining_flags:
                 # Check if there is an unfinished flag
-                if flag_arg_state is not None and flag_arg_state.count < flag_arg_state.min:
+                if (
+                    flag_arg_state is not None
+                    and isinstance(flag_arg_state.min, int)
+                    and flag_arg_state.count < flag_arg_state.min
+                ):
                     raise _UnfinishedFlagError(flag_arg_state)
 
                 # Otherwise end the current flag
@@ -328,7 +336,11 @@ class ArgparseCompleter:
             if _looks_like_flag(token, self._parser) and not skip_remaining_flags:
 
                 # Check if there is an unfinished flag
-                if flag_arg_state is not None and flag_arg_state.count < flag_arg_state.min:
+                if (
+                    flag_arg_state is not None
+                    and isinstance(flag_arg_state.min, int)
+                    and flag_arg_state.count < flag_arg_state.min
+                ):
                     raise _UnfinishedFlagError(flag_arg_state)
 
                 # Reset flag arg state but not positional tracking because flags can be
@@ -361,7 +373,7 @@ class ArgparseCompleter:
                     new_arg_state = _ArgumentState(action)
 
                     # Keep track of this flag if it can receive arguments
-                    if new_arg_state.max > 0:
+                    if new_arg_state.max > 0:  # type: ignore[operator]
                         flag_arg_state = new_arg_state
                         skip_remaining_flags = flag_arg_state.is_remainder
 
@@ -370,7 +382,7 @@ class ArgparseCompleter:
                 consume_argument(flag_arg_state)
 
                 # Check if we have finished with this flag
-                if flag_arg_state.count >= flag_arg_state.max:
+                if isinstance(flag_arg_state.max, (float, int)) and flag_arg_state.count >= flag_arg_state.max:
                     flag_arg_state = None
 
             # Otherwise treat as a positional argument
@@ -415,7 +427,7 @@ class ArgparseCompleter:
                         skip_remaining_flags = True
 
                     # Check if we have finished with this positional
-                    elif pos_arg_state.count >= pos_arg_state.max:
+                    elif isinstance(pos_arg_state.max, (float, int)) and pos_arg_state.count >= pos_arg_state.max:
                         pos_arg_state = None
 
                         # Check if the next positional has nargs set to argparse.REMAINDER.
@@ -432,7 +444,11 @@ class ArgparseCompleter:
         # the current argument. We will handle the completion of flags that start with only one prefix
         # character (-f) at the end.
         if _looks_like_flag(text, self._parser) and not skip_remaining_flags:
-            if flag_arg_state is not None and flag_arg_state.count < flag_arg_state.min:
+            if (
+                flag_arg_state is not None
+                and isinstance(flag_arg_state.min, int)
+                and flag_arg_state.count < flag_arg_state.min
+            ):
                 raise _UnfinishedFlagError(flag_arg_state)
             return self._complete_flags(text, line, begidx, endidx, matched_flags)
 
@@ -453,7 +469,7 @@ class ArgparseCompleter:
 
             # Otherwise, print a hint if the flag isn't finished or text isn't possibly the start of a flag
             elif (
-                flag_arg_state.count < flag_arg_state.min
+                (isinstance(flag_arg_state.min, int) and flag_arg_state.count < flag_arg_state.min)
                 or not _single_prefix_char(text, self._parser)
                 or skip_remaining_flags
             ):
@@ -523,14 +539,15 @@ class ArgparseCompleter:
 
         return matches
 
-    def _format_completions(self, arg_state: _ArgumentState, completions: List[Union[str, CompletionItem]]) -> List[str]:
+    def _format_completions(self, arg_state: _ArgumentState, completions: Union[List[str], List[CompletionItem]]) -> List[str]:
         # Check if the results are CompletionItems and that there aren't too many to display
         if 1 < len(completions) <= self._cmd2_app.max_completion_items and isinstance(completions[0], CompletionItem):
+            completion_items = cast(List[CompletionItem], completions)
             four_spaces = 4 * ' '
 
             # If the user has not already sorted the CompletionItems, then sort them before appending the descriptions
             if not self._cmd2_app.matches_sorted:
-                completions.sort(key=self._cmd2_app.default_sort_key)
+                completion_items.sort(key=self._cmd2_app.default_sort_key)
                 self._cmd2_app.matches_sorted = True
 
             # If a metavar was defined, use that instead of the dest field
@@ -556,7 +573,7 @@ class ArgparseCompleter:
             token_width = ansi.style_aware_wcswidth(destination)
             desc_width = ansi.widest_line(desc_header)
 
-            for item in completions:
+            for item in completion_items:
                 token_width = max(ansi.style_aware_wcswidth(item), token_width)
 
                 # Replace tabs with 4 spaces so we can calculate width
@@ -568,10 +585,10 @@ class ArgparseCompleter:
             cols.append(Column(desc_header, width=desc_width))
 
             hint_table = SimpleTable(cols, divider_char=None)
-            table_data = [[item, item.description] for item in completions]
+            table_data = [[item, item.description] for item in completion_items]
             self._cmd2_app.formatted_completions = hint_table.generate_table(table_data, row_spacing=0)
 
-        return completions
+        return cast(List[str], completions)
 
     def complete_subcommand_help(self, text: str, line: str, begidx: int, endidx: int, tokens: List[str]) -> List[str]:
         """
@@ -623,7 +640,7 @@ class ArgparseCompleter:
         arg_state: _ArgumentState,
         consumed_arg_values: Dict[str, List[str]],
         *,
-        cmd_set: Optional[CommandSet] = None
+        cmd_set: Optional[CommandSet] = None,
     ) -> List[str]:
         """
         Tab completion routine for an argparse argument
@@ -631,6 +648,7 @@ class ArgparseCompleter:
         :raises: CompletionError if the completer or choices function this calls raises one
         """
         # Check if the arg provides choices to the user
+        arg_choices: Union[List[str], ChoicesCallable]
         if arg_state.action.choices is not None:
             arg_choices = list(arg_state.action.choices)
             if not arg_choices:
@@ -645,11 +663,12 @@ class ArgparseCompleter:
             for index, choice in enumerate(arg_choices):
                 # Prevent converting anything that is already a str (i.e. CompletionItem)
                 if not isinstance(choice, str):
-                    arg_choices[index] = str(choice)
+                    arg_choices[index] = str(choice)  # type: ignore[unreachable]
         else:
-            arg_choices = getattr(arg_state.action, ATTR_CHOICES_CALLABLE, None)
-            if arg_choices is None:
+            choices_attr = getattr(arg_state.action, ATTR_CHOICES_CALLABLE, None)
+            if choices_attr is None:
                 return []
+            arg_choices = choices_attr
 
         # If we are going to call a completer/choices function, then set up the common arguments
         args = []
@@ -681,20 +700,32 @@ class ArgparseCompleter:
         # Check if the argument uses a specific tab completion function to provide its choices
         if isinstance(arg_choices, ChoicesCallable) and arg_choices.is_completer:
             args.extend([text, line, begidx, endidx])
-            results = arg_choices.to_call(*args, **kwargs)
+            results = arg_choices.completer(*args, **kwargs)  # type: ignore[arg-type]
 
         # Otherwise use basic_complete on the choices
         else:
             # Check if the choices come from a function
-            if isinstance(arg_choices, ChoicesCallable) and not arg_choices.is_completer:
-                arg_choices = arg_choices.to_call(*args, **kwargs)
+            completion_items: List[str] = []
+            if isinstance(arg_choices, ChoicesCallable):
+                if not arg_choices.is_completer:
+                    choices_func = arg_choices.choices_provider
+                    if isinstance(choices_func, ChoicesProviderFuncWithTokens):
+                        completion_items = choices_func(*args, **kwargs)  # type: ignore[arg-type]
+                    else:  # pragma: no cover
+                        # This won't hit because runtime checking doesn't check function argument types and will always
+                        # resolve true above. Mypy, however, does see the difference and gives an error that can't be
+                        # ignored. Mypy issue #5485 discusses this problem
+                        completion_items = choices_func(*args)  # type: ignore[arg-type]
+                # else case is already covered above
+            else:
+                completion_items = arg_choices
 
             # Filter out arguments we already used
             used_values = consumed_arg_values.get(arg_state.action.dest, [])
-            arg_choices = [choice for choice in arg_choices if choice not in used_values]
+            completion_items = [choice for choice in completion_items if choice not in used_values]
 
             # Do tab completion on the choices
-            results = self._cmd2_app.basic_complete(text, line, begidx, endidx, arg_choices)
+            results = self._cmd2_app.basic_complete(text, line, begidx, endidx, completion_items)
 
         if not results:
             # Reset the value for matches_sorted. This is because completion of flag names
diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py
index 9ce601c4..6b1c799d 100644
--- a/cmd2/argparse_custom.py
+++ b/cmd2/argparse_custom.py
@@ -225,10 +225,12 @@ from . import (
 try:
     from typing import (
         Protocol,
+        runtime_checkable,
     )
 except ImportError:
     from typing_extensions import (  # type: ignore[misc]
         Protocol,
+        runtime_checkable,
     )
 
 ############################################################################################################
@@ -299,7 +301,8 @@ class CompletionItem(str):
 ############################################################################################################
 
 
-class ChoicesProviderFunc(Protocol):
+@runtime_checkable
+class ChoicesProviderFuncBase(Protocol):
     """
     Function that returns a list of choices in support of tab completion
     """
@@ -308,16 +311,21 @@ class ChoicesProviderFunc(Protocol):
         ...  # pragma: no cover
 
 
+@runtime_checkable
 class ChoicesProviderFuncWithTokens(Protocol):
     """
     Function that returns a list of choices in support of tab completion and accepts a dictionary of prior arguments.
     """
 
-    def __call__(self, *, arg_tokens: Dict[str, List[str]]) -> List[str]:
+    def __call__(self, *, arg_tokens: Dict[str, List[str]] = {}) -> List[str]:
         ...  # pragma: no cover
 
 
-class CompleterFunc(Protocol):
+ChoicesProviderFunc = Union[ChoicesProviderFuncBase, ChoicesProviderFuncWithTokens]
+
+
+@runtime_checkable
+class CompleterFuncBase(Protocol):
     """
     Function to support tab completion with the provided state of the user prompt
     """
@@ -332,6 +340,7 @@ class CompleterFunc(Protocol):
         ...  # pragma: no cover
 
 
+@runtime_checkable
 class CompleterFuncWithTokens(Protocol):
     """
     Function to support tab completion with the provided state of the user prompt and accepts a dictionary of prior
@@ -345,11 +354,14 @@ class CompleterFuncWithTokens(Protocol):
         begidx: int,
         endidx: int,
         *,
-        arg_tokens: Dict[str, List[str]],
+        arg_tokens: Dict[str, List[str]] = {},
     ) -> List[str]:
         ...  # pragma: no cover
 
 
+CompleterFunc = Union[CompleterFuncBase, CompleterFuncWithTokens]
+
+
 class ChoicesCallable:
     """
     Enables using a callable as the choices provider for an argparse argument.
@@ -359,7 +371,7 @@ class ChoicesCallable:
     def __init__(
         self,
         is_completer: bool,
-        to_call: Union[CompleterFunc, CompleterFuncWithTokens, ChoicesProviderFunc, ChoicesProviderFuncWithTokens],
+        to_call: Union[CompleterFunc, ChoicesProviderFunc],
     ) -> None:
         """
         Initializer
@@ -368,8 +380,35 @@ class ChoicesCallable:
         :param to_call: the callable object that will be called to provide choices for the argument
         """
         self.is_completer = is_completer
+        if is_completer:
+            if not isinstance(to_call, (CompleterFuncBase, CompleterFuncWithTokens)):  # pragma: no cover
+                # runtime checking of Protocols do not currently check the parameters of a function.
+                raise ValueError(
+                    'With is_completer set to true, to_call must be either CompleterFunc, CompleterFuncWithTokens'
+                )
+        else:
+            if not isinstance(to_call, (ChoicesProviderFuncBase, ChoicesProviderFuncWithTokens)):  # pragma: no cover
+                # runtime checking of Protocols do not currently check the parameters of a function.
+                raise ValueError(
+                    'With is_completer set to false, to_call must be either: '
+                    'ChoicesProviderFuncBase, ChoicesProviderFuncWithTokens'
+                )
         self.to_call = to_call
 
+    @property
+    def completer(self) -> CompleterFunc:
+        if not isinstance(self.to_call, (CompleterFuncBase, CompleterFuncWithTokens)):  # pragma: no cover
+            # this should've been caught in the constructor, just a backup check
+            raise ValueError('Function is not a CompleterFunc')
+        return self.to_call
+
+    @property
+    def choices_provider(self) -> ChoicesProviderFunc:
+        if not isinstance(self.to_call, (ChoicesProviderFuncBase, ChoicesProviderFuncWithTokens)):  # pragma: no cover
+            # this should've been caught in the constructor, just a backup check
+            raise ValueError('Function is not a ChoicesProviderFunc')
+        return self.to_call
+
 
 def _set_choices_callable(action: argparse.Action, choices_callable: ChoicesCallable) -> None:
     """
@@ -394,7 +433,7 @@ def _set_choices_callable(action: argparse.Action, choices_callable: ChoicesCall
 
 def set_choices_provider(
     action: argparse.Action,
-    choices_provider: Union[ChoicesProviderFunc, ChoicesProviderFuncWithTokens],
+    choices_provider: ChoicesProviderFunc,
 ) -> None:
     """Set choices_provider on an argparse action"""
     _set_choices_callable(action, ChoicesCallable(is_completer=False, to_call=choices_provider))
@@ -402,7 +441,7 @@ def set_choices_provider(
 
 def set_completer(
     action: argparse.Action,
-    completer: Union[CompleterFunc, CompleterFuncWithTokens],
+    completer: CompleterFunc,
 ) -> None:
     """Set completer on an argparse action"""
     _set_choices_callable(action, ChoicesCallable(is_completer=True, to_call=completer))
@@ -421,8 +460,8 @@ def _add_argument_wrapper(
     self: argparse._ActionsContainer,
     *args: Any,
     nargs: Union[int, str, Tuple[int], Tuple[int, int], Tuple[int, float], None] = None,
-    choices_provider: Optional[Union[ChoicesProviderFunc, ChoicesProviderFuncWithTokens]] = None,
-    completer: Optional[Union[CompleterFunc, CompleterFuncWithTokens]] = None,
+    choices_provider: Optional[ChoicesProviderFunc] = None,
+    completer: Optional[CompleterFunc] = None,
     suppress_tab_hint: bool = False,
     descriptive_header: Optional[str] = None,
     **kwargs: Any,
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index d6a7ad17..ef288120 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -79,9 +79,7 @@ from . import (
 from .argparse_custom import (
     DEFAULT_ARGUMENT_PARSER,
     ChoicesProviderFunc,
-    ChoicesProviderFuncWithTokens,
     CompleterFunc,
-    CompleterFuncWithTokens,
     CompletionItem,
 )
 from .clipboard import (
@@ -401,7 +399,7 @@ class Cmd(cmd.Cmd):
             if os.path.exists(startup_script):
                 script_cmd = "run_script {}".format(utils.quote_string(startup_script))
                 if silent_startup_script:
-                    script_cmd += "> {}".format(os.devnull)
+                    script_cmd += " > {}".format(os.devnull)
                 self._startup_commands.append(script_cmd)
 
         # Transcript files to run instead of interactive command loop
@@ -672,7 +670,7 @@ class Cmd(cmd.Cmd):
 
         setattr(self, cmd_func_name, command_wrapper)
 
-    def _install_completer_function(self, cmd_name: str, cmd_completer: Union[CompleterFunc, CompleterFuncWithTokens]) -> None:
+    def _install_completer_function(self, cmd_name: str, cmd_completer: CompleterFunc) -> None:
         completer_func_name = COMPLETER_FUNC_PREFIX + cmd_name
 
         if hasattr(self, completer_func_name):
@@ -2809,8 +2807,8 @@ class Cmd(cmd.Cmd):
         completion_mode: utils.CompletionMode = utils.CompletionMode.NONE,
         preserve_quotes: bool = False,
         choices: Optional[Iterable[Any]] = None,
-        choices_provider: Optional[Union[ChoicesProviderFunc, ChoicesProviderFuncWithTokens]] = None,
-        completer: Optional[Union[CompleterFunc, CompleterFuncWithTokens]] = None,
+        choices_provider: Optional[ChoicesProviderFunc] = None,
+        completer: Optional[CompleterFunc] = None,
         parser: Optional[argparse.ArgumentParser] = None,
     ) -> str:
         """
@@ -2845,7 +2843,7 @@ class Cmd(cmd.Cmd):
         :raises: any exceptions raised by input() and stdin.readline()
         """
         readline_configured = False
-        saved_completer: Optional[Union[CompleterFunc, CompleterFuncWithTokens]] = None
+        saved_completer: Optional[CompleterFunc] = None
         saved_history: Optional[List[str]] = None
 
         def configure_readline() -> None:
@@ -3636,7 +3634,7 @@ class Cmd(cmd.Cmd):
                             stdout_orig = self.stdout
                             try:
                                 # redirect our internal stdout
-                                self.stdout = result
+                                self.stdout = cast(TextIO, result)
                                 help_func()
                             finally:
                                 # restore internal stdout
@@ -3707,11 +3705,12 @@ class Cmd(cmd.Cmd):
           | a list of tuples -> interpreted as (value, text), so
                                 that the return value can differ from
                                 the text advertised to the user"""
-
-        local_opts = opts
+        local_opts: Union[List[str], List[Tuple[Any, Optional[str]]]]
         if isinstance(opts, str):
-            local_opts = list(zip(opts.split(), opts.split()))
-        fulloptions = []
+            local_opts = cast(List[Tuple[Any, Optional[str]]], list(zip(opts.split(), opts.split())))
+        else:
+            local_opts = opts
+        fulloptions: List[Tuple[Any, Optional[str]]] = []
         for opt in local_opts:
             if isinstance(opt, str):
                 fulloptions.append((opt, opt))
@@ -3740,7 +3739,7 @@ class Cmd(cmd.Cmd):
                 choice = int(response)
                 if choice < 1:
                     raise IndexError
-                return fulloptions[choice - 1][0]
+                return str(fulloptions[choice - 1][0])
             except (ValueError, IndexError):
                 self.poutput("{!r} isn't a valid choice. Pick a number between 1 and {}:".format(response, len(fulloptions)))
 
@@ -3884,7 +3883,7 @@ class Cmd(cmd.Cmd):
                 shell=True,
             )
 
-            proc_reader = utils.ProcReader(proc, self.stdout, sys.stderr)
+            proc_reader = utils.ProcReader(proc, cast(TextIO, self.stdout), sys.stderr)
             proc_reader.wait()
 
             # Save the return code of the application for use in a pyscript
@@ -4193,18 +4192,16 @@ class Cmd(cmd.Cmd):
         """
         # Detect whether IPython is installed
         try:
-            from IPython import (
+            import traitlets.config.loader as TraitletsLoader  # type: ignore[import]
+            from IPython import (  # type: ignore[import]
                 start_ipython,
             )
-            from IPython.terminal.interactiveshell import (
+            from IPython.terminal.interactiveshell import (  # type: ignore[import]
                 TerminalInteractiveShell,
             )
-            from IPython.terminal.ipapp import (
+            from IPython.terminal.ipapp import (  # type: ignore[import]
                 TerminalIPythonApp,
             )
-            from traitlets.config.loader import (
-                Config as TraitletsConfig,
-            )
         except ImportError:
             self.perror("IPython package is not installed")
             return None
@@ -4230,7 +4227,7 @@ class Cmd(cmd.Cmd):
                 local_vars['self'] = self
 
             # Configure IPython
-            config = TraitletsConfig()
+            config = TraitletsLoader.Config()
             config.InteractiveShell.banner2 = (
                 'Entering an IPython shell. Type exit, quit, or Ctrl-D to exit.\n'
                 f'Run CLI commands with: {self.py_bridge_name}("command ...")\n'
@@ -4349,6 +4346,7 @@ class Cmd(cmd.Cmd):
             import tempfile
 
             fd, fname = tempfile.mkstemp(suffix='.txt', text=True)
+            fobj: TextIO
             with os.fdopen(fd, 'w') as fobj:
                 for command in history.values():
                     if command.statement.multiline_command:
@@ -4563,7 +4561,7 @@ class Cmd(cmd.Cmd):
             with self.sigint_protection:
                 # Restore altered attributes to their original state
                 self.echo = saved_echo
-                self.stdout = saved_stdout
+                self.stdout = cast(TextIO, saved_stdout)
 
         # Check if all commands ran
         if commands_run < len(history):
@@ -5210,7 +5208,11 @@ class Cmd(cmd.Cmd):
         self._validate_cmdfinalization_callable(func)
         self._cmdfinalization_hooks.append(func)
 
-    def _resolve_func_self(self, cmd_support_func: Callable[..., Any], cmd_self: Union[CommandSet, 'Cmd']) -> Optional[object]:
+    def _resolve_func_self(
+        self,
+        cmd_support_func: Callable[..., Any],
+        cmd_self: Union[CommandSet, 'Cmd', None],
+    ) -> Optional[object]:
         """
         Attempt to resolve a candidate instance to pass as 'self' for an unbound class method that was
         used when defining command's argparse object. Since we restrict registration to only a single CommandSet
diff --git a/cmd2/decorators.py b/cmd2/decorators.py
index 12b3372a..d5a7a190 100644
--- a/cmd2/decorators.py
+++ b/cmd2/decorators.py
@@ -8,6 +8,7 @@ from typing import (
     Dict,
     List,
     Optional,
+    Sequence,
     Tuple,
     Union,
 )
@@ -68,6 +69,9 @@ def with_category(category: str) -> Callable[[CommandFunc], CommandFunc]:
 ##########################
 
 
+RawCommandFuncOptionalBoolReturn = Callable[[Union[CommandSet, 'cmd2.Cmd'], Union[Statement, str]], Optional[bool]]
+
+
 def _parse_positionals(args: Tuple[Any, ...]) -> Tuple['cmd2.Cmd', Union[Statement, str]]:
     """
     Helper function for cmd2 decorators to inspect the positional arguments until the cmd2.Cmd argument is found
@@ -92,9 +96,10 @@ def _parse_positionals(args: Tuple[Any, ...]) -> Tuple['cmd2.Cmd', Union[Stateme
     raise TypeError('Expected arguments: cmd: cmd2.Cmd, statement: Union[Statement, str] Not found')  # pragma: no cover
 
 
-def _arg_swap(args: Union[Tuple[Any], List[Any]], search_arg: Any, *replace_arg: Any) -> List[Any]:
+def _arg_swap(args: Union[Sequence[Any]], search_arg: Any, *replace_arg: Any) -> List[Any]:
     """
     Helper function for cmd2 decorators to swap the Statement parameter with one or more decorator-specific parameters
+
     :param args: The original positional arguments
     :param search_arg: The argument to search for (usually the Statement)
     :param replace_arg: The arguments to substitute in
@@ -106,9 +111,34 @@ def _arg_swap(args: Union[Tuple[Any], List[Any]], search_arg: Any, *replace_arg:
     return args_list
 
 
+#: Function signature for an Command Function that accepts a pre-processed argument list from user input
+#: and optionally returns a boolean
+ArgListCommandFuncOptionalBoolReturn = Union[
+    Callable[['cmd2.Cmd', List[str]], Optional[bool]],
+    Callable[[CommandSet, List[str]], Optional[bool]],
+]
+#: Function signature for an Command Function that accepts a pre-processed argument list from user input
+#: and returns a boolean
+ArgListCommandFuncBoolReturn = Union[
+    Callable[['cmd2.Cmd', List[str]], bool],
+    Callable[[CommandSet, List[str]], bool],
+]
+#: Function signature for an Command Function that accepts a pre-processed argument list from user input
+#: and returns Nothing
+ArgListCommandFuncNoneReturn = Union[
+    Callable[['cmd2.Cmd', List[str]], None],
+    Callable[[CommandSet, List[str]], None],
+]
+
+#: Aggregate of all accepted function signatures for Command Functions that accept a pre-processed argument list
+ArgListCommandFunc = Union[ArgListCommandFuncOptionalBoolReturn, ArgListCommandFuncBoolReturn, ArgListCommandFuncNoneReturn]
+
+
 def with_argument_list(
-    func_arg: Optional[Callable[[List[str]], Optional[bool]]] = None, *, preserve_quotes: bool = False
-) -> Union[Callable[[List[str]], Optional[bool]]]:
+    func_arg: Optional[ArgListCommandFunc] = None,
+    *,
+    preserve_quotes: bool = False,
+) -> Union[RawCommandFuncOptionalBoolReturn, Callable[[ArgListCommandFunc], RawCommandFuncOptionalBoolReturn]]:
     """
     A decorator to alter the arguments passed to a ``do_*`` method. Default
     passes a string of whatever the user typed. With this decorator, the
@@ -128,7 +158,15 @@ def with_argument_list(
     """
     import functools
 
-    def arg_decorator(func: Callable[[List[str]], Optional[bool]]) -> Callable[..., Optional[bool]]:
+    def arg_decorator(func: ArgListCommandFunc) -> RawCommandFuncOptionalBoolReturn:
+        """
+        Decorator function that ingests an Argument List function and returns a raw command function.
+        The returned function will process the raw input into an argument list to be passed to the wrapped function.
+
+        :param func: The defined argument list command function
+        :return: Function that takes raw input and converts to an argument list to pass to the wrapped function.
+        """
+
         @functools.wraps(func)
         def cmd_wrapper(*args: Any, **kwargs: Any) -> Optional[bool]:
             """
@@ -143,7 +181,7 @@ def with_argument_list(
             cmd2_app, statement = _parse_positionals(args)
             _, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list(command_name, statement, preserve_quotes)
             args_list = _arg_swap(args, statement, parsed_arglist)
-            return func(*args_list, **kwargs)
+            return func(*args_list, **kwargs)  # type: ignore[call-arg]
 
         command_name = func.__name__[len(constants.COMMAND_FUNC_PREFIX) :]
         cmd_wrapper.__doc__ = func.__doc__
@@ -158,7 +196,7 @@ def with_argument_list(
 
 
 # noinspection PyProtectedMember
-def _set_parser_prog(parser: argparse.ArgumentParser, prog: str):
+def _set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None:
     """
     Recursively set prog attribute of a parser and all of its subparsers so that the root command
     is a command name and not sys.argv[0].
@@ -200,19 +238,32 @@ def _set_parser_prog(parser: argparse.ArgumentParser, prog: str):
             break
 
 
-ArgparseCommandFunc = Union[
+#: Function signature for an Command Function that uses an argparse.ArgumentParser to process user input
+#: and optionally returns a boolean
+ArgparseCommandFuncOptionalBoolReturn = Union[
     Callable[['cmd2.Cmd', argparse.Namespace], Optional[bool]],
     Callable[[CommandSet, argparse.Namespace], Optional[bool]],
 ]
+#: Function signature for an Command Function that uses an argparse.ArgumentParser to process user input
+#: and returns a boolean
 ArgparseCommandFuncBoolReturn = Union[
     Callable[['cmd2.Cmd', argparse.Namespace], bool],
     Callable[[CommandSet, argparse.Namespace], bool],
 ]
+#: Function signature for an Command Function that uses an argparse.ArgumentParser to process user input
+#: and returns nothing
 ArgparseCommandFuncNoneReturn = Union[
     Callable[['cmd2.Cmd', argparse.Namespace], None],
     Callable[[CommandSet, argparse.Namespace], None],
 ]
 
+#: Aggregate of all accepted function signatures for an argparse Command Function
+ArgparseCommandFunc = Union[
+    ArgparseCommandFuncOptionalBoolReturn,
+    ArgparseCommandFuncBoolReturn,
+    ArgparseCommandFuncNoneReturn,
+]
+
 
 def with_argparser(
     parser: argparse.ArgumentParser,
@@ -220,10 +271,7 @@ def with_argparser(
     ns_provider: Optional[Callable[..., argparse.Namespace]] = None,
     preserve_quotes: bool = False,
     with_unknown_args: bool = False,
-) -> Callable[
-    [Union[ArgparseCommandFunc, ArgparseCommandFuncNoneReturn, ArgparseCommandFuncBoolReturn]],
-    Callable[[Union['cmd2.Cmd', CommandSet], str], Optional[bool]],
-]:
+) -> Callable[[ArgparseCommandFunc], RawCommandFuncOptionalBoolReturn]:
     """A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments
     with the given instance of argparse.ArgumentParser.
 
@@ -269,9 +317,15 @@ def with_argparser(
     """
     import functools
 
-    def arg_decorator(
-        func: Union[ArgparseCommandFunc, ArgparseCommandFuncBoolReturn, ArgparseCommandFuncNoneReturn],
-    ) -> ArgparseCommandFunc:
+    def arg_decorator(func: ArgparseCommandFunc) -> RawCommandFuncOptionalBoolReturn:
+        """
+        Decorator function that ingests an Argparse Command Function and returns a raw command function.
+        The returned function will process the raw input into an argparse Namespace to be passed to the wrapped function.
+
+        :param func: The defined argparse command function
+        :return: Function that takes raw input and converts to an argparse Namespace to passed to the wrapped function.
+        """
+
         @functools.wraps(func)
         def cmd_wrapper(*args: Any, **kwargs: Dict[str, Any]) -> Optional[bool]:
             """
@@ -300,6 +354,7 @@ def with_argparser(
                 namespace = ns_provider(provider_self if not None else cmd2_app)
 
             try:
+                new_args: Union[Tuple[argparse.Namespace], Tuple[argparse.Namespace, List[str]]]
                 if with_unknown_args:
                     new_args = parser.parse_known_args(parsed_arglist, namespace)
                 else:
@@ -321,7 +376,7 @@ def with_argparser(
                     delattr(ns, constants.NS_ATTR_SUBCMD_HANDLER)
 
                 args_list = _arg_swap(args, statement_arg, *new_args)
-                return func(*args_list, **kwargs)
+                return func(*args_list, **kwargs)  # type: ignore[call-arg]
 
         # argparser defaults the program name to sys.argv[0], but we want it to be the name of our command
         command_name = func.__name__[len(constants.COMMAND_FUNC_PREFIX) :]
@@ -351,10 +406,7 @@ def as_subcommand_to(
     *,
     help: Optional[str] = None,
     aliases: Optional[List[str]] = None,
-) -> Callable[
-    [Union[ArgparseCommandFunc, ArgparseCommandFuncNoneReturn, ArgparseCommandFuncBoolReturn]],
-    Union[ArgparseCommandFunc, ArgparseCommandFuncBoolReturn, ArgparseCommandFuncNoneReturn],
-]:
+) -> Callable[[ArgparseCommandFunc], ArgparseCommandFunc]:
     """
     Tag this method as a subcommand to an existing argparse decorated command.
 
@@ -368,9 +420,7 @@ def as_subcommand_to(
     :return: Wrapper function that can receive an argparse.Namespace
     """
 
-    def arg_decorator(
-        func: Union[ArgparseCommandFunc, ArgparseCommandFuncBoolReturn, ArgparseCommandFuncNoneReturn]
-    ) -> Union[ArgparseCommandFunc, ArgparseCommandFuncBoolReturn, ArgparseCommandFuncNoneReturn]:
+    def arg_decorator(func: ArgparseCommandFunc) -> ArgparseCommandFunc:
         _set_parser_prog(parser, command + ' ' + subcommand)
 
         # If the description has not been set, then use the method docstring if one exists
diff --git a/cmd2/history.py b/cmd2/history.py
index 3a2a9ed5..c072d2e0 100644
--- a/cmd2/history.py
+++ b/cmd2/history.py
@@ -9,8 +9,11 @@ from collections import (
 )
 from typing import (
     Callable,
+    Iterable,
+    List,
     Optional,
     Union,
+    overload,
 )
 
 import attr
@@ -23,16 +26,16 @@ from .parsing import (
 )
 
 
-@attr.s(frozen=True)
+@attr.s(auto_attribs=True, frozen=True)
 class HistoryItem:
     """Class used to represent one command in the history list"""
 
     _listformat = ' {:>4}  {}'
     _ex_listformat = ' {:>4}x {}'
 
-    statement = attr.ib(default=None, validator=attr.validators.instance_of(Statement))
+    statement: Statement = attr.ib(default=None, validator=attr.validators.instance_of(Statement))
 
-    def __str__(self):
+    def __str__(self) -> str:
         """A convenient human readable representation of the history item"""
         return self.statement.raw
 
@@ -66,11 +69,11 @@ class HistoryItem:
         """
         if verbose:
             raw = self.raw.rstrip()
-            expanded = self.expanded
+            expanded_command = self.expanded
 
             ret_str = self._listformat.format(idx, raw)
-            if raw != expanded:
-                ret_str += '\n' + self._ex_listformat.format(idx, expanded)
+            if raw != expanded_command:
+                ret_str += '\n' + self._ex_listformat.format(idx, expanded_command)
         else:
             if expanded:
                 ret_str = self.expanded
@@ -92,7 +95,7 @@ class HistoryItem:
         return ret_str
 
 
-class History(list):
+class History(List[HistoryItem]):
     """A list of :class:`~cmd2.history.HistoryItem` objects with additional methods
     for searching and managing the list.
 
@@ -106,8 +109,8 @@ class History(list):
     class to gain access to the historical record.
     """
 
-    def __init__(self, seq=()) -> None:
-        super().__init__(seq)
+    def __init__(self, seq: Iterable[HistoryItem] = ()) -> None:
+        super(History, self).__init__(seq)
         self.session_start_index = 0
 
     def start_session(self) -> None:
@@ -122,14 +125,22 @@ class History(list):
             result -= 1
         return result
 
+    @overload
+    def append(self, new: HistoryItem) -> None:
+        ...  # pragma: no cover
+
+    @overload
     def append(self, new: Statement) -> None:
+        ...  # pragma: no cover
+
+    def append(self, new: Union[Statement, HistoryItem]) -> None:
         """Append a new statement to the end of the History list.
 
         :param new: Statement object which will be composed into a HistoryItem
                     and added to the end of the list
         """
-        history_item = HistoryItem(new)
-        super().append(history_item)
+        history_item = HistoryItem(new) if isinstance(new, Statement) else new
+        super(History, self).append(history_item)
 
     def clear(self) -> None:
         """Remove all items from the History list."""
@@ -206,17 +217,17 @@ class History(list):
             # our regex doesn't match the input, bail out
             raise ValueError('History indices must be positive or negative integers, and may not be zero.')
 
-        start = results.group('start')
-        if start:
-            start = min(self._zero_based_index(start), len(self) - 1)
+        start_token = results.group('start')
+        if start_token:
+            start = min(self._zero_based_index(start_token), len(self) - 1)
             if start < 0:
                 start = max(0, len(self) + start)
         else:
             start = 0 if include_persisted else self.session_start_index
 
-        end = results.group('end')
-        if end:
-            end = min(int(end), len(self))
+        end_token = results.group('end')
+        if end_token:
+            end = min(int(end_token), len(self))
             if end < 0:
                 end = max(0, len(self) + end + 1)
         else:
diff --git a/cmd2/parsing.py b/cmd2/parsing.py
index f97b0c64..46d32549 100755
--- a/cmd2/parsing.py
+++ b/cmd2/parsing.py
@@ -5,6 +5,7 @@
 import re
 import shlex
 from typing import (
+    Any,
     Dict,
     Iterable,
     List,
@@ -86,7 +87,7 @@ class Macro:
 
 
 @attr.s(auto_attribs=True, frozen=True)
-class Statement(str):
+class Statement(str):  # type: ignore[override]
     """String subclass with additional attributes to store the results of parsing.
 
     The ``cmd`` module in the standard library passes commands around as a
@@ -146,7 +147,7 @@ class Statement(str):
     # if output was redirected, the destination file token (quotes preserved)
     output_to: str = attr.ib(default='', validator=attr.validators.instance_of(str))
 
-    def __new__(cls, value: object, *pos_args, **kw_args):
+    def __new__(cls, value: object, *pos_args: Any, **kw_args: Any) -> 'Statement':
         """Create a new instance of Statement.
 
         We must override __new__ because we are subclassing `str` which is
@@ -241,18 +242,13 @@ class StatementParser:
         :param aliases: dictionary containing aliases
         :param shortcuts: dictionary containing shortcuts
         """
+        self.terminators: Tuple[str, ...]
         if terminators is None:
             self.terminators = (constants.MULTILINE_TERMINATOR,)
         else:
             self.terminators = tuple(terminators)
-        if multiline_commands is None:
-            self.multiline_commands = tuple()
-        else:
-            self.multiline_commands = tuple(multiline_commands)
-        if aliases is None:
-            self.aliases = dict()
-        else:
-            self.aliases = aliases
+        self.multiline_commands: Tuple[str, ...] = tuple(multiline_commands) if multiline_commands is not None else ()
+        self.aliases: Dict[str, str] = aliases if aliases is not None else {}
 
         if shortcuts is None:
             shortcuts = constants.DEFAULT_SHORTCUTS
@@ -315,7 +311,7 @@ class StatementParser:
         valid = False
 
         if not isinstance(word, str):
-            return False, 'must be a string. Received {} instead'.format(str(type(word)))
+            return False, 'must be a string. Received {} instead'.format(str(type(word)))  # type: ignore[unreachable]
 
         if not word:
             return False, 'cannot be an empty string'
@@ -670,7 +666,7 @@ class StatementParser:
         :param tokens: the tokens as parsed by shlex
         :return: a new list of tokens, further split using punctuation
         """
-        punctuation = []
+        punctuation: List[str] = []
         punctuation.extend(self.terminators)
         punctuation.extend(constants.REDIRECTION_CHARS)
 
diff --git a/cmd2/py_bridge.py b/cmd2/py_bridge.py
index fa6cd865..6fd00e07 100644
--- a/cmd2/py_bridge.py
+++ b/cmd2/py_bridge.py
@@ -17,6 +17,7 @@ from typing import (
     NamedTuple,
     Optional,
     TextIO,
+    Union,
     cast,
 )
 
@@ -110,7 +111,7 @@ class PyBridge:
             echo = self.cmd_echo
 
         # This will be used to capture _cmd2_app.stdout and sys.stdout
-        copy_cmd_stdout = StdSim(self._cmd2_app.stdout, echo=echo)
+        copy_cmd_stdout = StdSim(cast(Union[TextIO, StdSim], self._cmd2_app.stdout), echo=echo)
 
         # Pause the storing of stdout until onecmd_plus_hooks enables it
         copy_cmd_stdout.pause_storage = True
diff --git a/cmd2/table_creator.py b/cmd2/table_creator.py
index d9377bc4..ade40ae3 100644
--- a/cmd2/table_creator.py
+++ b/cmd2/table_creator.py
@@ -23,7 +23,7 @@ from typing import (
     Union,
 )
 
-from wcwidth import (
+from wcwidth import (  # type: ignore[import]
     wcwidth,
 )
 
@@ -89,7 +89,7 @@ class Column:
         if width is not None and width < 1:
             raise ValueError("Column width cannot be less than 1")
         else:
-            self.width = width
+            self.width: int = width if width is not None else -1
 
         self.header_horiz_align = header_horiz_align
         self.header_vert_align = header_vert_align
@@ -138,7 +138,7 @@ class TableCreator:
 
             # For headers with the width not yet set, use the width of the
             # widest line in the header or 1 if the header has no width
-            if col.width is None:
+            if col.width <= 0:
                 col.width = max(1, ansi.widest_line(col.header))
 
     @staticmethod
@@ -218,7 +218,11 @@ class TableCreator:
         :return: wrapped text
         """
 
-        def add_word(word_to_add: str, is_last_word: bool):
+        # MyPy Issue #7057 documents regression requiring nonlocals to be defined earlier
+        cur_line_width = 0
+        total_lines = 0
+
+        def add_word(word_to_add: str, is_last_word: bool) -> None:
             """
             Called from loop to add a word to the wrapped text
 
@@ -431,7 +435,7 @@ class TableCreator:
 
             def __init__(self) -> None:
                 # Data in this cell split into individual lines
-                self.lines = []
+                self.lines: Deque[str] = deque()
 
                 # Display width of this cell
                 self.width = 0
@@ -705,7 +709,7 @@ class BorderedTable(TableCreator):
         data_width = sum(col.width for col in self.cols)
         return base_width + data_width
 
-    def generate_table_top_border(self):
+    def generate_table_top_border(self) -> str:
         """Generate a border which appears at the top of the header and data section"""
         pre_line = '╔' + self.padding * '═'
 
@@ -720,7 +724,7 @@ class BorderedTable(TableCreator):
             row_data=self.empty_data, fill_char='═', pre_line=pre_line, inter_cell=inter_cell, post_line=post_line
         )
 
-    def generate_header_bottom_border(self):
+    def generate_header_bottom_border(self) -> str:
         """Generate a border which appears at the bottom of the header"""
         pre_line = '╠' + self.padding * '═'
 
@@ -735,7 +739,7 @@ class BorderedTable(TableCreator):
             row_data=self.empty_data, fill_char='═', pre_line=pre_line, inter_cell=inter_cell, post_line=post_line
         )
 
-    def generate_row_bottom_border(self):
+    def generate_row_bottom_border(self) -> str:
         """Generate a border which appears at the bottom of rows"""
         pre_line = '╟' + self.padding * '─'
 
@@ -750,7 +754,7 @@ class BorderedTable(TableCreator):
             row_data=self.empty_data, fill_char='─', pre_line=pre_line, inter_cell=inter_cell, post_line=post_line
         )
 
-    def generate_table_bottom_border(self):
+    def generate_table_bottom_border(self) -> str:
         """Generate a border which appears at the bottom of the table"""
         pre_line = '╚' + self.padding * '═'
 
diff --git a/cmd2/transcript.py b/cmd2/transcript.py
index c336c1a5..7ea5b8a9 100644
--- a/cmd2/transcript.py
+++ b/cmd2/transcript.py
@@ -13,8 +13,10 @@ import re
 import unittest
 from typing import (
     TYPE_CHECKING,
+    Iterator,
     List,
     Optional,
+    TextIO,
     Tuple,
     cast,
 )
@@ -42,26 +44,26 @@ class Cmd2TestCase(unittest.TestCase):
 
     cmdapp: Optional['Cmd'] = None
 
-    def setUp(self):
+    def setUp(self) -> None:
         if self.cmdapp:
             self._fetchTranscripts()
 
             # Trap stdout
             self._orig_stdout = self.cmdapp.stdout
-            self.cmdapp.stdout = utils.StdSim(self.cmdapp.stdout)
+            self.cmdapp.stdout = cast(TextIO, utils.StdSim(cast(TextIO, self.cmdapp.stdout)))
 
-    def tearDown(self):
+    def tearDown(self) -> None:
         if self.cmdapp:
             # Restore stdout
             self.cmdapp.stdout = self._orig_stdout
 
-    def runTest(self):  # was testall
+    def runTest(self) -> None:  # was testall
         if self.cmdapp:
             its = sorted(self.transcripts.items())
             for (fname, transcript) in its:
                 self._test_transcript(fname, transcript)
 
-    def _fetchTranscripts(self):
+    def _fetchTranscripts(self) -> None:
         self.transcripts = {}
         testfiles = cast(List[str], getattr(self.cmdapp, 'testfiles', []))
         for fname in testfiles:
@@ -69,7 +71,7 @@ class Cmd2TestCase(unittest.TestCase):
             self.transcripts[fname] = iter(tfile.readlines())
             tfile.close()
 
-    def _test_transcript(self, fname: str, transcript):
+    def _test_transcript(self, fname: str, transcript: Iterator[str]) -> None:
         if self.cmdapp is None:
             return
 
@@ -86,7 +88,7 @@ class Cmd2TestCase(unittest.TestCase):
                     finished = True
                     break
                 line_num += 1
-            command = [line[len(self.cmdapp.visible_prompt) :]]
+            command_parts = [line[len(self.cmdapp.visible_prompt) :]]
             try:
                 line = next(transcript)
             except StopIteration:
@@ -94,16 +96,16 @@ class Cmd2TestCase(unittest.TestCase):
             line_num += 1
             # Read the entirety of a multi-line command
             while line.startswith(self.cmdapp.continuation_prompt):
-                command.append(line[len(self.cmdapp.continuation_prompt) :])
+                command_parts.append(line[len(self.cmdapp.continuation_prompt) :])
                 try:
                     line = next(transcript)
                 except StopIteration as exc:
                     msg = 'Transcript broke off while reading command beginning at line {} with\n{}'.format(
-                        line_num, command[0]
+                        line_num, command_parts[0]
                     )
                     raise StopIteration(msg) from exc
                 line_num += 1
-            command = ''.join(command)
+            command = ''.join(command_parts)
             # Send the command into the application and capture the resulting output
             stop = self.cmdapp.onecmd_plus_hooks(command)
             result = self.cmdapp.stdout.read()
@@ -117,9 +119,9 @@ class Cmd2TestCase(unittest.TestCase):
                 # If the command signaled the application to quit there should be no more commands
                 self.assertFalse(stop, stop_msg)
                 continue
-            expected = []
+            expected_parts = []
             while not ansi.strip_style(line).startswith(self.cmdapp.visible_prompt):
-                expected.append(line)
+                expected_parts.append(line)
                 try:
                     line = next(transcript)
                 except StopIteration:
@@ -132,7 +134,7 @@ class Cmd2TestCase(unittest.TestCase):
                 self.assertTrue(finished, stop_msg)
 
             # transform the expected text into a valid regular expression
-            expected = ''.join(expected)
+            expected = ''.join(expected_parts)
             expected = self._transform_transcript_expected(expected)
             message = '\nFile {}, line {}\nCommand was:\n{}\nExpected:\n{}\nGot:\n{}\n'.format(
                 fname, line_num, command, expected, result
diff --git a/cmd2/utils.py b/cmd2/utils.py
index 292f88dc..4f3eae7b 100644
--- a/cmd2/utils.py
+++ b/cmd2/utils.py
@@ -35,9 +35,7 @@ from . import (
 )
 from .argparse_custom import (
     ChoicesProviderFunc,
-    ChoicesProviderFuncWithTokens,
     CompleterFunc,
-    CompleterFuncWithTokens,
 )
 
 if TYPE_CHECKING:  # pragma: no cover
@@ -121,8 +119,8 @@ class Settable:
         settable_attrib_name: Optional[str] = None,
         onchange_cb: Optional[Callable[[str, _T, _T], Any]] = None,
         choices: Optional[Iterable[Any]] = None,
-        choices_provider: Optional[Union[ChoicesProviderFunc, ChoicesProviderFuncWithTokens]] = None,
-        completer: Optional[Union[CompleterFunc, CompleterFuncWithTokens]] = None,
+        choices_provider: Optional[ChoicesProviderFunc] = None,
+        completer: Optional[CompleterFunc] = None,
     ) -> None:
         """
         Settable Initializer


Test patch:
diff --git a/plugins/ext_test/cmd2_ext_test/__init__.py b/plugins/ext_test/cmd2_ext_test/__init__.py
index f246e822..c17a329a 100644
--- a/plugins/ext_test/cmd2_ext_test/__init__.py
+++ b/plugins/ext_test/cmd2_ext_test/__init__.py
@@ -10,7 +10,9 @@ try:
     import importlib.metadata as importlib_metadata
 except ImportError:  # pragma: no cover
     # For everyone else
-    import importlib_metadata
+    # MyPy Issue # 1153 causes a spurious error that must be ignored
+    import importlib_metadata  # type: ignore
+
 try:
     __version__ = importlib_metadata.version(__name__)
 except importlib_metadata.PackageNotFoundError:  # pragma: no cover
diff --git a/tests/test_table_creator.py b/tests/test_table_creator.py
index ee15bb22..6e98baa4 100644
--- a/tests/test_table_creator.py
+++ b/tests/test_table_creator.py
@@ -40,31 +40,31 @@ def test_column_creation():
 
     # No width specified, blank label
     c = Column("")
-    assert c.width is None
+    assert c.width < 0
     tc = TableCreator([c])
     assert tc.cols[0].width == 1
 
     # No width specified, label isn't blank but has no width
     c = Column(ansi.style('', fg=ansi.fg.green))
-    assert c.width is None
+    assert c.width < 0
     tc = TableCreator([c])
     assert tc.cols[0].width == 1
 
     # No width specified, label has width
     c = Column("a line")
-    assert c.width is None
+    assert c.width < 0
     tc = TableCreator([c])
     assert tc.cols[0].width == ansi.style_aware_wcswidth("a line")
 
     # No width specified, label has width and multiple lines
     c = Column("short\nreally long")
-    assert c.width is None
+    assert c.width < 0
     tc = TableCreator([c])
     assert tc.cols[0].width == ansi.style_aware_wcswidth("really long")
 
     # No width specified, label has tabs
     c = Column("line\twith\ttabs")
-    assert c.width is None
+    assert c.width < 0
     tc = TableCreator([c])
     assert tc.cols[0].width == ansi.style_aware_wcswidth("line    with    tabs")