From 48d7691286ec10028d1dc28707e91c27cf35846d Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Tue, 16 Sep 2025 18:05:49 -0400 Subject: [PATCH 1/2] Delete the `cmd2_ext_test.py` plugin code from the cmd2 repo Also: - Copy the ExternalTestMixin class implementation to the `tests_isolated_test_commandset/conftest.py` file - Remove info on the External Test Plugin from the documentation - Add info on the cmd2 Plugin Template to the documentation - Remove loading of `cmd2-ext-test` source code from the pyproject.toml - Removed ext-test stuff from tasks.py for invoke usage There is likely a lot more cleanup that can and should be done. This is just a first attempt to get our GitHub Actions CI/CD working without this plugin's circular dependency. --- docs/plugins/external_test.md | 74 ------- docs/plugins/index.md | 2 +- docs/plugins/plugin_template.md | 7 + docs/testing.md | 7 +- mkdocs.yml | 2 +- plugins/ext_test/CHANGELOG.md | 22 -- plugins/ext_test/README.md | 82 ------- plugins/ext_test/build-pyenvs.sh | 53 ----- plugins/ext_test/cmd2_ext_test/__init__.py | 18 -- .../ext_test/cmd2_ext_test/cmd2_ext_test.py | 67 ------ plugins/ext_test/cmd2_ext_test/py.typed | 1 - plugins/ext_test/cmd2_ext_test/pylintrc | 10 - plugins/ext_test/examples/example.py | 37 ---- plugins/ext_test/noxfile.py | 7 - plugins/ext_test/pyproject.toml | 180 --------------- plugins/ext_test/setup.py | 49 ---- plugins/ext_test/tasks.py | 209 ------------------ plugins/ext_test/tests/__init__.py | 2 - plugins/ext_test/tests/pylintrc | 19 -- plugins/ext_test/tests/test_ext_test.py | 74 ------- plugins/tasks.py | 23 +- pyproject.toml | 6 +- tests_isolated/test_commandset/conftest.py | 76 ++++++- 23 files changed, 87 insertions(+), 940 deletions(-) delete mode 100644 docs/plugins/external_test.md create mode 100644 docs/plugins/plugin_template.md delete mode 100644 plugins/ext_test/CHANGELOG.md delete mode 100644 plugins/ext_test/README.md delete mode 100644 plugins/ext_test/build-pyenvs.sh delete mode 100644 plugins/ext_test/cmd2_ext_test/__init__.py delete mode 100644 plugins/ext_test/cmd2_ext_test/cmd2_ext_test.py delete mode 100644 plugins/ext_test/cmd2_ext_test/py.typed delete mode 100644 plugins/ext_test/cmd2_ext_test/pylintrc delete mode 100644 plugins/ext_test/examples/example.py delete mode 100644 plugins/ext_test/noxfile.py delete mode 100644 plugins/ext_test/pyproject.toml delete mode 100644 plugins/ext_test/setup.py delete mode 100644 plugins/ext_test/tasks.py delete mode 100644 plugins/ext_test/tests/__init__.py delete mode 100644 plugins/ext_test/tests/pylintrc delete mode 100644 plugins/ext_test/tests/test_ext_test.py diff --git a/docs/plugins/external_test.md b/docs/plugins/external_test.md deleted file mode 100644 index 0ebc3490f..000000000 --- a/docs/plugins/external_test.md +++ /dev/null @@ -1,74 +0,0 @@ -# External Test Plugin - -## Overview - -The [External Test Plugin](https://github.com/python-cmd2/cmd2/tree/main/plugins/ext_test) supports -testing of a cmd2 application by exposing access to cmd2 commands with the same context as from -within a cmd2 [Python Script](../features/scripting.md#python-scripts). This interface captures -`stdout`, `stderr`, as well as any application-specific data returned by the command. This also -allows for verification of an application's support for -[Python Scripts](../features/scripting.md#python-scripts) and enables the cmd2 application to be -tested as part of a larger system integration test. - -## Example cmd2 Application - -The following short example shows how to mix in the external test plugin to create a fixture for -testing your cmd2 application. - -Define your cmd2 application - -```py -import cmd2 -class ExampleApp(cmd2.Cmd): - """An class to show how to use a plugin""" - def __init__(self, *args, **kwargs): - # gotta have this or neither the plugin or cmd2 will initialize - super().__init__(*args, **kwargs) - - def do_something(self, arg): - self.last_result = 5 - self.poutput('this is the something command') -``` - -## Defining the test fixture - -In your test, define a fixture for your cmd2 application - -```py -import cmd2_ext_test -import pytest - -class ExampleAppTester(cmd2_ext_test.ExternalTestMixin, ExampleApp): - def __init__(self, *args, **kwargs): - # gotta have this or neither the plugin or cmd2 will initialize - super().__init__(*args, **kwargs) - -@pytest.fixture -def example_app(): - app = ExampleAppTester() - app.fixture_setup() - yield app - app.fixture_teardown() -``` - -## Writing Tests - -Now write your tests that validate your application using the -`cmd2_ext_test.ExternalTestMixin.app_cmd` function to access the cmd2 application's commands. This -allows invocation of the application's commands in the same format as a user would type. The results -from calling a command matches what is returned from running a Python script with cmd2's -[run_pyscript](../features/builtin_commands.md#run_pyscript) command, which provides `stdout`, -`stderr`, and the command's result data. - -```py -from cmd2 import CommandResult - -def test_something(example_app): - # execute a command - out = example_app.app_cmd("something") - - # validate the command output and result data - assert isinstance(out, CommandResult) - assert str(out.stdout).strip() == 'this is the something command' - assert out.data == 5 -``` diff --git a/docs/plugins/index.md b/docs/plugins/index.md index 0059915fe..d0ccbd766 100644 --- a/docs/plugins/index.md +++ b/docs/plugins/index.md @@ -2,6 +2,6 @@ -- [External Test Plugin](external_test.md) +- [cmd2 Plugin Template](plugin_template.md) diff --git a/docs/plugins/plugin_template.md b/docs/plugins/plugin_template.md new file mode 100644 index 000000000..b3560c75a --- /dev/null +++ b/docs/plugins/plugin_template.md @@ -0,0 +1,7 @@ +# cmd2 Plugin Template + +## Overview + +The [cmd2 Plugin Template](https://github.com/python-cmd2/cmd2/tree/main/plugins/template) is a +general example that shows you how you can develop a plugin for `cmd2`. Plugins are generally mixin +classes that add some extra functionality to your class which inherits from [cmd2.Cmd][]. diff --git a/docs/testing.md b/docs/testing.md index 8ba1a5837..176e9d94b 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -6,9 +6,10 @@ This covers special considerations when writing unit or integration tests for a ## Testing Commands -The [External Test Plugin](plugins/external_test.md) provides a mixin class with a function that -allows external calls to application commands. The `cmd2_ext_test.ExternalTestMixin.app_cmd` -function captures and returns stdout, stderr, and the command-specific result data. +We encourage `cmd2` application developers to look at the +[cmd2 tests](https://github.com/python-cmd2/cmd2/tree/main/tests) for examples of how to perform +unit and integration testing of `cmd2` commands. There are various helpers that will do things like +capture and return stdout, stderr, and command-specific result data. ## Mocking diff --git a/mkdocs.yml b/mkdocs.yml index 8712bbbfd..c0f6208b3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -195,7 +195,7 @@ nav: - examples/examples.md - Plugins: - plugins/index.md - - plugins/external_test.md + - plugins/plugin_template.md - Testing: - testing.md - API Reference: diff --git a/plugins/ext_test/CHANGELOG.md b/plugins/ext_test/CHANGELOG.md deleted file mode 100644 index b843b9cdb..000000000 --- a/plugins/ext_test/CHANGELOG.md +++ /dev/null @@ -1,22 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project -adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). - -## 0.2.0 (2020-09-11) - -- Updated documentation to reflect new home inside of main cmd2 repo. -- Updated python version requirements to match cmd2 - -## 0.1.2 (2020-08-03) - -- Bug Fixes - - Applied fix to match change in cmd2 APIs - -## 0.1.1 (2020-03-09) - -### Added - -- Initial contribution diff --git a/plugins/ext_test/README.md b/plugins/ext_test/README.md deleted file mode 100644 index 18cec0837..000000000 --- a/plugins/ext_test/README.md +++ /dev/null @@ -1,82 +0,0 @@ -# cmd2 External Test Plugin - -## Table of Contents - -- [Overview](#overview) -- [Example cmd2 Application](#example-cmd2-application) -- [Defining the test fixture](#defining-the-test-fixture) -- [Writing Tests](#writing-tests) -- [License](#license) - -## Overview - -This plugin supports testing of a cmd2 application by exposing access cmd2 commands with the same -context as from within a cmd2 pyscript. This allows for verification of an application's support for -pyscripts. - -## Example cmd2 Application - -The following short example shows how to mix in the external test plugin to create a fixture for -testing your cmd2 application. - -Define your cmd2 application - -```python -import cmd2 -class ExampleApp(cmd2.Cmd): - """An class to show how to use a plugin""" - def __init__(self, *args, **kwargs): - # gotta have this or neither the plugin or cmd2 will initialize - super().__init__(*args, **kwargs) - - def do_something(self, arg): - self.last_result = 5 - self.poutput('this is the something command') -``` - -## Defining the test fixture - -In your test, define a fixture for your cmd2 application - -```python -import cmd2_ext_test -import pytest - -class ExampleAppTester(cmd2_ext_test.ExternalTestMixin, ExampleApp): - def __init__(self, *args, **kwargs): - # gotta have this or neither the plugin or cmd2 will initialize - super().__init__(*args, **kwargs) - -@pytest.fixture -def example_app(): - app = ExampleAppTester() - app.fixture_setup() - yield app - app.fixture_teardown() - -``` - -## Writing Tests - -Now write your tests that validate your application using the `app_cmd` function to access the cmd2 -application's commands. This allows invocation of the application's commands in the same format as a -user would type. The results from calling a command matches what is returned from running an python -script with cmd2's pyscript command, which provides stdout, stderr, and the command's result data. - -```python -from cmd2 import CommandResult - -def test_something(example_app): - # execute a command - out = example_app.app_cmd("something") - - # validate the command output and result data - assert isinstance(out, CommandResult) - assert str(out.stdout).strip() == 'this is the something command' - assert out.data == 5 -``` - -## License - -cmd2 [uses the very liberal MIT license](https://github.com/python-cmd2/cmd2/blob/main/LICENSE). We -invite plugin authors to consider doing the same. diff --git a/plugins/ext_test/build-pyenvs.sh b/plugins/ext_test/build-pyenvs.sh deleted file mode 100644 index 9ee27578b..000000000 --- a/plugins/ext_test/build-pyenvs.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash -# - -# create pyenv environments for each minor version of python -# supported by this project -# -# this script uses terms from Semantic Versioning https://semver.org/ -# version numbers are: major.minor.patch -# -# this script will delete and recreate existing virtualenvs named -# cmd2-3.14, etc. It will also create a .python-version -# -# Prerequisites: -# - *nix-ish environment like macOS or Linux -# - pyenv installed -# - pyenv-virtualenv installed -# - readline and openssl libraries installed so pyenv can -# build pythons -# - -# Make a array of the python minor versions we want to install. -# Order matters in this list, because it's the order that the -# 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.10", "3.11", "3.12", "3.13", "3.14") - -# function to find the latest patch of a minor version of python -function find_latest_version { - pyenv install -l | \ - sed -En -e "s/^ *//g" -e "/(dev|b|rc)/d" -e "/^$1/p" | \ - tail -1 -} - -# empty out '.python-version' -> .python-version - -# loop through the pythons -for minor_version in "${pythons[@]}" -do - patch_version=$( find_latest_version "$minor_version" ) - # use pyenv to install the latest versions of python - # if it's already installed don't install it again - pyenv install -s "$patch_version" - - envname="cmd2-$minor_version" - # remove the associated virtualenv - pyenv uninstall -f "$envname" - # create a new virtualenv - pyenv virtualenv -p "python$minor_version" "$patch_version" "$envname" - # append the virtualenv to .python-version - echo "$envname" >> .python-version -done diff --git a/plugins/ext_test/cmd2_ext_test/__init__.py b/plugins/ext_test/cmd2_ext_test/__init__.py deleted file mode 100644 index 94796e7b3..000000000 --- a/plugins/ext_test/cmd2_ext_test/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -"""cmd2 External Python Testing Mixin - -Allows developers to exercise their cmd2 application using the PyScript interface -""" - -import importlib.metadata as importlib_metadata - -try: - __version__ = importlib_metadata.version(__name__) -except importlib_metadata.PackageNotFoundError: # pragma: no cover - # package is not installed - __version__ = 'unknown' - -from .cmd2_ext_test import ( - ExternalTestMixin, -) - -__all__ = ['ExternalTestMixin'] diff --git a/plugins/ext_test/cmd2_ext_test/cmd2_ext_test.py b/plugins/ext_test/cmd2_ext_test/cmd2_ext_test.py deleted file mode 100644 index 843d609f2..000000000 --- a/plugins/ext_test/cmd2_ext_test/cmd2_ext_test.py +++ /dev/null @@ -1,67 +0,0 @@ -"""External test interface plugin""" - -from typing import ( - TYPE_CHECKING, -) - -import cmd2 - -if TYPE_CHECKING: # pragma: no cover - _Base = cmd2.Cmd -else: - _Base = object - - -class ExternalTestMixin(_Base): - """A cmd2 plugin (mixin class) that exposes an interface to execute application commands from python""" - - def __init__(self, *args, **kwargs): - """ - - :type self: cmd2.Cmd - :param args: - :param kwargs: - """ - # code placed here runs before cmd2 initializes - super().__init__(*args, **kwargs) - assert isinstance(self, cmd2.Cmd) - # code placed here runs after cmd2 initializes - self._pybridge = cmd2.py_bridge.PyBridge(self) - - def app_cmd(self, command: str, echo: bool | None = None) -> cmd2.CommandResult: - """ - Run the application command - - :param command: The application command as it would be written on the cmd2 application prompt - :param echo: Flag whether the command's output should be echoed to stdout/stderr - :return: A CommandResult object that captures stdout, stderr, and the command's result object - """ - assert isinstance(self, cmd2.Cmd) - assert isinstance(self, ExternalTestMixin) - try: - self._in_py = True - - return self._pybridge(command, echo=echo) - - finally: - self._in_py = False - - def fixture_setup(self): - """ - Replicates the behavior of `cmdloop()` preparing the state of the application - :type self: cmd2.Cmd - """ - - for func in self._preloop_hooks: - func() - self.preloop() - - def fixture_teardown(self): - """ - Replicates the behavior of `cmdloop()` tearing down the application - - :type self: cmd2.Cmd - """ - for func in self._postloop_hooks: - func() - self.postloop() diff --git a/plugins/ext_test/cmd2_ext_test/py.typed b/plugins/ext_test/cmd2_ext_test/py.typed deleted file mode 100644 index 17af580fe..000000000 --- a/plugins/ext_test/cmd2_ext_test/py.typed +++ /dev/null @@ -1 +0,0 @@ -# PEP 561 diff --git a/plugins/ext_test/cmd2_ext_test/pylintrc b/plugins/ext_test/cmd2_ext_test/pylintrc deleted file mode 100644 index 2f6d3de24..000000000 --- a/plugins/ext_test/cmd2_ext_test/pylintrc +++ /dev/null @@ -1,10 +0,0 @@ -# -# pylint configuration -# -# $ pylint --rcfile=cmd2_myplugin/pylintrc cmd2_myplugin -# - -[messages control] -# too-few-public-methods pylint expects a class to have at -# least two public methods -disable=too-few-public-methods diff --git a/plugins/ext_test/examples/example.py b/plugins/ext_test/examples/example.py deleted file mode 100644 index c9c0ee267..000000000 --- a/plugins/ext_test/examples/example.py +++ /dev/null @@ -1,37 +0,0 @@ -import cmd2_ext_test - -import cmd2 -import cmd2.py_bridge - - -class Example(cmd2.Cmd): - """An class to show how to use a plugin""" - - def __init__(self, *args, **kwargs): - # gotta have this or neither the plugin or cmd2 will initialize - super().__init__(*args, **kwargs) - - def do_something(self, _arg): - self.last_result = 5 - self.poutput('this is the something command') - - -class ExampleTester(cmd2_ext_test.ExternalTestMixin, Example): - def __init__(self, *args, **kwargs): - # gotta have this or neither the plugin or cmd2 will initialize - super().__init__(*args, **kwargs) - - -if __name__ == '__main__': - app = ExampleTester() - - try: - app.fixture_setup() - - out = app.app_cmd("something") - assert isinstance(out, cmd2.CommandResult) - - assert out.data == 5 - - finally: - app.fixture_teardown() diff --git a/plugins/ext_test/noxfile.py b/plugins/ext_test/noxfile.py deleted file mode 100644 index 9a29eaabd..000000000 --- a/plugins/ext_test/noxfile.py +++ /dev/null @@ -1,7 +0,0 @@ -import nox - - -@nox.session(python=['3.10', '3.11', '3.12', '3.13', '3.14']) -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 deleted file mode 100644 index b4e48324a..000000000 --- a/plugins/ext_test/pyproject.toml +++ /dev/null @@ -1,180 +0,0 @@ -[build-system] -requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"] - -[tool.mypy] -disallow_incomplete_defs = true -disallow_untyped_calls = true -disallow_untyped_defs = true -exclude = [ - "^examples/", # examples directory - "^noxfile\\.py$", # nox config file - "setup\\.py$", # any files named setup.py - "^tasks\\.py$", # tasks.py invoke config file - "^tests/", # tests directory -] -show_column_numbers = true -show_error_codes = true -show_error_context = true -strict = true -warn_redundant_casts = true -warn_return_any = true -warn_unreachable = true -warn_unused_ignores = false - -[tool.ruff] -# Exclude a variety of commonly ignored directories. -exclude = [ - ".bzr", - ".direnv", - ".eggs", - ".git", - ".git-rewrite", - ".hg", - ".ipynb_checkpoints", - ".mypy_cache", - ".nox", - ".pants.d", - ".pyenv", - ".pytest_cache", - ".pytype", - ".ruff_cache", - ".svn", - ".tox", - ".venv", - ".vscode", - "__pypackages__", - "_build", - "buck-out", - "build", - "dist", - "node_modules", - "site-packages", - "venv", -] - -# Same as Black. -line-length = 127 -indent-width = 4 - -# Assume Python 3.13 -target-version = "py313" -output-format = "full" - -[tool.ruff.lint] -# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. -# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or -# McCabe complexity (`C901`) by default. -select = [ - # https://beta.ruff.rs/docs/rules - # "A", # flake8-builtins - # "ANN", # flake8-annotations - # "ARG", # flake8-unused-arguments - "ASYNC", # flake8-async - # "B", # flake8-bugbear - # "BLE", # flake8-blind-except - # "C4", # flake8-comprehensions - "C90", # McCabe cyclomatic complexity - # "COM", # flake8-commas - # "D", # pydocstyle - "DJ", # flake8-django - # "DTZ", # flake8-datetimez - "E", # pycodestyle - # "EM", # flake8-errmsg - # "ERA", # eradicate - # "EXE", # flake8-executable - "F", # Pyflakes - "FA", # flake8-future-annotations - # "FBT", # flake8-boolean-trap - "G", # flake8-logging-format - # "I", # isort - "ICN", # flake8-import-conventions - # "INP", # flake8-no-pep420 - "INT", # flake8-gettext - # "ISC", # flake8-implicit-str-concat - # "N", # pep8-naming - "NPY", # NumPy-specific rules - "PD", # pandas-vet - # "PGH", # pygrep-hooks - # "PIE", # flake8-pie - # "PL", # Pylint - # "PT", # flake8-pytest-style - # "PTH", # flake8-use-pathlib - # "PYI", # flake8-pyi - # "RET", # flake8-return - "RSE", # flake8-raise - # "Q", # flake8-quotes - # "RUF", # Ruff-specific rules - # "S", # flake8-bandit - # "SIM", # flake8-simplify - # "SLF", # flake8-self - # "T10", # flake8-debugger - # "T20", # flake8-print - # "TCH", # flake8-type-checking - # "TD", # flake8-todos - # "TID", # flake8-tidy-imports - # "TRY", # tryceratops - # "UP", # pyupgrade - # "W", # pycodestyle - # "YTT", # flake8-2020 -] -ignore = [ - # `ruff rule S101` for a description of that rule - "B904", # Within an `except` clause, raise exceptions with `raise ... from err` -- FIX ME - "B905", # `zip()` without an explicit `strict=` parameter -- FIX ME - "E501", # Line too long - "EM101", # Exception must not use a string literal, assign to variable first - "EXE001", # Shebang is present but file is not executable -- DO NOT FIX - "G004", # Logging statement uses f-string - "PLC1901", # `{}` can be simplified to `{}` as an empty string is falsey - "PLW060", # Using global for `{name}` but no assignment is done -- DO NOT FIX - "PLW2901", # PLW2901: Redefined loop variable -- FIX ME - "PT011", # `pytest.raises(Exception)` is too broad, set the `match` parameter or use a more specific exception - "PT018", # Assertion should be broken down into multiple parts - "S101", # Use of `assert` detected -- DO NOT FIX - "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes -- FIX ME - "SLF001", # Private member accessed: `_Iterator` -- FIX ME -] - -# Allow fix for all enabled rules (when `--fix`) is provided. -fixable = ["ALL"] -unfixable = [] - -# Allow unused variables when underscore-prefixed. -dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" - -mccabe.max-complexity = 49 - -per-file-ignores."examples/scripts/*.py" = [ - "F821", # Undefined name `app` -] - -per-file-ignores."tests/pyscript/*.py" = [ - "F821", # Undefined name `app` -] - -[tool.ruff.format] -# Like Black, use double quotes for strings. -quote-style = "preserve" - -# Like Black, indent with spaces, rather than tabs. -indent-style = "space" - -# Like Black, respect magic trailing commas. -skip-magic-trailing-comma = false - -# Like Black, automatically detect the appropriate line ending. -line-ending = "auto" - -# Enable auto-formatting of code examples in docstrings. Markdown, -# reStructuredText code/literal blocks and doctests are all supported. -# -# This is currently disabled by default, but it is planned for this -# to be opt-out in the future. -docstring-code-format = false - -# Set the line length limit used when formatting code snippets in -# docstrings. -# -# This only has an effect when the `docstring-code-format` setting is -# enabled. -docstring-code-line-length = "dynamic" diff --git a/plugins/ext_test/setup.py b/plugins/ext_test/setup.py deleted file mode 100644 index 244b85cf6..000000000 --- a/plugins/ext_test/setup.py +++ /dev/null @@ -1,49 +0,0 @@ -import os - -import setuptools - -# get the long description from the README file -here = os.path.abspath(os.path.dirname(__file__)) -with open(os.path.join(here, 'README.md'), encoding='utf-8') as f: - long_description = f.read() - -PACKAGE_DATA = { - 'cmd2_ext_test': ['py.typed'], -} - -setuptools.setup( - name='cmd2-ext-test', - version='2.0.0', - description='External test plugin for cmd2. Allows for external invocation of commands as if from a cmd2 pyscript', - long_description=long_description, - long_description_content_type='text/markdown', - keywords='cmd2 test plugin', - author='Eric Lin', - author_email='anselor@gmail.com', - url='https://github.com/python-cmd2/cmd2/tree/main/plugins/ext_test', - license='MIT', - package_data=PACKAGE_DATA, - packages=['cmd2_ext_test'], - python_requires='>=3.10', - install_requires=['cmd2 >= 2, <3'], - setup_requires=['setuptools >= 42', 'setuptools_scm >= 3.4'], - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Console', - 'Operating System :: OS Independent', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Programming Language :: Python :: 3.13', - 'Programming Language :: Python :: 3.14', - ], - # dependencies for development and testing - # $ pip install -e .[dev] - extras_require={ - 'test': ['codecov', 'coverage', 'pytest', 'pytest-cov'], - 'dev': ['setuptools_scm', 'pytest', 'codecov', 'pytest-cov', 'pylint', 'invoke', 'wheel', 'twine'], - }, -) diff --git a/plugins/ext_test/tasks.py b/plugins/ext_test/tasks.py deleted file mode 100644 index b7f369377..000000000 --- a/plugins/ext_test/tasks.py +++ /dev/null @@ -1,209 +0,0 @@ -"""Development related tasks to be run with 'invoke'. - -Make sure you satisfy the following Python module requirements if you are trying to publish a release to PyPI: - - twine >= 1.11.0 - - wheel >= 0.31.0 - - setuptools >= 39.1.0 -""" - -import contextlib -import os -import pathlib -import shutil - -import invoke - -TASK_ROOT = pathlib.Path(__file__).resolve().parent -TASK_ROOT_STR = str(TASK_ROOT) - - -# shared function -def rmrf(items, verbose=True): - """Silently remove a list of directories or files""" - if isinstance(items, str): - items = [items] - - for item in items: - if verbose: - print(f"Removing {item}") - shutil.rmtree(item, ignore_errors=True) - # rmtree doesn't remove bare files - with contextlib.suppress(FileNotFoundError): - os.remove(item) - - -# create namespaces -namespace = invoke.Collection() -namespace_clean = invoke.Collection('clean') -namespace.add_collection(namespace_clean, 'clean') - -##### -# -# pytest, pylint, and codecov -# -##### - - -@invoke.task -def pytest(context, junit=False, pty=True, append_cov=False): - """Run tests and code coverage using pytest""" - root_path = TASK_ROOT.parent.parent - - with context.cd(str(root_path)): - command_str = 'pytest --cov=cmd2_ext_test --cov-report=term --cov-report=html' - if append_cov: - command_str += ' --cov-append' - if junit: - command_str += ' --junitxml=junit/test-results.xml' - command_str += ' ' + str((TASK_ROOT / 'tests').relative_to(root_path)) - context.run(command_str, pty=pty) - - -namespace.add_task(pytest) - - -@invoke.task -def pytest_clean(context): - """Remove pytest cache and code coverage files and directories""" - # pylint: disable=unused-argument - with context.cd(TASK_ROOT_STR): - dirs = ['.pytest_cache', '.cache', 'htmlcov', '.coverage'] - rmrf(dirs) - - -namespace_clean.add_task(pytest_clean, 'pytest') - - -@invoke.task -def mypy(context): - """Run mypy optional static type checker""" - with context.cd(TASK_ROOT_STR): - context.run("mypy .") - - -namespace.add_task(mypy) - - -@invoke.task -def mypy_clean(context): - """Remove mypy cache directory""" - # pylint: disable=unused-argument - with context.cd(TASK_ROOT_STR): - dirs = ['.mypy_cache', 'dmypy.json', 'dmypy.sock'] - rmrf(dirs) - - -namespace_clean.add_task(mypy_clean, 'mypy') - - -##### -# -# documentation -# -##### - - -##### -# -# build and distribute -# -##### -BUILDDIR = 'build' -DISTDIR = 'dist' - - -@invoke.task -def build_clean(context): - """Remove the build directory""" - # pylint: disable=unused-argument - with context.cd(TASK_ROOT_STR): - rmrf(BUILDDIR) - - -namespace_clean.add_task(build_clean, 'build') - - -@invoke.task -def dist_clean(context): - """Remove the dist directory""" - # pylint: disable=unused-argument - with context.cd(TASK_ROOT_STR): - rmrf(DISTDIR) - - -namespace_clean.add_task(dist_clean, 'dist') - - -# -# make a dummy clean task which runs all the tasks in the clean namespace -clean_tasks = list(namespace_clean.tasks.values()) - - -@invoke.task(pre=list(namespace_clean.tasks.values()), default=True) -def clean_all(context): - """Run all clean tasks""" - # pylint: disable=unused-argument - - -namespace_clean.add_task(clean_all, 'all') - - -@invoke.task(pre=[clean_all]) -def sdist(context): - """Create a source distribution""" - with context.cd(TASK_ROOT_STR): - context.run('python -m build --sdist') - - -namespace.add_task(sdist) - - -@invoke.task(pre=[clean_all]) -def wheel(context): - """Build a wheel distribution""" - with context.cd(TASK_ROOT_STR): - context.run('python -m build --wheel') - - -namespace.add_task(wheel) - - -@invoke.task(pre=[sdist, wheel]) -def pypi(context): - """Build and upload a distribution to pypi""" - with context.cd(TASK_ROOT_STR): - context.run('twine upload dist/*') - - -namespace.add_task(pypi) - - -@invoke.task(pre=[sdist, wheel]) -def pypi_test(context): - """Build and upload a distribution to https://test.pypi.org""" - with context.cd(TASK_ROOT_STR): - context.run('twine upload --repository-url https://test.pypi.org/legacy/ dist/*') - - -namespace.add_task(pypi_test) - - -# ruff fast linter -@invoke.task -def lint(context): - """Run ruff fast linter""" - with context.cd(TASK_ROOT_STR): - context.run("ruff check") - - -namespace.add_task(lint) - - -@invoke.task -def format(context): # noqa: A001 - """Run ruff format --check""" - with context.cd(TASK_ROOT_STR): - context.run("ruff format --check") - - -namespace.add_task(format) diff --git a/plugins/ext_test/tests/__init__.py b/plugins/ext_test/tests/__init__.py deleted file mode 100644 index eb198dc04..000000000 --- a/plugins/ext_test/tests/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# -# empty file to create a package diff --git a/plugins/ext_test/tests/pylintrc b/plugins/ext_test/tests/pylintrc deleted file mode 100644 index 1dd17c1c7..000000000 --- a/plugins/ext_test/tests/pylintrc +++ /dev/null @@ -1,19 +0,0 @@ -# -# pylint configuration for tests package -# -# $ pylint --rcfile=tests/pylintrc tests -# - -[basic] -# allow for longer method and function names -method-rgx=(([a-z][a-z0-9_]{2,50})|(_[a-z0-9_]*))$ -function-rgx=(([a-z][a-z0-9_]{2,50})|(_[a-z0-9_]*))$ - -[messages control] -# too-many-public-methods -> test classes can have lots of methods, so let's ignore those -# missing-docstring -> prefer method names instead of docstrings -# no-self-use -> test methods part of a class hardly ever use self -# unused-variable -> sometimes we are expecting exceptions -# redefined-outer-name -> pylint fixtures cause these -# protected-access -> we want to test private methods -disable=too-many-public-methods,missing-docstring,no-self-use,unused-variable,redefined-outer-name,protected-access diff --git a/plugins/ext_test/tests/test_ext_test.py b/plugins/ext_test/tests/test_ext_test.py deleted file mode 100644 index df9216d8d..000000000 --- a/plugins/ext_test/tests/test_ext_test.py +++ /dev/null @@ -1,74 +0,0 @@ -import cmd2_ext_test -import pytest - -from cmd2 import ( - CommandResult, - cmd2, -) - -###### -# -# define a class which implements a simple cmd2 application -# -###### - -OUT_MSG = 'this is the something command' - - -class ExampleApp(cmd2.Cmd): - """An class to show how to use a plugin""" - - def __init__(self, *args, **kwargs): - # gotta have this or neither the plugin or cmd2 will initialize - super().__init__(*args, **kwargs) - - def do_something(self, _): - self.last_result = 5 - self.poutput(OUT_MSG) - - -# Define a tester class that brings in the external test mixin - - -class ExampleTester(cmd2_ext_test.ExternalTestMixin, ExampleApp): - def __init__(self, *args, **kwargs): - # gotta have this or neither the plugin or cmd2 will initialize - super().__init__(*args, **kwargs) - - -# -# You can't use a fixture to instantiate your app if you want to use -# to use the capsys fixture to capture the output. cmd2.Cmd sets -# internal variables to sys.stdout and sys.stderr on initialization -# and then uses those internal variables instead of sys.stdout. It does -# this so you can redirect output from within the app. The capsys fixture -# can't capture the output properly in this scenario. -# -# If you have extensive initialization needs, create a function -# to initialize your cmd2 application. - - -@pytest.fixture -def example_app(): - app = ExampleTester() - app.fixture_setup() - yield app - app.fixture_teardown() - - -##### -# -# unit tests -# -##### - - -def test_something(example_app): - # load our fixture - # execute a command - out = example_app.app_cmd("something") - - # validate the command output and result data - assert isinstance(out, CommandResult) - assert str(out.stdout).strip() == OUT_MSG - assert out.data == 5 diff --git a/plugins/tasks.py b/plugins/tasks.py index b2e2024ee..d6924edbc 100644 --- a/plugins/tasks.py +++ b/plugins/tasks.py @@ -10,16 +10,12 @@ import invoke -from plugins.ext_test import ( - tasks as ext_test_tasks, -) from plugins.template import ( tasks as template_tasks, ) # create namespaces namespace = invoke.Collection( - ext_test=ext_test_tasks, template=template_tasks, ) namespace_clean = invoke.Collection('clean') @@ -35,7 +31,6 @@ TASK_ROOT_STR = str(TASK_ROOT) -@invoke.task(pre=[ext_test_tasks.pytest]) @invoke.task() def pytest(_) -> None: """Run tests and code coverage using pytest.""" @@ -44,7 +39,7 @@ def pytest(_) -> None: namespace.add_task(pytest) -@invoke.task(pre=[ext_test_tasks.pytest_clean]) +@invoke.task() def pytest_clean(_) -> None: """Remove pytest cache and code coverage files and directories.""" @@ -52,7 +47,7 @@ def pytest_clean(_) -> None: namespace_clean.add_task(pytest_clean, 'pytest') -@invoke.task(pre=[ext_test_tasks.mypy]) +@invoke.task() def mypy(_) -> None: """Run mypy optional static type checker.""" @@ -60,7 +55,7 @@ def mypy(_) -> None: namespace.add_task(mypy) -@invoke.task(pre=[ext_test_tasks.mypy_clean]) +@invoke.task() def mypy_clean(_) -> None: """Remove mypy cache directory.""" # pylint: disable=unused-argument @@ -78,7 +73,7 @@ def mypy_clean(_) -> None: DISTDIR = 'dist' -@invoke.task(pre=[ext_test_tasks.build_clean]) +@invoke.task() def build_clean(_) -> None: """Remove the build directory.""" @@ -86,7 +81,7 @@ def build_clean(_) -> None: namespace_clean.add_task(build_clean, 'build') -@invoke.task(pre=[ext_test_tasks.dist_clean]) +@invoke.task() def dist_clean(_) -> None: """Remove the dist directory.""" @@ -107,7 +102,7 @@ def clean_all(_) -> None: namespace_clean.add_task(clean_all, 'all') -@invoke.task(pre=[clean_all], post=[ext_test_tasks.sdist]) +@invoke.task(pre=[clean_all]) def sdist(_) -> None: """Create a source distribution.""" @@ -115,7 +110,7 @@ def sdist(_) -> None: namespace.add_task(sdist) -@invoke.task(pre=[clean_all], post=[ext_test_tasks.wheel]) +@invoke.task(pre=[clean_all]) def wheel(_) -> None: """Build a wheel distribution.""" @@ -124,7 +119,7 @@ def wheel(_) -> None: # ruff linter -@invoke.task(pre=[ext_test_tasks.lint]) +@invoke.task() def lint(context) -> None: with context.cd(TASK_ROOT_STR): context.run("ruff check") @@ -134,7 +129,7 @@ def lint(context) -> None: # ruff formatter -@invoke.task(pre=[ext_test_tasks.format]) +@invoke.task() def format(context) -> None: # noqa: A001 """Run formatter.""" with context.cd(TASK_ROOT_STR): diff --git a/pyproject.toml b/pyproject.toml index da39552a4..24ba086c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,6 @@ docs = [ "setuptools>=64", "setuptools_scm>=8", ] -plugins = ["cmd2-ext-test"] quality = ["pre-commit>=2.20.0"] test = [ "codecov>=2", @@ -315,7 +314,4 @@ packages = ["cmd2"] [tool.setuptools_scm] [tool.uv] -default-groups = ["build", "dev", "plugins"] - -[tool.uv.sources] -cmd2-ext-test = { path = "plugins/ext_test", editable = true } +default-groups = ["build", "dev"] diff --git a/tests_isolated/test_commandset/conftest.py b/tests_isolated/test_commandset/conftest.py index ec476bbfc..ed05b9e71 100644 --- a/tests_isolated/test_commandset/conftest.py +++ b/tests_isolated/test_commandset/conftest.py @@ -5,22 +5,74 @@ redirect_stderr, redirect_stdout, ) -from unittest import ( - mock, -) +from typing import TYPE_CHECKING +from unittest import mock import pytest -from cmd2_ext_test import ( - ExternalTestMixin, -) import cmd2 -from cmd2.rl_utils import ( - readline, -) -from cmd2.utils import ( - StdSim, -) +from cmd2.rl_utils import readline +from cmd2.utils import StdSim + +if TYPE_CHECKING: + _Base = cmd2.Cmd +else: + _Base = object + + +class ExternalTestMixin: + """A cmd2 plugin (mixin class) that exposes an interface to execute application commands from python""" + + def __init__(self, *args, **kwargs): + """ + + :type self: cmd2.Cmd + :param args: + :param kwargs: + """ + # code placed here runs before cmd2 initializes + super().__init__(*args, **kwargs) + assert isinstance(self, cmd2.Cmd) + # code placed here runs after cmd2 initializes + self._pybridge = cmd2.py_bridge.PyBridge(self) + + def app_cmd(self, command: str, echo: bool | None = None) -> cmd2.CommandResult: + """ + Run the application command + + :param command: The application command as it would be written on the cmd2 application prompt + :param echo: Flag whether the command's output should be echoed to stdout/stderr + :return: A CommandResult object that captures stdout, stderr, and the command's result object + """ + assert isinstance(self, cmd2.Cmd) + assert isinstance(self, ExternalTestMixin) + try: + self._in_py = True + + return self._pybridge(command, echo=echo) + + finally: + self._in_py = False + + def fixture_setup(self): + """ + Replicates the behavior of `cmdloop()` preparing the state of the application + :type self: cmd2.Cmd + """ + + for func in self._preloop_hooks: + func() + self.preloop() + + def fixture_teardown(self): + """ + Replicates the behavior of `cmdloop()` tearing down the application + + :type self: cmd2.Cmd + """ + for func in self._postloop_hooks: + func() + self.postloop() def verify_help_text(cmd2_app: cmd2.Cmd, help_output: str | list[str], verbose_strings: list[str] | None = None) -> None: From d705fa11eec5c90f964f07f37ff205937fc2a69d Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Tue, 16 Sep 2025 18:13:49 -0400 Subject: [PATCH 2/2] Fixed a copy-paste bug. Thanks gemini-cli for catching it! --- tests_isolated/test_commandset/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests_isolated/test_commandset/conftest.py b/tests_isolated/test_commandset/conftest.py index ed05b9e71..5a58627ec 100644 --- a/tests_isolated/test_commandset/conftest.py +++ b/tests_isolated/test_commandset/conftest.py @@ -20,7 +20,7 @@ _Base = object -class ExternalTestMixin: +class ExternalTestMixin(_Base): """A cmd2 plugin (mixin class) that exposes an interface to execute application commands from python""" def __init__(self, *args, **kwargs):