diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 812c7153..514ac365 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -46,20 +46,21 @@ The tables below list all prerequisites along with the minimum required version
#### Prerequisites to run cmd2 applications
-| Prerequisite | Minimum Version |
-| --------------------------------------------------- | --------------- |
-| [python](https://www.python.org/downloads/) | `3.8` |
-| [pyperclip](https://github.com/asweigart/pyperclip) | `1.8.2` |
-| [wcwidth](https://pypi.python.org/pypi/wcwidth) | `0.2.12` |
+| Prerequisite | Minimum Version | Purpose |
+| -------------------------------------------------------- | --------------- | -------------------------------------- |
+| [python](https://www.python.org/downloads/) | `3.9` | Python programming language |
+| [pyperclip](https://github.com/asweigart/pyperclip) | `1.8.2` | Cross-platform clipboard functions |
+| [rich-argparse](https://pypi.org/project/rich-argparse/) | `1.6.0` | Rich help formatters for argparse |
+| [wcwidth](https://pypi.python.org/pypi/wcwidth) | `0.2.12` | Measure the displayed width of unicode |
#### Additional prerequisites to build and publish cmd2
-| Prerequisite | Minimum Version |
-| -------------------------------------------------------- | --------------- |
-| [build](https://pypi.org/project/build/) | `1.2.2` |
-| [setuptools](https://pypi.org/project/setuptools/) | `72.1.0` |
-| [setuptools-scm](https://github.com/pypa/setuptools-scm) | `8.0.4` |
-| [twine](https://github.com/pypa/twine) | `5.1.1` |
+| Prerequisite | Minimum Version | Purpose |
+| -------------------------------------------------------- | --------------- | ----------------------------------- |
+| [build](https://pypi.org/project/build/) | `1.2.2` | Python build frontend |
+| [setuptools](https://pypi.org/project/setuptools/) | `72.1.0` | Python package management |
+| [setuptools-scm](https://github.com/pypa/setuptools-scm) | `8.0.4` | Manage your versions by scm tag s |
+| [twine](https://github.com/pypa/twine) | `5.1.1` | Utilities for interacting with PyPI |
#### Additional prerequisites for developing cmd2
@@ -94,7 +95,7 @@ on all platforms (Windows, Mac, and Linux). You can install `uv` using instructi
You can then install multiple versions of Python using `uv` like so:
```sh
-uv python install 3.10 3.11 3.12 3.13
+uv python install 3.9 3.10 3.11 3.12 3.13
```
### Forking the project
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 5ab971b8..9f81a7a0 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -10,7 +10,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
- python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4 # https://github.com/actions/checkout
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 56d0a33d..ea4209fe 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,16 @@
+## 3.0.0 (TBD)
+
+- Breaking Changes
+ - `cmd2` 3.x supports Python 3.9+ (removed support for Python 3.8)
+ - Removed macros
+- Enhancements
+ - Simplified the process to set a custom parser for `cmd2's` built-in commands.
+ See [custom_parser.py](https://github.com/python-cmd2/cmd2/blob/master/examples/custom_parser.py)
+ example for more details.
+ - Integrated `rich-argparse` with `Cmd2HelpFormatter`.
+ - Added `RawDescriptionCmd2HelpFormatter`, `RawTextCmd2HelpFormatter`, `ArgumentDefaultsCmd2HelpFormatter`,
+ and `MetavarTypeCmd2HelpFormatter` and they all use `rich-argparse`.
+
## 2.5.11 (January 25, 2025)
- Bug Fixes
@@ -7,7 +20,7 @@
- Bug Fixes
- Fixed docstring style for MkDocs API documentation so parameters are displayed properly.
-
+
## 2.5.9 (January 17, 2025)
- Bug Fixes
diff --git a/Pipfile b/Pipfile
index b0ba2c0c..58e86480 100644
--- a/Pipfile
+++ b/Pipfile
@@ -5,6 +5,8 @@ verify_ssl = true
[packages]
pyperclip = "*"
+rich = "*"
+rich-argparse = "*"
setuptools = "*"
wcwidth = "*"
diff --git a/README.md b/README.md
index d34b9259..9a7def90 100755
--- a/README.md
+++ b/README.md
@@ -57,9 +57,9 @@ Deep extensive tab completion and help text generation based on the argparse lib
-cmd2 creates the second pillar of 'ease of transition to automation' through alias/macro creation, command line argument parsing and execution of cmd2 scripting.
+cmd2 creates the second pillar of 'ease of transition to automation' through alias creation, command line argument parsing and execution of cmd2 scripting.
-- Flexible alias and macro creation for quick abstraction of commands.
+- Flexible alias creation for quick abstraction of commands.
- Text file scripting of your application with `run_script` (`@`) and `_relative_run_script` (`@@`)
- Powerful and flexible built-in Python scripting of your application using the `run_pyscript` command
- Transcripts for use with built-in regression can be automatically generated from `history -t` or `run_script -t`
@@ -72,7 +72,7 @@ On all operating systems, the latest stable version of `cmd2` can be installed u
pip install -U cmd2
```
-cmd2 works with Python 3.8+ on Windows, macOS, and Linux. It is pure Python code with few 3rd-party dependencies.
+cmd2 works with Python 3.9+ on Windows, macOS, and Linux. It is pure Python code with few 3rd-party dependencies.
For information on other installation options, see
[Installation Instructions](https://cmd2.readthedocs.io/en/latest/overview/installation.html) in the cmd2
diff --git a/cmd2/__init__.py b/cmd2/__init__.py
index 8f1f030e..4b9f65ea 100644
--- a/cmd2/__init__.py
+++ b/cmd2/__init__.py
@@ -3,8 +3,6 @@
# flake8: noqa F401
"""This simply imports certain things for backwards compatibility."""
-import sys
-
import importlib.metadata as importlib_metadata
try:
@@ -13,19 +11,19 @@
# package is not installed
pass
-from typing import List
-
+from . import plugin
from .ansi import (
- Cursor,
Bg,
- Fg,
+ Cursor,
EightBitBg,
EightBitFg,
+ Fg,
RgbBg,
RgbFg,
TextStyle,
style,
)
+from .argparse_completer import set_default_ap_completer_type
from .argparse_custom import (
Cmd2ArgumentParser,
Cmd2AttributeWrapper,
@@ -33,23 +31,21 @@
register_argparse_argument_parameter,
set_default_argument_parser_type,
)
-
-# Check if user has defined a module that sets a custom value for argparse_custom.DEFAULT_ARGUMENT_PARSER.
-# Do this before loading cmd2.Cmd class so its commands use the custom parser.
-import argparse
-
-cmd2_parser_module = getattr(argparse, 'cmd2_parser_module', None)
-if cmd2_parser_module is not None:
- import importlib
-
- importlib.import_module(cmd2_parser_module)
-
-from .argparse_completer import set_default_ap_completer_type
-
from .cmd2 import Cmd
-from .command_definition import CommandSet, with_default_category
-from .constants import COMMAND_NAME, DEFAULT_SHORTCUTS
-from .decorators import with_argument_list, with_argparser, with_category, as_subcommand_to
+from .command_definition import (
+ CommandSet,
+ with_default_category,
+)
+from .constants import (
+ COMMAND_NAME,
+ DEFAULT_SHORTCUTS,
+)
+from .decorators import (
+ as_subcommand_to,
+ with_argparser,
+ with_argument_list,
+ with_category,
+)
from .exceptions import (
Cmd2ArgparseError,
CommandSetRegistrationError,
@@ -57,13 +53,16 @@
PassThroughException,
SkipPostcommandHooks,
)
-from . import plugin
from .parsing import Statement
from .py_bridge import CommandResult
-from .utils import categorize, CompletionMode, CustomCompletionSettings, Settable
-
+from .utils import (
+ CompletionMode,
+ CustomCompletionSettings,
+ Settable,
+ categorize,
+)
-__all__: List[str] = [
+__all__: list[str] = [
'COMMAND_NAME',
'DEFAULT_SHORTCUTS',
# ANSI Exports
@@ -81,8 +80,8 @@
'Cmd2AttributeWrapper',
'CompletionItem',
'register_argparse_argument_parameter',
- 'set_default_argument_parser_type',
'set_default_ap_completer_type',
+ 'set_default_argument_parser_type',
# Cmd2
'Cmd',
'CommandResult',
@@ -98,6 +97,7 @@
'Cmd2ArgparseError',
'CommandSetRegistrationError',
'CompletionError',
+ 'PassThroughException',
'SkipPostcommandHooks',
# modules
'plugin',
diff --git a/cmd2/ansi.py b/cmd2/ansi.py
index 8242957a..4399f2de 100644
--- a/cmd2/ansi.py
+++ b/cmd2/ansi.py
@@ -12,12 +12,11 @@
from typing import (
IO,
Any,
- List,
Optional,
cast,
)
-from wcwidth import ( # type: ignore[import]
+from wcwidth import ( # type: ignore[import-untyped]
wcswidth,
)
@@ -992,11 +991,11 @@ def style(
:raises TypeError: if bg isn't None or a subclass of BgColor
:return: the stylized string
"""
- # List of strings that add style
- additions: List[AnsiSequence] = []
+ # list of strings that add style
+ additions: list[AnsiSequence] = []
- # List of strings that remove style
- removals: List[AnsiSequence] = []
+ # list of strings that remove style
+ removals: list[AnsiSequence] = []
# Process the style settings
if fg is not None:
diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py
index 8dd543c2..3a0f3190 100644
--- a/cmd2/argparse_completer.py
+++ b/cmd2/argparse_completer.py
@@ -14,8 +14,6 @@
)
from typing import (
TYPE_CHECKING,
- Dict,
- List,
Optional,
Type,
Union,
@@ -172,7 +170,7 @@ class ArgparseCompleter:
"""Automatic command line tab completion based on argparse parameters"""
def __init__(
- self, parser: argparse.ArgumentParser, cmd2_app: 'Cmd', *, parent_tokens: Optional[Dict[str, List[str]]] = None
+ self, parser: argparse.ArgumentParser, cmd2_app: 'Cmd', *, parent_tokens: Optional[dict[str, list[str]]] = None
) -> None:
"""
Create an ArgparseCompleter
@@ -213,8 +211,8 @@ def __init__(
self._subcommand_action = action
def complete(
- self, text: str, line: str, begidx: int, endidx: int, tokens: List[str], *, cmd_set: Optional[CommandSet] = None
- ) -> List[str]:
+ self, text: str, line: str, begidx: int, endidx: int, tokens: list[str], *, cmd_set: Optional[CommandSet] = None
+ ) -> list[str]:
"""
Complete text using argparse metadata
@@ -245,13 +243,13 @@ def complete(
flag_arg_state: Optional[_ArgumentState] = None
# Non-reusable flags that we've parsed
- matched_flags: List[str] = []
+ matched_flags: list[str] = []
# Keeps track of arguments we've seen and any tokens they consumed
- consumed_arg_values: Dict[str, List[str]] = dict() # dict(arg_name -> List[tokens])
+ consumed_arg_values: dict[str, list[str]] = dict() # dict(arg_name -> l[tokens])
# Completed mutually exclusive groups
- completed_mutex_groups: Dict[argparse._MutuallyExclusiveGroup, argparse.Action] = dict()
+ completed_mutex_groups: dict[argparse._MutuallyExclusiveGroup, argparse.Action] = dict()
def consume_argument(arg_state: _ArgumentState) -> None:
"""Consuming token as an argument"""
@@ -507,7 +505,7 @@ def update_mutex_groups(arg_action: argparse.Action) -> None:
return completion_results
- def _complete_flags(self, text: str, line: str, begidx: int, endidx: int, matched_flags: List[str]) -> List[str]:
+ def _complete_flags(self, text: str, line: str, begidx: int, endidx: int, matched_flags: list[str]) -> list[str]:
"""Tab completion routine for a parsers unused flags"""
# Build a list of flags that can be tab completed
@@ -524,7 +522,7 @@ def _complete_flags(self, text: str, line: str, begidx: int, endidx: int, matche
matches = self._cmd2_app.basic_complete(text, line, begidx, endidx, match_against)
# Build a dictionary linking actions with their matched flag names
- matched_actions: Dict[argparse.Action, List[str]] = dict()
+ matched_actions: dict[argparse.Action, list[str]] = dict()
for flag in matches:
action = self._flag_to_action[flag]
matched_actions.setdefault(action, [])
@@ -541,14 +539,14 @@ def _complete_flags(self, text: str, line: str, begidx: int, endidx: int, matche
return matches
- def _format_completions(self, arg_state: _ArgumentState, completions: Union[List[str], List[CompletionItem]]) -> List[str]:
+ def _format_completions(self, arg_state: _ArgumentState, completions: Union[list[str], list[CompletionItem]]) -> list[str]:
"""Format CompletionItems into hint table"""
# Nothing to do if we don't have at least 2 completions which are all CompletionItems
if len(completions) < 2 or not all(isinstance(c, CompletionItem) for c in completions):
- return cast(List[str], completions)
+ return cast(list[str], completions)
- completion_items = cast(List[CompletionItem], completions)
+ completion_items = cast(list[CompletionItem], completions)
# Check if the data being completed have a numerical type
all_nums = all(isinstance(c.orig_value, numbers.Number) for c in completion_items)
@@ -557,7 +555,7 @@ def _format_completions(self, arg_state: _ArgumentState, completions: Union[List
if not self._cmd2_app.matches_sorted:
# If all orig_value types are numbers, then sort by that value
if all_nums:
- completion_items.sort(key=lambda c: c.orig_value) # type: ignore[no-any-return]
+ completion_items.sort(key=lambda c: c.orig_value)
# Otherwise sort as strings
else:
@@ -616,9 +614,9 @@ def _format_completions(self, arg_state: _ArgumentState, completions: Union[List
self._cmd2_app.formatted_completions = hint_table.generate_table(table_data, row_spacing=0)
# Return sorted list of completions
- return cast(List[str], completions)
+ return cast(list[str], completions)
- def complete_subcommand_help(self, text: str, line: str, begidx: int, endidx: int, tokens: List[str]) -> List[str]:
+ def complete_subcommand_help(self, text: str, line: str, begidx: int, endidx: int, tokens: list[str]) -> list[str]:
"""
Supports cmd2's help command in the completion of subcommand names
:param text: the string prefix we are attempting to match (all matches must begin with it)
@@ -626,7 +624,7 @@ def complete_subcommand_help(self, text: str, line: str, begidx: int, endidx: in
:param begidx: the beginning index of the prefix text
:param endidx: the ending index of the prefix text
:param tokens: arguments passed to command/subcommand
- :return: List of subcommand completions
+ :return: list of subcommand completions
"""
# If our parser has subcommands, we must examine the tokens and check if they are subcommands
# If so, we will let the subcommand's parser handle the rest of the tokens via another ArgparseCompleter.
@@ -645,7 +643,7 @@ def complete_subcommand_help(self, text: str, line: str, begidx: int, endidx: in
break
return []
- def format_help(self, tokens: List[str]) -> str:
+ def format_help(self, tokens: list[str]) -> str:
"""
Supports cmd2's help command in the retrieval of help text
:param tokens: arguments passed to help command
@@ -672,17 +670,17 @@ def _complete_arg(
begidx: int,
endidx: int,
arg_state: _ArgumentState,
- consumed_arg_values: Dict[str, List[str]],
+ consumed_arg_values: dict[str, list[str]],
*,
cmd_set: Optional[CommandSet] = None,
- ) -> List[str]:
+ ) -> list[str]:
"""
Tab completion routine for an argparse argument
:return: list of completions
: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]
+ 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:
@@ -739,17 +737,17 @@ def _complete_arg(
# Otherwise use basic_complete on the choices
else:
# Check if the choices come from a function
- completion_items: List[str] = []
+ 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]
+ completion_items = choices_func(*args, **kwargs)
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]
+ completion_items = choices_func(*args)
# else case is already covered above
else:
completion_items = arg_choices
diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py
index 68249d85..bd5ff0dd 100644
--- a/cmd2/argparse_custom.py
+++ b/cmd2/argparse_custom.py
@@ -7,7 +7,7 @@
parser that inherits from it. This will give a consistent look-and-feel between
the help/error output of built-in cmd2 commands and the app-specific commands.
If you wish to override the parser used by cmd2's built-in commands, see
-override_parser.py example.
+custom_parser.py example.
Since the new capabilities are added by patching at the argparse API level,
they are available whether or not Cmd2ArgumentParser is used. However, the help
@@ -238,21 +238,29 @@ def my_completer(self, text, line, begidx, endidx, arg_tokens)
TYPE_CHECKING,
Any,
Callable,
- Dict,
Iterable,
- List,
NoReturn,
Optional,
Protocol,
Sequence,
- Set,
- Tuple,
Type,
Union,
cast,
runtime_checkable,
)
+from rich.console import (
+ Group,
+ RenderableType,
+)
+from rich_argparse import (
+ ArgumentDefaultsRichHelpFormatter,
+ MetavarTypeRichHelpFormatter,
+ RawDescriptionRichHelpFormatter,
+ RawTextRichHelpFormatter,
+ RichHelpFormatter,
+)
+
from . import (
ansi,
constants,
@@ -325,7 +333,7 @@ class ChoicesProviderFuncBase(Protocol):
Function that returns a list of choices in support of tab completion
"""
- def __call__(self) -> List[str]: ... # pragma: no cover
+ def __call__(self) -> list[str]: ... # pragma: no cover
@runtime_checkable
@@ -334,7 +342,7 @@ 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]: ... # pragma: no cover
+ def __call__(self, *, arg_tokens: dict[str, list[str]] = {}) -> list[str]: ... # pragma: no cover
ChoicesProviderFunc = Union[ChoicesProviderFuncBase, ChoicesProviderFuncWithTokens]
@@ -352,7 +360,7 @@ def __call__(
line: str,
begidx: int,
endidx: int,
- ) -> List[str]: ... # pragma: no cover
+ ) -> list[str]: ... # pragma: no cover
@runtime_checkable
@@ -369,8 +377,8 @@ def __call__(
begidx: int,
endidx: int,
*,
- arg_tokens: Dict[str, List[str]] = {},
- ) -> List[str]: ... # pragma: no cover
+ arg_tokens: dict[str, list[str]] = {},
+ ) -> list[str]: ... # pragma: no cover
CompleterFunc = Union[CompleterFuncBase, CompleterFuncWithTokens]
@@ -572,7 +580,7 @@ def _action_set_descriptive_header(self: argparse.Action, descriptive_header: Op
############################################################################################################
# Patch argparse.Action with accessors for nargs_range attribute
############################################################################################################
-def _action_get_nargs_range(self: argparse.Action) -> Optional[Tuple[int, Union[int, float]]]:
+def _action_get_nargs_range(self: argparse.Action) -> Optional[tuple[int, Union[int, float]]]:
"""
Get the nargs_range attribute of an argparse Action.
@@ -583,13 +591,13 @@ def _action_get_nargs_range(self: argparse.Action) -> Optional[Tuple[int, Union[
:param self: argparse Action being queried
:return: The value of nargs_range or None if attribute does not exist
"""
- return cast(Optional[Tuple[int, Union[int, float]]], getattr(self, ATTR_NARGS_RANGE, None))
+ return cast(Optional[tuple[int, Union[int, float]]], getattr(self, ATTR_NARGS_RANGE, None))
setattr(argparse.Action, 'get_nargs_range', _action_get_nargs_range)
-def _action_set_nargs_range(self: argparse.Action, nargs_range: Optional[Tuple[int, Union[int, float]]]) -> None:
+def _action_set_nargs_range(self: argparse.Action, nargs_range: Optional[tuple[int, Union[int, float]]]) -> None:
"""
Set the nargs_range attribute of an argparse Action.
@@ -647,7 +655,7 @@ def _action_set_suppress_tab_hint(self: argparse.Action, suppress_tab_hint: bool
# Allow developers to add custom action attributes
############################################################################################################
-CUSTOM_ACTION_ATTRIBS: Set[str] = set()
+CUSTOM_ACTION_ATTRIBS: set[str] = set()
_CUSTOM_ATTRIB_PFX = '_attr_'
@@ -720,7 +728,7 @@ def _action_set_custom_parameter(self: argparse.Action, value: Any) -> None:
def _add_argument_wrapper(
self: argparse._ActionsContainer,
*args: Any,
- nargs: Union[int, str, Tuple[int], Tuple[int, int], Tuple[int, float], None] = None,
+ nargs: Union[int, str, tuple[int], tuple[int, int], tuple[int, float], None] = None,
choices_provider: Optional[ChoicesProviderFunc] = None,
completer: Optional[CompleterFunc] = None,
suppress_tab_hint: bool = False,
@@ -771,7 +779,7 @@ def _add_argument_wrapper(
nargs_range = None
if nargs is not None:
- nargs_adjusted: Union[int, str, Tuple[int], Tuple[int, int], Tuple[int, float], None]
+ nargs_adjusted: Union[int, str, tuple[int], tuple[int, int], tuple[int, float], None]
# Check if nargs was given as a range
if isinstance(nargs, tuple):
# Handle 1-item tuple by setting max to INFINITY
@@ -781,11 +789,11 @@ def _add_argument_wrapper(
# Validate nargs tuple
if (
len(nargs) != 2
- or not isinstance(nargs[0], int) # type: ignore[unreachable]
- or not (isinstance(nargs[1], int) or nargs[1] == constants.INFINITY) # type: ignore[misc]
+ or not isinstance(nargs[0], int)
+ or not (isinstance(nargs[1], int) or nargs[1] == constants.INFINITY)
):
raise ValueError('Ranged values for nargs must be a tuple of 1 or 2 integers')
- if nargs[0] >= nargs[1]: # type: ignore[misc]
+ if nargs[0] >= nargs[1]:
raise ValueError('Invalid nargs range. The first value must be less than the second')
if nargs[0] < 0:
raise ValueError('Negative numbers are invalid for nargs range')
@@ -793,7 +801,7 @@ def _add_argument_wrapper(
# Save the nargs tuple as our range setting
nargs_range = nargs
range_min = nargs_range[0]
- range_max = nargs_range[1] # type: ignore[misc]
+ range_max = nargs_range[1]
# Convert nargs into a format argparse recognizes
if range_min == 0:
@@ -821,7 +829,7 @@ def _add_argument_wrapper(
kwargs['nargs'] = nargs_adjusted
# Extract registered custom keyword arguments
- custom_attribs: Dict[str, Any] = {}
+ custom_attribs: dict[str, Any] = {}
for keyword, value in kwargs.items():
if keyword in CUSTOM_ACTION_ATTRIBS:
custom_attribs[keyword] = value
@@ -832,7 +840,7 @@ def _add_argument_wrapper(
new_arg = orig_actions_container_add_argument(self, *args, **kwargs)
# Set the custom attributes
- new_arg.set_nargs_range(nargs_range) # type: ignore[arg-type, attr-defined]
+ new_arg.set_nargs_range(nargs_range) # type: ignore[attr-defined]
if choices_provider:
new_arg.set_choices_provider(choices_provider) # type: ignore[attr-defined]
@@ -868,7 +876,7 @@ def _get_nargs_pattern_wrapper(self: argparse.ArgumentParser, action: argparse.A
if nargs_range[1] == constants.INFINITY:
range_max = ''
else:
- range_max = nargs_range[1] # type: ignore[assignment]
+ range_max = nargs_range[1]
nargs_pattern = f'(-*A{{{nargs_range[0]},{range_max}}}-*)'
@@ -1028,9 +1036,14 @@ def _SubParsersAction_remove_parser(self: argparse._SubParsersAction, name: str)
############################################################################################################
-class Cmd2HelpFormatter(argparse.RawTextHelpFormatter):
+class Cmd2HelpFormatter(RichHelpFormatter):
"""Custom help formatter to configure ordering of help text"""
+ # Render markup in usage, help, description, and epilog text.
+ RichHelpFormatter.usage_markup = True
+ RichHelpFormatter.help_markup = True
+ RichHelpFormatter.text_markup = True
+
def _format_usage(
self,
usage: Optional[str],
@@ -1093,9 +1106,9 @@ def _format_usage(
# End cmd2 customization
# helper for wrapping lines
- def get_lines(parts: List[str], indent: str, prefix: Optional[str] = None) -> List[str]:
- lines: List[str] = []
- line: List[str] = []
+ def get_lines(parts: list[str], indent: str, prefix: Optional[str] = None) -> list[str]:
+ lines: list[str] = []
+ line: list[str] = []
if prefix is not None:
line_len = len(prefix) - 1
else:
@@ -1157,7 +1170,7 @@ def _format_action_invocation(self, action: argparse.Action) -> str:
return metavar
else:
- parts: List[str] = []
+ parts: list[str] = []
# if the Optional doesn't take a value, format is:
# -s, --long
@@ -1178,8 +1191,8 @@ def _format_action_invocation(self, action: argparse.Action) -> str:
def _determine_metavar(
self,
action: argparse.Action,
- default_metavar: Union[str, Tuple[str, ...]],
- ) -> Union[str, Tuple[str, ...]]:
+ default_metavar: Union[str, tuple[str, ...]],
+ ) -> Union[str, tuple[str, ...]]:
"""Custom method to determine what to use as the metavar value of an action"""
if action.metavar is not None:
result = action.metavar
@@ -1195,11 +1208,11 @@ def _determine_metavar(
def _metavar_formatter(
self,
action: argparse.Action,
- default_metavar: Union[str, Tuple[str, ...]],
- ) -> Callable[[int], Tuple[str, ...]]:
+ default_metavar: Union[str, tuple[str, ...]],
+ ) -> Callable[[int], tuple[str, ...]]:
metavar = self._determine_metavar(action, default_metavar)
- def format(tuple_size: int) -> Tuple[str, ...]:
+ def format(tuple_size: int) -> tuple[str, ...]:
if isinstance(metavar, tuple):
return metavar
else:
@@ -1207,7 +1220,7 @@ def format(tuple_size: int) -> Tuple[str, ...]:
return format
- def _format_args(self, action: argparse.Action, default_metavar: Union[str, Tuple[str, ...]]) -> str:
+ def _format_args(self, action: argparse.Action, default_metavar: Union[str, tuple[str, ...]]) -> str:
"""Customized to handle ranged nargs and make other output less verbose"""
metavar = self._determine_metavar(action, default_metavar)
metavar_formatter = self._metavar_formatter(action, default_metavar)
@@ -1235,6 +1248,84 @@ def _format_args(self, action: argparse.Action, default_metavar: Union[str, Tupl
return super()._format_args(action, default_metavar) # type: ignore[arg-type]
+class RawDescriptionCmd2HelpFormatter(
+ RawDescriptionRichHelpFormatter,
+ Cmd2HelpFormatter,
+):
+ """Cmd2 help message formatter which retains any formatting in descriptions and epilogs."""
+
+
+class RawTextCmd2HelpFormatter(
+ RawTextRichHelpFormatter,
+ Cmd2HelpFormatter,
+):
+ """Cmd2 help message formatter which retains formatting of all help text."""
+
+
+class ArgumentDefaultsCmd2HelpFormatter(
+ ArgumentDefaultsRichHelpFormatter,
+ Cmd2HelpFormatter,
+):
+ """Cmd2 help message formatter which adds default values to argument help."""
+
+
+class MetavarTypeCmd2HelpFormatter(
+ MetavarTypeRichHelpFormatter,
+ Cmd2HelpFormatter,
+):
+ """
+ Cmd2 help message formatter which uses the argument 'type' as the default
+ metavar value (instead of the argument 'dest').
+ """
+
+
+class TextGroup:
+ """
+ A block of text which is formatted like an argparse argument group, including a title.
+
+ Title:
+ Here is the first row of text.
+ Here is yet another row of text.
+ """
+
+ def __init__(
+ self,
+ title: str,
+ text: RenderableType,
+ formatter_creator: Callable[[], Cmd2HelpFormatter],
+ ) -> None:
+ """
+ :param title: the group's title
+ :param text: the group's text (string or object that may be rendered by Rich)
+ :param formatter_creator: callable which returns a Cmd2HelpFormatter instance
+ """
+ self.title = title
+ self.text = text
+ self.formatter_creator = formatter_creator
+
+ def __rich__(self) -> Group:
+ """Custom rendering logic."""
+ import rich
+
+ formatter = self.formatter_creator()
+
+ styled_title = rich.text.Text(
+ type(formatter).group_name_formatter(f"{self.title}:"),
+ style=formatter.styles["argparse.groups"],
+ )
+
+ # Left pad the text like an argparse argument group does
+ left_padding = formatter._indent_increment
+
+ text_table = rich.table.Table(
+ box=None,
+ show_header=False,
+ padding=(0, 0, 0, left_padding),
+ )
+ text_table.add_row(self.text)
+ return Group(styled_title, text_table)
+
+
class Cmd2ArgumentParser(argparse.ArgumentParser):
"""Custom ArgumentParser class that improves error and help output"""
@@ -1242,10 +1333,10 @@ def __init__(
self,
prog: Optional[str] = None,
usage: Optional[str] = None,
- description: Optional[str] = None,
- epilog: Optional[str] = None,
+ description: Optional[RenderableType] = None,
+ epilog: Optional[RenderableType] = None,
parents: Sequence[argparse.ArgumentParser] = (),
- formatter_class: Type[argparse.HelpFormatter] = Cmd2HelpFormatter,
+ formatter_class: Type[Cmd2HelpFormatter] = Cmd2HelpFormatter,
prefix_chars: str = '-',
fromfile_prefix_chars: Optional[str] = None,
argument_default: Optional[str] = None,
@@ -1265,10 +1356,10 @@ def __init__(
super(Cmd2ArgumentParser, self).__init__(
prog=prog,
usage=usage,
- description=description,
- epilog=epilog,
+ description=description, # type: ignore[arg-type]
+ epilog=epilog, # type: ignore[arg-type]
parents=parents if parents else [],
- formatter_class=formatter_class, # type: ignore[arg-type]
+ formatter_class=formatter_class,
prefix_chars=prefix_chars,
fromfile_prefix_chars=fromfile_prefix_chars,
argument_default=argument_default,
@@ -1277,6 +1368,10 @@ def __init__(
allow_abbrev=allow_abbrev,
)
+ # Recast to assist type checkers since in a Cmd2HelpFormatter, these can be Rich renderables.
+ self.description: Optional[RenderableType] = self.description # type: ignore[assignment]
+ self.epilog: Optional[RenderableType] = self.epilog # type: ignore[assignment]
+
self.set_ap_completer_type(ap_completer_type) # type: ignore[attr-defined]
def add_subparsers(self, **kwargs: Any) -> argparse._SubParsersAction: # type: ignore
@@ -1307,12 +1402,16 @@ def error(self, message: str) -> NoReturn:
formatted_message = ansi.style_error(formatted_message)
self.exit(2, f'{formatted_message}\n\n')
+ def _get_formatter(self) -> Cmd2HelpFormatter:
+ """Copy of _get_formatter() with a different return type to assist type checkers."""
+ return cast(Cmd2HelpFormatter, super()._get_formatter())
+
def format_help(self) -> str:
"""Copy of format_help() from argparse.ArgumentParser with tweaks to separately display required parameters"""
formatter = self._get_formatter()
# usage
- formatter.add_usage(self.usage, self._actions, self._mutually_exclusive_groups) # type: ignore[arg-type]
+ formatter.add_usage(self.usage, self._actions, self._mutually_exclusive_groups)
# description
formatter.add_text(self.description)
@@ -1368,6 +1467,10 @@ def _print_message(self, message: str, file: Optional[IO[str]] = None) -> None:
file = sys.stderr
ansi.style_aware_write(file, message)
+ def create_text_group(self, title: str, text: RenderableType) -> TextGroup:
+ """Create a TextGroup using this parser's formatter creator."""
+ return TextGroup(title, text, self._get_formatter)
+
class Cmd2AttributeWrapper:
"""
@@ -1388,14 +1491,21 @@ def set(self, new_val: Any) -> None:
self.__attribute = new_val
-# The default ArgumentParser class for a cmd2 app
-DEFAULT_ARGUMENT_PARSER: Type[argparse.ArgumentParser] = Cmd2ArgumentParser
+# Parser type used by cmd2's built-in commands.
+# Set it using cmd2.set_default_argument_parser_type().
+DEFAULT_ARGUMENT_PARSER: Type[Cmd2ArgumentParser] = Cmd2ArgumentParser
-def set_default_argument_parser_type(parser_type: Type[argparse.ArgumentParser]) -> None:
+def set_default_argument_parser_type(parser_type: Type[Cmd2ArgumentParser]) -> None:
"""
- Set the default ArgumentParser class for a cmd2 app. This must be called prior to loading cmd2.py if
- you want to override the parser for cmd2's built-in commands. See examples/override_parser.py.
+ Set the default ArgumentParser class for cmd2's built-in commands.
+
+ Since built-in commands rely on customizations made in Cmd2ArgumentParser,
+ your custom parser class should inherit from Cmd2ArgumentParser.
+
+ This should be called prior to instantiating your CLI object.
+
+ See examples/custom_parser.py.
"""
global DEFAULT_ARGUMENT_PARSER
DEFAULT_ARGUMENT_PARSER = parser_type
diff --git a/cmd2/clipboard.py b/cmd2/clipboard.py
index 454e3484..376b0015 100644
--- a/cmd2/clipboard.py
+++ b/cmd2/clipboard.py
@@ -5,7 +5,7 @@
import typing
-import pyperclip # type: ignore[import]
+import pyperclip # type: ignore[import-untyped]
def get_paste_buffer() -> str:
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 7e34b7dc..123b7705 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -37,7 +37,6 @@
import inspect
import os
import pydoc
-import re
import sys
import tempfile
import threading
@@ -60,14 +59,10 @@
TYPE_CHECKING,
Any,
Callable,
- Dict,
Iterable,
- List,
Mapping,
Optional,
- Set,
TextIO,
- Tuple,
Type,
TypeVar,
Union,
@@ -84,6 +79,7 @@
)
from .argparse_custom import (
ChoicesProviderFunc,
+ Cmd2ArgumentParser,
CompleterFunc,
CompletionItem,
)
@@ -122,8 +118,6 @@
single_line_format,
)
from .parsing import (
- Macro,
- MacroArg,
Statement,
StatementParser,
shlex_split,
@@ -201,7 +195,7 @@ class _SavedCmd2Env:
def __init__(self) -> None:
self.readline_settings = _SavedReadlineSettings()
self.readline_module: Optional[ModuleType] = None
- self.history: List[str] = []
+ self.history: list[str] = []
self.sys_stdout: Optional[TextIO] = None
self.sys_stdin: Optional[TextIO] = None
@@ -230,7 +224,7 @@ def __init__(self, cmd: 'Cmd') -> None:
# Keyed by the fully qualified method names. This is more reliable than
# the methods themselves, since wrapping a method will change its address.
- self._parsers: Dict[str, argparse.ArgumentParser] = {}
+ self._parsers: dict[str, argparse.ArgumentParser] = {}
@staticmethod
def _fully_qualified_name(command_method: CommandFunc) -> str:
@@ -279,8 +273,8 @@ def get(self, command_method: CommandFunc) -> Optional[argparse.ArgumentParser]:
_set_parser_prog(parser, command)
# If the description has not been set, then use the method docstring if one exists
- if parser.description is None and hasattr(command_method, '__wrapped__') and command_method.__wrapped__.__doc__:
- parser.description = strip_doc_annotations(command_method.__wrapped__.__doc__)
+ if parser.description is None and command_method.__doc__:
+ parser.description = strip_doc_annotations(command_method.__doc__)
self._parsers[full_method_name] = parser
@@ -325,11 +319,11 @@ def __init__(
include_py: bool = False,
include_ipy: bool = False,
allow_cli_args: bool = True,
- transcript_files: Optional[List[str]] = None,
+ transcript_files: Optional[list[str]] = None,
allow_redirection: bool = True,
- multiline_commands: Optional[List[str]] = None,
- terminators: Optional[List[str]] = None,
- shortcuts: Optional[Dict[str, str]] = None,
+ multiline_commands: Optional[list[str]] = None,
+ terminators: Optional[list[str]] = None,
+ shortcuts: Optional[dict[str, str]] = None,
command_sets: Optional[Iterable[CommandSet]] = None,
auto_load_commands: bool = True,
allow_clipboard: bool = True,
@@ -419,12 +413,12 @@ def __init__(
self.max_completion_items = 50
# A dictionary mapping settable names to their Settable instance
- self._settables: Dict[str, Settable] = dict()
+ self._settables: dict[str, Settable] = dict()
self._always_prefix_settables: bool = False
# CommandSet containers
- self._installed_command_sets: Set[CommandSet] = set()
- self._cmd_to_command_sets: Dict[str, CommandSet] = {}
+ self._installed_command_sets: set[CommandSet] = set()
+ self._cmd_to_command_sets: dict[str, CommandSet] = {}
self.build_settables()
@@ -445,17 +439,14 @@ def __init__(
# Commands to exclude from the history command
self.exclude_from_history = ['eof', 'history']
- # Dictionary of macro names and their values
- self.macros: Dict[str, Macro] = dict()
-
# Keeps track of typed command history in the Python shell
- self._py_history: List[str] = []
+ self._py_history: list[str] = []
# The name by which Python environments refer to the PyBridge to call app commands
self.py_bridge_name = 'app'
# Defines app-specific variables/functions available in Python shells and pyscripts
- self.py_locals: Dict[str, Any] = dict()
+ self.py_locals: dict[str, Any] = dict()
# True if running inside a Python shell or pyscript, False otherwise
self._in_py = False
@@ -468,7 +459,7 @@ def __init__(
self.last_result: Any = None
# Used by run_script command to store current script dir as a LIFO queue to support _relative_run_script command
- self._script_dir: List[str] = []
+ self._script_dir: list[str] = []
# Context manager used to protect critical sections in the main thread from stopping due to a KeyboardInterrupt
self.sigint_protection = utils.ContextFlag()
@@ -493,13 +484,13 @@ def __init__(
self.help_error = "No help on {}"
# The error that prints when a non-existent command is run
- self.default_error = "{} is not a recognized command, alias, or macro."
+ self.default_error = "{} is not a recognized command or alias."
# If non-empty, this string will be displayed if a broken pipe error occurs
self.broken_pipe_warning = ''
# Commands that will run at the beginning of the command loop
- self._startup_commands: List[str] = []
+ self._startup_commands: list[str] = []
# If a startup script is provided and exists, then execute it in the startup commands
if startup_script:
@@ -511,7 +502,7 @@ def __init__(
self._startup_commands.append(script_cmd)
# Transcript files to run instead of interactive command loop
- self._transcript_files: Optional[List[str]] = None
+ self._transcript_files: Optional[list[str]] = None
# Check for command line args
if allow_cli_args:
@@ -554,7 +545,7 @@ def __init__(
# Commands that have been disabled from use. This is to support commands that are only available
# during specific states of the application. This dictionary's keys are the command names and its
# values are DisabledCommand objects.
- self.disabled_commands: Dict[str, DisabledCommand] = dict()
+ self.disabled_commands: dict[str, DisabledCommand] = dict()
# If any command has been categorized, then all other commands that haven't been categorized
# will display under this section in the help output.
@@ -564,7 +555,7 @@ def __init__(
# If natural sorting is preferred, then set this to NATURAL_SORT_KEY.
# cmd2 uses this key for sorting:
# command and category names
- # alias, macro, settable, and shortcut names
+ # alias, settable, and shortcut names
# tab completion results when self.matches_sorted is False
self.default_sort_key = Cmd.ALPHABETICAL_SORT_KEY
@@ -592,7 +583,7 @@ def __init__(
self.formatted_completions = ''
# Used by complete() for readline tab completion
- self.completion_matches: List[str] = []
+ self.completion_matches: list[str] = []
# Use this list if you need to display tab completion suggestions that are different than the actual text
# of the matches. For instance, if you are completing strings that contain a common delimiter and you only
@@ -600,7 +591,7 @@ def __init__(
# still must be returned from your completer function. For an example, look at path_complete() which
# uses this to show only the basename of paths as the suggestions. delimiter_complete() also populates
# this list. These are ignored if self.formatted_completions is populated.
- self.display_matches: List[str] = []
+ self.display_matches: list[str] = []
# Used by functions like path_complete() and delimiter_complete() to properly
# quote matches that are completed in a delimited fashion
@@ -642,7 +633,7 @@ def __init__(
# the current command being executed
self.current_command: Optional[Statement] = None
- def find_commandsets(self, commandset_type: Type[CommandSet], *, subclass_match: bool = False) -> List[CommandSet]:
+ def find_commandsets(self, commandset_type: Type[CommandSet], *, subclass_match: bool = False) -> list[CommandSet]:
"""
Find all CommandSets that match the provided CommandSet type.
By default, locates a CommandSet that is an exact type match but may optionally return all CommandSets that
@@ -671,7 +662,7 @@ def _autoload_commands(self) -> None:
all_commandset_defs = CommandSet.__subclasses__()
existing_commandset_types = [type(command_set) for command_set in self._installed_command_sets]
- def load_commandset_by_type(commandset_types: List[Type[CommandSet]]) -> None:
+ def load_commandset_by_type(commandset_types: list[Type[CommandSet]]) -> None:
for cmdset_type in commandset_types:
# check if the type has sub-classes. We will only auto-load leaf class types.
subclasses = cmdset_type.__subclasses__()
@@ -715,7 +706,7 @@ def register_command_set(self, cmdset: CommandSet) -> None:
cmdset.on_register(self)
methods = cast(
- List[Tuple[str, Callable[..., Any]]],
+ list[tuple[str, Callable[..., Any]]],
inspect.getmembers(
cmdset,
predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type]
@@ -825,11 +816,6 @@ def _install_command_function(self, command_func_name: str, command_method: Comm
self.pwarning(f"Deleting alias '{command}' because it shares its name with a new command")
del self.aliases[command]
- # Check if command shares a name with a macro
- if command in self.macros:
- self.pwarning(f"Deleting macro '{command}' because it shares its name with a new command")
- del self.macros[command]
-
setattr(self, command_func_name, command_method)
def _install_completer_function(self, cmd_name: str, cmd_completer: CompleterFunc) -> None:
@@ -857,7 +843,7 @@ def unregister_command_set(self, cmdset: CommandSet) -> None:
cmdset.on_unregister()
self._unregister_subcommands(cmdset)
- methods: List[Tuple[str, Callable[..., Any]]] = inspect.getmembers(
+ methods: list[tuple[str, Callable[..., Any]]] = inspect.getmembers(
cmdset,
predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type]
and hasattr(meth, '__name__')
@@ -903,7 +889,7 @@ def check_parser_uninstallable(parser: argparse.ArgumentParser) -> None:
check_parser_uninstallable(subparser)
break
- methods: List[Tuple[str, Callable[..., Any]]] = inspect.getmembers(
+ methods: list[tuple[str, Callable[..., Any]]] = inspect.getmembers(
cmdset,
predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type]
and hasattr(meth, '__name__')
@@ -966,7 +952,7 @@ def _register_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
f"Could not find argparser for command '{command_name}' needed by subcommand: {str(method)}"
)
- def find_subcommand(action: argparse.ArgumentParser, subcmd_names: List[str]) -> argparse.ArgumentParser:
+ def find_subcommand(action: argparse.ArgumentParser, subcmd_names: list[str]) -> argparse.ArgumentParser:
if not subcmd_names:
return action
cur_subcmd = subcmd_names.pop(0)
@@ -1072,7 +1058,7 @@ def _unregister_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
for action in command_parser._actions:
if isinstance(action, argparse._SubParsersAction):
- action.remove_parser(subcommand_name) # type: ignore[arg-type,attr-defined]
+ action.remove_parser(subcommand_name) # type: ignore[attr-defined]
break
@property
@@ -1144,7 +1130,7 @@ def remove_settable(self, name: str) -> None:
def build_settables(self) -> None:
"""Create the dictionary of user-settable parameters"""
- def get_allow_style_choices(cli_self: Cmd) -> List[str]:
+ def get_allow_style_choices(cli_self: Cmd) -> list[str]:
"""Used to tab complete allow_style values"""
return [val.name.lower() for val in ansi.AllowStyle]
@@ -1387,7 +1373,7 @@ def _reset_completion_defaults(self) -> None:
elif rl_type == RlType.PYREADLINE:
readline.rl.mode._display_completions = self._display_matches_pyreadline
- def tokens_for_completion(self, line: str, begidx: int, endidx: int) -> Tuple[List[str], List[str]]:
+ def tokens_for_completion(self, line: str, begidx: int, endidx: int) -> tuple[list[str], list[str]]:
"""Used by tab completion functions to get all tokens through the one being completed.
:param line: the current input line with leading whitespace removed
@@ -1458,7 +1444,7 @@ def basic_complete(
begidx: int,
endidx: int,
match_against: Iterable[str],
- ) -> List[str]:
+ ) -> list[str]:
"""
Basic tab completion function that matches against a list of strings without considering line contents
or cursor position. The args required by this function are defined in the header of Python's cmd.py.
@@ -1480,7 +1466,7 @@ def delimiter_complete(
endidx: int,
match_against: Iterable[str],
delimiter: str,
- ) -> List[str]:
+ ) -> list[str]:
"""
Performs tab completion against a list but each match is split on a delimiter and only
the portion of the match being tab completed is shown as the completion suggestions.
@@ -1546,10 +1532,10 @@ def flag_based_complete(
line: str,
begidx: int,
endidx: int,
- flag_dict: Dict[str, Union[Iterable[str], CompleterFunc]],
+ flag_dict: dict[str, Union[Iterable[str], CompleterFunc]],
*,
all_else: Union[None, Iterable[str], CompleterFunc] = None,
- ) -> List[str]:
+ ) -> list[str]:
"""Tab completes based on a particular flag preceding the token being completed.
:param text: the string prefix we are attempting to match (all matches must begin with it)
@@ -1598,7 +1584,7 @@ def index_based_complete(
index_dict: Mapping[int, Union[Iterable[str], CompleterFunc]],
*,
all_else: Optional[Union[Iterable[str], CompleterFunc]] = None,
- ) -> List[str]:
+ ) -> list[str]:
"""Tab completes based on a fixed position in the input string.
:param text: the string prefix we are attempting to match (all matches must begin with it)
@@ -1643,7 +1629,7 @@ def index_based_complete(
def path_complete(
self, text: str, line: str, begidx: int, endidx: int, *, path_filter: Optional[Callable[[str], bool]] = None
- ) -> List[str]:
+ ) -> list[str]:
"""Performs completion of local file system paths
:param text: the string prefix we are attempting to match (all matches must begin with it)
@@ -1657,7 +1643,7 @@ def path_complete(
"""
# Used to complete ~ and ~user strings
- def complete_users() -> List[str]:
+ def complete_users() -> list[str]:
users = []
# Windows lacks the pwd module so we can't get a list of users.
@@ -1784,7 +1770,7 @@ def complete_users() -> List[str]:
return matches
- def shell_cmd_complete(self, text: str, line: str, begidx: int, endidx: int, *, complete_blank: bool = False) -> List[str]:
+ def shell_cmd_complete(self, text: str, line: str, begidx: int, endidx: int, *, complete_blank: bool = False) -> list[str]:
"""Performs completion of executables either in a user's path or a given path
:param text: the string prefix we are attempting to match (all matches must begin with it)
@@ -1809,7 +1795,7 @@ def shell_cmd_complete(self, text: str, line: str, begidx: int, endidx: int, *,
text, line, begidx, endidx, path_filter=lambda path: os.path.isdir(path) or os.access(path, os.X_OK)
)
- def _redirect_complete(self, text: str, line: str, begidx: int, endidx: int, compfunc: CompleterFunc) -> List[str]:
+ def _redirect_complete(self, text: str, line: str, begidx: int, endidx: int, compfunc: CompleterFunc) -> list[str]:
"""Called by complete() as the first tab completion function for all commands
It determines if it should tab complete for redirection (|, >, >>) or use the
completer function for the current command
@@ -1890,7 +1876,7 @@ def _redirect_complete(self, text: str, line: str, begidx: int, endidx: int, com
return compfunc(text, line, begidx, endidx)
@staticmethod
- def _pad_matches_to_display(matches_to_display: List[str]) -> Tuple[List[str], int]: # pragma: no cover
+ def _pad_matches_to_display(matches_to_display: list[str]) -> tuple[list[str], int]: # pragma: no cover
"""Adds padding to the matches being displayed as tab completion suggestions.
The default padding of readline/pyreadine is small and not visually appealing
especially if matches have spaces. It appears very squished together.
@@ -1912,7 +1898,7 @@ def _pad_matches_to_display(matches_to_display: List[str]) -> Tuple[List[str], i
return [cur_match + padding for cur_match in matches_to_display], len(padding)
def _display_matches_gnu_readline(
- self, substitution: str, matches: List[str], longest_match_length: int
+ self, substitution: str, matches: list[str], longest_match_length: int
) -> None: # pragma: no cover
"""Prints a match list using GNU readline's rl_display_match_list()
@@ -1960,7 +1946,7 @@ def _display_matches_gnu_readline(
# rl_display_match_list() expects matches to be in argv format where
# substitution is the first element, followed by the matches, and then a NULL.
- strings_array = cast(List[Optional[bytes]], (ctypes.c_char_p * (1 + len(encoded_matches) + 1))())
+ strings_array = cast(list[Optional[bytes]], (ctypes.c_char_p * (1 + len(encoded_matches) + 1))())
# Copy in the encoded strings and add a NULL to the end
strings_array[0] = encoded_substitution
@@ -1973,7 +1959,7 @@ def _display_matches_gnu_readline(
# Redraw prompt and input line
rl_force_redisplay()
- def _display_matches_pyreadline(self, matches: List[str]) -> None: # pragma: no cover
+ def _display_matches_pyreadline(self, matches: list[str]) -> None: # pragma: no cover
"""Prints a match list using pyreadline3's _display_completions()
:param matches: the tab completion matches to display
@@ -2074,12 +2060,8 @@ def _perform_completion(
# Determine the completer function to use for the command's argument
if custom_settings is None:
- # Check if a macro was entered
- if command in self.macros:
- completer_func = self.path_complete
-
# Check if a command was entered
- elif command in self.get_all_commands():
+ if command in self.get_all_commands():
# Get the completer function for this command
func_attr = getattr(self, constants.COMPLETER_FUNC_PREFIX + command, None)
@@ -2103,15 +2085,15 @@ def _perform_completion(
completer.complete, tokens=raw_tokens[1:] if preserve_quotes else tokens[1:], cmd_set=cmd_set
)
else:
- completer_func = self.completedefault # type: ignore[assignment]
+ completer_func = self.completedefault
- # Not a recognized macro or command
+ # Not a recognized command
else:
# Check if this command should be run as a shell command
if self.default_to_shell and command in utils.get_exes_in_path(command):
completer_func = self.path_complete
else:
- completer_func = self.completedefault # type: ignore[assignment]
+ completer_func = self.completedefault
# Otherwise we are completing the command token or performing custom completion
else:
@@ -2268,8 +2250,8 @@ def complete( # type: ignore[override]
parser.add_argument(
'command',
metavar="COMMAND",
- help="command, alias, or macro name",
- choices=self._get_commands_aliases_and_macros_for_completion(),
+ help="command or alias name",
+ choices=self._get_commands_and_aliases_for_completion(),
)
custom_settings = utils.CustomCompletionSettings(parser)
@@ -2320,15 +2302,15 @@ def in_pyscript(self) -> bool:
return self._in_py
@property
- def aliases(self) -> Dict[str, str]:
+ def aliases(self) -> dict[str, str]:
"""Read-only property to access the aliases stored in the StatementParser"""
return self.statement_parser.aliases
- def get_names(self) -> List[str]:
+ def get_names(self) -> list[str]:
"""Return an alphabetized list of names comprising the attributes of the cmd2 class instance."""
return dir(self)
- def get_all_commands(self) -> List[str]:
+ def get_all_commands(self) -> list[str]:
"""Return a list of all commands"""
return [
name[len(constants.COMMAND_FUNC_PREFIX) :]
@@ -2336,7 +2318,7 @@ def get_all_commands(self) -> List[str]:
if name.startswith(constants.COMMAND_FUNC_PREFIX) and callable(getattr(self, name))
]
- def get_visible_commands(self) -> List[str]:
+ def get_visible_commands(self) -> list[str]:
"""Return a list of commands that have not been hidden or disabled"""
return [
command
@@ -2347,9 +2329,9 @@ def get_visible_commands(self) -> List[str]:
# Table displayed when tab completing aliases
_alias_completion_table = SimpleTable([Column('Value', width=80)], divider_char=None)
- def _get_alias_completion_items(self) -> List[CompletionItem]:
+ def _get_alias_completion_items(self) -> list[CompletionItem]:
"""Return list of alias names and values as CompletionItems"""
- results: List[CompletionItem] = []
+ results: list[CompletionItem] = []
for cur_key in self.aliases:
row_data = [self.aliases[cur_key]]
@@ -2357,25 +2339,12 @@ def _get_alias_completion_items(self) -> List[CompletionItem]:
return results
- # Table displayed when tab completing macros
- _macro_completion_table = SimpleTable([Column('Value', width=80)], divider_char=None)
-
- def _get_macro_completion_items(self) -> List[CompletionItem]:
- """Return list of macro names and values as CompletionItems"""
- results: List[CompletionItem] = []
-
- for cur_key in self.macros:
- row_data = [self.macros[cur_key].value]
- results.append(CompletionItem(cur_key, self._macro_completion_table.generate_data_row(row_data)))
-
- return results
-
# Table displayed when tab completing Settables
_settable_completion_table = SimpleTable([Column('Value', width=30), Column('Description', width=60)], divider_char=None)
- def _get_settable_completion_items(self) -> List[CompletionItem]:
+ def _get_settable_completion_items(self) -> list[CompletionItem]:
"""Return list of Settable names, values, and descriptions as CompletionItems"""
- results: List[CompletionItem] = []
+ results: list[CompletionItem] = []
for cur_key in self.settables:
row_data = [self.settables[cur_key].get_value(), self.settables[cur_key].description]
@@ -2383,14 +2352,13 @@ def _get_settable_completion_items(self) -> List[CompletionItem]:
return results
- def _get_commands_aliases_and_macros_for_completion(self) -> List[str]:
- """Return a list of visible commands, aliases, and macros for tab completion"""
+ def _get_commands_and_aliases_for_completion(self) -> list[str]:
+ """Return a list of visible commands and aliases for tab completion"""
visible_commands = set(self.get_visible_commands())
alias_names = set(self.aliases)
- macro_names = set(self.macros)
- return list(visible_commands | alias_names | macro_names)
+ return list(visible_commands | alias_names)
- def get_help_topics(self) -> List[str]:
+ def get_help_topics(self) -> list[str]:
"""Return a list of help topics"""
all_topics = [
name[len(constants.HELP_FUNC_PREFIX) :]
@@ -2490,7 +2458,7 @@ def postloop(self) -> None:
"""
pass
- def parseline(self, line: str) -> Tuple[str, str, str]:
+ def parseline(self, line: str) -> tuple[str, str, str]:
"""Parse the line into a command name and a string containing the arguments.
NOTE: This is an override of a parent class method. It is only used by other parent class methods.
@@ -2534,7 +2502,7 @@ def onecmd_plus_hooks(
try:
# Convert the line into a Statement
- statement = self._input_line_to_statement(line, orig_rl_history_length=orig_rl_history_length)
+ statement = self._complete_statement(line, orig_rl_history_length=orig_rl_history_length)
# call the postparsing hooks
postparsing_data = plugin.PostparsingData(False, statement)
@@ -2653,7 +2621,7 @@ def _run_cmdfinalization_hooks(self, stop: bool, statement: Optional[Statement])
def runcmds_plus_hooks(
self,
- cmds: Union[List[HistoryItem], List[str]],
+ cmds: Union[list[HistoryItem], list[str]],
*,
add_to_history: bool = True,
stop_on_keyboard_interrupt: bool = False,
@@ -2779,101 +2747,6 @@ def combine_rl_history(statement: Statement) -> None:
return statement
- def _input_line_to_statement(self, line: str, *, orig_rl_history_length: Optional[int] = None) -> Statement:
- """
- Parse the user's input line and convert it to a Statement, ensuring that all macros are also resolved
-
- :param line: the line being parsed
- :param orig_rl_history_length: Optional length of the readline history before the current command was typed.
- This is used to assist in combining multiline readline history entries and is only
- populated by cmd2. Defaults to None.
- :return: parsed command line as a Statement
- :raises Cmd2ShlexError: if a shlex error occurs (e.g. No closing quotation)
- :raises EmptyStatement: when the resulting Statement is blank
- """
- used_macros = []
- orig_line = None
-
- # Continue until all macros are resolved
- while True:
- # Make sure all input has been read and convert it to a Statement
- statement = self._complete_statement(line, orig_rl_history_length=orig_rl_history_length)
-
- # If this is the first loop iteration, save the original line and stop
- # combining multiline history entries in the remaining iterations.
- if orig_line is None:
- orig_line = statement.raw
- orig_rl_history_length = None
-
- # Check if this command matches a macro and wasn't already processed to avoid an infinite loop
- if statement.command in self.macros.keys() and statement.command not in used_macros:
- used_macros.append(statement.command)
- resolve_result = self._resolve_macro(statement)
- if resolve_result is None:
- raise EmptyStatement
- line = resolve_result
- else:
- break
-
- # This will be true when a macro was used
- if orig_line != statement.raw:
- # Build a Statement that contains the resolved macro line
- # but the originally typed line for its raw member.
- statement = Statement(
- statement.args,
- raw=orig_line,
- command=statement.command,
- arg_list=statement.arg_list,
- multiline_command=statement.multiline_command,
- terminator=statement.terminator,
- suffix=statement.suffix,
- pipe_to=statement.pipe_to,
- output=statement.output,
- output_to=statement.output_to,
- )
- return statement
-
- def _resolve_macro(self, statement: Statement) -> Optional[str]:
- """
- Resolve a macro and return the resulting string
-
- :param statement: the parsed statement from the command line
- :return: the resolved macro or None on error
- """
- if statement.command not in self.macros.keys():
- raise KeyError(f"{statement.command} is not a macro")
-
- macro = self.macros[statement.command]
-
- # Make sure enough arguments were passed in
- if len(statement.arg_list) < macro.minimum_arg_count:
- plural = '' if macro.minimum_arg_count == 1 else 's'
- self.perror(f"The macro '{statement.command}' expects at least {macro.minimum_arg_count} argument{plural}")
- return None
-
- # Resolve the arguments in reverse and read their values from statement.argv since those
- # are unquoted. Macro args should have been quoted when the macro was created.
- resolved = macro.value
- reverse_arg_list = sorted(macro.arg_list, key=lambda ma: ma.start_index, reverse=True)
-
- for macro_arg in reverse_arg_list:
- if macro_arg.is_escaped:
- to_replace = '{{' + macro_arg.number_str + '}}'
- replacement = '{' + macro_arg.number_str + '}'
- else:
- to_replace = '{' + macro_arg.number_str + '}'
- replacement = statement.argv[int(macro_arg.number_str)]
-
- parts = resolved.rsplit(to_replace, maxsplit=1)
- resolved = parts[0] + replacement + parts[1]
-
- # Append extra arguments and use statement.arg_list since these arguments need their quotes preserved
- for stmt_arg in statement.arg_list[macro.minimum_arg_count :]:
- resolved += ' ' + stmt_arg
-
- # Restore any terminator, suffix, redirection, etc.
- return resolved + statement.post_command
-
def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
"""Set up a command's output redirection for >, >>, and |.
@@ -2907,7 +2780,7 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
# Create pipe process in a separate group to isolate our signals from it. If a Ctrl-C event occurs,
# our sigint handler will forward it only to the most recent pipe process. This makes sure pipe
# processes close in the right order (most recent first).
- kwargs: Dict[str, Any] = dict()
+ kwargs: dict[str, Any] = dict()
if sys.platform == 'win32':
kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP
else:
@@ -2919,11 +2792,11 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
kwargs['executable'] = shell
# For any stream that is a StdSim, we will use a pipe so we can capture its output
- proc = subprocess.Popen( # type: ignore[call-overload]
+ proc = subprocess.Popen(
statement.pipe_to,
stdin=subproc_stdin,
stdout=subprocess.PIPE if isinstance(self.stdout, utils.StdSim) else self.stdout, # type: ignore[unreachable]
- stderr=subprocess.PIPE if isinstance(sys.stderr, utils.StdSim) else sys.stderr, # type: ignore[unreachable]
+ stderr=subprocess.PIPE if isinstance(sys.stderr, utils.StdSim) else sys.stderr,
shell=True,
**kwargs,
)
@@ -2943,7 +2816,7 @@ def _redirect_output(self, statement: Statement) -> utils.RedirectionSavedState:
new_stdout.close()
raise RedirectionError(f'Pipe process exited with code {proc.returncode} before command could run')
else:
- redir_saved_state.redirecting = True # type: ignore[unreachable]
+ redir_saved_state.redirecting = True
cmd_pipe_proc_reader = utils.ProcReader(proc, cast(TextIO, self.stdout), sys.stderr)
sys.stdout = self.stdout = new_stdout
@@ -3049,7 +2922,7 @@ def onecmd(self, statement: Union[Statement, str], *, add_to_history: bool = Tru
"""
# For backwards compatibility with cmd, allow a str to be passed in
if not isinstance(statement, Statement):
- statement = self._input_line_to_statement(statement)
+ statement = self._complete_statement(statement)
func = self.cmd_func(statement.command)
if func:
@@ -3098,7 +2971,7 @@ def read_input(
self,
prompt: str,
*,
- history: Optional[List[str]] = None,
+ history: Optional[list[str]] = None,
completion_mode: utils.CompletionMode = utils.CompletionMode.NONE,
preserve_quotes: bool = False,
choices: Optional[Iterable[Any]] = None,
@@ -3139,7 +3012,7 @@ def read_input(
"""
readline_configured = False
saved_completer: Optional[CompleterFunc] = None
- saved_history: Optional[List[str]] = None
+ saved_history: Optional[list[str]] = None
def configure_readline() -> None:
"""Configure readline tab completion and history"""
@@ -3174,7 +3047,7 @@ def complete_none(text: str, state: int) -> Optional[str]: # pragma: no cover
parser.add_argument(
'arg',
suppress_tab_hint=True,
- choices=choices, # type: ignore[arg-type]
+ choices=choices,
choices_provider=choices_provider,
completer=completer,
)
@@ -3380,50 +3253,67 @@ def _cmdloop(self) -> None:
#############################################################
# Top-level parser for alias
- alias_description = "Manage aliases\n\nAn alias is a command that enables replacement of a word by another string."
- alias_epilog = "See also:\n macro"
- alias_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_description, epilog=alias_epilog)
- alias_parser.add_subparsers(metavar='SUBCOMMAND', required=True)
+ @staticmethod
+ def _build_alias_parser() -> Cmd2ArgumentParser:
+ alias_description = "Manage aliases\n\nAn alias is a command that enables replacement of a word by another string."
+ alias_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_description)
+ alias_parser.add_subparsers(metavar='SUBCOMMAND', required=True)
+
+ return alias_parser
# Preserve quotes since we are passing strings to other commands
- @with_argparser(alias_parser, preserve_quotes=True)
+ @with_argparser(_build_alias_parser, preserve_quotes=True)
def do_alias(self, args: argparse.Namespace) -> None:
- """Manage aliases"""
+ """Manage aliases."""
# Call handler for whatever subcommand was selected
handler = args.cmd2_handler.get()
handler(args)
# alias -> create
- alias_create_description = "Create or overwrite an alias"
-
- alias_create_epilog = (
- "Notes:\n"
- " If you want to use redirection, pipes, or terminators in the value of the\n"
- " alias, then quote them.\n"
- "\n"
- " Since aliases are resolved during parsing, tab completion will function as\n"
- " it would for the actual command the alias resolves to.\n"
- "\n"
- "Examples:\n"
- " alias create ls !ls -lF\n"
- " alias create show_log !cat \"log file.txt\"\n"
- " alias create save_results print_results \">\" out.txt\n"
- )
+ @staticmethod
+ def _build_alias_create_parser() -> Cmd2ArgumentParser:
+ from rich.console import Group
- alias_create_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(
- description=alias_create_description, epilog=alias_create_epilog
- )
- alias_create_parser.add_argument('name', help='name of this alias')
- alias_create_parser.add_argument(
- 'command', help='what the alias resolves to', choices_provider=_get_commands_aliases_and_macros_for_completion
- )
- alias_create_parser.add_argument(
- 'command_args', nargs=argparse.REMAINDER, help='arguments to pass to command', completer=path_complete
- )
+ alias_create_description = "Create or overwrite an alias."
+
+ alias_create_notes = (
+ "If you want to use redirection, pipes, or terminators in the value of the alias, then quote them.\n"
+ "\n"
+ "Since aliases are resolved during parsing, tab completion will function as it would "
+ "for the actual command the alias resolves to."
+ )
+
+ alias_create_examples = (
+ "alias create ls !ls -lF\n"
+ "alias create show_log !cat \"log file.txt\"\n"
+ "alias create save_results print_results \">\" out.txt"
+ )
+
+ alias_create_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_create_description)
- @as_subcommand_to('alias', 'create', alias_create_parser, help=alias_create_description.lower())
+ # Create epilog
+ notes_group = alias_create_parser.create_text_group("Notes", alias_create_notes)
+ examples_group = alias_create_parser.create_text_group("Examples", alias_create_examples)
+ alias_create_parser.epilog = Group(notes_group, "\n", examples_group)
+
+ alias_create_parser.add_argument('name', help='name of this alias')
+ alias_create_parser.add_argument(
+ 'command',
+ help='what the alias resolves to',
+ choices_provider=Cmd._get_commands_and_aliases_for_completion,
+ )
+ alias_create_parser.add_argument(
+ 'command_args',
+ nargs=argparse.REMAINDER,
+ help='arguments to pass to command',
+ completer=Cmd.path_complete,
+ )
+
+ return alias_create_parser
+
+ @as_subcommand_to('alias', 'create', _build_alias_create_parser, help="create or overwrite an alias")
def _alias_create(self, args: argparse.Namespace) -> None:
- """Create or overwrite an alias"""
+ """Create or overwrite an alias."""
self.last_result = False
# Validate the alias name
@@ -3436,10 +3326,6 @@ def _alias_create(self, args: argparse.Namespace) -> None:
self.perror("Alias cannot have the same name as a command")
return
- if args.name in self.macros:
- self.perror("Alias cannot have the same name as a macro")
- return
-
# Unquote redirection and terminator tokens
tokens_to_unquote = constants.REDIRECTION_TOKENS
tokens_to_unquote.extend(self.statement_parser.terminators)
@@ -3458,22 +3344,25 @@ def _alias_create(self, args: argparse.Namespace) -> None:
self.last_result = True
# alias -> delete
- alias_delete_help = "delete aliases"
- alias_delete_description = "Delete specified aliases or all aliases if --all is used"
-
- alias_delete_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_delete_description)
- alias_delete_parser.add_argument('-a', '--all', action='store_true', help="delete all aliases")
- alias_delete_parser.add_argument(
- 'names',
- nargs=argparse.ZERO_OR_MORE,
- help='alias(es) to delete',
- choices_provider=_get_alias_completion_items,
- descriptive_header=_alias_completion_table.generate_header(),
- )
+ @staticmethod
+ def _build_alias_delete_parser() -> Cmd2ArgumentParser:
+ alias_delete_description = "Delete specified aliases or all aliases if --all is used."
+
+ alias_delete_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_delete_description)
+ alias_delete_parser.add_argument('-a', '--all', action='store_true', help="delete all aliases")
+ alias_delete_parser.add_argument(
+ 'names',
+ nargs=argparse.ZERO_OR_MORE,
+ help='alias(es) to delete',
+ choices_provider=Cmd._get_alias_completion_items,
+ descriptive_header=Cmd._alias_completion_table.generate_header(),
+ )
+
+ return alias_delete_parser
- @as_subcommand_to('alias', 'delete', alias_delete_parser, help=alias_delete_help)
+ @as_subcommand_to('alias', 'delete', _build_alias_delete_parser, help="delete aliases")
def _alias_delete(self, args: argparse.Namespace) -> None:
- """Delete aliases"""
+ """Delete aliases."""
self.last_result = True
if args.all:
@@ -3491,27 +3380,25 @@ def _alias_delete(self, args: argparse.Namespace) -> None:
self.perror(f"Alias '{cur_name}' does not exist")
# alias -> list
- alias_list_help = "list aliases"
- alias_list_description = (
- "List specified aliases in a reusable form that can be saved to a startup\n"
- "script to preserve aliases across sessions\n"
- "\n"
- "Without arguments, all aliases will be listed."
- )
+ @staticmethod
+ def _build_alias_list_parser() -> Cmd2ArgumentParser:
+ alias_list_description = "List specified aliases or all aliases if no arguments are given."
+
+ alias_list_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_list_description)
+ alias_list_parser.add_argument(
+ 'names',
+ nargs=argparse.ZERO_OR_MORE,
+ help='alias(es) to list',
+ choices_provider=Cmd._get_alias_completion_items,
+ descriptive_header=Cmd._alias_completion_table.generate_header(),
+ )
- alias_list_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=alias_list_description)
- alias_list_parser.add_argument(
- 'names',
- nargs=argparse.ZERO_OR_MORE,
- help='alias(es) to list',
- choices_provider=_get_alias_completion_items,
- descriptive_header=_alias_completion_table.generate_header(),
- )
+ return alias_list_parser
- @as_subcommand_to('alias', 'list', alias_list_parser, help=alias_list_help)
+ @as_subcommand_to('alias', 'list', _build_alias_list_parser, help="list aliases")
def _alias_list(self, args: argparse.Namespace) -> None:
- """List some or all aliases as 'alias create' commands"""
- self.last_result = {} # Dict[alias_name, alias_value]
+ """List some or all aliases as 'alias create' commands."""
+ self.last_result = {} # dict[alias_name, alias_value]
tokens_to_quote = constants.REDIRECTION_TOKENS
tokens_to_quote.extend(self.statement_parser.terminators)
@@ -3521,7 +3408,7 @@ def _alias_list(self, args: argparse.Namespace) -> None:
else:
to_list = sorted(self.aliases, key=self.default_sort_key)
- not_found: List[str] = []
+ not_found: list[str] = []
for name in to_list:
if name not in self.aliases:
not_found.append(name)
@@ -3543,243 +3430,7 @@ def _alias_list(self, args: argparse.Namespace) -> None:
for name in not_found:
self.perror(f"Alias '{name}' not found")
- #############################################################
- # Parsers and functions for macro command and subcommands
- #############################################################
-
- # Top-level parser for macro
- macro_description = "Manage macros\n\nA macro is similar to an alias, but it can contain argument placeholders."
- macro_epilog = "See also:\n alias"
- macro_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=macro_description, epilog=macro_epilog)
- macro_parser.add_subparsers(metavar='SUBCOMMAND', required=True)
-
- # Preserve quotes since we are passing strings to other commands
- @with_argparser(macro_parser, preserve_quotes=True)
- def do_macro(self, args: argparse.Namespace) -> None:
- """Manage macros"""
- # Call handler for whatever subcommand was selected
- handler = args.cmd2_handler.get()
- handler(args)
-
- # macro -> create
- macro_create_help = "create or overwrite a macro"
- macro_create_description = "Create or overwrite a macro"
-
- macro_create_epilog = (
- "A macro is similar to an alias, but it can contain argument placeholders.\n"
- "Arguments are expressed when creating a macro using {#} notation where {1}\n"
- "means the first argument.\n"
- "\n"
- "The following creates a macro called my_macro that expects two arguments:\n"
- "\n"
- " macro create my_macro make_dinner --meat {1} --veggie {2}\n"
- "\n"
- "When the macro is called, the provided arguments are resolved and the\n"
- "assembled command is run. For example:\n"
- "\n"
- " my_macro beef broccoli ---> make_dinner --meat beef --veggie broccoli\n"
- "\n"
- "Notes:\n"
- " To use the literal string {1} in your command, escape it this way: {{1}}.\n"
- "\n"
- " Extra arguments passed to a macro are appended to resolved command.\n"
- "\n"
- " An argument number can be repeated in a macro. In the following example the\n"
- " first argument will populate both {1} instances.\n"
- "\n"
- " macro create ft file_taxes -p {1} -q {2} -r {1}\n"
- "\n"
- " To quote an argument in the resolved command, quote it during creation.\n"
- "\n"
- " macro create backup !cp \"{1}\" \"{1}.orig\"\n"
- "\n"
- " If you want to use redirection, pipes, or terminators in the value of the\n"
- " macro, then quote them.\n"
- "\n"
- " macro create show_results print_results -type {1} \"|\" less\n"
- "\n"
- " Because macros do not resolve until after hitting Enter, tab completion\n"
- " will only complete paths while typing a macro."
- )
-
- macro_create_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(
- description=macro_create_description, epilog=macro_create_epilog
- )
- macro_create_parser.add_argument('name', help='name of this macro')
- macro_create_parser.add_argument(
- 'command', help='what the macro resolves to', choices_provider=_get_commands_aliases_and_macros_for_completion
- )
- macro_create_parser.add_argument(
- 'command_args', nargs=argparse.REMAINDER, help='arguments to pass to command', completer=path_complete
- )
-
- @as_subcommand_to('macro', 'create', macro_create_parser, help=macro_create_help)
- def _macro_create(self, args: argparse.Namespace) -> None:
- """Create or overwrite a macro"""
- self.last_result = False
-
- # Validate the macro name
- valid, errmsg = self.statement_parser.is_valid_command(args.name)
- if not valid:
- self.perror(f"Invalid macro name: {errmsg}")
- return
-
- if args.name in self.get_all_commands():
- self.perror("Macro cannot have the same name as a command")
- return
-
- if args.name in self.aliases:
- self.perror("Macro cannot have the same name as an alias")
- return
-
- # Unquote redirection and terminator tokens
- tokens_to_unquote = constants.REDIRECTION_TOKENS
- tokens_to_unquote.extend(self.statement_parser.terminators)
- utils.unquote_specific_tokens(args.command_args, tokens_to_unquote)
-
- # Build the macro value string
- value = args.command
- if args.command_args:
- value += ' ' + ' '.join(args.command_args)
-
- # Find all normal arguments
- arg_list = []
- normal_matches = re.finditer(MacroArg.macro_normal_arg_pattern, value)
- max_arg_num = 0
- arg_nums = set()
-
- while True:
- try:
- cur_match = normal_matches.__next__()
-
- # Get the number string between the braces
- cur_num_str = re.findall(MacroArg.digit_pattern, cur_match.group())[0]
- cur_num = int(cur_num_str)
- if cur_num < 1:
- self.perror("Argument numbers must be greater than 0")
- return
-
- arg_nums.add(cur_num)
- if cur_num > max_arg_num:
- max_arg_num = cur_num
-
- arg_list.append(MacroArg(start_index=cur_match.start(), number_str=cur_num_str, is_escaped=False))
-
- except StopIteration:
- break
-
- # Make sure the argument numbers are continuous
- if len(arg_nums) != max_arg_num:
- self.perror(f"Not all numbers between 1 and {max_arg_num} are present in the argument placeholders")
- return
-
- # Find all escaped arguments
- escaped_matches = re.finditer(MacroArg.macro_escaped_arg_pattern, value)
-
- while True:
- try:
- cur_match = escaped_matches.__next__()
-
- # Get the number string between the braces
- cur_num_str = re.findall(MacroArg.digit_pattern, cur_match.group())[0]
-
- arg_list.append(MacroArg(start_index=cur_match.start(), number_str=cur_num_str, is_escaped=True))
- except StopIteration:
- break
-
- # Set the macro
- result = "overwritten" if args.name in self.macros else "created"
- self.poutput(f"Macro '{args.name}' {result}")
-
- self.macros[args.name] = Macro(name=args.name, value=value, minimum_arg_count=max_arg_num, arg_list=arg_list)
- self.last_result = True
-
- # macro -> delete
- macro_delete_help = "delete macros"
- macro_delete_description = "Delete specified macros or all macros if --all is used"
- macro_delete_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=macro_delete_description)
- macro_delete_parser.add_argument('-a', '--all', action='store_true', help="delete all macros")
- macro_delete_parser.add_argument(
- 'names',
- nargs=argparse.ZERO_OR_MORE,
- help='macro(s) to delete',
- choices_provider=_get_macro_completion_items,
- descriptive_header=_macro_completion_table.generate_header(),
- )
-
- @as_subcommand_to('macro', 'delete', macro_delete_parser, help=macro_delete_help)
- def _macro_delete(self, args: argparse.Namespace) -> None:
- """Delete macros"""
- self.last_result = True
-
- if args.all:
- self.macros.clear()
- self.poutput("All macros deleted")
- elif not args.names:
- self.perror("Either --all or macro name(s) must be specified")
- self.last_result = False
- else:
- for cur_name in utils.remove_duplicates(args.names):
- if cur_name in self.macros:
- del self.macros[cur_name]
- self.poutput(f"Macro '{cur_name}' deleted")
- else:
- self.perror(f"Macro '{cur_name}' does not exist")
-
- # macro -> list
- macro_list_help = "list macros"
- macro_list_description = (
- "List specified macros in a reusable form that can be saved to a startup script\n"
- "to preserve macros across sessions\n"
- "\n"
- "Without arguments, all macros will be listed."
- )
-
- macro_list_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=macro_list_description)
- macro_list_parser.add_argument(
- 'names',
- nargs=argparse.ZERO_OR_MORE,
- help='macro(s) to list',
- choices_provider=_get_macro_completion_items,
- descriptive_header=_macro_completion_table.generate_header(),
- )
-
- @as_subcommand_to('macro', 'list', macro_list_parser, help=macro_list_help)
- def _macro_list(self, args: argparse.Namespace) -> None:
- """List some or all macros as 'macro create' commands"""
- self.last_result = {} # Dict[macro_name, macro_value]
-
- tokens_to_quote = constants.REDIRECTION_TOKENS
- tokens_to_quote.extend(self.statement_parser.terminators)
-
- if args.names:
- to_list = utils.remove_duplicates(args.names)
- else:
- to_list = sorted(self.macros, key=self.default_sort_key)
-
- not_found: List[str] = []
- for name in to_list:
- if name not in self.macros:
- not_found.append(name)
- continue
-
- # Quote redirection and terminator tokens for the 'macro create' command
- tokens = shlex_split(self.macros[name].value)
- command = tokens[0]
- command_args = tokens[1:]
- utils.quote_specific_tokens(command_args, tokens_to_quote)
-
- val = command
- if command_args:
- val += ' ' + ' '.join(command_args)
-
- self.poutput(f"macro create {name} {val}")
- self.last_result[name] = val
-
- for name in not_found:
- self.perror(f"Macro '{name}' not found")
-
- def complete_help_command(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
+ def complete_help_command(self, text: str, line: str, begidx: int, endidx: int) -> list[str]:
"""Completes the command argument of help"""
# Complete token against topics and visible commands
@@ -3789,8 +3440,8 @@ def complete_help_command(self, text: str, line: str, begidx: int, endidx: int)
return self.basic_complete(text, line, begidx, endidx, strs_to_match)
def complete_help_subcommands(
- self, text: str, line: str, begidx: int, endidx: int, arg_tokens: Dict[str, List[str]]
- ) -> List[str]:
+ self, text: str, line: str, begidx: int, endidx: int, arg_tokens: dict[str, list[str]]
+ ) -> list[str]:
"""Completes the subcommands argument of help"""
# Make sure we have a command whose subcommands we will complete
@@ -3805,26 +3456,38 @@ def complete_help_subcommands(
completer = argparse_completer.DEFAULT_AP_COMPLETER(argparser, self)
return completer.complete_subcommand_help(text, line, begidx, endidx, arg_tokens['subcommands'])
- help_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(
- description="List available commands or provide detailed help for a specific command"
- )
- help_parser.add_argument(
- '-v', '--verbose', action='store_true', help="print a list of all commands with descriptions of each"
- )
- help_parser.add_argument(
- 'command', nargs=argparse.OPTIONAL, help="command to retrieve help for", completer=complete_help_command
- )
- help_parser.add_argument(
- 'subcommands', nargs=argparse.REMAINDER, help="subcommand(s) to retrieve help for", completer=complete_help_subcommands
- )
+ @staticmethod
+ def _build_help_parser() -> Cmd2ArgumentParser:
+ help_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(
+ description="List available commands or provide detailed help for a specific command."
+ )
+ help_parser.add_argument(
+ '-v',
+ '--verbose',
+ action='store_true',
+ help="print a list of all commands with descriptions of each",
+ )
+ help_parser.add_argument(
+ 'command',
+ nargs=argparse.OPTIONAL,
+ help="command to retrieve help for",
+ completer=Cmd.complete_help_command,
+ )
+ help_parser.add_argument(
+ 'subcommands',
+ nargs=argparse.REMAINDER,
+ help="subcommand(s) to retrieve help for",
+ completer=Cmd.complete_help_subcommands,
+ )
+ return help_parser
# Get rid of cmd's complete_help() functions so ArgparseCompleter will complete the help command
if getattr(cmd.Cmd, 'complete_help', None) is not None:
delattr(cmd.Cmd, 'complete_help')
- @with_argparser(help_parser)
+ @with_argparser(_build_help_parser)
def do_help(self, args: argparse.Namespace) -> None:
- """List available commands or provide detailed help for a specific command"""
+ """List available commands or provide detailed help for a specific command."""
self.last_result = True
if not args.command or args.verbose:
@@ -3859,7 +3522,7 @@ def do_help(self, args: argparse.Namespace) -> None:
self.perror(err_msg, apply_style=False)
self.last_result = False
- def print_topics(self, header: str, cmds: Optional[List[str]], cmdlen: int, maxcol: int) -> None:
+ def print_topics(self, header: str, cmds: Optional[list[str]], cmdlen: int, maxcol: int) -> None:
"""
Print groups of commands and topics in columns and an optional header
Override of cmd's print_topics() to handle headers with newlines, ANSI style sequences, and wide characters
@@ -3877,7 +3540,7 @@ def print_topics(self, header: str, cmds: Optional[List[str]], cmdlen: int, maxc
self.columnize(cmds, maxcol - 1)
self.poutput()
- def columnize(self, str_list: Optional[List[str]], display_width: int = 80) -> None:
+ def columnize(self, str_list: Optional[list[str]], display_width: int = 80) -> None:
"""Display a list of single-line strings as a compact set of columns.
Override of cmd's columnize() to handle strings with ANSI style sequences and wide characters
@@ -3953,15 +3616,15 @@ def _help_menu(self, verbose: bool = False) -> None:
self.print_topics(self.misc_header, help_topics, 15, 80)
self.print_topics(self.undoc_header, cmds_undoc, 15, 80)
- def _build_command_info(self) -> Tuple[Dict[str, List[str]], List[str], List[str], List[str]]:
+ def _build_command_info(self) -> tuple[dict[str, list[str]], list[str], list[str], list[str]]:
# Get a sorted list of help topics
help_topics = sorted(self.get_help_topics(), key=self.default_sort_key)
# Get a sorted list of visible command names
visible_commands = sorted(self.get_visible_commands(), key=self.default_sort_key)
- cmds_doc: List[str] = []
- cmds_undoc: List[str] = []
- cmds_cats: Dict[str, List[str]] = {}
+ cmds_doc: list[str] = []
+ cmds_undoc: list[str] = []
+ cmds_cats: dict[str, list[str]] = {}
for command in visible_commands:
func = cast(CommandFunc, self.cmd_func(command))
has_help_func = False
@@ -3984,7 +3647,7 @@ def _build_command_info(self) -> Tuple[Dict[str, List[str]], List[str], List[str
cmds_undoc.append(command)
return cmds_cats, cmds_doc, cmds_undoc, help_topics
- def _print_topics(self, header: str, cmds: List[str], verbose: bool) -> None:
+ def _print_topics(self, header: str, cmds: list[str], verbose: bool) -> None:
"""Customized version of print_topics that can switch between verbose or traditional output"""
import io
@@ -4018,12 +3681,8 @@ def _print_topics(self, header: str, cmds: List[str], verbose: bool) -> None:
doc: Optional[str]
- # If this is an argparse command, use its description.
- if (cmd_parser := self._command_parsers.get(cmd_func)) is not None:
- doc = cmd_parser.description
-
# Non-argparse commands can have help_functions for their documentation
- elif command in topics:
+ if command in topics:
help_func = getattr(self, constants.HELP_FUNC_PREFIX + command)
result = io.StringIO()
@@ -4052,22 +3711,30 @@ def _print_topics(self, header: str, cmds: List[str], verbose: bool) -> None:
self.poutput(table_str_buf.getvalue())
- shortcuts_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="List available shortcuts")
+ @staticmethod
+ def _build_shortcuts_parser() -> Cmd2ArgumentParser:
+ return argparse_custom.DEFAULT_ARGUMENT_PARSER(description="List available shortcuts.")
- @with_argparser(shortcuts_parser)
+ @with_argparser(_build_shortcuts_parser)
def do_shortcuts(self, _: argparse.Namespace) -> None:
- """List available shortcuts"""
+ """List available shortcuts."""
# Sort the shortcut tuples by name
sorted_shortcuts = sorted(self.statement_parser.shortcuts, key=lambda x: self.default_sort_key(x[0]))
result = "\n".join('{}: {}'.format(sc[0], sc[1]) for sc in sorted_shortcuts)
self.poutput(f"Shortcuts for other commands:\n{result}")
self.last_result = True
- eof_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(
- description="Called when Ctrl-D is pressed", epilog=INTERNAL_COMMAND_EPILOG
- )
+ @staticmethod
+ def _build_eof_parser() -> Cmd2ArgumentParser:
+ eof_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Called when Ctrl-D is pressed.")
+ eof_parser.epilog = eof_parser.create_text_group(
+ "Note",
+ "This command is for internal use and is not intended to be called from the command line.",
+ )
- @with_argparser(eof_parser)
+ return eof_parser
+
+ @with_argparser(_build_eof_parser)
def do_eof(self, _: argparse.Namespace) -> Optional[bool]:
"""
Called when Ctrl-D is pressed and calls quit with no arguments.
@@ -4078,16 +3745,18 @@ def do_eof(self, _: argparse.Namespace) -> Optional[bool]:
# self.last_result will be set by do_quit()
return self.do_quit('')
- quit_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Exit this application")
+ @staticmethod
+ def _build_quit_parser() -> Cmd2ArgumentParser:
+ return argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Exit this application.")
- @with_argparser(quit_parser)
+ @with_argparser(_build_quit_parser)
def do_quit(self, _: argparse.Namespace) -> Optional[bool]:
- """Exit this application"""
+ """Exit this application."""
# Return True to stop the command loop
self.last_result = True
return True
- def select(self, opts: Union[str, List[str], List[Tuple[Any, Optional[str]]]], prompt: str = 'Your choice? ') -> Any:
+ def select(self, opts: Union[str, list[str], list[tuple[Any, Optional[str]]]], prompt: str = 'Your choice? ') -> Any:
"""Presents a numbered menu to the user. Modeled after
the bash shell's SELECT. Returns the item chosen.
@@ -4098,12 +3767,12 @@ def select(self, opts: Union[str, List[str], List[Tuple[Any, Optional[str]]]], p
| a list of tuples -> interpreted as (value, text), so
that the return value can differ from
the text advertised to the user"""
- local_opts: Union[List[str], List[Tuple[Any, Optional[str]]]]
+ local_opts: Union[list[str], list[tuple[Any, Optional[str]]]]
if isinstance(opts, str):
- local_opts = cast(List[Tuple[Any, Optional[str]]], list(zip(opts.split(), opts.split())))
+ local_opts = cast(list[tuple[Any, Optional[str]]], list(zip(opts.split(), opts.split())))
else:
local_opts = opts
- fulloptions: List[Tuple[Any, Optional[str]]] = []
+ fulloptions: list[tuple[Any, Optional[str]]] = []
for opt in local_opts:
if isinstance(opt, str):
fulloptions.append((opt, opt))
@@ -4137,8 +3806,8 @@ def select(self, opts: Union[str, List[str], List[Tuple[Any, Optional[str]]]], p
self.poutput(f"'{response}' isn't a valid choice. Pick a number between 1 and {len(fulloptions)}:")
def complete_set_value(
- self, text: str, line: str, begidx: int, endidx: int, arg_tokens: Dict[str, List[str]]
- ) -> List[str]:
+ self, text: str, line: str, begidx: int, endidx: int, arg_tokens: dict[str, list[str]]
+ ) -> list[str]:
"""Completes the value argument of set"""
param = arg_tokens['param'][0]
try:
@@ -4147,7 +3816,7 @@ def complete_set_value(
raise CompletionError(param + " is not a settable parameter")
# Create a parser with a value field based on this settable
- settable_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(parents=[Cmd.set_parser_parent])
+ settable_parser = Cmd._build_base_set_parser()
# Settables with choices list the values of those choices instead of the arg name
# in help text and this shows in tab completion hints. Set metavar to avoid this.
@@ -4156,7 +3825,7 @@ def complete_set_value(
arg_name,
metavar=arg_name,
help=settable.description,
- choices=settable.choices, # type: ignore[arg-type]
+ choices=settable.choices,
choices_provider=settable.choices_provider,
completer=settable.completer,
)
@@ -4167,30 +3836,45 @@ def complete_set_value(
_, raw_tokens = self.tokens_for_completion(line, begidx, endidx)
return completer.complete(text, line, begidx, endidx, raw_tokens[1:])
- # When tab completing value, we recreate the set command parser with a value argument specific to
- # the settable being edited. To make this easier, define a parent parser with all the common elements.
- set_description = (
- "Set a settable parameter or show current settings of parameters\n"
- "Call without arguments for a list of all settable parameters with their values.\n"
- "Call with just param to view that parameter's value."
- )
- set_parser_parent = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=set_description, add_help=False)
- set_parser_parent.add_argument(
- 'param',
- nargs=argparse.OPTIONAL,
- help='parameter to set or view',
- choices_provider=_get_settable_completion_items,
- descriptive_header=_settable_completion_table.generate_header(),
- )
+ @staticmethod
+ def _build_base_set_parser() -> Cmd2ArgumentParser:
+ # When tab completing value, we recreate the set command parser with a value argument specific to
+ # the settable being edited. To make this easier, define a base parser with all the common elements.
+ set_description = "Set a settable parameter or show current settings of parameters."
+ base_set_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=set_description)
+ base_set_parser.add_argument(
+ 'param',
+ nargs=argparse.OPTIONAL,
+ help='parameter to set or view',
+ choices_provider=Cmd._get_settable_completion_items,
+ descriptive_header=Cmd._settable_completion_table.generate_header(),
+ )
- # Create the parser for the set command
- set_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(parents=[set_parser_parent])
- set_parser.add_argument(
- 'value', nargs=argparse.OPTIONAL, help='new value for settable', completer=complete_set_value, suppress_tab_hint=True
- )
+ base_set_parser_notes = (
+ "Call without arguments for a list of all settable parameters with their values.\n"
+ "\n"
+ "Call with just param to view that parameter's value."
+ )
+ base_set_parser.epilog = base_set_parser.create_text_group("Notes", base_set_parser_notes)
+
+ return base_set_parser
+
+ @staticmethod
+ def _build_set_parser() -> Cmd2ArgumentParser:
+ # Create the parser for the set command
+ set_parser = Cmd._build_base_set_parser()
+ set_parser.add_argument(
+ 'value',
+ nargs=argparse.OPTIONAL,
+ help='new value for settable',
+ completer=Cmd.complete_set_value,
+ suppress_tab_hint=True,
+ )
+
+ return set_parser
# Preserve quotes so users can pass in quoted empty strings and flags (e.g. -h) as the value
- @with_argparser(set_parser, preserve_quotes=True)
+ @with_argparser(_build_set_parser, preserve_quotes=True)
def do_set(self, args: argparse.Namespace) -> None:
"""Set a settable parameter or show current settings of parameters"""
self.last_result = False
@@ -4229,7 +3913,7 @@ def do_set(self, args: argparse.Namespace) -> None:
max_name_width = max([ansi.style_aware_wcswidth(param) for param in to_show])
max_name_width = max(max_name_width, ansi.style_aware_wcswidth(name_label))
- cols: List[Column] = [
+ cols: list[Column] = [
Column(name_label, width=max_name_width),
Column('Value', width=30),
Column('Description', width=60),
@@ -4239,7 +3923,7 @@ def do_set(self, args: argparse.Namespace) -> None:
self.poutput(table.generate_header())
# Build the table and populate self.last_result
- self.last_result = {} # Dict[settable_name, settable_value]
+ self.last_result = {} # dict[settable_name, settable_value]
for param in sorted(to_show, key=self.default_sort_key):
settable = self.settables[param]
@@ -4247,20 +3931,24 @@ def do_set(self, args: argparse.Namespace) -> None:
self.poutput(table.generate_data_row(row_data))
self.last_result[param] = settable.get_value()
- shell_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Execute a command as if at the OS prompt")
- shell_parser.add_argument('command', help='the command to run', completer=shell_cmd_complete)
- shell_parser.add_argument(
- 'command_args', nargs=argparse.REMAINDER, help='arguments to pass to command', completer=path_complete
- )
+ @staticmethod
+ def _build_shell_parser() -> Cmd2ArgumentParser:
+ shell_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Execute a command as if at the OS prompt.")
+ shell_parser.add_argument('command', help='the command to run', completer=Cmd.shell_cmd_complete)
+ shell_parser.add_argument(
+ 'command_args', nargs=argparse.REMAINDER, help='arguments to pass to command', completer=Cmd.path_complete
+ )
+
+ return shell_parser
# Preserve quotes since we are passing these strings to the shell
- @with_argparser(shell_parser, preserve_quotes=True)
+ @with_argparser(_build_shell_parser, preserve_quotes=True)
def do_shell(self, args: argparse.Namespace) -> None:
- """Execute a command as if at the OS prompt"""
+ """Execute a command as if at the OS prompt."""
import signal
import subprocess
- kwargs: Dict[str, Any] = dict()
+ kwargs: dict[str, Any] = dict()
# Set OS-specific parameters
if sys.platform.startswith('win'):
@@ -4292,15 +3980,15 @@ def do_shell(self, args: argparse.Namespace) -> None:
# still receive the SIGINT since it is in the same process group as us.
with self.sigint_protection:
# For any stream that is a StdSim, we will use a pipe so we can capture its output
- proc = subprocess.Popen( # type: ignore[call-overload]
+ proc = subprocess.Popen(
expanded_command,
stdout=subprocess.PIPE if isinstance(self.stdout, utils.StdSim) else self.stdout, # type: ignore[unreachable]
- stderr=subprocess.PIPE if isinstance(sys.stderr, utils.StdSim) else sys.stderr, # type: ignore[unreachable]
+ stderr=subprocess.PIPE if isinstance(sys.stderr, utils.StdSim) else sys.stderr,
shell=True,
**kwargs,
)
- proc_reader = utils.ProcReader(proc, cast(TextIO, self.stdout), sys.stderr) # type: ignore[arg-type]
+ 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
@@ -4392,10 +4080,10 @@ def _set_up_py_shell_env(self, interp: InteractiveConsole) -> _SavedCmd2Env:
self._reset_py_display()
cmd2_env.sys_stdout = sys.stdout
- sys.stdout = self.stdout # type: ignore[assignment]
+ sys.stdout = self.stdout
cmd2_env.sys_stdin = sys.stdin
- sys.stdin = self.stdin # type: ignore[assignment]
+ sys.stdin = self.stdin
return cmd2_env
@@ -4405,8 +4093,8 @@ def _restore_cmd2_env(self, cmd2_env: _SavedCmd2Env) -> None:
:param cmd2_env: the environment settings to restore
"""
- sys.stdout = cmd2_env.sys_stdout # type: ignore[assignment]
- sys.stdin = cmd2_env.sys_stdin # type: ignore[assignment]
+ sys.stdout = cmd2_env.sys_stdout
+ sys.stdin = cmd2_env.sys_stdin
# Set up readline for cmd2
if rl_type != RlType.NONE:
@@ -4552,27 +4240,36 @@ def py_quit() -> None:
return py_bridge.stop
- py_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Run an interactive Python shell")
+ @staticmethod
+ def _build_py_parser() -> Cmd2ArgumentParser:
+ return argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Run an interactive Python shell.")
- @with_argparser(py_parser)
+ @with_argparser(_build_py_parser)
def do_py(self, _: argparse.Namespace) -> Optional[bool]:
"""
- Run an interactive Python shell
+ Run an interactive Python shell.
+
:return: True if running of commands should stop
"""
# self.last_resort will be set by _run_python()
return self._run_python()
- run_pyscript_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Run a Python script file inside the console")
- run_pyscript_parser.add_argument('script_path', help='path to the script file', completer=path_complete)
- run_pyscript_parser.add_argument(
- 'script_arguments', nargs=argparse.REMAINDER, help='arguments to pass to script', completer=path_complete
- )
+ @staticmethod
+ def _build_run_pyscript_parser() -> Cmd2ArgumentParser:
+ run_pyscript_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(
+ description="Run Python script within this application's environment."
+ )
+ run_pyscript_parser.add_argument('script_path', help='path to the script file', completer=Cmd.path_complete)
+ run_pyscript_parser.add_argument(
+ 'script_arguments', nargs=argparse.REMAINDER, help='arguments to pass to script', completer=Cmd.path_complete
+ )
+
+ return run_pyscript_parser
- @with_argparser(run_pyscript_parser)
+ @with_argparser(_build_run_pyscript_parser)
def do_run_pyscript(self, args: argparse.Namespace) -> Optional[bool]:
"""
- Run a Python script file inside the console
+ Run Python script within this application's environment.
:return: True if running of commands should stop
"""
@@ -4604,12 +4301,14 @@ def do_run_pyscript(self, args: argparse.Namespace) -> Optional[bool]:
return py_return
- ipython_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Run an interactive IPython shell")
+ @staticmethod
+ def _build_ipython_parser() -> Cmd2ArgumentParser:
+ return argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Run an interactive IPython shell.")
- @with_argparser(ipython_parser)
+ @with_argparser(_build_ipython_parser)
def do_ipy(self, _: argparse.Namespace) -> Optional[bool]: # pragma: no cover
"""
- Enter an interactive IPython shell
+ Run an interactive IPython shell.
:return: True if running of commands should stop
"""
@@ -4679,57 +4378,75 @@ def do_ipy(self, _: argparse.Namespace) -> Optional[bool]: # pragma: no cover
finally:
self._in_py = False
- history_description = "View, run, edit, save, or clear previously entered commands"
+ @staticmethod
+ def _build_history_parser() -> Cmd2ArgumentParser:
+ from .argparse_custom import RawTextCmd2HelpFormatter
- history_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=history_description)
- history_action_group = history_parser.add_mutually_exclusive_group()
- history_action_group.add_argument('-r', '--run', action='store_true', help='run selected history items')
- history_action_group.add_argument('-e', '--edit', action='store_true', help='edit and then run selected history items')
- history_action_group.add_argument(
- '-o', '--output_file', metavar='FILE', help='output commands to a script file, implies -s', completer=path_complete
- )
- history_action_group.add_argument(
- '-t',
- '--transcript',
- metavar='TRANSCRIPT_FILE',
- help='output commands and results to a transcript file,\nimplies -s',
- completer=path_complete,
- )
- history_action_group.add_argument('-c', '--clear', action='store_true', help='clear all history')
+ history_description = "View, run, edit, save, or clear previously entered commands."
- history_format_group = history_parser.add_argument_group(title='formatting')
- history_format_group.add_argument(
- '-s', '--script', action='store_true', help='output commands in script format, i.e. without command\nnumbers'
- )
- history_format_group.add_argument(
- '-x',
- '--expanded',
- action='store_true',
- help='output fully parsed commands with any aliases and\nmacros expanded, instead of typed commands',
- )
- history_format_group.add_argument(
- '-v',
- '--verbose',
- action='store_true',
- help='display history and include expanded commands if they\ndiffer from the typed command',
- )
- history_format_group.add_argument(
- '-a', '--all', action='store_true', help='display all commands, including ones persisted from\nprevious sessions'
- )
+ history_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(
+ description=history_description, formatter_class=RawTextCmd2HelpFormatter
+ )
+ history_action_group = history_parser.add_mutually_exclusive_group()
+ history_action_group.add_argument('-r', '--run', action='store_true', help='run selected history items')
+ history_action_group.add_argument('-e', '--edit', action='store_true', help='edit and then run selected history items')
+ history_action_group.add_argument(
+ '-o',
+ '--output_file',
+ metavar='FILE',
+ help='output commands to a script file, implies --script',
+ completer=Cmd.path_complete,
+ )
+ history_action_group.add_argument(
+ '-t',
+ '--transcript',
+ metavar='TRANSCRIPT_FILE',
+ help='output commands and results to a transcript file,\nimplies --script',
+ completer=Cmd.path_complete,
+ )
+ history_action_group.add_argument('-c', '--clear', action='store_true', help='clear all history')
+
+ history_format_group = history_parser.add_argument_group(title='formatting')
+ history_format_group.add_argument(
+ '-s',
+ '--script',
+ action='store_true',
+ help='output commands in script format, i.e. without command\nnumbers',
+ )
+ history_format_group.add_argument(
+ '-x',
+ '--expanded',
+ action='store_true',
+ help='output fully parsed commands with aliases and shortcuts\nexpanded',
+ )
+ history_format_group.add_argument(
+ '-v',
+ '--verbose',
+ action='store_true',
+ help='display history and include expanded commands if they\ndiffer from the typed command',
+ )
+ history_format_group.add_argument(
+ '-a',
+ '--all',
+ action='store_true',
+ help='display all commands, including ones persisted from\nprevious sessions',
+ )
- history_arg_help = (
- "empty all history items\n"
- "a one history item by number\n"
- "a..b, a:b, a:, ..b items by indices (inclusive)\n"
- "string items containing string\n"
- "/regex/ items matching regular expression"
- )
- history_parser.add_argument('arg', nargs=argparse.OPTIONAL, help=history_arg_help)
+ history_arg_help = (
+ "empty all history items\n"
+ "a one history item by number\n"
+ "a..b, a:b, a:, ..b items by indices (inclusive)\n"
+ "string items containing string\n"
+ "/regex/ items matching regular expression"
+ )
+ history_parser.add_argument('arg', nargs=argparse.OPTIONAL, help=history_arg_help)
- @with_argparser(history_parser)
+ return history_parser
+
+ @with_argparser(_build_history_parser)
def do_history(self, args: argparse.Namespace) -> Optional[bool]:
"""
- View, run, edit, save, or clear previously entered commands
+ View, run, edit, save, or clear previously entered commands.
:return: True if running of commands should stop
"""
@@ -4739,13 +4456,11 @@ def do_history(self, args: argparse.Namespace) -> Optional[bool]:
if args.verbose:
if args.clear or args.edit or args.output_file or args.run or args.transcript or args.expanded or args.script:
self.poutput("-v cannot be used with any other options")
- self.poutput(self.history_parser.format_usage())
return None
# -s and -x can only be used if none of these options are present: [-c -r -e -o -t]
if (args.script or args.expanded) and (args.clear or args.edit or args.output_file or args.run or args.transcript):
self.poutput("-s and -x cannot be used with -c, -r, -e, -o, or -t")
- self.poutput(self.history_parser.format_usage())
return None
if args.clear:
@@ -4820,7 +4535,7 @@ def do_history(self, args: argparse.Namespace) -> Optional[bool]:
self.last_result = history
return None
- def _get_history(self, args: argparse.Namespace) -> 'OrderedDict[int, HistoryItem]':
+ def _get_history(self, args: argparse.Namespace) -> OrderedDict[int, HistoryItem]:
"""If an argument was supplied, then retrieve partial contents of the history; otherwise retrieve entire history.
This function returns a dictionary with history items keyed by their 1-based index in ascending order.
@@ -4898,11 +4613,11 @@ def _initialize_history(self, hist_file: str) -> None:
try:
import lzma as decompress_lib
- decompress_exceptions: Tuple[type[Exception]] = (decompress_lib.LZMAError,)
+ decompress_exceptions: tuple[type[Exception]] = (decompress_lib.LZMAError,)
except ModuleNotFoundError: # pragma: no cover
import bz2 as decompress_lib # type: ignore[no-redef]
- decompress_exceptions: Tuple[type[Exception]] = (OSError, ValueError) # type: ignore[no-redef]
+ decompress_exceptions: tuple[type[Exception]] = (OSError, ValueError) # type: ignore[no-redef]
try:
history_json = decompress_lib.decompress(compressed_bytes).decode(encoding='utf-8')
@@ -4959,7 +4674,7 @@ def _persist_history(self) -> None:
def _generate_transcript(
self,
- history: Union[List[HistoryItem], List[str]],
+ history: Union[list[HistoryItem], list[str]],
transcript_file: str,
*,
add_to_history: bool = True,
@@ -5055,22 +4770,29 @@ def _generate_transcript(
self.pfeedback(f"{commands_run} {plural} saved to transcript file '{transcript_path}'")
self.last_result = True
- edit_description = (
- "Run a text editor and optionally open a file with it\n"
- "\n"
- "The editor used is determined by a settable parameter. To set it:\n"
- "\n"
- " set editor (program-name)"
- )
+ @staticmethod
+ def _build_edit_parser() -> Cmd2ArgumentParser:
+ from rich.markdown import Markdown
- edit_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=edit_description)
- edit_parser.add_argument(
- 'file_path', nargs=argparse.OPTIONAL, help="optional path to a file to open in editor", completer=path_complete
- )
+ edit_description = "Run a text editor and optionally open a file with it."
+ edit_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=edit_description)
+
+ edit_parser.epilog = edit_parser.create_text_group(
+ "Note",
+ Markdown("To set a new editor, run: `set editor `"),
+ )
- @with_argparser(edit_parser)
+ edit_parser.add_argument(
+ 'file_path',
+ nargs=argparse.OPTIONAL,
+ help="optional path to a file to open in editor",
+ completer=Cmd.path_complete,
+ )
+ return edit_parser
+
+ @with_argparser(_build_edit_parser)
def do_edit(self, args: argparse.Namespace) -> None:
- """Run a text editor and optionally open a file with it"""
+ """Run a text editor and optionally open a file with it."""
# self.last_result will be set by do_shell() which is called by run_editor()
self.run_editor(args.file_path)
@@ -5099,29 +4821,39 @@ def _current_script_dir(self) -> Optional[str]:
else:
return None
- run_script_description = (
- "Run commands in script file that is encoded as either ASCII or UTF-8 text\n"
- "\n"
- "Script should contain one command per line, just like the command would be\n"
- "typed in the console.\n"
- "\n"
- "If the -t/--transcript flag is used, this command instead records\n"
- "the output of the script commands to a transcript for testing purposes.\n"
- )
+ @staticmethod
+ def _build_base_run_script_parser() -> Cmd2ArgumentParser:
+ from rich.table import Table
- run_script_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=run_script_description)
- run_script_parser.add_argument(
- '-t',
- '--transcript',
- metavar='TRANSCRIPT_FILE',
- help='record the output of the script as a transcript file',
- completer=path_complete,
- )
- run_script_parser.add_argument('script_path', help="path to the script file", completer=path_complete)
+ run_script_description = Table(
+ box=None,
+ show_header=False,
+ padding=(0, 0),
+ )
+ run_script_description.add_row("Run text script.")
+ run_script_description.add_row()
+ run_script_description.add_row("Scripts contain one command, written as it would be typed in the console, per line.")
+
+ return argparse_custom.DEFAULT_ARGUMENT_PARSER(description=run_script_description)
+
+ @staticmethod
+ def _build_run_script_parser() -> Cmd2ArgumentParser:
+ run_script_parser = Cmd._build_base_run_script_parser()
+ run_script_parser.add_argument(
+ '-t',
+ '--transcript',
+ metavar='TRANSCRIPT_FILE',
+ help='record the output of the script as a transcript file',
+ completer=Cmd.path_complete,
+ )
+ run_script_parser.add_argument('script_path', help="path to the script file", completer=Cmd.path_complete)
+
+ return run_script_parser
- @with_argparser(run_script_parser)
+ @with_argparser(_build_run_script_parser)
def do_run_script(self, args: argparse.Namespace) -> Optional[bool]:
- """Run commands in script file that is encoded as either ASCII or UTF-8 text.
+ """
+ Run text script.
:return: True if running of commands should stop
"""
@@ -5182,35 +4914,41 @@ def do_run_script(self, args: argparse.Namespace) -> Optional[bool]:
self._script_dir.pop()
return None
- relative_run_script_description = run_script_description
- relative_run_script_description += (
- "\n\n"
- "If this is called from within an already-running script, the filename will be\n"
- "interpreted relative to the already-running script's directory."
- )
+ @staticmethod
+ def _build_relative_run_script_parser() -> Cmd2ArgumentParser:
+ from rich.table import Table
+
+ relative_run_script_parser = Cmd._build_base_run_script_parser()
+ relative_run_script_parser.description = cast(Table, relative_run_script_parser.description)
+ relative_run_script_parser.description.add_row()
+ relative_run_script_parser.description.add_row(
+ "If this is run from within a script, script_path will be interpreted relative to that script's directory."
+ )
- relative_run_script_epilog = "Notes:\n This command is intended to only be used within text file scripts."
+ relative_run_script_parser.epilog = relative_run_script_parser.create_text_group(
+ "Note",
+ "This command is intended to be used from within a text script.",
+ )
- relative_run_script_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(
- description=relative_run_script_description, epilog=relative_run_script_epilog
- )
- relative_run_script_parser.add_argument('file_path', help='a file path pointing to a script')
+ relative_run_script_parser.add_argument('script_path', help="path to the script file")
+
+ return relative_run_script_parser
- @with_argparser(relative_run_script_parser)
+ @with_argparser(_build_relative_run_script_parser)
def do__relative_run_script(self, args: argparse.Namespace) -> Optional[bool]:
"""
Run commands in script file that is encoded as either ASCII or UTF-8 text
:return: True if running of commands should stop
"""
- file_path = args.file_path
+ script_path = args.script_path
# NOTE: Relative path is an absolute path, it is just relative to the current script directory
- relative_path = os.path.join(self._current_script_dir or '', file_path)
+ relative_path = os.path.join(self._current_script_dir or '', script_path)
# self.last_result will be set by do_run_script()
return self.do_run_script(utils.quote_string(relative_path))
- def _run_transcript_tests(self, transcript_paths: List[str]) -> None:
+ def _run_transcript_tests(self, transcript_paths: list[str]) -> None:
"""Runs transcript tests for provided file(s).
This is called when either -t is provided on the command line or the transcript_files argument is provided
@@ -5599,12 +5337,12 @@ def cmdloop(self, intro: Optional[str] = None) -> int: # type: ignore[override]
###
def _initialize_plugin_system(self) -> None:
"""Initialize the plugin system"""
- self._preloop_hooks: List[Callable[[], None]] = []
- self._postloop_hooks: List[Callable[[], None]] = []
- self._postparsing_hooks: List[Callable[[plugin.PostparsingData], plugin.PostparsingData]] = []
- self._precmd_hooks: List[Callable[[plugin.PrecommandData], plugin.PrecommandData]] = []
- self._postcmd_hooks: List[Callable[[plugin.PostcommandData], plugin.PostcommandData]] = []
- self._cmdfinalization_hooks: List[Callable[[plugin.CommandFinalizationData], plugin.CommandFinalizationData]] = []
+ self._preloop_hooks: list[Callable[[], None]] = []
+ self._postloop_hooks: list[Callable[[], None]] = []
+ self._postparsing_hooks: list[Callable[[plugin.PostparsingData], plugin.PostparsingData]] = []
+ self._precmd_hooks: list[Callable[[plugin.PrecommandData], plugin.PrecommandData]] = []
+ self._postcmd_hooks: list[Callable[[plugin.PostcommandData], plugin.PostcommandData]] = []
+ self._cmdfinalization_hooks: list[Callable[[plugin.CommandFinalizationData], plugin.CommandFinalizationData]] = []
@classmethod
def _validate_callable_param_count(cls, func: Callable[..., Any], count: int) -> None:
@@ -5738,7 +5476,7 @@ def _resolve_func_self(
else:
# Search all registered CommandSets
func_self = None
- candidate_sets: List[CommandSet] = []
+ candidate_sets: list[CommandSet] = []
for installed_cmd_set in self._installed_command_sets:
if type(installed_cmd_set) == func_class: # noqa: E721
# Case 2: CommandSet is an exact type match for the function's CommandSet
diff --git a/cmd2/command_definition.py b/cmd2/command_definition.py
index 5bac8ef3..1762d909 100644
--- a/cmd2/command_definition.py
+++ b/cmd2/command_definition.py
@@ -6,7 +6,6 @@
from typing import (
TYPE_CHECKING,
Callable,
- Dict,
Mapping,
Optional,
Type,
@@ -100,7 +99,7 @@ def __init__(self) -> None:
# accessed by child classes using the self._cmd property.
self.__cmd_internal: Optional[cmd2.Cmd] = None
- self._settables: Dict[str, Settable] = {}
+ self._settables: dict[str, Settable] = {}
self._settable_prefix = self.__class__.__name__
@property
diff --git a/cmd2/decorators.py b/cmd2/decorators.py
index 343beaa2..03a7ad50 100644
--- a/cmd2/decorators.py
+++ b/cmd2/decorators.py
@@ -6,11 +6,8 @@
TYPE_CHECKING,
Any,
Callable,
- Dict,
- List,
Optional,
Sequence,
- Tuple,
Type,
TypeVar,
Union,
@@ -79,7 +76,7 @@ def cat_decorator(func: CommandFunc) -> CommandFunc:
# in cmd2 command functions/callables. As long as the 2-ple of arguments we expect to be there can be
# found we can swap out the statement with each decorator's specific parameters
##########################
-def _parse_positionals(args: Tuple[Any, ...]) -> Tuple['cmd2.Cmd', Union[Statement, str]]:
+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
Assumes that we will find cmd2.Cmd followed by the command statement object or string.
@@ -103,7 +100,7 @@ def _parse_positionals(args: Tuple[Any, ...]) -> Tuple['cmd2.Cmd', Union[Stateme
raise TypeError('Expected arguments: cmd: cmd2.Cmd, statement: Union[Statement, str] Not found')
-def _arg_swap(args: Union[Sequence[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
@@ -120,13 +117,13 @@ def _arg_swap(args: Union[Sequence[Any]], search_arg: Any, *replace_arg: Any) ->
#: Function signature for a command function that accepts a pre-processed argument list from user input
#: and optionally returns a boolean
-ArgListCommandFuncOptionalBoolReturn = Callable[[CommandParent, List[str]], Optional[bool]]
-#: Function signature for a command function that accepts a pre-processed argument list from user input
+ArgListCommandFuncOptionalBoolReturn = Callable[[CommandParent, 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 = Callable[[CommandParent, List[str]], bool]
-#: Function signature for a command function that accepts a pre-processed argument list from user input
+ArgListCommandFuncBoolReturn = Callable[[CommandParent, list[str]], bool]
+#: Function signature for an Command Function that accepts a pre-processed argument list from user input
#: and returns Nothing
-ArgListCommandFuncNoneReturn = Callable[[CommandParent, List[str]], None]
+ArgListCommandFuncNoneReturn = Callable[[CommandParent, list[str]], None]
#: Aggregate of all accepted function signatures for command functions that accept a pre-processed argument list
ArgListCommandFunc = Union[
@@ -187,7 +184,7 @@ def cmd_wrapper(*args: Any, **kwargs: Any) -> Optional[bool]:
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) # type: ignore[call-arg]
+ return func(*args_list, **kwargs)
command_name = func.__name__[len(constants.COMMAND_FUNC_PREFIX) :]
cmd_wrapper.__doc__ = func.__doc__
@@ -209,7 +206,7 @@ def _set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None:
"""
# Set the prog value for this parser
parser.prog = prog
- req_args: List[str] = []
+ req_args: list[str] = []
# Set the prog value for the parser's subcommands
for action in parser._actions:
@@ -252,17 +249,17 @@ def _set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None:
#: Function signatures for command functions that use an argparse.ArgumentParser to process user input
#: and optionally return a boolean
ArgparseCommandFuncOptionalBoolReturn = Callable[[CommandParent, argparse.Namespace], Optional[bool]]
-ArgparseCommandFuncWithUnknownArgsOptionalBoolReturn = Callable[[CommandParent, argparse.Namespace, List[str]], Optional[bool]]
+ArgparseCommandFuncWithUnknownArgsOptionalBoolReturn = Callable[[CommandParent, argparse.Namespace, list[str]], Optional[bool]]
#: Function signatures for command functions that use an argparse.ArgumentParser to process user input
#: and return a boolean
ArgparseCommandFuncBoolReturn = Callable[[CommandParent, argparse.Namespace], bool]
-ArgparseCommandFuncWithUnknownArgsBoolReturn = Callable[[CommandParent, argparse.Namespace, List[str]], bool]
+ArgparseCommandFuncWithUnknownArgsBoolReturn = Callable[[CommandParent, argparse.Namespace, list[str]], bool]
#: Function signatures for command functions that use an argparse.ArgumentParser to process user input
#: and return nothing
ArgparseCommandFuncNoneReturn = Callable[[CommandParent, argparse.Namespace], None]
-ArgparseCommandFuncWithUnknownArgsNoneReturn = Callable[[CommandParent, argparse.Namespace, List[str]], None]
+ArgparseCommandFuncWithUnknownArgsNoneReturn = Callable[[CommandParent, argparse.Namespace, list[str]], None]
#: Aggregate of all accepted function signatures for an argparse command function
ArgparseCommandFunc = Union[
@@ -344,7 +341,7 @@ def arg_decorator(func: ArgparseCommandFunc[CommandParent]) -> RawCommandFuncOpt
"""
@functools.wraps(func)
- def cmd_wrapper(*args: Any, **kwargs: Dict[str, Any]) -> Optional[bool]:
+ def cmd_wrapper(*args: Any, **kwargs: dict[str, Any]) -> Optional[bool]:
"""
Command function wrapper which translates command line into argparse Namespace and calls actual
command function
@@ -377,7 +374,7 @@ def cmd_wrapper(*args: Any, **kwargs: Dict[str, Any]) -> Optional[bool]:
namespace = ns_provider(provider_self if provider_self is not None else cmd2_app)
try:
- new_args: Union[Tuple[argparse.Namespace], Tuple[argparse.Namespace, List[str]]]
+ new_args: Union[tuple[argparse.Namespace], tuple[argparse.Namespace, list[str]]]
if with_unknown_args:
new_args = arg_parser.parse_known_args(parsed_arglist, namespace)
else:
@@ -399,7 +396,7 @@ def cmd_wrapper(*args: Any, **kwargs: Dict[str, Any]) -> Optional[bool]:
delattr(ns, constants.NS_ATTR_SUBCMD_HANDLER)
args_list = _arg_swap(args, statement_arg, *new_args)
- return func(*args_list, **kwargs) # type: ignore[call-arg]
+ return func(*args_list, **kwargs)
command_name = func.__name__[len(constants.COMMAND_FUNC_PREFIX) :]
@@ -422,7 +419,7 @@ def as_subcommand_to(
],
*,
help: Optional[str] = None,
- aliases: Optional[List[str]] = None,
+ aliases: Optional[list[str]] = None,
) -> Callable[[ArgparseCommandFunc[CommandParent]], ArgparseCommandFunc[CommandParent]]:
"""
Tag this method as a subcommand to an existing argparse decorated command.
@@ -444,7 +441,7 @@ def arg_decorator(func: ArgparseCommandFunc[CommandParent]) -> ArgparseCommandFu
setattr(func, constants.SUBCMD_ATTR_NAME, subcommand)
# Keyword arguments for subparsers.add_parser()
- add_parser_kwargs: Dict[str, Any] = dict()
+ add_parser_kwargs: dict[str, Any] = dict()
if help is not None:
add_parser_kwargs['help'] = help
if aliases:
diff --git a/cmd2/history.py b/cmd2/history.py
index 289615c8..dbd5ba0f 100644
--- a/cmd2/history.py
+++ b/cmd2/history.py
@@ -14,9 +14,7 @@
from typing import (
Any,
Callable,
- Dict,
Iterable,
- List,
Optional,
Union,
overload,
@@ -132,12 +130,12 @@ def pr(self, idx: int, script: bool = False, expanded: bool = False, verbose: bo
return ret_str
- def to_dict(self) -> Dict[str, Any]:
+ def to_dict(self) -> dict[str, Any]:
"""Utility method to convert this HistoryItem into a dictionary for use in persistent JSON history files"""
return {HistoryItem._statement_field: self.statement.to_dict()}
@staticmethod
- def from_dict(source_dict: Dict[str, Any]) -> 'HistoryItem':
+ def from_dict(source_dict: dict[str, Any]) -> 'HistoryItem':
"""
Utility method to restore a HistoryItem from a dictionary
@@ -149,7 +147,7 @@ def from_dict(source_dict: Dict[str, Any]) -> 'HistoryItem':
return HistoryItem(Statement.from_dict(statement_dict))
-class History(List[HistoryItem]):
+class History(list[HistoryItem]):
"""A list of [HistoryItem][cmd2.history.HistoryItem] objects with additional methods
for searching and managing the list.
@@ -242,7 +240,7 @@ def get(self, index: int) -> HistoryItem:
#
spanpattern = re.compile(r'^\s*(?P-?[1-9]\d*)?(?P:|(\.{2,}))(?P-?[1-9]\d*)?\s*$')
- def span(self, span: str, include_persisted: bool = False) -> 'OrderedDict[int, HistoryItem]':
+ def span(self, span: str, include_persisted: bool = False) -> OrderedDict[int, HistoryItem]:
"""Return a slice of the History list
:param span: string containing an index or a slice
@@ -291,7 +289,7 @@ def span(self, span: str, include_persisted: bool = False) -> 'OrderedDict[int,
return self._build_result_dictionary(start, end)
- def str_search(self, search: str, include_persisted: bool = False) -> 'OrderedDict[int, HistoryItem]':
+ def str_search(self, search: str, include_persisted: bool = False) -> OrderedDict[int, HistoryItem]:
"""Find history items which contain a given string
:param search: the string to search for
@@ -310,7 +308,7 @@ def isin(history_item: HistoryItem) -> bool:
start = 0 if include_persisted else self.session_start_index
return self._build_result_dictionary(start, len(self), isin)
- def regex_search(self, regex: str, include_persisted: bool = False) -> 'OrderedDict[int, HistoryItem]':
+ def regex_search(self, regex: str, include_persisted: bool = False) -> OrderedDict[int, HistoryItem]:
"""Find history items which match a given regular expression
:param regex: the regular expression to search for.
@@ -346,7 +344,7 @@ def truncate(self, max_length: int) -> None:
def _build_result_dictionary(
self, start: int, end: int, filter_func: Optional[Callable[[HistoryItem], bool]] = None
- ) -> 'OrderedDict[int, HistoryItem]':
+ ) -> OrderedDict[int, HistoryItem]:
"""
Build history search results
:param start: start index to search from
diff --git a/cmd2/parsing.py b/cmd2/parsing.py
index c42ac22b..827f440f 100644
--- a/cmd2/parsing.py
+++ b/cmd2/parsing.py
@@ -10,11 +10,8 @@
)
from typing import (
Any,
- Dict,
Iterable,
- List,
Optional,
- Tuple,
Union,
)
@@ -27,7 +24,7 @@
)
-def shlex_split(str_to_split: str) -> List[str]:
+def shlex_split(str_to_split: str) -> list[str]:
"""
A wrapper around shlex.split() that uses cmd2's preferred arguments.
This allows other classes to easily call split() the same way StatementParser does.
@@ -39,57 +36,7 @@ def shlex_split(str_to_split: str) -> List[str]:
@dataclass(frozen=True)
-class MacroArg:
- """
- Information used to replace or unescape arguments in a macro value when the macro is resolved
- Normal argument syntax: {5}
- Escaped argument syntax: {{5}}
- """
-
- # The starting index of this argument in the macro value
- start_index: int
-
- # The number string that appears between the braces
- # This is a string instead of an int because we support unicode digits and must be able
- # to reproduce this string later
- number_str: str
-
- # Tells if this argument is escaped and therefore needs to be unescaped
- is_escaped: bool
-
- # Pattern used to find normal argument
- # Digits surrounded by exactly 1 brace on a side and 1 or more braces on the opposite side
- # Match strings like: {5}, {{{{{4}, {2}}}}}
- macro_normal_arg_pattern = re.compile(r'(? str:
return self.command_and_args + self.post_command
@property
- def argv(self) -> List[str]:
+ def argv(self) -> list[str]:
"""a list of arguments a-la ``sys.argv``.
- The first element of the list is the command after shortcut and macro
- expansion. Subsequent elements of the list contain any additional
- arguments, with quotes removed, just like bash would. This is very
- useful if you are going to use ``argparse.parse_args()``.
+ The first element of the list is the command after shortcut expansion.
+ Subsequent elements of the list contain any additional arguments,
+ with quotes removed, just like bash would. This is very useful if
+ you are going to use ``argparse.parse_args()``.
If you want to strip quotes from the input, you can use ``argv[1:]``.
"""
@@ -225,12 +172,12 @@ def argv(self) -> List[str]:
return rtn
- def to_dict(self) -> Dict[str, Any]:
+ def to_dict(self) -> dict[str, Any]:
"""Utility method to convert this Statement into a dictionary for use in persistent JSON history files"""
return self.__dict__.copy()
@staticmethod
- def from_dict(source_dict: Dict[str, Any]) -> 'Statement':
+ def from_dict(source_dict: dict[str, Any]) -> 'Statement':
"""
Utility method to restore a Statement from a dictionary
@@ -258,8 +205,8 @@ def __init__(
self,
terminators: Optional[Iterable[str]] = None,
multiline_commands: Optional[Iterable[str]] = None,
- aliases: Optional[Dict[str, str]] = None,
- shortcuts: Optional[Dict[str, str]] = None,
+ aliases: Optional[dict[str, str]] = None,
+ shortcuts: Optional[dict[str, str]] = None,
) -> None:
"""Initialize an instance of StatementParser.
@@ -271,13 +218,13 @@ def __init__(
:param aliases: dictionary containing aliases
:param shortcuts: dictionary containing shortcuts
"""
- self.terminators: Tuple[str, ...]
+ self.terminators: tuple[str, ...]
if terminators is None:
self.terminators = (constants.MULTILINE_TERMINATOR,)
else:
self.terminators = tuple(terminators)
- 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 {}
+ 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
@@ -318,7 +265,7 @@ def __init__(
expr = rf'\A\s*(\S*?)({second_group})'
self._command_pattern = re.compile(expr)
- def is_valid_command(self, word: str, *, is_subcommand: bool = False) -> Tuple[bool, str]:
+ def is_valid_command(self, word: str, *, is_subcommand: bool = False) -> tuple[bool, str]:
"""Determine whether a word is a valid name for a command.
Commands cannot include redirection characters, whitespace,
@@ -369,7 +316,7 @@ def is_valid_command(self, word: str, *, is_subcommand: bool = False) -> Tuple[b
errmsg = ''
return valid, errmsg
- def tokenize(self, line: str) -> List[str]:
+ def tokenize(self, line: str) -> list[str]:
"""
Lex a string into a list of tokens. Shortcuts and aliases are expanded and
comments are removed.
@@ -607,7 +554,7 @@ def parse_command_only(self, rawinput: str) -> Statement:
def get_command_arg_list(
self, command_name: str, to_parse: Union[Statement, str], preserve_quotes: bool
- ) -> Tuple[Statement, List[str]]:
+ ) -> tuple[Statement, list[str]]:
"""
Convenience method used by the argument parsing decorators.
@@ -676,7 +623,7 @@ def _expand(self, line: str) -> str:
return line
@staticmethod
- def _command_and_args(tokens: List[str]) -> Tuple[str, str]:
+ def _command_and_args(tokens: list[str]) -> tuple[str, str]:
"""Given a list of tokens, return a tuple of the command
and the args as a string.
"""
@@ -691,7 +638,7 @@ def _command_and_args(tokens: List[str]) -> Tuple[str, str]:
return command, args
- def split_on_punctuation(self, tokens: List[str]) -> List[str]:
+ def split_on_punctuation(self, tokens: list[str]) -> list[str]:
"""Further splits tokens from a command line using punctuation characters.
Punctuation characters are treated as word breaks when they are in
@@ -701,7 +648,7 @@ def split_on_punctuation(self, tokens: List[str]) -> List[str]:
:param tokens: the tokens as parsed by shlex
:return: a new list of tokens, further split using punctuation
"""
- punctuation: List[str] = []
+ 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 2955cfa3..a8262ef3 100644
--- a/cmd2/py_bridge.py
+++ b/cmd2/py_bridge.py
@@ -13,7 +13,6 @@
IO,
TYPE_CHECKING,
Any,
- List,
NamedTuple,
Optional,
TextIO,
@@ -99,9 +98,9 @@ def __init__(self, cmd2_app: 'cmd2.Cmd', *, add_to_history: bool = True) -> None
# Tells if any of the commands run via __call__ returned True for stop
self.stop = False
- def __dir__(self) -> List[str]:
+ def __dir__(self) -> list[str]:
"""Return a custom set of attribute names"""
- attributes: List[str] = []
+ attributes: list[str] = []
attributes.insert(0, 'cmd_echo')
return attributes
diff --git a/cmd2/table_creator.py b/cmd2/table_creator.py
index 285253c8..27d27cc1 100644
--- a/cmd2/table_creator.py
+++ b/cmd2/table_creator.py
@@ -17,14 +17,12 @@
from typing import (
Any,
Deque,
- List,
Optional,
Sequence,
- Tuple,
Union,
)
-from wcwidth import ( # type: ignore[import]
+from wcwidth import ( # type: ignore[import-untyped]
wcwidth,
)
@@ -156,7 +154,7 @@ def __init__(self, cols: Sequence[Column], *, tab_width: int = 4) -> None:
col.width = max(1, ansi.widest_line(col.header))
@staticmethod
- def _wrap_long_word(word: str, max_width: int, max_lines: Union[int, float], is_last_word: bool) -> Tuple[str, int, int]:
+ def _wrap_long_word(word: str, max_width: int, max_lines: Union[int, float], is_last_word: bool) -> tuple[str, int, int]:
"""
Used by _wrap_text() to wrap a long word over multiple lines
@@ -164,7 +162,7 @@ def _wrap_long_word(word: str, max_width: int, max_lines: Union[int, float], is_
:param max_width: maximum display width of a line
:param max_lines: maximum lines to wrap before ending the last line displayed with an ellipsis
:param is_last_word: True if this is the last word of the total text being wrapped
- :return: Tuple(wrapped text, lines used, display width of last line)
+ :return: tuple(wrapped text, lines used, display width of last line)
"""
styles_dict = utils.get_styles_dict(word)
wrapped_buf = io.StringIO()
@@ -382,7 +380,7 @@ def add_word(word_to_add: str, is_last_word: bool) -> None:
return wrapped_buf.getvalue()
- def _generate_cell_lines(self, cell_data: Any, is_header: bool, col: Column, fill_char: str) -> Tuple[Deque[str], int]:
+ def _generate_cell_lines(self, cell_data: Any, is_header: bool, col: Column, fill_char: str) -> tuple[Deque[str], int]:
"""
Generate the lines of a table cell
@@ -392,7 +390,7 @@ def _generate_cell_lines(self, cell_data: Any, is_header: bool, col: Column, fil
:param col: Column definition for this cell
:param fill_char: character that fills remaining space in a cell. If your text has a background color,
then give fill_char the same background color. (Cannot be a line breaking character)
- :return: Tuple(deque of cell lines, display width of the cell)
+ :return: tuple(deque of cell lines, display width of the cell)
"""
# Convert data to string and replace tabs with spaces
data_str = str(cell_data).replace('\t', SPACE * self.tab_width)
@@ -654,7 +652,7 @@ def generate_header(self) -> str:
inter_cell = self.apply_header_bg(self.column_spacing * SPACE)
# Apply background color to header text in Columns which allow it
- to_display: List[Any] = []
+ to_display: list[Any] = []
for col in self.cols:
if col.style_header_text:
to_display.append(self.apply_header_bg(col.header))
@@ -694,7 +692,7 @@ def generate_data_row(self, row_data: Sequence[Any]) -> str:
inter_cell = self.apply_data_bg(self.column_spacing * SPACE)
# Apply background color to data text in Columns which allow it
- to_display: List[Any] = []
+ to_display: list[Any] = []
for index, col in enumerate(self.cols):
if col.style_data_text:
to_display.append(self.apply_data_bg(row_data[index]))
@@ -949,7 +947,7 @@ def generate_header(self) -> str:
post_line = self.apply_header_bg(self.padding * SPACE) + self.apply_border_color('â•‘')
# Apply background color to header text in Columns which allow it
- to_display: List[Any] = []
+ to_display: list[Any] = []
for col in self.cols:
if col.style_header_text:
to_display.append(self.apply_header_bg(col.header))
@@ -993,7 +991,7 @@ def generate_data_row(self, row_data: Sequence[Any]) -> str:
post_line = self.apply_data_bg(self.padding * SPACE) + self.apply_border_color('â•‘')
# Apply background color to data text in Columns which allow it
- to_display: List[Any] = []
+ to_display: list[Any] = []
for index, col in enumerate(self.cols):
if col.style_data_text:
to_display.append(self.apply_data_bg(row_data[index]))
diff --git a/cmd2/transcript.py b/cmd2/transcript.py
index f4781fd9..47c76878 100644
--- a/cmd2/transcript.py
+++ b/cmd2/transcript.py
@@ -15,10 +15,8 @@ class is used in cmd2.py::run_transcript_tests()
from typing import (
TYPE_CHECKING,
Iterator,
- List,
Optional,
TextIO,
- Tuple,
cast,
)
@@ -66,7 +64,7 @@ def runTest(self) -> None: # was testall
def _fetchTranscripts(self) -> None:
self.transcripts = {}
- testfiles = cast(List[str], getattr(self.cmdapp, 'testfiles', []))
+ testfiles = cast(list[str], getattr(self.cmdapp, 'testfiles', []))
for fname in testfiles:
tfile = open(fname)
self.transcripts[fname] = iter(tfile.readlines())
@@ -184,7 +182,7 @@ def _transform_transcript_expected(self, s: str) -> str:
return regex
@staticmethod
- def _escaped_find(regex: str, s: str, start: int, in_regex: bool) -> Tuple[str, int, int]:
+ def _escaped_find(regex: str, s: str, start: int, in_regex: bool) -> tuple[str, int, int]:
"""Find the next slash in {s} after {start} that is not preceded by a backslash.
If we find an escaped slash, add everything up to and including it to regex,
diff --git a/cmd2/utils.py b/cmd2/utils.py
index 2ad86417..804d5221 100644
--- a/cmd2/utils.py
+++ b/cmd2/utils.py
@@ -23,9 +23,7 @@
TYPE_CHECKING,
Any,
Callable,
- Dict,
Iterable,
- List,
Optional,
TextIO,
Type,
@@ -160,7 +158,7 @@ def __init__(
"""
if val_type is bool:
- def get_bool_choices(_) -> List[str]: # type: ignore[no-untyped-def]
+ def get_bool_choices(_) -> list[str]: # type: ignore[no-untyped-def]
"""Used to tab complete lowercase boolean values"""
return ['true', 'false']
@@ -231,7 +229,7 @@ def is_text_file(file_path: str) -> bool:
return valid_text_file
-def remove_duplicates(list_to_prune: List[_T]) -> List[_T]:
+def remove_duplicates(list_to_prune: list[_T]) -> list[_T]:
"""Removes duplicates from a list while preserving order of the items.
:param list_to_prune: the list being pruned of duplicates
@@ -253,7 +251,7 @@ def norm_fold(astr: str) -> str:
return unicodedata.normalize('NFC', astr).casefold()
-def alphabetical_sort(list_to_sort: Iterable[str]) -> List[str]:
+def alphabetical_sort(list_to_sort: Iterable[str]) -> list[str]:
"""Sorts a list of strings alphabetically.
For example: ['a1', 'A11', 'A2', 'a22', 'a3']
@@ -280,7 +278,7 @@ def try_int_or_force_to_lower_case(input_str: str) -> Union[int, str]:
return norm_fold(input_str)
-def natural_keys(input_str: str) -> List[Union[int, str]]:
+def natural_keys(input_str: str) -> list[Union[int, str]]:
"""
Converts a string into a list of integers and strings to support natural sorting (see natural_sort).
@@ -291,7 +289,7 @@ def natural_keys(input_str: str) -> List[Union[int, str]]:
return [try_int_or_force_to_lower_case(substr) for substr in re.split(r'(\d+)', input_str)]
-def natural_sort(list_to_sort: Iterable[str]) -> List[str]:
+def natural_sort(list_to_sort: Iterable[str]) -> list[str]:
"""
Sorts a list of strings case insensitively as well as numerically.
@@ -307,7 +305,7 @@ def natural_sort(list_to_sort: Iterable[str]) -> List[str]:
return sorted(list_to_sort, key=natural_keys)
-def quote_specific_tokens(tokens: List[str], tokens_to_quote: List[str]) -> None:
+def quote_specific_tokens(tokens: list[str], tokens_to_quote: list[str]) -> None:
"""
Quote specific tokens in a list
@@ -319,7 +317,7 @@ def quote_specific_tokens(tokens: List[str], tokens_to_quote: List[str]) -> None
tokens[i] = quote_string(token)
-def unquote_specific_tokens(tokens: List[str], tokens_to_unquote: List[str]) -> None:
+def unquote_specific_tokens(tokens: list[str], tokens_to_unquote: list[str]) -> None:
"""
Unquote specific tokens in a list
@@ -353,7 +351,7 @@ def expand_user(token: str) -> str:
return token
-def expand_user_in_tokens(tokens: List[str]) -> None:
+def expand_user_in_tokens(tokens: list[str]) -> None:
"""
Call expand_user() on all tokens in a list of strings
:param tokens: tokens to expand
@@ -395,7 +393,7 @@ def find_editor() -> Optional[str]:
return editor
-def files_from_glob_pattern(pattern: str, access: int = os.F_OK) -> List[str]:
+def files_from_glob_pattern(pattern: str, access: int = os.F_OK) -> list[str]:
"""Return a list of file paths based on a glob pattern.
Only files are returned, not directories, and optionally only files for which the user has a specified access to.
@@ -407,7 +405,7 @@ def files_from_glob_pattern(pattern: str, access: int = os.F_OK) -> List[str]:
return [f for f in glob.glob(pattern) if os.path.isfile(f) and os.access(f, access)]
-def files_from_glob_patterns(patterns: List[str], access: int = os.F_OK) -> List[str]:
+def files_from_glob_patterns(patterns: list[str], access: int = os.F_OK) -> list[str]:
"""Return a list of file paths based on a list of glob patterns.
Only files are returned, not directories, and optionally only files for which the user has a specified access to.
@@ -423,7 +421,7 @@ def files_from_glob_patterns(patterns: List[str], access: int = os.F_OK) -> List
return files
-def get_exes_in_path(starts_with: str) -> List[str]:
+def get_exes_in_path(starts_with: str) -> list[str]:
"""Returns names of executables in a user's path
:param starts_with: what the exes should start with. leave blank for all exes in path.
@@ -744,7 +742,7 @@ def __init__(
self.saved_redirecting = saved_redirecting
-def _remove_overridden_styles(styles_to_parse: List[str]) -> List[str]:
+def _remove_overridden_styles(styles_to_parse: list[str]) -> list[str]:
"""
Utility function for align_text() / truncate_line() which filters a style list down
to only those which would still be in effect if all were processed in order.
@@ -765,7 +763,7 @@ class StyleState:
def __init__(self) -> None:
# Contains styles still in effect, keyed by their index in styles_to_parse
- self.style_dict: Dict[int, str] = dict()
+ self.style_dict: dict[int, str] = dict()
# Indexes into style_dict
self.reset_all: Optional[int] = None
@@ -903,7 +901,7 @@ def align_text(
# ANSI style sequences that may affect subsequent lines will be cancelled by the fill_char's style.
# To avoid this, we save styles which are still in effect so we can restore them when beginning the next line.
# This also allows lines to be used independently and still have their style. TableCreator does this.
- previous_styles: List[str] = []
+ previous_styles: list[str] = []
for index, line in enumerate(lines):
if index > 0:
@@ -1114,7 +1112,7 @@ def truncate_line(line: str, max_width: int, *, tab_width: int = 4) -> str:
return truncated_buf.getvalue()
-def get_styles_dict(text: str) -> Dict[int, str]:
+def get_styles_dict(text: str) -> dict[int, str]:
"""
Return an OrderedDict containing all ANSI style sequences found in a string
@@ -1170,7 +1168,7 @@ def do_echo(self, arglist):
setattr(item, constants.CMD_ATTR_HELP_CATEGORY, category)
else:
if inspect.ismethod(func):
- setattr(func.__func__, constants.CMD_ATTR_HELP_CATEGORY, category) # type: ignore[attr-defined]
+ setattr(func.__func__, constants.CMD_ATTR_HELP_CATEGORY, category)
else:
setattr(func, constants.CMD_ATTR_HELP_CATEGORY, category)
@@ -1190,7 +1188,7 @@ def get_defining_class(meth: Callable[..., Any]) -> Optional[Type[Any]]:
if inspect.ismethod(meth) or (
inspect.isbuiltin(meth) and getattr(meth, '__self__') is not None and getattr(meth.__self__, '__class__')
):
- for cls in inspect.getmro(meth.__self__.__class__): # type: ignore[attr-defined]
+ for cls in inspect.getmro(meth.__self__.__class__):
if meth.__name__ in cls.__dict__:
return cls
meth = getattr(meth, '__func__', meth) # fallback to __qualname__ parsing
diff --git a/examples/argparse_completion.py b/examples/argparse_completion.py
index e0ad101b..b1efe4e5 100755
--- a/examples/argparse_completion.py
+++ b/examples/argparse_completion.py
@@ -5,10 +5,6 @@
"""
import argparse
-from typing import (
- Dict,
- List,
-)
from cmd2 import (
Cmd,
@@ -28,11 +24,11 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball']
- def choices_provider(self) -> List[str]:
+ def choices_provider(self) -> list[str]:
"""A choices provider is useful when the choice list is based on instance data of your application"""
return self.sport_item_strs
- def choices_completion_error(self) -> List[str]:
+ def choices_completion_error(self) -> list[str]:
"""
CompletionErrors can be raised if an error occurs while tab completing.
@@ -44,14 +40,14 @@ def choices_completion_error(self) -> List[str]:
return self.sport_item_strs
raise CompletionError("debug must be true")
- def choices_completion_item(self) -> List[CompletionItem]:
+ def choices_completion_item(self) -> list[CompletionItem]:
"""Return CompletionItem instead of strings. These give more context to what's being tab completed."""
fancy_item = "These things can\ncontain newlines and\n"
fancy_item += ansi.style("styled text!!", fg=ansi.Fg.LIGHT_YELLOW, underline=True)
items = {1: "My item", 2: "Another item", 3: "Yet another item", 4: fancy_item}
return [CompletionItem(item_id, description) for item_id, description in items.items()]
- def choices_arg_tokens(self, arg_tokens: Dict[str, List[str]]) -> List[str]:
+ def choices_arg_tokens(self, arg_tokens: dict[str, list[str]]) -> list[str]:
"""
If a choices or completer function/method takes a value called arg_tokens, then it will be
passed a dictionary that maps the command line tokens up through the one being completed
diff --git a/examples/async_printing.py b/examples/async_printing.py
index e94ee89a..221c52e7 100755
--- a/examples/async_printing.py
+++ b/examples/async_printing.py
@@ -8,9 +8,6 @@
import random
import threading
import time
-from typing import (
- List,
-)
import cmd2
from cmd2 import (
@@ -89,7 +86,7 @@ def do_stop_alerts(self, _):
else:
print("The alert thread is already stopped")
- def _get_alerts(self) -> List[str]:
+ def _get_alerts(self) -> list[str]:
"""
Reports alerts
:return: the list of alerts
diff --git a/examples/basic_completion.py b/examples/basic_completion.py
index c713f2b0..ba32d67b 100755
--- a/examples/basic_completion.py
+++ b/examples/basic_completion.py
@@ -14,13 +14,10 @@
"""
import functools
-from typing import (
- List,
-)
import cmd2
-# List of strings used with completion functions
+# list of strings used with completion functions
food_item_strs = ['Pizza', 'Ham', 'Ham Sandwich', 'Potato']
sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball']
@@ -46,7 +43,7 @@ def do_flag_based(self, statement: cmd2.Statement):
"""
self.poutput("Args: {}".format(statement.args))
- def complete_flag_based(self, text, line, begidx, endidx) -> List[str]:
+ def complete_flag_based(self, text, line, begidx, endidx) -> list[str]:
"""Completion function for do_flag_based"""
flag_dict = {
# Tab complete food items after -f and --food flags in command line
@@ -66,7 +63,7 @@ def do_index_based(self, statement: cmd2.Statement):
"""Tab completes first 3 arguments using index_based_complete"""
self.poutput("Args: {}".format(statement.args))
- def complete_index_based(self, text, line, begidx, endidx) -> List[str]:
+ def complete_index_based(self, text, line, begidx, endidx) -> list[str]:
"""Completion function for do_index_based"""
index_dict = {
1: food_item_strs, # Tab complete food items at index 1 in command line
@@ -87,7 +84,7 @@ def do_raise_error(self, statement: cmd2.Statement):
"""Demonstrates effect of raising CompletionError"""
self.poutput("Args: {}".format(statement.args))
- def complete_raise_error(self, text, line, begidx, endidx) -> List[str]:
+ def complete_raise_error(self, text, line, begidx, endidx) -> list[str]:
"""
CompletionErrors can be raised if an error occurs while tab completing.
diff --git a/examples/custom_parser.py b/examples/custom_parser.py
index 94df3b05..419c0a54 100644
--- a/examples/custom_parser.py
+++ b/examples/custom_parser.py
@@ -1,25 +1,29 @@
# coding=utf-8
"""
-Defines the CustomParser used with override_parser.py example
+The standard parser used by cmd2 built-in commands is Cmd2ArgumentParser.
+The following code shows how to override it with your own parser class.
"""
import sys
+from typing import NoReturn
from cmd2 import (
Cmd2ArgumentParser,
ansi,
+ cmd2,
set_default_argument_parser_type,
)
-# First define the parser
+# Since built-in commands rely on customizations made in Cmd2ArgumentParser,
+# your custom parser class should inherit from Cmd2ArgumentParser.
class CustomParser(Cmd2ArgumentParser):
- """Overrides error class"""
+ """Overrides error method"""
- def __init__(self, *args, **kwargs) -> None:
+ def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
super().__init__(*args, **kwargs)
- def error(self, message: str) -> None:
+ def error(self, message: str) -> NoReturn:
"""Custom override that applies custom formatting to the error message"""
lines = message.split('\n')
linum = 0
@@ -38,5 +42,13 @@ def error(self, message: str) -> None:
self.exit(2, '{}\n\n'.format(formatted_message))
-# Now set the default parser for a cmd2 app
-set_default_argument_parser_type(CustomParser)
+if __name__ == '__main__':
+ import sys
+
+ # Set the default parser type before instantiating app.
+ set_default_argument_parser_type(CustomParser)
+
+ app = cmd2.Cmd(include_ipy=True, persistent_history_file='cmd2_history.dat')
+ app.self_in_py = True # Enable access to "self" within the py command
+ app.debug = True # Show traceback if/when an exception occurs
+ sys.exit(app.cmdloop())
diff --git a/examples/decorator_example.py b/examples/decorator_example.py
index ea8fd3b5..2a5449dc 100755
--- a/examples/decorator_example.py
+++ b/examples/decorator_example.py
@@ -12,9 +12,6 @@
"""
import argparse
-from typing import (
- List,
-)
import cmd2
@@ -76,7 +73,7 @@ def do_tag(self, args: argparse.Namespace):
self.poutput('<{0}>{1}{0}>'.format(args.tag, ' '.join(args.content)))
@cmd2.with_argument_list
- def do_tagg(self, arglist: List[str]):
+ def do_tagg(self, arglist: list[str]):
"""version of creating an html tag using arglist instead of argparser"""
if len(arglist) >= 2:
tag = arglist[0]
diff --git a/examples/exit_code.py b/examples/exit_code.py
index d8e538ce..12d73176 100755
--- a/examples/exit_code.py
+++ b/examples/exit_code.py
@@ -2,10 +2,6 @@
# coding=utf-8
"""A simple example demonstrating the following how to emit a non-zero exit code in your cmd2 application."""
-from typing import (
- List,
-)
-
import cmd2
@@ -16,7 +12,7 @@ def __init__(self):
super().__init__()
@cmd2.with_argument_list
- def do_exit(self, arg_list: List[str]) -> bool:
+ def do_exit(self, arg_list: list[str]) -> bool:
"""Exit the application with an optional exit code.
Usage: exit [exit_code]
diff --git a/examples/help_categories.py b/examples/help_categories.py
index 5c349422..bb5a1d43 100755
--- a/examples/help_categories.py
+++ b/examples/help_categories.py
@@ -11,7 +11,6 @@
import cmd2
from cmd2 import (
COMMAND_NAME,
- argparse_custom,
)
@@ -37,6 +36,9 @@ class HelpCategories(cmd2.Cmd):
def __init__(self):
super().__init__()
+ # Set the default category for uncategorized commands
+ self.default_category = 'Other'
+
def do_connect(self, _):
"""Connect command"""
self.poutput('Connect')
@@ -57,8 +59,9 @@ def do_deploy(self, _):
"""Deploy command"""
self.poutput('Deploy')
- start_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(
- description='Start', epilog='my_decorator runs even with argparse errors'
+ start_parser = cmd2.Cmd2ArgumentParser(
+ description='Start',
+ epilog='my_decorator runs even with argparse errors',
)
start_parser.add_argument('when', choices=START_TIMES, help='Specify when to start')
@@ -76,8 +79,9 @@ def do_redeploy(self, _):
"""Redeploy command"""
self.poutput('Redeploy')
- restart_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(
- description='Restart', epilog='my_decorator does not run when argparse errors'
+ restart_parser = cmd2.Cmd2ArgumentParser(
+ description='Restart',
+ epilog='my_decorator does not run when argparse errors',
)
restart_parser.add_argument('when', choices=START_TIMES, help='Specify when to restart')
diff --git a/examples/hooks.py b/examples/hooks.py
index 97b90739..6c1001e5 100755
--- a/examples/hooks.py
+++ b/examples/hooks.py
@@ -10,9 +10,6 @@
"""
import re
-from typing import (
- List,
-)
import cmd2
@@ -102,7 +99,7 @@ def proof_hook(self, data: cmd2.plugin.PostcommandData) -> cmd2.plugin.Postcomma
return data
@cmd2.with_argument_list
- def do_list(self, arglist: List[str]) -> None:
+ def do_list(self, arglist: list[str]) -> None:
"""Generate a list of 10 numbers."""
if arglist:
first = arglist[0]
diff --git a/examples/modular_commands/commandset_basic.py b/examples/modular_commands/commandset_basic.py
index 8587b98d..59a08de0 100644
--- a/examples/modular_commands/commandset_basic.py
+++ b/examples/modular_commands/commandset_basic.py
@@ -3,10 +3,6 @@
A simple example demonstrating a loadable command set
"""
-from typing import (
- List,
-)
-
from cmd2 import (
CommandSet,
CompletionError,
@@ -18,7 +14,7 @@
@with_default_category('Basic Completion')
class BasicCompletionCommandSet(CommandSet):
- # List of strings used with completion functions
+ # list of strings used with completion functions
food_item_strs = ['Pizza', 'Ham', 'Ham Sandwich', 'Potato']
sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball']
@@ -39,7 +35,7 @@ def do_flag_based(self, statement: Statement) -> None:
"""
self._cmd.poutput("Args: {}".format(statement.args))
- def complete_flag_based(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
+ def complete_flag_based(self, text: str, line: str, begidx: int, endidx: int) -> list[str]:
"""Completion function for do_flag_based"""
flag_dict = {
# Tab complete food items after -f and --food flags in command line
@@ -59,7 +55,7 @@ def do_index_based(self, statement: Statement) -> None:
"""Tab completes first 3 arguments using index_based_complete"""
self._cmd.poutput("Args: {}".format(statement.args))
- def complete_index_based(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
+ def complete_index_based(self, text: str, line: str, begidx: int, endidx: int) -> list[str]:
"""Completion function for do_index_based"""
index_dict = {
1: self.food_item_strs, # Tab complete food items at index 1 in command line
@@ -73,14 +69,14 @@ def do_delimiter_complete(self, statement: Statement) -> None:
"""Tab completes files from a list using delimiter_complete"""
self._cmd.poutput("Args: {}".format(statement.args))
- def complete_delimiter_complete(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
+ def complete_delimiter_complete(self, text: str, line: str, begidx: int, endidx: int) -> list[str]:
return self._cmd.delimiter_complete(text, line, begidx, endidx, match_against=self.file_strs, delimiter='/')
def do_raise_error(self, statement: Statement) -> None:
"""Demonstrates effect of raising CompletionError"""
self._cmd.poutput("Args: {}".format(statement.args))
- def complete_raise_error(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
+ def complete_raise_error(self, text: str, line: str, begidx: int, endidx: int) -> list[str]:
"""
CompletionErrors can be raised if an error occurs while tab completing.
diff --git a/examples/modular_commands/commandset_complex.py b/examples/modular_commands/commandset_complex.py
index 7ab84ac3..e89884d7 100644
--- a/examples/modular_commands/commandset_complex.py
+++ b/examples/modular_commands/commandset_complex.py
@@ -5,9 +5,6 @@
"""
import argparse
-from typing import (
- List,
-)
import cmd2
@@ -25,7 +22,7 @@ def do_banana(self, statement: cmd2.Statement):
cranberry_parser.add_argument('arg1', choices=['lemonade', 'juice', 'sauce'])
@cmd2.with_argparser(cranberry_parser, with_unknown_args=True)
- def do_cranberry(self, ns: argparse.Namespace, unknown: List[str]):
+ def do_cranberry(self, ns: argparse.Namespace, unknown: list[str]):
self._cmd.poutput('Cranberry {}!!'.format(ns.arg1))
if unknown and len(unknown):
self._cmd.poutput('Unknown: ' + ', '.join(['{}'] * len(unknown)).format(*unknown))
@@ -36,12 +33,12 @@ def help_cranberry(self):
@cmd2.with_argument_list
@cmd2.with_category('Also Alone')
- def do_durian(self, args: List[str]):
+ def do_durian(self, args: list[str]):
"""Durian Command"""
self._cmd.poutput('{} Arguments: '.format(len(args)))
self._cmd.poutput(', '.join(['{}'] * len(args)).format(*args))
- def complete_durian(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
+ def complete_durian(self, text: str, line: str, begidx: int, endidx: int) -> list[str]:
return self._cmd.basic_complete(text, line, begidx, endidx, ['stinks', 'smells', 'disgusting'])
elderberry_parser = cmd2.Cmd2ArgumentParser()
diff --git a/examples/modular_commands_main.py b/examples/modular_commands_main.py
index e544b3db..773e042b 100755
--- a/examples/modular_commands_main.py
+++ b/examples/modular_commands_main.py
@@ -8,7 +8,6 @@
import argparse
from typing import (
Iterable,
- List,
Optional,
)
@@ -35,7 +34,7 @@ def __init__(self, command_sets: Optional[Iterable[CommandSet]] = None):
super().__init__(command_sets=command_sets)
self.sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball']
- def choices_provider(self) -> List[str]:
+ def choices_provider(self) -> list[str]:
"""A choices provider is useful when the choice list is based on instance data of your application"""
return self.sport_item_strs
diff --git a/examples/override_parser.py b/examples/override_parser.py
deleted file mode 100755
index 0ae279e7..00000000
--- a/examples/override_parser.py
+++ /dev/null
@@ -1,28 +0,0 @@
-#!/usr/bin/env python
-# coding=utf-8
-# flake8: noqa F402
-"""
-The standard parser used by cmd2 built-in commands is Cmd2ArgumentParser.
-The following code shows how to override it with your own parser class.
-"""
-
-# First set a value called argparse.cmd2_parser_module with the module that defines the custom parser.
-# See the code for custom_parser.py. It simply defines a parser and calls cmd2.set_default_argument_parser_type()
-# with the custom parser's type.
-import argparse
-
-argparse.cmd2_parser_module = 'custom_parser'
-
-# Next import from cmd2. It will import your module just before the cmd2.Cmd class file is imported
-# and therefore override the parser class it uses on its commands.
-from cmd2 import (
- cmd2,
-)
-
-if __name__ == '__main__':
- import sys
-
- app = cmd2.Cmd(include_ipy=True, persistent_history_file='cmd2_history.dat')
- app.self_in_py = True # Enable access to "self" within the py command
- app.debug = True # Show traceback if/when an exception occurs
- sys.exit(app.cmdloop())
diff --git a/examples/paged_output.py b/examples/paged_output.py
index 0f7173b2..4f4f8a5a 100755
--- a/examples/paged_output.py
+++ b/examples/paged_output.py
@@ -3,9 +3,6 @@
"""A simple example demonstrating the using paged output via the ppaged() method."""
import os
-from typing import (
- List,
-)
import cmd2
@@ -27,7 +24,7 @@ def page_file(self, file_path: str, chop: bool = False):
self.pexcept('Error reading {!r}: {}'.format(filename, ex))
@cmd2.with_argument_list
- def do_page_wrap(self, args: List[str]):
+ def do_page_wrap(self, args: list[str]):
"""Read in a text file and display its output in a pager, wrapping long lines if they don't fit.
Usage: page_wrap
@@ -40,7 +37,7 @@ def do_page_wrap(self, args: List[str]):
complete_page_wrap = cmd2.Cmd.path_complete
@cmd2.with_argument_list
- def do_page_truncate(self, args: List[str]):
+ def do_page_truncate(self, args: list[str]):
"""Read in a text file and display its output in a pager, truncating long lines if they don't fit.
Truncated lines can still be accessed by scrolling to the right using the arrow keys.
diff --git a/examples/read_input.py b/examples/read_input.py
index bfc43380..87f4719f 100755
--- a/examples/read_input.py
+++ b/examples/read_input.py
@@ -4,10 +4,6 @@
A simple example demonstrating the various ways to call cmd2.Cmd.read_input() for input history and tab completion
"""
-from typing import (
- List,
-)
-
import cmd2
EXAMPLE_COMMANDS = "Example Commands"
@@ -64,7 +60,7 @@ def do_custom_choices(self, _) -> None:
else:
self.custom_history.append(input_str)
- def choices_provider(self) -> List[str]:
+ def choices_provider(self) -> list[str]:
"""Example choices provider function"""
return ["from_provider_1", "from_provider_2", "from_provider_3"]
diff --git a/examples/scripts/save_help_text.py b/examples/scripts/save_help_text.py
index b8ba9624..3590dd32 100644
--- a/examples/scripts/save_help_text.py
+++ b/examples/scripts/save_help_text.py
@@ -8,15 +8,12 @@
import argparse
import os
import sys
-from typing import (
- List,
- TextIO,
-)
+from typing import TextIO
ASTERISKS = "********************************************************"
-def get_sub_commands(parser: argparse.ArgumentParser) -> List[str]:
+def get_sub_commands(parser: argparse.ArgumentParser) -> list[str]:
"""Get a list of subcommands for an ArgumentParser"""
sub_cmds = []
diff --git a/examples/table_creation.py b/examples/table_creation.py
index 0849a0b2..3cffa45f 100755
--- a/examples/table_creation.py
+++ b/examples/table_creation.py
@@ -4,10 +4,7 @@
import functools
import sys
-from typing import (
- Any,
- List,
-)
+from typing import Any
from cmd2 import (
EightBitBg,
@@ -63,8 +60,8 @@ def __init__(self, name: str, birthday: str, place_of_birth: str) -> None:
self.name = name
self.birthday = birthday
self.place_of_birth = place_of_birth
- self.books: List[Book] = []
- self.relatives: List[Relative] = []
+ self.books: list[Book] = []
+ self.relatives: list[Relative] = []
def ansi_print(text):
@@ -76,7 +73,7 @@ def basic_tables():
"""Demonstrates basic examples of the table classes"""
# Table data which demonstrates handling of wrapping and text styles
- data_list: List[List[Any]] = list()
+ data_list: list[list[Any]] = list()
data_list.append(["Billy Smith", "123 Sesame St.\nFake Town, USA 33445", DollarFormatter(100333.03)])
data_list.append(
[
@@ -96,7 +93,7 @@ def basic_tables():
data_list.append(["John Jones", "9235 Highway 32\n" + green("Greenville") + ", SC 29604", DollarFormatter(82987.71)])
# Table Columns (width does not account for any borders or padding which may be added)
- columns: List[Column] = list()
+ columns: list[Column] = list()
columns.append(Column("Name", width=20))
columns.append(Column("Address", width=38))
columns.append(
@@ -123,7 +120,7 @@ def nested_tables():
"""
# Create data for this example
- author_data: List[Author] = []
+ author_data: list[Author] = []
author_1 = Author("Frank Herbert", "10/08/1920", "Tacoma, Washington")
author_1.books.append(Book("Dune", "1965"))
author_1.books.append(Book("Dune Messiah", "1969"))
@@ -159,7 +156,7 @@ def nested_tables():
# Define table which presents Author data fields vertically with no header.
# This will be nested in the parent table's first column.
- author_columns: List[Column] = list()
+ author_columns: list[Column] = list()
author_columns.append(Column("", width=14))
author_columns.append(Column("", width=20))
@@ -174,7 +171,7 @@ def nested_tables():
# Define AlternatingTable for books checked out by people in the first table.
# This will be nested in the parent table's second column.
- books_columns: List[Column] = list()
+ books_columns: list[Column] = list()
books_columns.append(Column(ansi.style("Title", bold=True), width=25))
books_columns.append(
Column(
@@ -196,7 +193,7 @@ def nested_tables():
# Define BorderedTable for relatives of the author
# This will be nested in the parent table's third column.
- relative_columns: List[Column] = list()
+ relative_columns: list[Column] = list()
relative_columns.append(Column(ansi.style("Name", bold=True), width=25))
relative_columns.append(Column(ansi.style("Relationship", bold=True), width=12))
@@ -220,7 +217,7 @@ def nested_tables():
)
# Define parent AlternatingTable which contains Author and Book tables
- parent_tbl_columns: List[Column] = list()
+ parent_tbl_columns: list[Column] = list()
# All of the nested tables already have background colors. Set style_data_text
# to False so the parent AlternatingTable does not apply background color to them.
@@ -242,7 +239,7 @@ def nested_tables():
)
# Construct the tables
- parent_table_data: List[List[Any]] = []
+ parent_table_data: list[list[Any]] = []
for row, author in enumerate(author_data, start=1):
# First build the author table and color it based on row number
author_tbl = even_author_tbl if row % 2 == 0 else odd_author_tbl
diff --git a/plugins/ext_test/build-pyenvs.sh b/plugins/ext_test/build-pyenvs.sh
index 20f5c8d4..4b515bbf 100644
--- a/plugins/ext_test/build-pyenvs.sh
+++ b/plugins/ext_test/build-pyenvs.sh
@@ -8,7 +8,7 @@
# version numbers are: major.minor.patch
#
# this script will delete and recreate existing virtualenvs named
-# cmd2-3.8, etc. It will also create a .python-version
+# cmd2-3.9, etc. It will also create a .python-version
#
# Prerequisites:
# - *nix-ish environment like macOS or Linux
@@ -23,7 +23,7 @@
# virtualenvs will be added to '.python-version'. Feel free to modify
# this list, but note that this script intentionally won't install
# dev, rc, or beta python releases
-declare -a pythons=("3.8" "3.9", "3.10", "3.11", "3.12")
+declare -a pythons=("3.9", "3.10", "3.11", "3.12", "3.13")
# function to find the latest patch of a minor version of python
function find_latest_version {
diff --git a/plugins/ext_test/noxfile.py b/plugins/ext_test/noxfile.py
index 25a95067..d8aa344b 100644
--- a/plugins/ext_test/noxfile.py
+++ b/plugins/ext_test/noxfile.py
@@ -1,7 +1,7 @@
import nox
-@nox.session(python=['3.8', '3.9', '3.10', '3.11', '3.12'])
+@nox.session(python=['3.9', '3.10', '3.11', '3.12', '3.13'])
def tests(session):
session.install('invoke', './[test]')
session.run('invoke', 'pytest', '--junit', '--no-pty')
diff --git a/plugins/ext_test/pyproject.toml b/plugins/ext_test/pyproject.toml
index 715301a9..5dbbd826 100644
--- a/plugins/ext_test/pyproject.toml
+++ b/plugins/ext_test/pyproject.toml
@@ -145,19 +145,10 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
mccabe.max-complexity = 49
-per-file-ignores."cmd2/__init__.py" = [
- "E402", # Module level import not at top of file
- "F401", # Unused import
-]
-
per-file-ignores."docs/conf.py" = [
"F401", # Unused import
]
-per-file-ignores."examples/override_parser.py" = [
- "E402", # Module level import not at top of file
-]
-
per-file-ignores."examples/scripts/*.py" = [
"F821", # Undefined name `app`
]
diff --git a/plugins/ext_test/setup.py b/plugins/ext_test/setup.py
index 45110413..a6487e54 100644
--- a/plugins/ext_test/setup.py
+++ b/plugins/ext_test/setup.py
@@ -33,7 +33,7 @@
license='MIT',
package_data=PACKAGE_DATA,
packages=['cmd2_ext_test'],
- python_requires='>=3.8',
+ python_requires='>=3.9',
install_requires=['cmd2 >= 2, <3'],
setup_requires=['setuptools >= 42', 'setuptools_scm >= 3.4'],
classifiers=[
@@ -43,11 +43,11 @@
'Topic :: Software Development :: Libraries :: Python Modules',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
- 'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
+ 'Programming Language :: Python :: 3.13',
],
# dependencies for development and testing
# $ pip install -e .[dev]
diff --git a/plugins/template/README.md b/plugins/template/README.md
index 775a9a81..9981987f 100644
--- a/plugins/template/README.md
+++ b/plugins/template/README.md
@@ -224,16 +224,16 @@ If you prefer to create these virtualenvs by hand, do the following:
```
$ cd cmd2_abbrev
-$ pyenv install 3.8.5
-$ pyenv virtualenv -p python3.8 3.8.5 cmd2-3.8
-$ pyenv install 3.9.0
-$ pyenv virtualenv -p python3.9 3.9.0 cmd2-3.9
+$ pyenv install 3.12
+$ pyenv virtualenv -p python3.12 3.12 cmd2-3.12
+$ pyenv install 3.13
+$ pyenv virtualenv -p python3.13 3.13 cmd2-3.13
```
Now set pyenv to make both of those available at the same time:
```
-$ pyenv local cmd2-3.8 cmd2-3.9
+$ pyenv local cmd2-3.13 cmd2-3.13
```
Whether you ran the script, or did it by hand, you now have isolated virtualenvs
@@ -241,12 +241,12 @@ for each of the major python versions. This table shows various python commands,
the version of python which will be executed, and the virtualenv it will
utilize.
-| Command | python | virtualenv |
-| ----------- | ------ | ---------- |
-| `python3.8` | 3.8.5 | cmd2-3.8 |
-| `python3.9` | 3.9.0 | cmd2-3.9 |
-| `pip3.8` | 3.8.5 | cmd2-3.8 |
-| `pip3.9` | 3.9.0 | cmd2-3.9 |
+| Command | python | virtualenv |
+| ------------ | ------ | ---------- |
+| `python3.12` | 3.12 | cmd2-3.12 |
+| `python3.13` | 3.13 | cmd2-3.13 |
+| `pip3.12` | 3.12 | cmd2-3.12 |
+| `pip3.13` | 3.13 | cmd2-3.13 |
## Install Dependencies
@@ -259,11 +259,11 @@ $ pip install -e .[dev]
This command also installs `cmd2-myplugin` "in-place", so the package points to
the source code instead of copying files to the python `site-packages` folder.
-All the dependencies now have been installed in the `cmd2-3.8`
+All the dependencies now have been installed in the `cmd2-3.13`
virtualenv. If you want to work in other virtualenvs, you'll need to manually
select it, and install again::
-$ pyenv shell cmd2-3.4
+$ pyenv shell cmd2-3.13
$ pip install -e .[dev]
Now that you have your python environments created, you need to install the
@@ -280,8 +280,8 @@ unit tests found in the `tests` directory.
### Use nox to run unit tests in multiple versions of python
-The included `noxfile.py` is setup to run the unit tests in python 3.8, 3.9
-3.10, 3.11, and 3.12 You can run your unit tests in all of these versions
+The included `noxfile.py` is setup to run the unit tests in python 3.9
+3.10, 3.11, 3.12, and 3.13 You can run your unit tests in all of these versions
of python by:
```
diff --git a/plugins/template/build-pyenvs.sh b/plugins/template/build-pyenvs.sh
index 4a6e1578..4b515bbf 100644
--- a/plugins/template/build-pyenvs.sh
+++ b/plugins/template/build-pyenvs.sh
@@ -8,7 +8,7 @@
# version numbers are: major.minor.patch
#
# this script will delete and recreate existing virtualenvs named
-# cmd2-3.8, etc. It will also create a .python-version
+# cmd2-3.9, etc. It will also create a .python-version
#
# Prerequisites:
# - *nix-ish environment like macOS or Linux
@@ -23,7 +23,7 @@
# virtualenvs will be added to '.python-version'. Feel free to modify
# this list, but note that this script intentionally won't install
# dev, rc, or beta python releases
-declare -a pythons=("3.8" "3.9" "3.10" "3.11", "3.12")
+declare -a pythons=("3.9", "3.10", "3.11", "3.12", "3.13")
# function to find the latest patch of a minor version of python
function find_latest_version {
diff --git a/plugins/template/noxfile.py b/plugins/template/noxfile.py
index 25a95067..d8aa344b 100644
--- a/plugins/template/noxfile.py
+++ b/plugins/template/noxfile.py
@@ -1,7 +1,7 @@
import nox
-@nox.session(python=['3.8', '3.9', '3.10', '3.11', '3.12'])
+@nox.session(python=['3.9', '3.10', '3.11', '3.12', '3.13'])
def tests(session):
session.install('invoke', './[test]')
session.run('invoke', 'pytest', '--junit', '--no-pty')
diff --git a/plugins/template/setup.py b/plugins/template/setup.py
index 7e872cd8..60028c11 100644
--- a/plugins/template/setup.py
+++ b/plugins/template/setup.py
@@ -24,7 +24,7 @@
url='https://github.com/python-cmd2/cmd2-plugin-template',
license='MIT',
packages=['cmd2_myplugin'],
- python_requires='>=3.8',
+ python_requires='>=3.9',
install_requires=['cmd2 >= 2, <3'],
setup_requires=['setuptools_scm'],
classifiers=[
@@ -34,11 +34,11 @@
'Topic :: Software Development :: Libraries :: Python Modules',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
- 'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
+ 'Programming Language :: Python :: 3.13',
],
# dependencies for development and testing
# $ pip install -e .[dev]
diff --git a/pyproject.toml b/pyproject.toml
index 68007ead..ebb13ace 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -8,8 +8,15 @@ dynamic = ["version"]
description = "cmd2 - quickly build feature-rich and user-friendly interactive command line applications in Python"
authors = [{ name = "cmd2 Contributors" }]
readme = "README.md"
-requires-python = ">=3.8"
-keywords = ["CLI", "cmd", "command", "interactive", "prompt", "Python"]
+requires-python = ">=3.9"
+keywords = [
+ "CLI",
+ "cmd",
+ "command",
+ "interactive",
+ "prompt",
+ "Python",
+]
license = { file = "LICENSE" }
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -19,7 +26,6 @@ classifiers = [
"Intended Audience :: System Administrators",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3 :: Only",
- "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
@@ -31,6 +37,7 @@ dependencies = [
"gnureadline>=8; platform_system == 'Darwin'",
"pyperclip>=1.8",
"pyreadline3>=3.4; platform_system == 'Windows'",
+ "rich-argparse>=1.4",
"wcwidth>=0.2.10",
]
@@ -176,7 +183,7 @@ select = [
"FA", # flake8-future-annotations
# "FBT", # flake8-boolean-trap
"G", # flake8-logging-format
- # "I", # isort
+ "I", # isort
"ICN", # flake8-import-conventions
# "INP", # flake8-no-pep420
"INT", # flake8-gettext
@@ -235,19 +242,10 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
mccabe.max-complexity = 49
-per-file-ignores."cmd2/__init__.py" = [
- "E402", # Module level import not at top of file
- "F401", # Unused import
-]
-
per-file-ignores."docs/conf.py" = [
"F401", # Unused import
]
-per-file-ignores."examples/override_parser.py" = [
- "E402", # Module level import not at top of file
-]
-
per-file-ignores."examples/scripts/*.py" = [
"F821", # Undefined name `app`
]
diff --git a/tests/conftest.py b/tests/conftest.py
index 644ae7cc..aae8a3bd 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -10,7 +10,6 @@
redirect_stdout,
)
from typing import (
- List,
Optional,
Union,
)
@@ -32,7 +31,7 @@
def verify_help_text(
- cmd2_app: cmd2.Cmd, help_output: Union[str, List[str]], verbose_strings: Optional[List[str]] = None
+ cmd2_app: cmd2.Cmd, help_output: Union[str, list[str]], verbose_strings: Optional[list[str]] = None
) -> None:
"""This function verifies that all expected commands are present in the help text.
@@ -53,36 +52,36 @@ def verify_help_text(
assert verbose_string in help_text
-# Help text for the history command
+# Help text for the history command (Generated when terminal width is 80)
HELP_HISTORY = """Usage: history [-h] [-r | -e | -o FILE | -t TRANSCRIPT_FILE | -c] [-s] [-x]
[-v] [-a]
[arg]
-View, run, edit, save, or clear previously entered commands
+View, run, edit, save, or clear previously entered commands.
-positional arguments:
+Positional Arguments:
arg empty all history items
a one history item by number
a..b, a:b, a:, ..b items by indices (inclusive)
string items containing string
/regex/ items matching regular expression
-optional arguments:
+Optional Arguments:
-h, --help show this help message and exit
-r, --run run selected history items
-e, --edit edit and then run selected history items
-o, --output_file FILE
- output commands to a script file, implies -s
+ output commands to a script file, implies --script
-t, --transcript TRANSCRIPT_FILE
output commands and results to a transcript file,
- implies -s
+ implies --script
-c, --clear clear all history
-formatting:
+Formatting:
-s, --script output commands in script format, i.e. without command
numbers
- -x, --expanded output fully parsed commands with any aliases and
- macros expanded, instead of typed commands
+ -x, --expanded output fully parsed commands with aliases and shortcuts
+ expanded
-v, --verbose display history and include expanded commands if they
differ from the typed command
-a, --all display all commands, including ones persisted from
@@ -195,7 +194,7 @@ def get_endidx():
return app.complete(text, 0)
-def find_subcommand(action: argparse.ArgumentParser, subcmd_names: List[str]) -> argparse.ArgumentParser:
+def find_subcommand(action: argparse.ArgumentParser, subcmd_names: list[str]) -> argparse.ArgumentParser:
if not subcmd_names:
return action
cur_subcmd = subcmd_names.pop(0)
diff --git a/tests/test_argparse.py b/tests/test_argparse.py
index f800c84a..61e126c0 100644
--- a/tests/test_argparse.py
+++ b/tests/test_argparse.py
@@ -40,8 +40,7 @@ def _say_parser_builder() -> cmd2.Cmd2ArgumentParser:
@cmd2.with_argparser(_say_parser_builder)
def do_say(self, args, *, keyword_arg: Optional[str] = None):
"""
- Repeat what you
- tell me to.
+ Repeat what you tell me to.
:param args: argparse namespace
:param keyword_arg: Optional keyword arguments
@@ -218,8 +217,7 @@ def test_argparse_help_docstring(argparse_app):
out, err = run_cmd(argparse_app, 'help say')
assert out[0].startswith('Usage: say')
assert out[1] == ''
- assert out[2] == 'Repeat what you'
- assert out[3] == 'tell me to.'
+ assert out[2] == 'Repeat what you tell me to.'
for line in out:
assert not line.startswith(':')
@@ -362,40 +360,26 @@ def test_subcommand_help(subcommand_app):
# foo has no aliases
out, err = run_cmd(subcommand_app, 'help base foo')
assert out[0].startswith('Usage: base foo')
- assert out[1] == ''
- assert out[2] == 'positional arguments:'
# bar has aliases (usage should never show alias name)
out, err = run_cmd(subcommand_app, 'help base bar')
assert out[0].startswith('Usage: base bar')
- assert out[1] == ''
- assert out[2] == 'positional arguments:'
out, err = run_cmd(subcommand_app, 'help base bar_1')
assert out[0].startswith('Usage: base bar')
- assert out[1] == ''
- assert out[2] == 'positional arguments:'
out, err = run_cmd(subcommand_app, 'help base bar_2')
assert out[0].startswith('Usage: base bar')
- assert out[1] == ''
- assert out[2] == 'positional arguments:'
# helpless has aliases and no help text (usage should never show alias name)
out, err = run_cmd(subcommand_app, 'help base helpless')
assert out[0].startswith('Usage: base helpless')
- assert out[1] == ''
- assert out[2] == 'positional arguments:'
out, err = run_cmd(subcommand_app, 'help base helpless_1')
assert out[0].startswith('Usage: base helpless')
- assert out[1] == ''
- assert out[2] == 'positional arguments:'
out, err = run_cmd(subcommand_app, 'help base helpless_2')
assert out[0].startswith('Usage: base helpless')
- assert out[1] == ''
- assert out[2] == 'positional arguments:'
def test_subcommand_invalid_help(subcommand_app):
diff --git a/tests/test_argparse_completer.py b/tests/test_argparse_completer.py
index 1f9178f8..e06b53a8 100644
--- a/tests/test_argparse_completer.py
+++ b/tests/test_argparse_completer.py
@@ -6,11 +6,7 @@
import argparse
import numbers
-from typing import (
- Dict,
- List,
- cast,
-)
+from typing import cast
import pytest
@@ -39,11 +35,11 @@
standalone_completions = ['standalone', 'completer']
-def standalone_choice_provider(cli: cmd2.Cmd) -> List[str]:
+def standalone_choice_provider(cli: cmd2.Cmd) -> list[str]:
return standalone_choices
-def standalone_completer(cli: cmd2.Cmd, text: str, line: str, begidx: int, endidx: int) -> List[str]:
+def standalone_completer(cli: cmd2.Cmd, text: str, line: str, begidx: int, endidx: int) -> list[str]:
return cli.basic_complete(text, line, begidx, endidx, standalone_completions)
@@ -125,11 +121,11 @@ def do_pos_and_flag(self, args: argparse.Namespace) -> None:
# This tests that CompletionItems created with numerical values are sorted as numbers.
num_completion_items = [CompletionItem(5, "Five"), CompletionItem(1.5, "One.Five"), CompletionItem(2, "Five")]
- def choices_provider(self) -> List[str]:
+ def choices_provider(self) -> list[str]:
"""Method that provides choices"""
return self.choices_from_provider
- def completion_item_method(self) -> List[CompletionItem]:
+ def completion_item_method(self) -> list[CompletionItem]:
"""Choices method that returns CompletionItems"""
items = []
for i in range(0, 10):
@@ -191,13 +187,13 @@ def do_choices(self, args: argparse.Namespace) -> None:
completions_for_pos_1 = ['completions', 'positional_1', 'probably', 'missed', 'spot']
completions_for_pos_2 = ['completions', 'positional_2', 'probably', 'missed', 'me']
- def flag_completer(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
+ def flag_completer(self, text: str, line: str, begidx: int, endidx: int) -> list[str]:
return self.basic_complete(text, line, begidx, endidx, self.completions_for_flag)
- def pos_1_completer(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
+ def pos_1_completer(self, text: str, line: str, begidx: int, endidx: int) -> list[str]:
return self.basic_complete(text, line, begidx, endidx, self.completions_for_pos_1)
- def pos_2_completer(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
+ def pos_2_completer(self, text: str, line: str, begidx: int, endidx: int) -> list[str]:
return self.basic_complete(text, line, begidx, endidx, self.completions_for_pos_2)
completer_parser = Cmd2ArgumentParser()
@@ -265,11 +261,11 @@ def do_hint(self, args: argparse.Namespace) -> None:
############################################################################################################
# Begin code related to CompletionError
############################################################################################################
- def completer_raise_error(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
+ def completer_raise_error(self, text: str, line: str, begidx: int, endidx: int) -> list[str]:
"""Raises CompletionError"""
raise CompletionError('completer broke something')
- def choice_raise_error(self) -> List[str]:
+ def choice_raise_error(self) -> list[str]:
"""Raises CompletionError"""
raise CompletionError('choice broke something')
@@ -284,13 +280,13 @@ def do_raise_completion_error(self, args: argparse.Namespace) -> None:
############################################################################################################
# Begin code related to receiving arg_tokens
############################################################################################################
- def choices_takes_arg_tokens(self, arg_tokens: Dict[str, List[str]]) -> List[str]:
+ def choices_takes_arg_tokens(self, arg_tokens: dict[str, list[str]]) -> list[str]:
"""Choices function that receives arg_tokens from ArgparseCompleter"""
return [arg_tokens['parent_arg'][0], arg_tokens['subcommand'][0]]
def completer_takes_arg_tokens(
- self, text: str, line: str, begidx: int, endidx: int, arg_tokens: Dict[str, List[str]]
- ) -> List[str]:
+ self, text: str, line: str, begidx: int, endidx: int, arg_tokens: dict[str, list[str]]
+ ) -> list[str]:
"""Completer function that receives arg_tokens from ArgparseCompleter"""
match_against = [arg_tokens['parent_arg'][0], arg_tokens['subcommand'][0]]
return self.basic_complete(text, line, begidx, endidx, match_against)
@@ -1204,7 +1200,7 @@ def test_complete_standalone(ac_app, flag, completions):
# Custom ArgparseCompleter-based class
class CustomCompleter(argparse_completer.ArgparseCompleter):
- def _complete_flags(self, text: str, line: str, begidx: int, endidx: int, matched_flags: List[str]) -> List[str]:
+ def _complete_flags(self, text: str, line: str, begidx: int, endidx: int, matched_flags: list[str]) -> list[str]:
"""Override so flags with 'complete_when_ready' set to True will complete only when app is ready"""
# Find flags which should not be completed and place them in matched_flags
diff --git a/tests/test_argparse_custom.py b/tests/test_argparse_custom.py
index a3f85558..3a5b14aa 100644
--- a/tests/test_argparse_custom.py
+++ b/tests/test_argparse_custom.py
@@ -240,30 +240,7 @@ def test_apcustom_required_options():
# Make sure a 'required arguments' section shows when a flag is marked required
parser = Cmd2ArgumentParser()
parser.add_argument('--required_flag', required=True)
- assert 'required arguments' in parser.format_help()
-
-
-def test_override_parser():
- """Test overriding argparse_custom.DEFAULT_ARGUMENT_PARSER"""
- import importlib
-
- from cmd2 import (
- argparse_custom,
- )
-
- # The standard parser is Cmd2ArgumentParser
- assert argparse_custom.DEFAULT_ARGUMENT_PARSER == Cmd2ArgumentParser
-
- # Set our parser module and force a reload of cmd2 so it loads the module
- argparse.cmd2_parser_module = 'examples.custom_parser'
- importlib.reload(cmd2)
-
- # Verify DEFAULT_ARGUMENT_PARSER is now our CustomParser
- from examples.custom_parser import (
- CustomParser,
- )
-
- assert argparse_custom.DEFAULT_ARGUMENT_PARSER == CustomParser
+ assert 'Required Arguments' in parser.format_help()
def test_apcustom_metavar_tuple():
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 721be0a2..6092c54e 100755
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -1256,9 +1256,10 @@ def test_help_multiline_docstring(help_app):
assert help_app.last_result is True
-def test_help_verbose_uses_parser_description(help_app: HelpApp):
+def test_help_verbose_uses_parser_docstring(help_app: HelpApp):
out, err = run_cmd(help_app, 'help --verbose')
- verify_help_text(help_app, out, verbose_strings=[help_app.parser_cmd_parser.description])
+ expected_verbose = utils.strip_doc_annotations(help_app.do_parser_cmd.__doc__)
+ verify_help_text(help_app, out, verbose_strings=[expected_verbose])
class HelpCategoriesApp(cmd2.Cmd):
@@ -1579,7 +1580,7 @@ def test_help_with_no_docstring(capsys):
out
== """Usage: greet [-h] [-s]
-optional arguments:
+Optional Arguments:
-h, --help show this help message and exit
-s, --shout N00B EMULATION MODE
@@ -1642,8 +1643,8 @@ def test_multiline_complete_statement_with_unclosed_quotes(multiline_app):
assert statement.terminator == ';'
-def test_multiline_input_line_to_statement(multiline_app):
- # Verify _input_line_to_statement saves the fully entered input line for multiline commands
+def test_multiline_complete_statement(multiline_app):
+ # Verify _complete_statement saves the fully entered input line for multiline commands
# Mock out the input call so we don't actually wait for a user's response
# on stdin when it looks for more input
@@ -1651,7 +1652,7 @@ def test_multiline_input_line_to_statement(multiline_app):
builtins.input = m
line = 'orate hi'
- statement = multiline_app._input_line_to_statement(line)
+ statement = multiline_app._complete_statement(line)
assert statement.raw == 'orate hi\nperson\n'
assert statement == 'hi person'
assert statement.command == 'orate'
@@ -2010,8 +2011,7 @@ def test_poutput_ansi_never(outsim_app):
assert out == expected
-# These are invalid names for aliases and macros
-invalid_command_name = [
+invalid_alias_names = [
'""', # Blank name
constants.COMMENT_CHAR,
'!no_shortcut',
@@ -2037,19 +2037,6 @@ def test_get_alias_completion_items(base_app):
assert cur_res.description.rstrip() == base_app.aliases[cur_res]
-def test_get_macro_completion_items(base_app):
- run_cmd(base_app, 'macro create foo !echo foo')
- run_cmd(base_app, 'macro create bar !echo bar')
-
- results = base_app._get_macro_completion_items()
- assert len(results) == len(base_app.macros)
-
- for cur_res in results:
- assert cur_res in base_app.macros
- # Strip trailing spaces from table output
- assert cur_res.description.rstrip() == base_app.macros[cur_res].value
-
-
def test_get_settable_completion_items(base_app):
results = base_app._get_settable_completion_items()
assert len(results) == len(base_app.settables)
@@ -2125,7 +2112,7 @@ def test_alias_create_with_quoted_tokens(base_app):
assert base_app.last_result[alias_name] == alias_command
-@pytest.mark.parametrize('alias_name', invalid_command_name)
+@pytest.mark.parametrize('alias_name', invalid_alias_names)
def test_alias_create_invalid_name(base_app, alias_name, capsys):
out, err = run_cmd(base_app, 'alias create {} help'.format(alias_name))
assert "Invalid alias name" in err[0]
@@ -2138,14 +2125,6 @@ def test_alias_create_with_command_name(base_app):
assert base_app.last_result is False
-def test_alias_create_with_macro_name(base_app):
- macro = "my_macro"
- run_cmd(base_app, 'macro create {} help'.format(macro))
- out, err = run_cmd(base_app, 'alias create {} help'.format(macro))
- assert "Alias cannot have the same name as a macro" in err[0]
- assert base_app.last_result is False
-
-
def test_alias_that_resolves_into_comment(base_app):
# Create the alias
out, err = run_cmd(base_app, 'alias create fake ' + constants.COMMENT_CHAR + ' blah blah')
@@ -2204,228 +2183,6 @@ def test_multiple_aliases(base_app):
verify_help_text(base_app, out)
-def test_macro_no_subcommand(base_app):
- out, err = run_cmd(base_app, 'macro')
- assert "Usage: macro [-h]" in err[0]
- assert "Error: the following arguments are required: SUBCOMMAND" in err[1]
-
-
-def test_macro_create(base_app):
- # Create the macro
- out, err = run_cmd(base_app, 'macro create fake run_pyscript')
- assert out == normalize("Macro 'fake' created")
- assert base_app.last_result is True
-
- # Use the macro
- out, err = run_cmd(base_app, 'fake')
- assert "the following arguments are required: script_path" in err[1]
-
- # See a list of macros
- out, err = run_cmd(base_app, 'macro list')
- assert out == normalize('macro create fake run_pyscript')
- assert len(base_app.last_result) == len(base_app.macros)
- assert base_app.last_result['fake'] == "run_pyscript"
-
- # Look up the new macro
- out, err = run_cmd(base_app, 'macro list fake')
- assert out == normalize('macro create fake run_pyscript')
- assert len(base_app.last_result) == 1
- assert base_app.last_result['fake'] == "run_pyscript"
-
- # Overwrite macro
- out, err = run_cmd(base_app, 'macro create fake help')
- assert out == normalize("Macro 'fake' overwritten")
- assert base_app.last_result is True
-
- # Look up the updated macro
- out, err = run_cmd(base_app, 'macro list fake')
- assert out == normalize('macro create fake help')
- assert len(base_app.last_result) == 1
- assert base_app.last_result['fake'] == "help"
-
-
-def test_macro_create_with_quoted_tokens(base_app):
- """Demonstrate that quotes in macro value will be preserved"""
- macro_name = "fake"
- macro_command = 'help ">" "out file.txt" ";"'
- create_command = f"macro create {macro_name} {macro_command}"
-
- # Create the macro
- out, err = run_cmd(base_app, create_command)
- assert out == normalize("Macro 'fake' created")
-
- # Look up the new macro and verify all quotes are preserved
- out, err = run_cmd(base_app, 'macro list fake')
- assert out == normalize(create_command)
- assert len(base_app.last_result) == 1
- assert base_app.last_result[macro_name] == macro_command
-
-
-@pytest.mark.parametrize('macro_name', invalid_command_name)
-def test_macro_create_invalid_name(base_app, macro_name):
- out, err = run_cmd(base_app, 'macro create {} help'.format(macro_name))
- assert "Invalid macro name" in err[0]
- assert base_app.last_result is False
-
-
-def test_macro_create_with_command_name(base_app):
- out, err = run_cmd(base_app, 'macro create help stuff')
- assert "Macro cannot have the same name as a command" in err[0]
- assert base_app.last_result is False
-
-
-def test_macro_create_with_alias_name(base_app):
- macro = "my_macro"
- run_cmd(base_app, 'alias create {} help'.format(macro))
- out, err = run_cmd(base_app, 'macro create {} help'.format(macro))
- assert "Macro cannot have the same name as an alias" in err[0]
- assert base_app.last_result is False
-
-
-def test_macro_create_with_args(base_app):
- # Create the macro
- out, err = run_cmd(base_app, 'macro create fake {1} {2}')
- assert out == normalize("Macro 'fake' created")
-
- # Run the macro
- out, err = run_cmd(base_app, 'fake help -v')
- verify_help_text(base_app, out)
-
-
-def test_macro_create_with_escaped_args(base_app):
- # Create the macro
- out, err = run_cmd(base_app, 'macro create fake help {{1}}')
- assert out == normalize("Macro 'fake' created")
-
- # Run the macro
- out, err = run_cmd(base_app, 'fake')
- assert err[0].startswith('No help on {1}')
-
-
-def test_macro_usage_with_missing_args(base_app):
- # Create the macro
- out, err = run_cmd(base_app, 'macro create fake help {1} {2}')
- assert out == normalize("Macro 'fake' created")
-
- # Run the macro
- out, err = run_cmd(base_app, 'fake arg1')
- assert "expects at least 2 arguments" in err[0]
-
-
-def test_macro_usage_with_exta_args(base_app):
- # Create the macro
- out, err = run_cmd(base_app, 'macro create fake help {1}')
- assert out == normalize("Macro 'fake' created")
-
- # Run the macro
- out, err = run_cmd(base_app, 'fake alias create')
- assert "Usage: alias create" in out[0]
-
-
-def test_macro_create_with_missing_arg_nums(base_app):
- # Create the macro
- out, err = run_cmd(base_app, 'macro create fake help {1} {3}')
- assert "Not all numbers between 1 and 3" in err[0]
- assert base_app.last_result is False
-
-
-def test_macro_create_with_invalid_arg_num(base_app):
- # Create the macro
- out, err = run_cmd(base_app, 'macro create fake help {1} {-1} {0}')
- assert "Argument numbers must be greater than 0" in err[0]
- assert base_app.last_result is False
-
-
-def test_macro_create_with_unicode_numbered_arg(base_app):
- # Create the macro expecting 1 argument
- out, err = run_cmd(base_app, 'macro create fake help {\N{ARABIC-INDIC DIGIT ONE}}')
- assert out == normalize("Macro 'fake' created")
-
- # Run the macro
- out, err = run_cmd(base_app, 'fake')
- assert "expects at least 1 argument" in err[0]
-
-
-def test_macro_create_with_missing_unicode_arg_nums(base_app):
- out, err = run_cmd(base_app, 'macro create fake help {1} {\N{ARABIC-INDIC DIGIT THREE}}')
- assert "Not all numbers between 1 and 3" in err[0]
- assert base_app.last_result is False
-
-
-def test_macro_that_resolves_into_comment(base_app):
- # Create the macro
- out, err = run_cmd(base_app, 'macro create fake {1} blah blah')
- assert out == normalize("Macro 'fake' created")
-
- # Use the macro
- out, err = run_cmd(base_app, 'fake ' + constants.COMMENT_CHAR)
- assert not out
- assert not err
-
-
-def test_macro_list_invalid_macro(base_app):
- # Look up invalid macro
- out, err = run_cmd(base_app, 'macro list invalid')
- assert "Macro 'invalid' not found" in err[0]
- assert base_app.last_result == {}
-
-
-def test_macro_delete(base_app):
- # Create an macro
- run_cmd(base_app, 'macro create fake run_pyscript')
-
- # Delete the macro
- out, err = run_cmd(base_app, 'macro delete fake')
- assert out == normalize("Macro 'fake' deleted")
- assert base_app.last_result is True
-
-
-def test_macro_delete_all(base_app):
- out, err = run_cmd(base_app, 'macro delete --all')
- assert out == normalize("All macros deleted")
- assert base_app.last_result is True
-
-
-def test_macro_delete_non_existing(base_app):
- out, err = run_cmd(base_app, 'macro delete fake')
- assert "Macro 'fake' does not exist" in err[0]
- assert base_app.last_result is True
-
-
-def test_macro_delete_no_name(base_app):
- out, err = run_cmd(base_app, 'macro delete')
- assert "Either --all or macro name(s)" in err[0]
- assert base_app.last_result is False
-
-
-def test_multiple_macros(base_app):
- macro1 = 'h1'
- macro2 = 'h2'
- run_cmd(base_app, 'macro create {} help'.format(macro1))
- run_cmd(base_app, 'macro create {} help -v'.format(macro2))
- out, err = run_cmd(base_app, macro1)
- verify_help_text(base_app, out)
-
- out2, err2 = run_cmd(base_app, macro2)
- verify_help_text(base_app, out2)
- assert len(out2) > len(out)
-
-
-def test_nonexistent_macro(base_app):
- from cmd2.parsing import (
- StatementParser,
- )
-
- exception = None
-
- try:
- base_app._resolve_macro(StatementParser().parse('fake'))
- except KeyError as e:
- exception = e
-
- assert exception is not None
-
-
@with_ansi_style(ansi.AllowStyle.ALWAYS)
def test_perror_style(base_app, capsys):
msg = 'testing...'
@@ -2565,7 +2322,6 @@ def test_get_all_commands(base_app):
'help',
'history',
'ipy',
- 'macro',
'py',
'quit',
'run_pyscript',
diff --git a/tests/test_completion.py b/tests/test_completion.py
index cd2a2af0..ec731a72 100755
--- a/tests/test_completion.py
+++ b/tests/test_completion.py
@@ -26,8 +26,6 @@
from .conftest import (
complete_tester,
- normalize,
- run_cmd,
)
# List of strings used with completion functions
@@ -186,25 +184,6 @@ def test_complete_exception(cmd2_app, capsys):
assert "IndexError" in err
-def test_complete_macro(base_app, request):
- # Create the macro
- out, err = run_cmd(base_app, 'macro create fake run_pyscript {1}')
- assert out == normalize("Macro 'fake' created")
-
- # Macros do path completion
- test_dir = os.path.dirname(request.module.__file__)
-
- text = os.path.join(test_dir, 's')
- line = 'fake {}'.format(text)
-
- endidx = len(line)
- begidx = endidx - len(text)
-
- expected = [text + 'cript.py', text + 'cript.txt', text + 'cripts' + os.path.sep]
- first_match = complete_tester(text, line, begidx, endidx, base_app)
- assert first_match is not None and base_app.completion_matches == expected
-
-
def test_default_sort_key(cmd2_app):
text = ''
line = 'test_sort_key {}'.format(text)
diff --git a/tests/test_history.py b/tests/test_history.py
index 1a3bd744..f2e12277 100755
--- a/tests/test_history.py
+++ b/tests/test_history.py
@@ -772,9 +772,7 @@ def test_history_verbose_with_other_options(base_app):
options_to_test = ['-r', '-e', '-o file', '-t file', '-c', '-x']
for opt in options_to_test:
out, err = run_cmd(base_app, 'history -v ' + opt)
- assert len(out) == 4
- assert out[0] == '-v cannot be used with any other options'
- assert out[1].startswith('Usage:')
+ assert '-v cannot be used with any other options' in out
assert base_app.last_result is False
@@ -800,9 +798,7 @@ def test_history_script_with_invalid_options(base_app):
options_to_test = ['-r', '-e', '-o file', '-t file', '-c']
for opt in options_to_test:
out, err = run_cmd(base_app, 'history -s ' + opt)
- assert len(out) == 4
- assert out[0] == '-s and -x cannot be used with -c, -r, -e, -o, or -t'
- assert out[1].startswith('Usage:')
+ assert '-s and -x cannot be used with -c, -r, -e, -o, or -t' in out
assert base_app.last_result is False
@@ -820,9 +816,7 @@ def test_history_expanded_with_invalid_options(base_app):
options_to_test = ['-r', '-e', '-o file', '-t file', '-c']
for opt in options_to_test:
out, err = run_cmd(base_app, 'history -x ' + opt)
- assert len(out) == 4
- assert out[0] == '-s and -x cannot be used with -c, -r, -e, -o, or -t'
- assert out[1].startswith('Usage:')
+ assert '-s and -x cannot be used with -c, -r, -e, -o, or -t' in out
assert base_app.last_result is False
diff --git a/tests/test_parsing.py b/tests/test_parsing.py
index e3d42d7c..ed5a00f9 100755
--- a/tests/test_parsing.py
+++ b/tests/test_parsing.py
@@ -1034,111 +1034,3 @@ def test_is_valid_command_valid(parser):
valid, errmsg = parser.is_valid_command('!subcmd', is_subcommand=True)
assert valid
assert not errmsg
-
-
-def test_macro_normal_arg_pattern():
- # This pattern matches digits surrounded by exactly 1 brace on a side and 1 or more braces on the opposite side
- from cmd2.parsing import (
- MacroArg,
- )
-
- pattern = MacroArg.macro_normal_arg_pattern
-
- # Valid strings
- matches = pattern.findall('{5}')
- assert matches == ['{5}']
-
- matches = pattern.findall('{233}')
- assert matches == ['{233}']
-
- matches = pattern.findall('{{{{{4}')
- assert matches == ['{4}']
-
- matches = pattern.findall('{2}}}}}')
- assert matches == ['{2}']
-
- matches = pattern.findall('{3}{4}{5}')
- assert matches == ['{3}', '{4}', '{5}']
-
- matches = pattern.findall('{3} {4} {5}')
- assert matches == ['{3}', '{4}', '{5}']
-
- matches = pattern.findall('{3} {{{4} {5}}}}')
- assert matches == ['{3}', '{4}', '{5}']
-
- matches = pattern.findall('{3} text {4} stuff {5}}}}')
- assert matches == ['{3}', '{4}', '{5}']
-
- # Unicode digit
- matches = pattern.findall('{\N{ARABIC-INDIC DIGIT ONE}}')
- assert matches == ['{\N{ARABIC-INDIC DIGIT ONE}}']
-
- # Invalid strings
- matches = pattern.findall('5')
- assert not matches
-
- matches = pattern.findall('{5')
- assert not matches
-
- matches = pattern.findall('5}')
- assert not matches
-
- matches = pattern.findall('{{5}}')
- assert not matches
-
- matches = pattern.findall('{5text}')
- assert not matches
-
-
-def test_macro_escaped_arg_pattern():
- # This pattern matches digits surrounded by 2 or more braces on both sides
- from cmd2.parsing import (
- MacroArg,
- )
-
- pattern = MacroArg.macro_escaped_arg_pattern
-
- # Valid strings
- matches = pattern.findall('{{5}}')
- assert matches == ['{{5}}']
-
- matches = pattern.findall('{{233}}')
- assert matches == ['{{233}}']
-
- matches = pattern.findall('{{{{{4}}')
- assert matches == ['{{4}}']
-
- matches = pattern.findall('{{2}}}}}')
- assert matches == ['{{2}}']
-
- matches = pattern.findall('{{3}}{{4}}{{5}}')
- assert matches == ['{{3}}', '{{4}}', '{{5}}']
-
- matches = pattern.findall('{{3}} {{4}} {{5}}')
- assert matches == ['{{3}}', '{{4}}', '{{5}}']
-
- matches = pattern.findall('{{3}} {{{4}} {{5}}}}')
- assert matches == ['{{3}}', '{{4}}', '{{5}}']
-
- matches = pattern.findall('{{3}} text {{4}} stuff {{5}}}}')
- assert matches == ['{{3}}', '{{4}}', '{{5}}']
-
- # Unicode digit
- matches = pattern.findall('{{\N{ARABIC-INDIC DIGIT ONE}}}')
- assert matches == ['{{\N{ARABIC-INDIC DIGIT ONE}}}']
-
- # Invalid strings
- matches = pattern.findall('5')
- assert not matches
-
- matches = pattern.findall('{{5')
- assert not matches
-
- matches = pattern.findall('5}}')
- assert not matches
-
- matches = pattern.findall('{5}')
- assert not matches
-
- matches = pattern.findall('{{5text}}')
- assert not matches
diff --git a/tests/test_transcript.py b/tests/test_transcript.py
index 986221ff..3567c55c 100644
--- a/tests/test_transcript.py
+++ b/tests/test_transcript.py
@@ -48,7 +48,9 @@ def __init__(self, *args, **kwargs):
speak_parser = cmd2.Cmd2ArgumentParser()
speak_parser.add_argument('-p', '--piglatin', action="store_true", help="atinLay")
speak_parser.add_argument('-s', '--shout', action="store_true", help="N00B EMULATION MODE")
- speak_parser.add_argument('-r', '--repeat', type=int, help="output [n] times")
+
+ # Escape open bracket since help text can contain rich markup
+ speak_parser.add_argument('-r', '--repeat', type=int, help=r"output \[n] times")
@cmd2.with_argparser(speak_parser, with_unknown_args=True)
def do_speak(self, opts, arg):
diff --git a/tests/test_utils.py b/tests/test_utils.py
index a173f7f4..665261ab 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -336,10 +336,6 @@ def test_context_flag_exit_err(context_flag):
def test_remove_overridden_styles():
- from typing import (
- List,
- )
-
from cmd2 import (
Bg,
EightBitBg,
@@ -350,7 +346,7 @@ def test_remove_overridden_styles():
TextStyle,
)
- def make_strs(styles_list: List[ansi.AnsiSequence]) -> List[str]:
+ def make_strs(styles_list: list[ansi.AnsiSequence]) -> list[str]:
return [str(s) for s in styles_list]
# Test Reset All
diff --git a/tests/transcripts/from_cmdloop.txt b/tests/transcripts/from_cmdloop.txt
index f1c68d81..da536383 100644
--- a/tests/transcripts/from_cmdloop.txt
+++ b/tests/transcripts/from_cmdloop.txt
@@ -6,7 +6,7 @@ Usage: speak [-h] [-p] [-s] [-r REPEAT]/ */
Repeats what you tell me to./ */
-optional arguments:/ */
+Optional Arguments:/ */
-h, --help show this help message and exit/ */
-p, --piglatin atinLay/ */
-s, --shout N00B EMULATION MODE/ */
diff --git a/tests_isolated/test_commandset/conftest.py b/tests_isolated/test_commandset/conftest.py
index c8c6d34b..423523b5 100644
--- a/tests_isolated/test_commandset/conftest.py
+++ b/tests_isolated/test_commandset/conftest.py
@@ -9,7 +9,6 @@
redirect_stdout,
)
from typing import (
- List,
Optional,
Union,
)
@@ -34,7 +33,7 @@
def verify_help_text(
- cmd2_app: cmd2.Cmd, help_output: Union[str, List[str]], verbose_strings: Optional[List[str]] = None
+ cmd2_app: cmd2.Cmd, help_output: Union[str, list[str]], verbose_strings: Optional[list[str]] = None
) -> None:
"""This function verifies that all expected commands are present in the help text.
@@ -83,8 +82,7 @@ def verify_help_text(
formatting:
-s, --script output commands in script format, i.e. without command
numbers
- -x, --expanded output fully parsed commands with any aliases and
- macros expanded, instead of typed commands
+ -x, --expanded output fully parsed commands with aliases and shortcuts expanded
-v, --verbose display history and include expanded commands if they
differ from the typed command
-a, --all display all commands, including ones persisted from
diff --git a/tests_isolated/test_commandset/test_argparse_subcommands.py b/tests_isolated/test_commandset/test_argparse_subcommands.py
index 3a9ab595..35cd99d6 100644
--- a/tests_isolated/test_commandset/test_argparse_subcommands.py
+++ b/tests_isolated/test_commandset/test_argparse_subcommands.py
@@ -98,39 +98,39 @@ def test_subcommand_help(subcommand_app):
out, err = run_cmd(subcommand_app, 'help base foo')
assert out[0].startswith('Usage: base foo')
assert out[1] == ''
- assert out[2] == 'positional arguments:'
+ assert out[2] == 'Positional Arguments:'
# bar has aliases (usage should never show alias name)
out, err = run_cmd(subcommand_app, 'help base bar')
assert out[0].startswith('Usage: base bar')
assert out[1] == ''
- assert out[2] == 'positional arguments:'
+ assert out[2] == 'Positional Arguments:'
out, err = run_cmd(subcommand_app, 'help base bar_1')
assert out[0].startswith('Usage: base bar')
assert out[1] == ''
- assert out[2] == 'positional arguments:'
+ assert out[2] == 'Positional Arguments:'
out, err = run_cmd(subcommand_app, 'help base bar_2')
assert out[0].startswith('Usage: base bar')
assert out[1] == ''
- assert out[2] == 'positional arguments:'
+ assert out[2] == 'Positional Arguments:'
# helpless has aliases and no help text (usage should never show alias name)
out, err = run_cmd(subcommand_app, 'help base helpless')
assert out[0].startswith('Usage: base helpless')
assert out[1] == ''
- assert out[2] == 'positional arguments:'
+ assert out[2] == 'Positional Arguments:'
out, err = run_cmd(subcommand_app, 'help base helpless_1')
assert out[0].startswith('Usage: base helpless')
assert out[1] == ''
- assert out[2] == 'positional arguments:'
+ assert out[2] == 'Positional Arguments:'
out, err = run_cmd(subcommand_app, 'help base helpless_2')
assert out[0].startswith('Usage: base helpless')
assert out[1] == ''
- assert out[2] == 'positional arguments:'
+ assert out[2] == 'Positional Arguments:'
def test_subcommand_invalid_help(subcommand_app):
diff --git a/tests_isolated/test_commandset/test_commandset.py b/tests_isolated/test_commandset/test_commandset.py
index 89c982f3..434e5d6a 100644
--- a/tests_isolated/test_commandset/test_commandset.py
+++ b/tests_isolated/test_commandset/test_commandset.py
@@ -6,9 +6,6 @@
import argparse
import signal
-from typing import (
- List,
-)
import pytest
@@ -61,7 +58,7 @@ def do_banana(self, statement: cmd2.Statement):
cranberry_parser.add_argument('arg1', choices=['lemonade', 'juice', 'sauce'])
@cmd2.with_argparser(cranberry_parser, with_unknown_args=True)
- def do_cranberry(self, ns: argparse.Namespace, unknown: List[str]):
+ def do_cranberry(self, ns: argparse.Namespace, unknown: list[str]):
self._cmd.poutput('Cranberry {}!!'.format(ns.arg1))
if unknown and len(unknown):
self._cmd.poutput('Unknown: ' + ', '.join(['{}'] * len(unknown)).format(*unknown))
@@ -72,13 +69,13 @@ def help_cranberry(self):
@cmd2.with_argument_list
@cmd2.with_category('Also Alone')
- def do_durian(self, args: List[str]):
+ def do_durian(self, args: list[str]):
"""Durian Command"""
self._cmd.poutput('{} Arguments: '.format(len(args)))
self._cmd.poutput(', '.join(['{}'] * len(args)).format(*args))
self._cmd.last_result = {'args': args}
- def complete_durian(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
+ def complete_durian(self, text: str, line: str, begidx: int, endidx: int) -> list[str]:
return self._cmd.basic_complete(text, line, begidx, endidx, ['stinks', 'smells', 'disgusting'])
elderberry_parser = cmd2.Cmd2ArgumentParser()
@@ -350,17 +347,17 @@ def test_load_commandset_errors(command_sets_manual, capsys):
delattr(command_sets_manual, 'do_durian')
- # pre-create intentionally conflicting macro and alias names
- command_sets_manual.app_cmd('macro create apple run_pyscript')
+ # pre-create intentionally conflicting aliases
+ command_sets_manual.app_cmd('alias create apple run_pyscript')
command_sets_manual.app_cmd('alias create banana run_pyscript')
# now install a command set and verify the commands are now present
command_sets_manual.register_command_set(cmd_set)
out, err = capsys.readouterr()
- # verify aliases and macros are deleted with warning if they conflict with a command
+ # verify aliases are deleted with warning if they conflict with a command
+ assert "Deleting alias 'apple'" in err
assert "Deleting alias 'banana'" in err
- assert "Deleting macro 'apple'" in err
# verify command functions which don't start with "do_" raise an exception
with pytest.raises(CommandSetRegistrationError):
@@ -504,7 +501,7 @@ def __init__(self, dummy):
def do_arugula(self, _: cmd2.Statement):
self._cmd.poutput('Arugula')
- def complete_style_arg(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
+ def complete_style_arg(self, text: str, line: str, begidx: int, endidx: int) -> list[str]:
return ['quartered', 'diced']
bokchoy_parser = cmd2.Cmd2ArgumentParser()
@@ -742,7 +739,7 @@ def cut_banana(self, ns: argparse.Namespace):
"""Cut banana"""
self.poutput('cutting banana: ' + ns.direction)
- def complete_style_arg(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
+ def complete_style_arg(self, text: str, line: str, begidx: int, endidx: int) -> list[str]:
return ['quartered', 'diced']
bokchoy_parser = cmd2.Cmd2ArgumentParser()
@@ -797,7 +794,7 @@ def __init__(self, dummy):
"""dummy variable prevents this from being autoloaded in other tests"""
super(SupportFuncProvider, self).__init__()
- def complete_states(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
+ def complete_states(self, text: str, line: str, begidx: int, endidx: int) -> list[str]:
assert self is complete_states_expected_self
return self._cmd.basic_complete(text, line, begidx, endidx, self.states)