Skip to content

Commit e7f3313

Browse files
Drop support for Python 3.9 and require 3.10 minimum (#1473)
* Drop support for Python 3.9 and require 3.10 minimum The primary change is that type hints now support the `|` opertor and both `Optional` and `Union` are needed much less frequently from the typing module. Also: - Upgrade versions of ruff and prettier used by pre-commit * Restored some comments. --------- Co-authored-by: Kevin Van Brunt <kmvanbrunt@gmail.com>
1 parent 5ac81d4 commit e7f3313

36 files changed

+305
-366
lines changed

.github/CONTRIBUTING.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ See the `dependencies` list under the `[project]` heading in [pyproject.toml](..
6262

6363
| Prerequisite | Minimum Version | Purpose |
6464
| --------------------------------------------------- | --------------- | -------------------------------------- |
65-
| [python](https://www.python.org/downloads/) | `3.9` | Python programming language |
65+
| [python](https://www.python.org/downloads/) | `3.10` | Python programming language |
6666
| [pyperclip](https://github.com/asweigart/pyperclip) | `1.8` | Cross-platform clipboard functions |
6767
| [wcwidth](https://pypi.python.org/pypi/wcwidth) | `0.2.10` | Measure the displayed width of unicode |
6868

@@ -520,13 +520,11 @@ on how to do it.
520520

521521
4. The title (also called the subject) of your PR should be descriptive of your changes and
522522
succinctly indicate what is being fixed
523-
524523
- **Do not add the issue number in the PR title or commit message**
525524

526525
- Examples: `Add test cases for Unicode support`; `Correct typo in overview documentation`
527526

528527
5. In the body of your PR include a more detailed summary of the changes you made and why
529-
530528
- If the PR is meant to fix an existing bug/issue, then, at the end of your PR's description,
531529
append the keyword `closes` and #xxxx (where xxxx is the issue number). Example:
532530
`closes #1337`. This tells GitHub to close the existing issue if the PR is merged.

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
strategy:
1313
matrix:
1414
os: [ubuntu-latest, macos-latest, windows-latest]
15-
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"]
15+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"]
1616
fail-fast: false
1717

1818
runs-on: ${{ matrix.os }}

.github/workflows/typecheck.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
runs-on: ubuntu-latest
1616
strategy:
1717
matrix:
18-
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
18+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
1919
fail-fast: false
2020
defaults:
2121
run:

.pre-commit-config.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ repos:
99
- id: trailing-whitespace
1010

1111
- repo: https://github.com/astral-sh/ruff-pre-commit
12-
rev: "v0.12.4"
12+
rev: "v0.12.7"
1313
hooks:
1414
- id: ruff-format
1515
args: [--config=pyproject.toml]
@@ -21,5 +21,5 @@ repos:
2121
hooks:
2222
- id: prettier
2323
additional_dependencies:
24-
- prettier@3.5.3
25-
- prettier-plugin-toml@2.0.5
24+
- prettier@3.6.2
25+
- prettier-plugin-toml@2.0.6

CHANGELOG.md

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
## 3.0.0 (TBD)
22

33
- Breaking Changes
4-
4+
- `cmd2` 3.0 supports Python 3.10+ (removed support for Python 3.9)
55
- No longer setting parser's `prog` value in `with_argparser()` since it gets set in
66
`Cmd._build_parser()`. This code had previously been restored to support backward
77
compatibility in `cmd2` 2.0 family.
88

99
- Enhancements
10-
1110
- Simplified the process to set a custom parser for `cmd2's` built-in commands. See
1211
[custom_parser.py](https://github.com/python-cmd2/cmd2/blob/main/examples/custom_parser.py)
1312
example for more details.
@@ -30,7 +29,6 @@
3029
## 2.6.2 (June 26, 2025)
3130

3231
- Enhancements
33-
3432
- Added explicit support for free-threaded versions of Python, starting with version 3.14
3533

3634
- Bug Fixes
@@ -1316,12 +1314,10 @@
13161314
## 0.8.5 (April 15, 2018)
13171315

13181316
- Bug Fixes
1319-
13201317
- Fixed a bug with all argument decorators where the wrapped function wasn't returning a value
13211318
and thus couldn't cause the cmd2 app to quit
13221319

13231320
- Enhancements
1324-
13251321
- Added support for verbose help with -v where it lists a brief summary of what each command
13261322
does
13271323
- Added support for categorizing commands into groups within the help menu
@@ -1353,12 +1349,10 @@
13531349
## 0.8.3 (April 09, 2018)
13541350

13551351
- Bug Fixes
1356-
13571352
- Fixed `help` command not calling functions for help topics
13581353
- Fixed not being able to use quoted paths when redirecting with `<` and `>`
13591354

13601355
- Enhancements
1361-
13621356
- Tab completion has been overhauled and now supports completion of strings with quotes and
13631357
spaces.
13641358
- Tab completion will automatically add an opening quote if a string with a space is completed.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ On all operating systems, the latest stable version of `cmd2` can be installed u
8787
pip install -U cmd2
8888
```
8989

90-
cmd2 works with Python 3.9+ on Windows, macOS, and Linux. It is pure Python code with few 3rd-party
90+
cmd2 works with Python 3.10+ on Windows, macOS, and Linux. It is pure Python code with few 3rd-party
9191
dependencies. It works with both conventional CPython and free-threaded variants.
9292

9393
For information on other installation options, see

cmd2/ansi.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from typing import (
1212
IO,
1313
Any,
14-
Optional,
1514
cast,
1615
)
1716

@@ -924,14 +923,14 @@ def __str__(self) -> str:
924923
def style(
925924
value: Any,
926925
*,
927-
fg: Optional[FgColor] = None,
928-
bg: Optional[BgColor] = None,
929-
bold: Optional[bool] = None,
930-
dim: Optional[bool] = None,
931-
italic: Optional[bool] = None,
932-
overline: Optional[bool] = None,
933-
strikethrough: Optional[bool] = None,
934-
underline: Optional[bool] = None,
926+
fg: FgColor | None = None,
927+
bg: BgColor | None = None,
928+
bold: bool | None = None,
929+
dim: bool | None = None,
930+
italic: bool | None = None,
931+
overline: bool | None = None,
932+
strikethrough: bool | None = None,
933+
underline: bool | None = None,
935934
) -> str:
936935
"""Apply ANSI colors and/or styles to a string and return it.
937936

cmd2/argparse_completer.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212
from typing import (
1313
IO,
1414
TYPE_CHECKING,
15-
Optional,
16-
Union,
1715
cast,
1816
)
1917

@@ -105,8 +103,8 @@ class _ArgumentState:
105103

106104
def __init__(self, arg_action: argparse.Action) -> None:
107105
self.action = arg_action
108-
self.min: Union[int, str]
109-
self.max: Union[float, int, str]
106+
self.min: int | str
107+
self.max: float | int | str
110108
self.count = 0
111109
self.is_remainder = self.action.nargs == argparse.REMAINDER
112110

@@ -141,7 +139,7 @@ def __init__(self, flag_arg_state: _ArgumentState) -> None:
141139
:param flag_arg_state: information about the unfinished flag action.
142140
"""
143141
arg = f'{argparse._get_action_name(flag_arg_state.action)}'
144-
err = f'{generate_range_error(cast(int, flag_arg_state.min), cast(Union[int, float], flag_arg_state.max))}'
142+
err = f'{generate_range_error(cast(int, flag_arg_state.min), cast(int | float, flag_arg_state.max))}'
145143
error = f"Error: argument {arg}: {err} ({flag_arg_state.count} entered)"
146144
super().__init__(error)
147145

@@ -163,7 +161,7 @@ class ArgparseCompleter:
163161
"""Automatic command line tab completion based on argparse parameters."""
164162

165163
def __init__(
166-
self, parser: argparse.ArgumentParser, cmd2_app: 'Cmd', *, parent_tokens: Optional[dict[str, list[str]]] = None
164+
self, parser: argparse.ArgumentParser, cmd2_app: 'Cmd', *, parent_tokens: dict[str, list[str]] | None = None
167165
) -> None:
168166
"""Create an ArgparseCompleter.
169167
@@ -203,7 +201,7 @@ def __init__(
203201
self._subcommand_action = action
204202

205203
def complete(
206-
self, text: str, line: str, begidx: int, endidx: int, tokens: list[str], *, cmd_set: Optional[CommandSet] = None
204+
self, text: str, line: str, begidx: int, endidx: int, tokens: list[str], *, cmd_set: CommandSet | None = None
207205
) -> list[str]:
208206
"""Complete text using argparse metadata.
209207
@@ -228,10 +226,10 @@ def complete(
228226
skip_remaining_flags = False
229227

230228
# _ArgumentState of the current positional
231-
pos_arg_state: Optional[_ArgumentState] = None
229+
pos_arg_state: _ArgumentState | None = None
232230

233231
# _ArgumentState of the current flag
234-
flag_arg_state: Optional[_ArgumentState] = None
232+
flag_arg_state: _ArgumentState | None = None
235233

236234
# Non-reusable flags that we've parsed
237235
matched_flags: list[str] = []
@@ -523,7 +521,7 @@ def _complete_flags(self, text: str, line: str, begidx: int, endidx: int, matche
523521

524522
return matches
525523

526-
def _format_completions(self, arg_state: _ArgumentState, completions: Union[list[str], list[CompletionItem]]) -> list[str]:
524+
def _format_completions(self, arg_state: _ArgumentState, completions: list[str] | list[CompletionItem]) -> list[str]:
527525
"""Format CompletionItems into hint table."""
528526
# Nothing to do if we don't have at least 2 completions which are all CompletionItems
529527
if len(completions) < 2 or not all(isinstance(c, CompletionItem) for c in completions):
@@ -625,7 +623,7 @@ def complete_subcommand_help(self, text: str, line: str, begidx: int, endidx: in
625623
break
626624
return []
627625

628-
def print_help(self, tokens: list[str], file: Optional[IO[str]] = None) -> None:
626+
def print_help(self, tokens: list[str], file: IO[str] | None = None) -> None:
629627
"""Supports cmd2's help command in the printing of help text.
630628
631629
:param tokens: arguments passed to help command
@@ -636,7 +634,7 @@ def print_help(self, tokens: list[str], file: Optional[IO[str]] = None) -> None:
636634
# If so, we will let the subcommand's parser handle the rest of the tokens via another ArgparseCompleter.
637635
if tokens and self._subcommand_action is not None:
638636
parser = cast(
639-
Optional[argparse.ArgumentParser],
637+
argparse.ArgumentParser | None,
640638
self._subcommand_action.choices.get(tokens[0]),
641639
)
642640

@@ -657,15 +655,15 @@ def _complete_arg(
657655
arg_state: _ArgumentState,
658656
consumed_arg_values: dict[str, list[str]],
659657
*,
660-
cmd_set: Optional[CommandSet] = None,
658+
cmd_set: CommandSet | None = None,
661659
) -> list[str]:
662660
"""Tab completion routine for an argparse argument.
663661
664662
:return: list of completions
665663
:raises CompletionError: if the completer or choices function this calls raises one.
666664
"""
667665
# Check if the arg provides choices to the user
668-
arg_choices: Union[list[str], ChoicesCallable]
666+
arg_choices: list[str] | ChoicesCallable
669667
if arg_state.action.choices is not None:
670668
arg_choices = list(arg_state.action.choices)
671669
if not arg_choices:

0 commit comments

Comments
 (0)