summaryrefslogtreecommitdiff
path: root/plugins
diff options
context:
space:
mode:
authorEric Lin <anselor@gmail.com>2020-07-07 13:59:43 -0400
committeranselor <anselor@gmail.com>2020-07-11 17:30:40 -0400
commit28e43bf24f8a5bc0b2e896938e76e17524d12ed3 (patch)
treeff423668e7e07233e65cf5b4958d3c0d768e69f0 /plugins
parentff64eff8854c9b52a1f48e4b843e9a738d2b388d (diff)
downloadcmd2-git-28e43bf24f8a5bc0b2e896938e76e17524d12ed3.tar.gz
Copied cmd2 ext test into cmd2 baseline and linked up invoke
Diffstat (limited to 'plugins')
-rw-r--r--plugins/cmd2_ext_test/CHANGELOG.md12
-rw-r--r--plugins/cmd2_ext_test/LICENSE21
-rw-r--r--plugins/cmd2_ext_test/README.md84
-rw-r--r--plugins/cmd2_ext_test/build-pyenvs.sh53
-rw-r--r--plugins/cmd2_ext_test/cmd2_ext_test/__init__.py15
-rw-r--r--plugins/cmd2_ext_test/cmd2_ext_test/cmd2_ext_test.py62
-rw-r--r--plugins/cmd2_ext_test/cmd2_ext_test/pylintrc10
-rw-r--r--plugins/cmd2_ext_test/examples/example.py38
-rw-r--r--plugins/cmd2_ext_test/setup.py52
-rw-r--r--plugins/cmd2_ext_test/tasks.py197
-rw-r--r--plugins/cmd2_ext_test/tests/__init__.py2
-rw-r--r--plugins/cmd2_ext_test/tests/pylintrc19
-rw-r--r--plugins/cmd2_ext_test/tests/test_ext_test.py70
-rw-r--r--plugins/tasks.py166
14 files changed, 801 insertions, 0 deletions
diff --git a/plugins/cmd2_ext_test/CHANGELOG.md b/plugins/cmd2_ext_test/CHANGELOG.md
new file mode 100644
index 00000000..c6eae3f7
--- /dev/null
+++ b/plugins/cmd2_ext_test/CHANGELOG.md
@@ -0,0 +1,12 @@
+# 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).
+
+## 1.0.0 (2020-03-09)
+
+### Added
+- Initial contribution
+
+
diff --git a/plugins/cmd2_ext_test/LICENSE b/plugins/cmd2_ext_test/LICENSE
new file mode 100644
index 00000000..b1784d5d
--- /dev/null
+++ b/plugins/cmd2_ext_test/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 Jared Crapo
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/plugins/cmd2_ext_test/README.md b/plugins/cmd2_ext_test/README.md
new file mode 100644
index 00000000..6f8a2b8c
--- /dev/null
+++ b/plugins/cmd2_ext_test/README.md
@@ -0,0 +1,84 @@
+# 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/master/LICENSE).
+We invite plugin authors to consider doing the same.
diff --git a/plugins/cmd2_ext_test/build-pyenvs.sh b/plugins/cmd2_ext_test/build-pyenvs.sh
new file mode 100644
index 00000000..39c28aa1
--- /dev/null
+++ b/plugins/cmd2_ext_test/build-pyenvs.sh
@@ -0,0 +1,53 @@
+#!/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.7, 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.7" "3.6" "3.5" "3.4")
+
+# 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/cmd2_ext_test/cmd2_ext_test/__init__.py b/plugins/cmd2_ext_test/cmd2_ext_test/__init__.py
new file mode 100644
index 00000000..da3fae9a
--- /dev/null
+++ b/plugins/cmd2_ext_test/cmd2_ext_test/__init__.py
@@ -0,0 +1,15 @@
+#
+# coding=utf-8
+"""Description of myplugin
+
+An overview of what myplugin does.
+"""
+
+from pkg_resources import get_distribution, DistributionNotFound
+
+from .cmd2_ext_test import ExternalTestMixin
+
+try:
+ __version__ = get_distribution(__name__).version
+except DistributionNotFound:
+ __version__ = 'unknown'
diff --git a/plugins/cmd2_ext_test/cmd2_ext_test/cmd2_ext_test.py b/plugins/cmd2_ext_test/cmd2_ext_test/cmd2_ext_test.py
new file mode 100644
index 00000000..02fd29b1
--- /dev/null
+++ b/plugins/cmd2_ext_test/cmd2_ext_test/cmd2_ext_test.py
@@ -0,0 +1,62 @@
+#
+# coding=utf-8
+"""External test interface plugin"""
+
+from typing import Optional
+
+import cmd2
+
+
+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: Optional[bool] = 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) and 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
+ """
+ # assert isinstance(self, cmd2.Cmd) and isinstance(self, ExternalTestMixin)
+ for func in self._postloop_hooks:
+ func()
+ self.postloop()
diff --git a/plugins/cmd2_ext_test/cmd2_ext_test/pylintrc b/plugins/cmd2_ext_test/cmd2_ext_test/pylintrc
new file mode 100644
index 00000000..2f6d3de2
--- /dev/null
+++ b/plugins/cmd2_ext_test/cmd2_ext_test/pylintrc
@@ -0,0 +1,10 @@
+#
+# 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/cmd2_ext_test/examples/example.py b/plugins/cmd2_ext_test/examples/example.py
new file mode 100644
index 00000000..649f8627
--- /dev/null
+++ b/plugins/cmd2_ext_test/examples/example.py
@@ -0,0 +1,38 @@
+#
+# coding=utf-8
+# import cmd2
+import cmd2
+import cmd2_ext_test
+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/cmd2_ext_test/setup.py b/plugins/cmd2_ext_test/setup.py
new file mode 100644
index 00000000..cb55c16a
--- /dev/null
+++ b/plugins/cmd2_ext_test/setup.py
@@ -0,0 +1,52 @@
+#
+# coding=utf-8
+
+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()
+
+setuptools.setup(
+ name='cmd2-ext-test',
+ use_scm_version=True,
+
+ 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-ext-test',
+ license='MIT',
+
+ packages=['cmd2_ext_test'],
+
+ python_requires='>=3.4',
+ install_requires=['cmd2 >= 0.9.4, <=2'],
+ setup_requires=['setuptools_scm'],
+
+ classifiers=[
+ 'Development Status :: 4 - Beta',
+ 'Environment :: Console',
+ 'Operating System :: OS Independent',
+ 'Topic :: Software Development :: Libraries :: Python Modules',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: MIT License',
+ 'Programming Language :: Python :: 3.4',
+ 'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: 3.6',
+ 'Programming Language :: Python :: 3.7',
+ ],
+
+ # dependencies for development and testing
+ # $ pip install -e .[dev]
+ extras_require={
+ 'dev': ['setuptools_scm', 'pytest', 'codecov', 'pytest-cov',
+ 'pylint', 'invoke', 'wheel', 'twine']
+ },
+)
diff --git a/plugins/cmd2_ext_test/tasks.py b/plugins/cmd2_ext_test/tasks.py
new file mode 100644
index 00000000..6bb8d307
--- /dev/null
+++ b/plugins/cmd2_ext_test/tasks.py
@@ -0,0 +1,197 @@
+#
+# coding=utf-8
+# flake8: noqa E302
+"""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 os
+import pathlib
+import re
+import shutil
+import sys
+
+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("Removing {}".format(item))
+ shutil.rmtree(item, ignore_errors=True)
+ # rmtree doesn't remove bare files
+ try:
+ os.remove(item)
+ except FileNotFoundError:
+ pass
+
+
+# create namespaces
+namespace = invoke.Collection()
+namespace_clean = invoke.Collection('clean')
+namespace.add_collection(namespace_clean, 'clean')
+
+#####
+#
+# pytest, tox, pylint, and codecov
+#
+#####
+@invoke.task
+def pytest(context):
+ "Run tests and code coverage using pytest"
+ with context.cd(TASK_ROOT_STR):
+ context.run("pytest --cov=cmd2_ext_test --cov-report=term --cov-report=html", pty=True)
+namespace.add_task(pytest)
+
+@invoke.task
+def pytest_junit(context):
+ "Run tests and code coverage using pytest"
+ with context.cd(TASK_ROOT_STR):
+ context.run("pytest --cov --junitxml=junit/test-results.xml", pty=True)
+namespace.add_task(pytest_junit)
+
+@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 main.py")
+ namespace.add_task(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')
+
+@invoke.task
+def eggs_clean(context):
+ "Remove egg directories"
+ #pylint: disable=unused-argument
+ with context.cd(TASK_ROOT_STR):
+ dirs = set()
+ dirs.add('.eggs')
+ for name in os.listdir(os.curdir):
+ if name.endswith('.egg-info'):
+ dirs.add(name)
+ if name.endswith('.egg'):
+ dirs.add(name)
+ rmrf(dirs)
+namespace_clean.add_task(eggs_clean, 'eggs')
+
+@invoke.task
+def pycache_clean(context):
+ "Remove __pycache__ directories"
+ #pylint: disable=unused-argument
+ with context.cd(TASK_ROOT_STR):
+ dirs = set()
+ for root, dirnames, _ in os.walk(os.curdir):
+ if '__pycache__' in dirnames:
+ dirs.add(os.path.join(root, '__pycache__'))
+ print("Removing __pycache__ directories")
+ rmrf(dirs, verbose=False)
+namespace_clean.add_task(pycache_clean, 'pycache')
+
+#
+# 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
+ pass
+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 setup.py 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 setup.py bdist_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)
+
+
+# Flake8 - linter and tool for style guide enforcement and linting
+@invoke.task
+def flake8(context):
+ "Run flake8 linter and tool for style guide enforcement"
+ with context.cd(TASK_ROOT_STR):
+ context.run("flake8 --ignore=E252,W503 --max-complexity=26 --max-line-length=127 --show-source --statistics --exclude=.git,__pycache__,.tox,.eggs,*.egg,.venv,.idea,.pytest_cache,.vscode,build,dist,htmlcov")
+namespace.add_task(flake8)
diff --git a/plugins/cmd2_ext_test/tests/__init__.py b/plugins/cmd2_ext_test/tests/__init__.py
new file mode 100644
index 00000000..eb198dc0
--- /dev/null
+++ b/plugins/cmd2_ext_test/tests/__init__.py
@@ -0,0 +1,2 @@
+#
+# empty file to create a package
diff --git a/plugins/cmd2_ext_test/tests/pylintrc b/plugins/cmd2_ext_test/tests/pylintrc
new file mode 100644
index 00000000..1dd17c1c
--- /dev/null
+++ b/plugins/cmd2_ext_test/tests/pylintrc
@@ -0,0 +1,19 @@
+#
+# 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/cmd2_ext_test/tests/test_ext_test.py b/plugins/cmd2_ext_test/tests/test_ext_test.py
new file mode 100644
index 00000000..cf5429b8
--- /dev/null
+++ b/plugins/cmd2_ext_test/tests/test_ext_test.py
@@ -0,0 +1,70 @@
+#
+# coding=utf-8
+
+import pytest
+
+from cmd2 import cmd2, CommandResult
+import cmd2_ext_test
+
+######
+#
+# 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
new file mode 100644
index 00000000..4aac4f77
--- /dev/null
+++ b/plugins/tasks.py
@@ -0,0 +1,166 @@
+#
+# coding=utf-8
+# flake8: noqa E302
+"""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 os
+import re
+import shutil
+import sys
+
+import invoke
+from plugins.cmd2_ext_test import tasks as ext_test_tasks
+
+# create namespaces
+namespace = invoke.Collection()
+namespace_clean = invoke.Collection('clean')
+namespace.add_collection(namespace_clean, 'clean')
+
+#####
+#
+# pytest, tox, pylint, and codecov
+#
+#####
+@invoke.task(pre=[ext_test_tasks.pytest])
+def pytest(_):
+ """Run tests and code coverage using pytest"""
+ pass
+
+
+namespace.add_task(pytest)
+
+
+@invoke.task(pre=[ext_test_tasks.pytest_junit])
+def pytest_junit(_):
+ """Run tests and code coverage using pytest"""
+ pass
+
+
+namespace.add_task(pytest_junit)
+
+
+@invoke.task(pre=[ext_test_tasks.pytest_clean])
+def pytest_clean(_):
+ """Remove pytest cache and code coverage files and directories"""
+ pass
+
+
+namespace_clean.add_task(pytest_clean, 'pytest')
+
+
+@invoke.task(pre=[ext_test_tasks.mypy])
+def mypy(context):
+ """Run mypy optional static type checker"""
+ pass
+
+
+namespace.add_task(mypy)
+
+@invoke.task(pre=[ext_test_tasks.mypy_clean])
+def mypy_clean(context):
+ """Remove mypy cache directory"""
+ #pylint: disable=unused-argument
+ pass
+
+
+namespace_clean.add_task(mypy_clean, 'mypy')
+
+
+#####
+#
+# build and distribute
+#
+#####
+BUILDDIR = 'build'
+DISTDIR = 'dist'
+
+@invoke.task(pre=[ext_test_tasks.build_clean])
+def build_clean(_):
+ """Remove the build directory"""
+
+
+namespace_clean.add_task(build_clean, 'build')
+
+
+@invoke.task(pre=[ext_test_tasks.dist_clean])
+def dist_clean(_):
+ """Remove the dist directory"""
+ pass
+
+
+namespace_clean.add_task(dist_clean, 'dist')
+
+
+@invoke.task(pre=[ext_test_tasks.eggs_clean])
+def eggs_clean(context):
+ """Remove egg directories"""
+ pass
+
+
+namespace_clean.add_task(eggs_clean, 'eggs')
+
+
+@invoke.task(pre=[ext_test_tasks.pycache_clean])
+def pycache_clean(context):
+ """Remove __pycache__ directories"""
+ pass
+
+
+namespace_clean.add_task(pycache_clean, 'pycache')
+
+
+# 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
+ pass
+
+
+namespace_clean.add_task(clean_all, 'all')
+
+
+@invoke.task(pre=[clean_all], post=[ext_test_tasks.sdist])
+def sdist(context):
+ "Create a source distribution"
+ context.run('python setup.py sdist')
+
+
+namespace.add_task(sdist)
+
+
+@invoke.task(pre=[clean_all], post=[ext_test_tasks.wheel])
+def wheel(context):
+ "Build a wheel distribution"
+ context.run('python setup.py bdist_wheel')
+namespace.add_task(wheel)
+
+
+@invoke.task(pre=[sdist, wheel])
+def pypi(context):
+ "Build and upload a distribution to pypi"
+ 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"
+ context.run('twine upload --repository-url https://test.pypi.org/legacy/ dist/*')
+namespace.add_task(pypi_test)
+
+
+# Flake8 - linter and tool for style guide enforcement and linting
+@invoke.task(pre=[ext_test_tasks.flake8])
+def flake8(context):
+ "Run flake8 linter and tool for style guide enforcement"
+ context.run("flake8 --ignore=E252,W503 --max-complexity=26 --max-line-length=127 --show-source --statistics --exclude=.git,__pycache__,.tox,.eggs,*.egg,.venv,.idea,.pytest_cache,.vscode,build,dist,htmlcov")
+namespace.add_task(flake8)