From 70713213763310d1e2715db17f315eaf6cd1282b Mon Sep 17 00:00:00 2001 From: Ricahrd Hammond Date: Wed, 14 Feb 2024 12:46:52 -0500 Subject: [PATCH 01/10] Add required args to subcommand program --- cmd2/decorators.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cmd2/decorators.py b/cmd2/decorators.py index 3a163fda..c9fd159e 100644 --- a/cmd2/decorators.py +++ b/cmd2/decorators.py @@ -209,6 +209,7 @@ def _set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None: """ # Set the prog value for this parser parser.prog = prog + req_args = [] # Set the prog value for the parser's subcommands for action in parser._actions: @@ -233,13 +234,18 @@ def _set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None: if subcmd_parser in processed_parsers: continue - subcmd_prog = parser.prog + ' ' + subcmd_name + subcmd_prog = parser.prog + if req_args: + subcmd_prog += " " + " ".join(req_args) + subcmd_prog += " " + subcmd_name _set_parser_prog(subcmd_parser, subcmd_prog) processed_parsers.append(subcmd_parser) # We can break since argparse only allows 1 group of subcommands per level break - + # need to save required args so they can be prepended to the subcommand usage + elif action.required: + req_args.append(action.dest) #: Function signature for a Command Function that uses an argparse.ArgumentParser to process user input #: and optionally returns a boolean From acd6806a2f012381fd5e605dfaabae855168acc9 Mon Sep 17 00:00:00 2001 From: Ricahrd Hammond Date: Thu, 15 Feb 2024 15:23:12 -0500 Subject: [PATCH 02/10] Address formatting issues --- cmd2/argparse_custom.py | 12 ++++-------- cmd2/decorators.py | 13 +++++-------- cmd2/history.py | 6 ++---- 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index 2371fa54..595737c5 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -352,8 +352,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 @@ -362,8 +361,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] @@ -381,8 +379,7 @@ def __call__( line: str, begidx: int, endidx: int, - ) -> List[str]: - ... # pragma: no cover + ) -> List[str]: ... # pragma: no cover @runtime_checkable @@ -400,8 +397,7 @@ def __call__( endidx: int, *, arg_tokens: Dict[str, List[str]] = {}, - ) -> List[str]: - ... # pragma: no cover + ) -> List[str]: ... # pragma: no cover CompleterFunc = Union[CompleterFuncBase, CompleterFuncWithTokens] diff --git a/cmd2/decorators.py b/cmd2/decorators.py index c9fd159e..fdebdd4e 100644 --- a/cmd2/decorators.py +++ b/cmd2/decorators.py @@ -247,6 +247,7 @@ def _set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None: elif action.required: req_args.append(action.dest) + #: Function signature for a Command Function that uses an argparse.ArgumentParser to process user input #: and optionally returns a boolean ArgparseCommandFuncOptionalBoolReturn = Callable[[CommandParent, argparse.Namespace], Optional[bool]] @@ -272,8 +273,7 @@ def with_argparser( ns_provider: Optional[Callable[..., argparse.Namespace]] = None, preserve_quotes: bool = False, with_unknown_args: bool = False, -) -> Callable[[ArgparseCommandFunc[CommandParent]], RawCommandFuncOptionalBoolReturn[CommandParent]]: - ... # pragma: no cover +) -> Callable[[ArgparseCommandFunc[CommandParent]], RawCommandFuncOptionalBoolReturn[CommandParent]]: ... # pragma: no cover @overload @@ -283,8 +283,7 @@ def with_argparser( ns_provider: Optional[Callable[..., argparse.Namespace]] = None, preserve_quotes: bool = False, with_unknown_args: bool = False, -) -> Callable[[ArgparseCommandFunc[CommandParent]], RawCommandFuncOptionalBoolReturn[CommandParent]]: - ... # pragma: no cover +) -> Callable[[ArgparseCommandFunc[CommandParent]], RawCommandFuncOptionalBoolReturn[CommandParent]]: ... # pragma: no cover def with_argparser( @@ -424,8 +423,7 @@ def as_subcommand_to( *, help: Optional[str] = None, aliases: Optional[List[str]] = None, -) -> Callable[[ArgparseCommandFunc[CommandParent]], ArgparseCommandFunc[CommandParent]]: - ... # pragma: no cover +) -> Callable[[ArgparseCommandFunc[CommandParent]], ArgparseCommandFunc[CommandParent]]: ... # pragma: no cover @overload @@ -436,8 +434,7 @@ def as_subcommand_to( *, help: Optional[str] = None, aliases: Optional[List[str]] = None, -) -> Callable[[ArgparseCommandFunc[CommandParent]], ArgparseCommandFunc[CommandParent]]: - ... # pragma: no cover +) -> Callable[[ArgparseCommandFunc[CommandParent]], ArgparseCommandFunc[CommandParent]]: ... # pragma: no cover def as_subcommand_to( diff --git a/cmd2/history.py b/cmd2/history.py index a7d6baff..c79a19dd 100644 --- a/cmd2/history.py +++ b/cmd2/history.py @@ -154,12 +154,10 @@ def _zero_based_index(self, onebased: Union[int, str]) -> int: return result @overload - def append(self, new: HistoryItem) -> None: - ... # pragma: no cover + def append(self, new: HistoryItem) -> None: ... # pragma: no cover @overload - def append(self, new: Statement) -> None: - ... # pragma: no cover + def append(self, new: Statement) -> None: ... # pragma: no cover def append(self, new: Union[Statement, HistoryItem]) -> None: """Append a new statement to the end of the History list. From 19ed6ae98bc7d36e9485efadb8d8041810798b77 Mon Sep 17 00:00:00 2001 From: Ricahrd Hammond Date: Thu, 15 Feb 2024 15:28:03 -0500 Subject: [PATCH 03/10] Add type hint --- cmd2/decorators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd2/decorators.py b/cmd2/decorators.py index fdebdd4e..6d0a139f 100644 --- a/cmd2/decorators.py +++ b/cmd2/decorators.py @@ -209,7 +209,7 @@ def _set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None: """ # Set the prog value for this parser parser.prog = prog - req_args = [] + req_args: List[str] = [] # Set the prog value for the parser's subcommands for action in parser._actions: From 57212c980cc0cfbc47815e67eebbb289fe726f41 Mon Sep 17 00:00:00 2001 From: Ricahrd Hammond Date: Mon, 8 Jul 2024 07:49:55 -0400 Subject: [PATCH 04/10] Ignore E704 per PR feedback --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 5d42015c..5f26578f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,7 +9,7 @@ addopts = [flake8] count = True -ignore = E203,W503 +ignore = E203,W503,E704 max-complexity = 26 max-line-length = 127 show-source = True From a6d63c68e110c16c8fc4ca044abb804551e80357 Mon Sep 17 00:00:00 2001 From: Ricahrd Hammond Date: Wed, 10 Jul 2024 08:00:29 -0400 Subject: [PATCH 05/10] Address mypy errors --- cmd2/cmd2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index c5c0db78..c22fdf33 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -778,7 +778,7 @@ def unregister_command_set(self, cmdset: CommandSet) -> None: cmdset.on_unregister() self._unregister_subcommands(cmdset) - methods = inspect.getmembers( + methods: List[Tuple[str, Callable[[Any], Any]]] = inspect.getmembers( cmdset, predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type] and hasattr(meth, '__name__') @@ -809,7 +809,7 @@ def unregister_command_set(self, cmdset: CommandSet) -> None: self._installed_command_sets.remove(cmdset) def _check_uninstallable(self, cmdset: CommandSet) -> None: - methods = inspect.getmembers( + methods: List[Tuple[str, Callable[[Any], Any]]] = inspect.getmembers( cmdset, predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type] and hasattr(meth, '__name__') From c2f9cc89eece5f1f5db951e7514d86e96ca837ed Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Thu, 12 Sep 2024 15:46:27 -0400 Subject: [PATCH 06/10] Remove support for Python 3.7. Add unit testing for Python 3.12. Remove deprecated features. --- .github/CONTRIBUTING.md | 50 +++++++++++----------- .github/workflows/ci.yml | 2 +- .github/workflows/doc.yml | 2 +- .github/workflows/format.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/mypy.yml | 2 +- CHANGELOG.md | 5 ++- README.md | 18 ++++---- cmd2/argparse_custom.py | 27 +++--------- cmd2/cmd2.py | 13 ++---- cmd2/decorators.py | 12 ++---- cmd2/history.py | 6 +-- docs/features/argument_processing.rst | 5 +-- docs/overview/installation.rst | 4 +- noxfile.py | 6 +-- plugins/ext_test/build-pyenvs.sh | 4 +- plugins/ext_test/cmd2_ext_test/__init__.py | 8 +--- plugins/ext_test/noxfile.py | 2 +- plugins/ext_test/setup.py | 4 +- plugins/template/README.md | 18 +++----- plugins/template/build-pyenvs.sh | 4 +- plugins/template/cmd2_myplugin/__init__.py | 8 +--- plugins/template/noxfile.py | 2 +- plugins/template/setup.py | 4 +- setup.py | 4 +- tests/test_cmd2.py | 18 -------- tests/test_transcript.py | 2 +- 27 files changed, 88 insertions(+), 146 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index abd05ccd..72a063cb 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -10,7 +10,7 @@ We welcome pull requests from cmd2 users and seasoned Python developers alike! F Remember to feel free to ask for help by leaving a comment within the Issue. -Working on your first pull request? You can learn how from this *free* series +Working on your first pull request? You can learn how from this *free* series [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github). ###### If you've found a bug that is not on the board, [follow these steps](README.md#found-a-bug). @@ -47,7 +47,7 @@ The tables below list all prerequisites along with the minimum required version | Prerequisite | Minimum Version | | --------------------------------------------------- |-----------------| -| [python](https://www.python.org/downloads/) | `3.7` | +| [python](https://www.python.org/downloads/) | `3.8` | | [pyperclip](https://github.com/asweigart/pyperclip) | `1.6` | | [setuptools](https://pypi.org/project/setuptools/) | `34.4` | | [wcwidth](https://pypi.python.org/pypi/wcwidth) | `0.1.7` | @@ -81,17 +81,17 @@ $ pip freeze | grep pyperclip If your versions are lower than the prerequisite versions, you should update. -If you do not already have Python installed on your machine, we recommend using the -[Anaconda](https://www.continuum.io/downloads) distribution because it provides an excellent out-of-the-box install on -all platforms (Windows, Mac, and Linux) and because it supports having multiple Python environments (versions of Python) +If you do not already have Python installed on your machine, we recommend using the +[Anaconda](https://www.continuum.io/downloads) distribution because it provides an excellent out-of-the-box install on +all platforms (Windows, Mac, and Linux) and because it supports having multiple Python environments (versions of Python) installed simultaneously. ### Forking the project #### Setting up your system -1. Install [Git](https://git-scm.com/) or your favorite Git client. If you aren't comfortable with Git at the -command-line, then both [SmartGit](http://www.syntevo.com/smartgit/) and [GitKraken](https://www.gitkraken.com) are +1. Install [Git](https://git-scm.com/) or your favorite Git client. If you aren't comfortable with Git at the +command-line, then both [SmartGit](http://www.syntevo.com/smartgit/) and [GitKraken](https://www.gitkraken.com) are excellent cross-platform graphical Git clients. 2. (Optional) [Set up an SSH key](https://help.github.com/articles/generating-an-ssh-key/) for GitHub. 3. Create a parent projects directory on your system. For this guide, it will be assumed that it is `~/src`. @@ -99,14 +99,14 @@ excellent cross-platform graphical Git clients. #### Forking cmd2 1. Go to the top-level cmd2 repository: -2. Click the "Fork" button in the upper right hand corner of the interface +2. Click the "Fork" button in the upper right hand corner of the interface ([more details here](https://help.github.com/articles/fork-a-repo/)) 3. After the repository has been forked, you will be taken to your copy of the cmd2 repo at `yourUsername/cmd2` #### Cloning your fork 1. Open a terminal / command line / Bash shell in your projects directory (_e.g.: `~/src/`_) -2. Clone your fork of cmd2, making sure to replace `yourUsername` with your GitHub username. This will download the +2. Clone your fork of cmd2, making sure to replace `yourUsername` with your GitHub username. This will download the entire cmd2 repo to your projects directory. ```sh @@ -164,13 +164,13 @@ Do this prior to every time you create a branch for a PR: ### Creating a branch -Before you start working, you will need to create a separate branch specific to the issue or feature you're working on. +Before you start working, you will need to create a separate branch specific to the issue or feature you're working on. You will push your work to this branch. #### Naming your branch -Name the branch something like `fix/xxx` or `feature/xxx` where `xxx` is a short description of the changes or feature -you are attempting to add. For example `fix/script-files` would be a branch where you fix something specific to script +Name the branch something like `fix/xxx` or `feature/xxx` where `xxx` is a short description of the changes or feature +you are attempting to add. For example `fix/script-files` would be a branch where you fix something specific to script files. #### Adding your branch @@ -191,22 +191,22 @@ $ git push origin [name_of_your_new_branch] ### Setting up for cmd2 development -For doing cmd2 development, it is recommended you create a virtual environment using Conda or Virtualenv and install the +For doing cmd2 development, it is recommended you create a virtual environment using Conda or Virtualenv and install the package from the source. #### Create a new environment for cmd2 using Pipenv -`cmd2` has support for using [Pipenv](https://docs.pipenv.org/en/latest/) for development. +`cmd2` has support for using [Pipenv](https://docs.pipenv.org/en/latest/) for development. `Pipenv` essentially combines the features of `pip` and `virtualenv` into a single tool. `cmd2` contains a Pipfile which - makes it extremely easy to setup a `cmd2` development environment using `pipenv`. + makes it extremely easy to setup a `cmd2` development environment using `pipenv`. -To create a virtual environment and install everything needed for `cmd2` development using `pipenv`, do the following +To create a virtual environment and install everything needed for `cmd2` development using `pipenv`, do the following from a GitHub checkout: ```sh pipenv install --dev ``` -To create a new virtualenv, using a specific version of Python you have installed (and on your PATH), use the +To create a new virtualenv, using a specific version of Python you have installed (and on your PATH), use the --python VERSION flag, like so: ```sh pipenv install --dev --python 3.8 @@ -219,8 +219,8 @@ pipenv shell #### Create a new environment for cmd2 using Conda ```sh -$ conda create -n cmd2_py37 python=3.7 -$ conda activate cmd2_py37 +$ conda create -n cmd2_py38 python=3.8 +$ conda activate cmd2_py38 ``` #### Create a new environment for cmd using Virtualenv @@ -233,15 +233,15 @@ pyenv versions # Install python version defined pyenv install 3.8.2 ``` -With the Python version installed, you can set the virtualenv properly. +With the Python version installed, you can set the virtualenv properly. ```sh $ cd ~/src/cmd2 -$ virtualenv -p $(pyenv root)/versions/3.8.2/ cmd_py38 +$ virtualenv -p $(pyenv root)/versions/3.8.2/ cmd_py38 $ source ~/src/cmd2/bin/activate ``` -Assuming you cloned the repository to `~/src/cmd2` you can install cmd2 in +Assuming you cloned the repository to `~/src/cmd2` you can install cmd2 in [editable mode](https://pip.pypa.io/en/stable/reference/pip_install/#editable-installs). Changes to the source code are immediately available when the python interpreter imports `cmd2`, there is no need to re-install the module after every change. This @@ -326,9 +326,9 @@ served (usually [http://localhost:8000](http://localhost:8000)). ### Static code analysis -You should have some sort of [PEP 8](https://www.python.org/dev/peps/pep-0008/)-based linting running in your editor or +You should have some sort of [PEP 8](https://www.python.org/dev/peps/pep-0008/)-based linting running in your editor or IDE or at the command line before you commit code. `cmd2` uses [flake8](http://flake8.pycqa.org/en/latest/) as part of -its continuous integration (CI) process. [pylint](https://www.pylint.org) is another good Python linter which can be +its continuous integration (CI) process. [pylint](https://www.pylint.org) is another good Python linter which can be run at the command line but also can integrate with many IDEs and editors. > Please do not ignore any linting errors in code you write or modify, as they are meant to **help** you and to ensure a clean and simple code base. Don't worry about linting errors in code you don't touch though - cleaning up the legacy code is a work in progress. @@ -579,7 +579,7 @@ mostly automated. The manual steps are all git operations. Here's the checklist: 1. Make sure all the unit tests pass with `invoke pytest` or `py.test` 1. Make sure latest year in `LICENSE` matches current year 1. Make sure `CHANGELOG.md` describes the version and has the correct release date -1. Add a git tag representing the version number using ``invoke tag x.y.z`` +1. Add a git tag representing the version number using ``invoke tag x.y.z`` * Where x, y, and z are all small non-negative integers 1. (Optional) Run `invoke pypi-test` to clean, build, and upload a new release to [Test PyPi](https://test.pypi.org) 1. Run `invoke pypi` to clean, build, and upload a new release to [PyPi](https://pypi.org/) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e16e694..b35ac514 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] fail-fast: false runs-on: ${{ matrix.os }} steps: diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index 77f18218..9605605f 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: ["3.11"] + python-version: ["3.12"] fail-fast: false runs-on: ${{ matrix.os }} steps: diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 3a8d2ab7..f168f598 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: ["3.11"] + python-version: ["3.12"] fail-fast: false runs-on: ${{ matrix.os }} steps: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9eb8ebdc..f7a97227 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: ["3.11"] + python-version: ["3.12"] fail-fast: false runs-on: ${{ matrix.os }} steps: diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index c5f2aca6..727ed43d 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: ["3.11"] + python-version: ["3.12"] fail-fast: false runs-on: ${{ matrix.os }} steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index e65c6b40..dac58426 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,13 @@ ## 2.5.0 (TBD) * Breaking Change - * `cmd2` 2.5 supports Python 3.7+ (removed support for Python 3.6) + * `cmd2` 2.5 supports Python 3.8+ (removed support for Python 3.6 and 3.7) * Enhancements * Removed dependency on `attrs` and replaced with [dataclasses](https://docs.python.org/3/library/dataclasses.html) * add `allow_clipboard` initialization parameter and attribute to disable ability to add output to the operating system clipboard + * Updated unit tests to be Python 3.12 compliant. +* Deletions (potentially breaking changes) + * Removed `apply_style` from `Cmd.pwarning()`. ## 2.4.3 (January 27, 2023) diff --git a/README.md b/README.md index 09e083df..d15d58eb 100755 --- a/README.md +++ b/README.md @@ -32,14 +32,14 @@ The developers toolbox ![system schema](https://raw.githubusercontent.com/python-cmd2/cmd2/master/.github/images/graph.drawio.png) -When creating solutions developers have no shortage of tools to create rich and smart user interfaces. -System administrators have long been duct taping together brittle workflows based on a menagerie of simple command line tools created by strangers on github and the guy down the hall. -Unfortunately, when CLIs become significantly complex the ease of command discoverability tends to fade quickly. -On the other hand, Web and traditional desktop GUIs are first in class when it comes to easily discovering functionality. +When creating solutions developers have no shortage of tools to create rich and smart user interfaces. +System administrators have long been duct taping together brittle workflows based on a menagerie of simple command line tools created by strangers on github and the guy down the hall. +Unfortunately, when CLIs become significantly complex the ease of command discoverability tends to fade quickly. +On the other hand, Web and traditional desktop GUIs are first in class when it comes to easily discovering functionality. The price we pay for beautifully colored displays is complexity required to aggregate disperate applications into larger systems. -`cmd2` fills the niche between high [ease of command discovery](https://clig.dev/#ease-of-discovery) applications and smart workflow automation systems. +`cmd2` fills the niche between high [ease of command discovery](https://clig.dev/#ease-of-discovery) applications and smart workflow automation systems. -The `cmd2` framework provides a great mixture of both worlds. Application designers can easily create complex applications and rely on the cmd2 library to offer effortless user facing help and extensive tab completion. +The `cmd2` framework provides a great mixture of both worlds. Application designers can easily create complex applications and rely on the cmd2 library to offer effortless user facing help and extensive tab completion. When users become comfortable with functionality, cmd2 turns into a feature rich library enabling a smooth transition to full automation. If designed with enough forethought, a well implemented cmd2 application can serve as a boutique workflow tool. `cmd2` pulls off this flexibility based on two pillars of philosophy: * Tab Completion @@ -78,7 +78,7 @@ On all operating systems, the latest stable version of `cmd2` can be installed u pip install -U cmd2 ``` -cmd2 works with Python 3.7+ on Windows, macOS, and Linux. It is pure Python code with few 3rd-party dependencies. +cmd2 works with Python 3.8+ 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 @@ -97,7 +97,7 @@ The best way to learn the cmd2 api is to delve into the example applications loc Tutorials --------- -* PyOhio 2019 presentation: +* PyOhio 2019 presentation: * [video](https://www.youtube.com/watch?v=pebeWrTqIIw) * [slides](https://github.com/python-cmd2/talks/blob/master/PyOhio_2019/cmd2-PyOhio_2019.pdf) * [example code](https://github.com/python-cmd2/talks/tree/master/PyOhio_2019/examples) @@ -161,4 +161,4 @@ Projects using cmd2 Possibly defunct but still good examples * [JSShell](https://github.com/Den1al/JSShell) -* [FLASHMINGO](https://github.com/fireeye/flashmingo) +* [FLASHMINGO](https://github.com/fireeye/flashmingo) diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index 2371fa54..78a492f6 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -257,12 +257,14 @@ def my_completer(self, text, line, begidx, endidx, arg_tokens) List, NoReturn, Optional, + Protocol, Sequence, Set, Tuple, Type, Union, cast, + runtime_checkable, ) from . import ( @@ -270,19 +272,6 @@ def my_completer(self, text, line, begidx, endidx, arg_tokens) constants, ) -try: - from typing import ( - Protocol, - runtime_checkable, - ) -except ImportError: - # Remove these imports when we no longer support Python 3.7 - from typing_extensions import ( # type: ignore[assignment] - Protocol, - runtime_checkable, - ) - - if TYPE_CHECKING: # pragma: no cover from .argparse_completer import ( ArgparseCompleter, @@ -352,8 +341,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 @@ -362,8 +350,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] @@ -381,8 +368,7 @@ def __call__( line: str, begidx: int, endidx: int, - ) -> List[str]: - ... # pragma: no cover + ) -> List[str]: ... # pragma: no cover @runtime_checkable @@ -400,8 +386,7 @@ def __call__( endidx: int, *, arg_tokens: Dict[str, List[str]] = {}, - ) -> List[str]: - ... # pragma: no cover + ) -> List[str]: ... # pragma: no cover CompleterFunc = Union[CompleterFuncBase, CompleterFuncWithTokens] diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index c5c0db78..a7f5b64d 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -780,7 +780,7 @@ def unregister_command_set(self, cmdset: CommandSet) -> None: methods = inspect.getmembers( cmdset, - predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type] + predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type, var-annotated] and hasattr(meth, '__name__') and meth.__name__.startswith(COMMAND_FUNC_PREFIX), ) @@ -811,7 +811,7 @@ def unregister_command_set(self, cmdset: CommandSet) -> None: def _check_uninstallable(self, cmdset: CommandSet) -> None: methods = inspect.getmembers( cmdset, - predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type] + predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type, var-annotated] and hasattr(meth, '__name__') and meth.__name__.startswith(COMMAND_FUNC_PREFIX), ) @@ -1223,7 +1223,6 @@ def pwarning( msg: Any = '', *, end: str = '\n', - apply_style: bool = True, paged: bool = False, chop: bool = False, ) -> None: @@ -1231,16 +1230,10 @@ def pwarning( :param msg: object to print :param end: string appended after the end of the message, default a newline - :param apply_style: - If True, then ansi.style_warning will be applied to the message text. Set to False in cases - where the message text already has the desired style. Defaults to True. - - .. deprecated: 2.4.4 - Use :meth:`~cmd2.Cmd.print_to` instead to print to stderr without style applied. :param paged: If True, pass the output through the configured pager. :param chop: If paged is True, True to truncate long lines or False to wrap long lines. """ - self.print_to(sys.stderr, msg, end=end, style=ansi.style_warning if apply_style else None, paged=paged, chop=chop) + self.print_to(sys.stderr, msg, end=end, style=ansi.style_warning, paged=paged, chop=chop) def pfailure( self, diff --git a/cmd2/decorators.py b/cmd2/decorators.py index 3a163fda..361b71ac 100644 --- a/cmd2/decorators.py +++ b/cmd2/decorators.py @@ -266,8 +266,7 @@ def with_argparser( ns_provider: Optional[Callable[..., argparse.Namespace]] = None, preserve_quotes: bool = False, with_unknown_args: bool = False, -) -> Callable[[ArgparseCommandFunc[CommandParent]], RawCommandFuncOptionalBoolReturn[CommandParent]]: - ... # pragma: no cover +) -> Callable[[ArgparseCommandFunc[CommandParent]], RawCommandFuncOptionalBoolReturn[CommandParent]]: ... # pragma: no cover @overload @@ -277,8 +276,7 @@ def with_argparser( ns_provider: Optional[Callable[..., argparse.Namespace]] = None, preserve_quotes: bool = False, with_unknown_args: bool = False, -) -> Callable[[ArgparseCommandFunc[CommandParent]], RawCommandFuncOptionalBoolReturn[CommandParent]]: - ... # pragma: no cover +) -> Callable[[ArgparseCommandFunc[CommandParent]], RawCommandFuncOptionalBoolReturn[CommandParent]]: ... # pragma: no cover def with_argparser( @@ -418,8 +416,7 @@ def as_subcommand_to( *, help: Optional[str] = None, aliases: Optional[List[str]] = None, -) -> Callable[[ArgparseCommandFunc[CommandParent]], ArgparseCommandFunc[CommandParent]]: - ... # pragma: no cover +) -> Callable[[ArgparseCommandFunc[CommandParent]], ArgparseCommandFunc[CommandParent]]: ... # pragma: no cover @overload @@ -430,8 +427,7 @@ def as_subcommand_to( *, help: Optional[str] = None, aliases: Optional[List[str]] = None, -) -> Callable[[ArgparseCommandFunc[CommandParent]], ArgparseCommandFunc[CommandParent]]: - ... # pragma: no cover +) -> Callable[[ArgparseCommandFunc[CommandParent]], ArgparseCommandFunc[CommandParent]]: ... # pragma: no cover def as_subcommand_to( diff --git a/cmd2/history.py b/cmd2/history.py index a7d6baff..c79a19dd 100644 --- a/cmd2/history.py +++ b/cmd2/history.py @@ -154,12 +154,10 @@ def _zero_based_index(self, onebased: Union[int, str]) -> int: return result @overload - def append(self, new: HistoryItem) -> None: - ... # pragma: no cover + def append(self, new: HistoryItem) -> None: ... # pragma: no cover @overload - def append(self, new: Statement) -> None: - ... # pragma: no cover + def append(self, new: Statement) -> None: ... # pragma: no cover def append(self, new: Union[Statement, HistoryItem]) -> None: """Append a new statement to the end of the History list. diff --git a/docs/features/argument_processing.rst b/docs/features/argument_processing.rst index 67b94878..16031200 100644 --- a/docs/features/argument_processing.rst +++ b/docs/features/argument_processing.rst @@ -78,9 +78,8 @@ Here's what it looks like:: .. warning:: It is important that each command which uses the ``@with_argparser`` - decorator be passed a unique instance of a parser. This limitation is due - to bugs in CPython prior to Python 3.7 which make it impossible to make a - deep copy of an instance of a ``argparse.ArgumentParser``. + decorator be passed a unique instance of a parser since command-specific + changes could be made to it. .. note:: diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst index 743e0aca..841807c8 100644 --- a/docs/overview/installation.rst +++ b/docs/overview/installation.rst @@ -7,7 +7,7 @@ Installation Instructions .. _setuptools: https://pypi.org/project/setuptools .. _PyPI: https://pypi.org -``cmd2`` works on Linux, macOS, and Windows. It requires Python 3.7 or +``cmd2`` works on Linux, macOS, and Windows. It requires Python 3.8 or higher, pip_, and setuptools_. If you've got all that, then you can just: .. code-block:: shell @@ -30,7 +30,7 @@ higher, pip_, and setuptools_. If you've got all that, then you can just: Prerequisites ------------- -If you have Python 3 >=3.7 installed from `python.org +If you have Python 3 >=3.8 installed from `python.org `_, you will already have pip_ and setuptools_, but may need to upgrade to the latest versions: diff --git a/noxfile.py b/noxfile.py index f7a705fb..9ec2ce16 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,7 +1,7 @@ import nox -@nox.session(python=['3.11']) +@nox.session(python=['3.12']) def docs(session): session.install( 'sphinx', @@ -17,7 +17,7 @@ def docs(session): ) -@nox.session(python=['3.7', '3.8', '3.9', '3.10', '3.11']) +@nox.session(python=['3.8', '3.9', '3.10', '3.11', '3.12']) @nox.parametrize('plugin', [None, 'ext_test', 'template', 'coverage']) def tests(session, plugin): if plugin is None: @@ -41,7 +41,7 @@ def tests(session, plugin): ) -@nox.session(python=['3.8', '3.9', '3.10', '3.11']) +@nox.session(python=['3.8', '3.9', '3.10', '3.11', '3.12']) @nox.parametrize('step', ['mypy', 'flake8']) def validate(session, step): session.install('invoke', './[validate]') diff --git a/plugins/ext_test/build-pyenvs.sh b/plugins/ext_test/build-pyenvs.sh index 572db568..20f5c8d4 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.7, etc. It will also create a .python-version +# cmd2-3.8, 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.7" "3.8" "3.9", "3.10", "3.11") +declare -a pythons=("3.8" "3.9", "3.10", "3.11", "3.12") # function to find the latest patch of a minor version of python function find_latest_version { diff --git a/plugins/ext_test/cmd2_ext_test/__init__.py b/plugins/ext_test/cmd2_ext_test/__init__.py index b30c949d..b154f0ed 100644 --- a/plugins/ext_test/cmd2_ext_test/__init__.py +++ b/plugins/ext_test/cmd2_ext_test/__init__.py @@ -5,13 +5,7 @@ Allows developers to exercise their cmd2 application using the PyScript interface """ -try: - # For python 3.8 and later - import importlib.metadata as importlib_metadata -except ImportError: # pragma: no cover - # Remove this import when we no longer support Python 3.7 - # MyPy Issue # 1153 causes a spurious error that must be ignored - import importlib_metadata # type: ignore +import importlib.metadata as importlib_metadata try: __version__ = importlib_metadata.version(__name__) diff --git a/plugins/ext_test/noxfile.py b/plugins/ext_test/noxfile.py index 9872e193..25a95067 100644 --- a/plugins/ext_test/noxfile.py +++ b/plugins/ext_test/noxfile.py @@ -1,7 +1,7 @@ import nox -@nox.session(python=['3.7', '3.8', '3.9', '3.10', '3.11']) +@nox.session(python=['3.8', '3.9', '3.10', '3.11', '3.12']) def tests(session): session.install('invoke', './[test]') session.run('invoke', 'pytest', '--junit', '--no-pty') diff --git a/plugins/ext_test/setup.py b/plugins/ext_test/setup.py index 644d2677..45110413 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.7', + python_requires='>=3.8', 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.7', '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', ], # dependencies for development and testing # $ pip install -e .[dev] diff --git a/plugins/template/README.md b/plugins/template/README.md index f7f6d057..8617acd7 100644 --- a/plugins/template/README.md +++ b/plugins/template/README.md @@ -210,7 +210,7 @@ tiered testing strategy to accomplish this objective. - [pytest](https://pytest.org) runs the unit tests - [nox](https://nox.thea.codes/en/stable/) runs the unit tests on multiple versions of python -- [GitHub Actions](https://github.com/features/actions) runs the tests on the various +- [GitHub Actions](https://github.com/features/actions) runs the tests on the various supported platforms This plugin template is set up to use the same strategy. @@ -231,17 +231,15 @@ automates the creation of these environments. If you prefer to create these virtualenvs by hand, do the following: ``` $ cd cmd2_abbrev -$ pyenv install 3.7.0 -$ pyenv virtualenv -p python3.7 3.7.0 cmd2-3.7 $ 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 ``` -Now set pyenv to make all three of those available at the same time: +Now set pyenv to make both of those available at the same time: ``` -$ pyenv local cmd2-3.7 cmd2-3.8 cmd2-3.9 +$ pyenv local cmd2-3.8 cmd2-3.9 ``` Whether you ran the script, or did it by hand, you now have isolated virtualenvs @@ -251,10 +249,8 @@ utilize. | Command | python | virtualenv | | ----------- | ------ | ---------- | -| `python3.7` | 3.7.0 | cmd2-3.7 | | `python3.8` | 3.8.5 | cmd2-3.8 | | `python3.9` | 3.9.0 | cmd2-3.9 | -| `pip3.7` | 3.7.0 | cmd2-3.7 | | `pip3.8` | 3.8.5 | cmd2-3.8 | | `pip3.9` | 3.9.0 | cmd2-3.9 | @@ -268,7 +264,7 @@ $ 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.7` +All the dependencies now have been installed in the `cmd2-3.8` virtualenv. If you want to work in other virtualenvs, you'll need to manually select it, and install again:: @@ -290,9 +286,9 @@ 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.7, 3.8, -3.9, 3.10, and 3.11 You can run your unit tests in all of these versions of -python by: +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 +of python by: ``` $ nox ``` diff --git a/plugins/template/build-pyenvs.sh b/plugins/template/build-pyenvs.sh index f4084494..4a6e1578 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.7, etc. It will also create a .python-version +# cmd2-3.8, 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.7" "3.8" "3.9" "3.10" "3.11") +declare -a pythons=("3.8" "3.9" "3.10" "3.11", "3.12") # function to find the latest patch of a minor version of python function find_latest_version { diff --git a/plugins/template/cmd2_myplugin/__init__.py b/plugins/template/cmd2_myplugin/__init__.py index 394b219b..a35976b7 100644 --- a/plugins/template/cmd2_myplugin/__init__.py +++ b/plugins/template/cmd2_myplugin/__init__.py @@ -5,17 +5,13 @@ An overview of what myplugin does. """ +import importlib.metadata as importlib_metadata + from .myplugin import ( # noqa: F401 MyPluginMixin, empty_decorator, ) -try: - # For python 3.8 and later - import importlib.metadata as importlib_metadata -except ImportError: # pragma: no cover - # Remove this import when we no longer support Python 3.7 - import importlib_metadata try: __version__ = importlib_metadata.version(__name__) except importlib_metadata.PackageNotFoundError: # pragma: no cover diff --git a/plugins/template/noxfile.py b/plugins/template/noxfile.py index 9872e193..25a95067 100644 --- a/plugins/template/noxfile.py +++ b/plugins/template/noxfile.py @@ -1,7 +1,7 @@ import nox -@nox.session(python=['3.7', '3.8', '3.9', '3.10', '3.11']) +@nox.session(python=['3.8', '3.9', '3.10', '3.11', '3.12']) 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 47cc72c2..7e872cd8 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.7', + python_requires='>=3.8', 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.7', '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', ], # dependencies for development and testing # $ pip install -e .[dev] diff --git a/setup.py b/setup.py index dae53695..004cbee9 100755 --- a/setup.py +++ b/setup.py @@ -28,11 +28,11 @@ License :: OSI Approved :: MIT License Programming Language :: Python Programming Language :: Python :: 3 -Programming Language :: Python :: 3.7 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 :: Implementation :: CPython Topic :: Software Development :: Libraries :: Python Modules """.splitlines(), @@ -105,7 +105,7 @@ package_data=PACKAGE_DATA, packages=['cmd2'], keywords='command prompt console cmd', - python_requires='>=3.7', + python_requires='>=3.8', setup_requires=SETUP_REQUIRES, install_requires=INSTALL_REQUIRES, extras_require=EXTRAS_REQUIRE, diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 054eef6e..5686e9f9 100755 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -2316,24 +2316,6 @@ def test_perror_no_style(base_app, capsys): assert err == msg + end -@with_ansi_style(ansi.AllowStyle.ALWAYS) -def test_pwarning_style(base_app, capsys): - msg = 'testing...' - end = '\n' - base_app.pwarning(msg) - out, err = capsys.readouterr() - assert err == ansi.style_warning(msg) + end - - -@with_ansi_style(ansi.AllowStyle.ALWAYS) -def test_pwarning_no_style(base_app, capsys): - msg = 'testing...' - end = '\n' - base_app.pwarning(msg, apply_style=False) - out, err = capsys.readouterr() - assert err == msg + end - - @with_ansi_style(ansi.AllowStyle.ALWAYS) def test_pexcept_style(base_app, capsys): msg = Exception('testing...') diff --git a/tests/test_transcript.py b/tests/test_transcript.py index 39c87532..ed193a00 100644 --- a/tests/test_transcript.py +++ b/tests/test_transcript.py @@ -260,7 +260,7 @@ def test_generate_transcript_stop(capsys): # strings with zero or one slash or with escaped slashes means no regular # expression present, so the result should just be what re.escape returns. # we don't use static strings in these tests because re.escape behaves - # differently in python 3.7 than in prior versions + # differently in python 3.7+ than in prior versions ('text with no slashes', re.escape('text with no slashes')), ('specials .*', re.escape('specials .*')), ('use 2/3 cup', re.escape('use 2/3 cup')), From f82ff6ea133e9dbcb662490e663340e34283c230 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Thu, 12 Sep 2024 16:50:20 -0400 Subject: [PATCH 07/10] Fix validation tests. --- setup.cfg | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 5d42015c..5f26578f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,7 +9,7 @@ addopts = [flake8] count = True -ignore = E203,W503 +ignore = E203,W503,E704 max-complexity = 26 max-line-length = 127 show-source = True diff --git a/setup.py b/setup.py index 004cbee9..ce46c17d 100755 --- a/setup.py +++ b/setup.py @@ -82,7 +82,7 @@ 'validate': [ 'flake8', 'mypy', - 'types-pkg-resources', + 'types-setuptools', ], } From a9fd1bf432d85cf60566c06e77d6b63bc3a7d418 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 16:31:03 +0000 Subject: [PATCH 08/10] Bump github/codeql-action from 2 to 3 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v2...v3) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 33957f0e..edc40ddb 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -45,7 +45,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -56,7 +56,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -70,4 +70,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 From ecbe0845b3735ce1a11d02b284c7e507ce9d83a3 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 13 Sep 2024 11:35:29 -0400 Subject: [PATCH 09/10] Removed Python version check related to argparse parser deep copies since we only support Python 3.8+ now. --- cmd2/__init__.py | 8 ++------ cmd2/cmd2.py | 26 +++++++++----------------- cmd2/decorators.py | 3 ++- docs/features/argument_processing.rst | 7 ------- 4 files changed, 13 insertions(+), 31 deletions(-) diff --git a/cmd2/__init__.py b/cmd2/__init__.py index 9b3fdbc3..8f1f030e 100644 --- a/cmd2/__init__.py +++ b/cmd2/__init__.py @@ -5,12 +5,8 @@ import sys -# For python 3.8 and later -if sys.version_info >= (3, 8): - import importlib.metadata as importlib_metadata -else: - # For everyone else - import importlib_metadata +import importlib.metadata as importlib_metadata + try: __version__ = importlib_metadata.version(__name__) except importlib_metadata.PackageNotFoundError: # pragma: no cover diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 6b9ea792..5f95b2ab 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -700,10 +700,7 @@ def _build_parser( elif callable(parser_builder): parser = parser_builder() elif isinstance(parser_builder, argparse.ArgumentParser): - if sys.version_info >= (3, 6, 4): - parser = copy.deepcopy(parser_builder) - else: # pragma: no cover - parser = parser_builder + parser = copy.deepcopy(parser_builder) return parser def _register_command_parser(self, command: str, command_method: Callable[..., Any]) -> None: @@ -780,7 +777,7 @@ def unregister_command_set(self, cmdset: CommandSet) -> None: methods: List[Tuple[str, Callable[[Any], Any]]] = inspect.getmembers( cmdset, - predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type, var-annotated] + predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type] and hasattr(meth, '__name__') and meth.__name__.startswith(COMMAND_FUNC_PREFIX), ) @@ -811,7 +808,7 @@ def unregister_command_set(self, cmdset: CommandSet) -> None: def _check_uninstallable(self, cmdset: CommandSet) -> None: methods: List[Tuple[str, Callable[[Any], Any]]] = inspect.getmembers( cmdset, - predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type, var-annotated] + predicate=lambda meth: isinstance(meth, Callable) # type: ignore[arg-type] and hasattr(meth, '__name__') and meth.__name__.startswith(COMMAND_FUNC_PREFIX), ) @@ -3328,19 +3325,14 @@ def _cmdloop(self) -> None: ############################################################# # Top-level parser for alias - @staticmethod - def _build_alias_parser() -> argparse.ArgumentParser: - alias_description = ( - "Manage aliases\n" "\n" "An 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_subparsers = alias_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND') - alias_subparsers.required = True - return alias_parser + alias_description = "Manage aliases\n" "\n" "An 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_subparsers = alias_parser.add_subparsers(dest='subcommand', metavar='SUBCOMMAND') + alias_subparsers.required = True # Preserve quotes since we are passing strings to other commands - @with_argparser(_build_alias_parser, preserve_quotes=True) + @with_argparser(alias_parser, preserve_quotes=True) def do_alias(self, args: argparse.Namespace) -> None: """Manage aliases""" # Call handler for whatever subcommand was selected diff --git a/cmd2/decorators.py b/cmd2/decorators.py index 6d0a139f..3aca8f0a 100644 --- a/cmd2/decorators.py +++ b/cmd2/decorators.py @@ -243,7 +243,8 @@ def _set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None: # We can break since argparse only allows 1 group of subcommands per level break - # need to save required args so they can be prepended to the subcommand usage + + # Need to save required args so they can be prepended to the subcommand usage elif action.required: req_args.append(action.dest) diff --git a/docs/features/argument_processing.rst b/docs/features/argument_processing.rst index 16031200..d6090465 100644 --- a/docs/features/argument_processing.rst +++ b/docs/features/argument_processing.rst @@ -75,13 +75,6 @@ Here's what it looks like:: for i in range(min(repetitions, self.maxrepeats)): self.poutput(arg) -.. warning:: - - It is important that each command which uses the ``@with_argparser`` - decorator be passed a unique instance of a parser since command-specific - changes could be made to it. - - .. note:: The ``@with_argparser`` decorator sets the ``prog`` variable in the argument From 3972880b9fe9461721f10e3a56a04a8637607c10 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Thu, 12 Sep 2024 18:13:50 -0400 Subject: [PATCH 10/10] Fixed issue where persistent history file was not saved upon SIGHUP and SIGTERM signals. --- CHANGELOG.md | 2 ++ cmd2/cmd2.py | 40 ++++++++++++++++++++++++++++++++++------ tests/test_cmd2.py | 11 +++++++++++ 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dac58426..378fffcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## 2.5.0 (TBD) * Breaking Change * `cmd2` 2.5 supports Python 3.8+ (removed support for Python 3.6 and 3.7) +* Bug Fixes + * Fixed issue where persistent history file was not saved upon SIGHUP and SIGTERM signals. * Enhancements * Removed dependency on `attrs` and replaced with [dataclasses](https://docs.python.org/3/library/dataclasses.html) * add `allow_clipboard` initialization parameter and attribute to disable ability to diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 5f95b2ab..1381f0d5 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -2405,13 +2405,13 @@ def get_help_topics(self) -> List[str]: return [topic for topic in all_topics if topic not in self.hidden_commands and topic not in self.disabled_commands] # noinspection PyUnusedLocal - def sigint_handler(self, signum: int, _: FrameType) -> None: + def sigint_handler(self, signum: int, _: Optional[FrameType]) -> None: """Signal handler for SIGINTs which typically come from Ctrl-C events. - If you need custom SIGINT behavior, then override this function. + If you need custom SIGINT behavior, then override this method. :param signum: signal number - :param _: required param for signal handlers + :param _: the current stack frame or None """ if self._cur_pipe_proc_reader is not None: # Pass the SIGINT to the current pipe process @@ -2427,6 +2427,23 @@ def sigint_handler(self, signum: int, _: FrameType) -> None: if raise_interrupt: self._raise_keyboard_interrupt() + def termination_signal_handler(self, signum: int, _: Optional[FrameType]) -> None: + """ + Signal handler for SIGHUP and SIGTERM. Only runs on Linux and Mac. + + SIGHUP - received when terminal window is closed + SIGTERM - received when this app has been requested to terminate + + The basic purpose of this method is to call sys.exit() so our exit handler will run + and save the persistent history file. If you need more complex behavior like killing + threads and performing cleanup, then override this method. + + :param signum: signal number + :param _: the current stack frame or None + """ + # POSIX systems add 128 to signal numbers for the exit code + sys.exit(128 + signum) + def _raise_keyboard_interrupt(self) -> None: """Helper function to raise a KeyboardInterrupt""" raise KeyboardInterrupt("Got a keyboard interrupt") @@ -5426,11 +5443,18 @@ def cmdloop(self, intro: Optional[str] = None) -> int: # type: ignore[override] if not threading.current_thread() is threading.main_thread(): raise RuntimeError("cmdloop must be run in the main thread") - # Register a SIGINT signal handler for Ctrl+C + # Register signal handlers import signal original_sigint_handler = signal.getsignal(signal.SIGINT) - signal.signal(signal.SIGINT, self.sigint_handler) # type: ignore + signal.signal(signal.SIGINT, self.sigint_handler) + + if not sys.platform.startswith('win'): + original_sighup_handler = signal.getsignal(signal.SIGHUP) + signal.signal(signal.SIGHUP, self.termination_signal_handler) + + original_sigterm_handler = signal.getsignal(signal.SIGTERM) + signal.signal(signal.SIGTERM, self.termination_signal_handler) # Grab terminal lock before the command line prompt has been drawn by readline self.terminal_lock.acquire() @@ -5464,9 +5488,13 @@ def cmdloop(self, intro: Optional[str] = None) -> int: # type: ignore[override] # This will also zero the lock count in case cmdloop() is called again self.terminal_lock.release() - # Restore the original signal handler + # Restore original signal handlers signal.signal(signal.SIGINT, original_sigint_handler) + if not sys.platform.startswith('win'): + signal.signal(signal.SIGHUP, original_sighup_handler) + signal.signal(signal.SIGTERM, original_sigterm_handler) + return self.exit_code ### diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 5686e9f9..2130cbf1 100755 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -1038,6 +1038,17 @@ def test_raise_keyboard_interrupt(base_app): assert 'Got a keyboard interrupt' in str(excinfo.value) +@pytest.mark.skipif(sys.platform.startswith('win'), reason="SIGTERM only handeled on Linux/Mac") +def test_termination_signal_handler(base_app): + with pytest.raises(SystemExit) as excinfo: + base_app.termination_signal_handler(signal.SIGHUP, 1) + assert excinfo.value.code == signal.SIGHUP + 128 + + with pytest.raises(SystemExit) as excinfo: + base_app.termination_signal_handler(signal.SIGTERM, 1) + assert excinfo.value.code == signal.SIGTERM + 128 + + class HookFailureApp(cmd2.Cmd): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs)