diff options
author | Eric Lin <anselor@gmail.com> | 2020-07-13 15:28:40 -0400 |
---|---|---|
committer | anselor <anselor@gmail.com> | 2020-07-14 19:26:30 -0400 |
commit | e38684d219847f636562ab2720b82aae4a6fd408 (patch) | |
tree | ec20d7a46b8cba1a662d46e79d5e004590bee93b /plugins/template | |
parent | 683d049299c0cf7a2821b639a95ad0911bab1bc7 (diff) | |
download | cmd2-git-e38684d219847f636562ab2720b82aae4a6fd408.tar.gz |
Brought in cmd2 plugin template as a first-class member of cmd2 proper
Diffstat (limited to 'plugins/template')
-rw-r--r-- | plugins/template/CHANGELOG.md | 12 | ||||
-rw-r--r-- | plugins/template/LICENSE | 21 | ||||
-rw-r--r-- | plugins/template/README.md | 329 | ||||
-rw-r--r-- | plugins/template/build-pyenvs.sh | 53 | ||||
-rw-r--r-- | plugins/template/cmd2_myplugin/__init__.py | 15 | ||||
-rw-r--r-- | plugins/template/cmd2_myplugin/myplugin.py | 69 | ||||
-rw-r--r-- | plugins/template/cmd2_myplugin/pylintrc | 10 | ||||
-rw-r--r-- | plugins/template/examples/example.py | 21 | ||||
-rw-r--r-- | plugins/template/setup.py | 59 | ||||
-rw-r--r-- | plugins/template/tasks.py | 203 | ||||
-rw-r--r-- | plugins/template/tests/__init__.py | 2 | ||||
-rw-r--r-- | plugins/template/tests/pylintrc | 19 | ||||
-rw-r--r-- | plugins/template/tests/test_myplugin.py | 67 |
13 files changed, 880 insertions, 0 deletions
diff --git a/plugins/template/CHANGELOG.md b/plugins/template/CHANGELOG.md new file mode 100644 index 00000000..adf951ec --- /dev/null +++ b/plugins/template/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 (2018-07-24) + +### Added +- Created plugin template and initial documentation + + diff --git a/plugins/template/LICENSE b/plugins/template/LICENSE new file mode 100644 index 00000000..b1784d5d --- /dev/null +++ b/plugins/template/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/template/README.md b/plugins/template/README.md new file mode 100644 index 00000000..327a226e --- /dev/null +++ b/plugins/template/README.md @@ -0,0 +1,329 @@ +# cmd2 Plugin Template + +## Table of Contents + +- [Using this template](#using-this-template) +- [Naming](#naming) +- [Adding functionality](#adding-functionality) +- [Examples](#examples) +- [Development Tasks](#development-tasks) +- [Packaging and Distribution](#packaging-and-distribution) +- [License](#license) + + +## Using this template + +This template assumes you are creating a new cmd2 plugin called `myplugin`. Your +plugin will have a different name. You will need to rename some of the files and +directories in this template. Don't forget to modify the imports and `setup.py`. + +You'll probably also want to rewrite the README :) + + +## Naming + +You should prefix the name of your project with `cmd2-`. Within that project, +you should have a package with a prefix of `cmd2_`. + + +## Adding functionality + +There are many ways to add functionality to `cmd2` using a plugin. Most plugins +will be implemented as a mixin. A mixin is a class that encapsulates and injects +code into another class. Developers who use a plugin in their `cmd2` project, +will inject the plugin's code into their subclass of `cmd2.Cmd`. + + +### Mixin and Initialization + +The following short example shows how to mix in a plugin and how the plugin +gets initialized. + +Here's the plugin: + +```python +class MyPlugin: + def __init__(self, *args, **kwargs): + # code placed here runs before cmd2.Cmd initializes + super().__init__(*args, **kwargs) + # code placed here runs after cmd2.Cmd initializes +``` + +and an example app which uses the plugin: + +```python +import cmd2 +import cmd2_myplugin + +class Example(cmd2_myplugin.MyPlugin, cmd2.Cmd): + """An class to show how to use a plugin""" + def __init__(self, *args, **kwargs): + # code placed here runs before cmd2.Cmd or + # any plugins initialize + super().__init__(*args, **kwargs) + # code placed here runs after cmd2.Cmd and + # all plugins have initialized +``` + +Note how the plugin must be inherited (or mixed in) before `cmd2.Cmd`. This is +required for two reasons: + +- The `cmd.Cmd.__init__()` method in the python standard library does not call + `super().__init__()`. Because of this oversight, if you don't inherit from `MyPlugin` first, the + `MyPlugin.__init__()` method will never be called. +- You may want your plugin to be able to override methods from `cmd2.Cmd`. + If you mixin the plugin after `cmd2.Cmd`, the python method resolution order + will call `cmd2.Cmd` methods before it calls those in your plugin. + + +### Add commands + +Your plugin can add user visable commands. You do it the same way in a plugin +that you would in a `cmd2.Cmd` app: + +```python +class MyPlugin: + + def do_say(self, statement): + """Simple say command""" + self.poutput(statement) +``` + +You have all the same capabilities within the plugin that you do inside a +`cmd2.Cmd` app, including argument parsing via decorators and custom help +methods. + +### Add (or hide) settings + +A plugin may add user controllable settings to the application. Here's an +example: + +```python +class MyPlugin: + def __init__(self, *args, **kwargs): + # code placed here runs before cmd2.Cmd initializes + super().__init__(*args, **kwargs) + # code placed here runs after cmd2.Cmd initializes + self.mysetting = 'somevalue' + self.settable.update({'mysetting': 'short help message for mysetting'}) +``` + +You can also hide settings from the user by removing them from `self.settable`. + +### Decorators + +Your plugin can provide a decorator which users of your plugin can use to wrap +functionality around their own commands. + +### Override methods + +Your plugin can override core `cmd2.Cmd` methods, changing their behavior. +This approach should be used sparingly, because it is very brittle. If a +developer chooses to use multiple plugins in their application, and several of +the plugins override the same method, only the first plugin to be mixed in +will have the overridden method called. + +Hooks are a much better approach. + +### Hooks + +Plugins can register hooks, which are called by `cmd2.Cmd` during various points +in the application and command processing lifecycle. Plugins should not override +any of the deprecated hook methods, instead they should register their hooks as +[described](https://cmd2.readthedocs.io/en/latest/hooks.html) in the cmd2 +documentation. + +You should name your hooks so that they begin with the name of your plugin. Hook +methods get mixed into the `cmd2` application and this naming convention helps +avoid unintentional method overriding. + + +Here's a simple example: + +```python +class MyPlugin: + + def __init__(self, *args, **kwargs): + # code placed here runs before cmd2 initializes + super().__init__(*args, **kwargs) + # code placed here runs after cmd2 initializes + # this is where you register any hook functions + self.register_postparsing_hook(self.cmd2_myplugin_postparsing_hook) + + def cmd2_myplugin_postparsing_hook(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData: + """Method to be called after parsing user input, but before running the command""" + self.poutput('in postparsing_hook') + return data +``` + +Registration allows multiple plugins (or even the application itself) to each inject code +to be called during the application or command processing lifecycle. + +See the [cmd2 hook documentation](https://cmd2.readthedocs.io/en/latest/hooks.html) +for full details of the application and command lifecycle, including all +available hooks and the ways hooks can influence the lifecycle. + + +### Classes and Functions + +Your plugin can also provide classes and functions which can be used by +developers of cmd2 based applications. Describe these classes and functions in +your documentation so users of your plugin will know what's available. + + +## Examples + +Include an example or two in the `examples` directory which demonstrate how your +plugin works. This will help developers utilize it from within their +application. + + +## Development Tasks + +This project uses many other python modules for various development tasks, +including testing, linting, building wheels, and distributing releases. These +modules can be configured many different ways, which can make it difficult to +learn the specific incantations required for each project you are familiar with. + +This project uses [invoke](<http://www.pyinvoke.org>) to provide a clean, +high level interface for these development tasks. To see the full list of +functions available: +``` +$ invoke -l +``` + +You can run multiple tasks in a single invocation, for example: +``` +$ invoke clean docs sdist wheel +``` + +That one command will remove all superflous cache, testing, and build +files, render the documentation, and build a source distribution and a +wheel distribution. + +For more information, read `tasks.py`. + +While developing your plugin, you should make sure you support all versions of +python supported by cmd2, and all supported platforms. cmd2 uses a three +tiered testing strategy to accomplish this objective. + +- [pytest](https://pytest.org) runs the unit tests +- [tox](https://tox.readthedocs.io/) runs the unit tests on multiple versions + of python +- [AppVeyor](https://www.appveyor.com/) and [TravisCI](https://travis-ci.com) + run the tests on the various supported platforms + +This plugin template is set up to use the same strategy. + + +### Create python environments + +This project uses [tox](https://tox.readthedocs.io/en/latest/) to run the test +suite against multiple python versions. I recommend +[pyenv](https://github.com/pyenv/pyenv) with the +[pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv>) plugin to manage +these various versions. If you are a Windows user, `pyenv` won't work for you, +but [conda](https://conda.io/) can also be used to solve this problem. + +This distribution includes a shell script `build-pyenvs.sh` which +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.6.5 +$ pyenv virtualenv -p python3.6 3.6.5 cmd2-3.6 +$ pyenv install 3.5.5 +$ pyenv virtualenv -p python3.5 3.5.5 cmd2-3.5 +$ pyenv install 3.4.8 +$ pyenv virtualenv -p python3.4 3.4.8 cmd2-3.4 +``` + +Now set pyenv to make all three of those available at the same time: +``` +$ pyenv local cmd2-3.7 cmd2-3.6 cmd2-3.5 cmd2-3.4 +``` + +Whether you ran the script, or did it by hand, you now have isolated virtualenvs +for each of the major python versions. This table shows various python commands, +the version of python which will be executed, and the virtualenv it will +utilize. + +| Command | python | virtualenv | +| ----------- | ------ | ---------- | +| `python` | 3.7.0 | cmd2-3.6 | +| `python3` | 3.7.0 | cmd2-3.6 | +| `python3.7` | 3.7.0 | cmd2-3.7 | +| `python3.6` | 3.6.5 | cmd2-3.6 | +| `python3.5` | 3.5.5 | cmd2-3.5 | +| `python3.4` | 3.4.8 | cmd2-3.4 | +| `pip` | 3.7.0 | cmd2-3.6 | +| `pip3` | 3.7.0 | cmd2-3.6 | +| `pip3.7` | 3.7.0 | cmd2-3.7 | +| `pip3.6` | 3.6.5 | cmd2-3.6 | +| `pip3.5` | 3.5.5 | cmd2-3.5 | +| `pip3.4` | 3.4.8 | cmd2-3.4 | + +## Install Dependencies + +Install all the development dependencies: +``` +$ 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` +virtualenv. If you want to work in other virtualenvs, you'll need to manually +select it, and install again:: + + $ pyenv shell cmd2-3.4 + $ pip install -e .[dev] + +Now that you have your python environments created, you need to install the +package in place, along with all the other development dependencies: +``` +$ pip install -e .[dev] +``` + + +### Running unit tests + +Run `invoke pytest` from the top level directory of your plugin to run all the +unit tests found in the `tests` directory. + + +### Use tox to run unit tests in multiple versions of python + +The included `tox.ini` is setup to run the unit tests in python 3.4, 3.5, 3.6, +and 3.7. You can run your unit tests in all of these versions of python by: +``` +$ invoke tox +``` + + +### Run unit tests on multiple platforms + +[AppVeyor](https://github.com/marketplace/appveyor) and +[TravisCI](https://docs.travis-ci.com/user/getting-started/) offer free plans +for open source projects. + + +## Packaging and Distribution + +When creating your `setup.py` file, keep the following in mind: + +- use the keywords `cmd2 plugin` to make it easier for people to find your plugin +- since cmd2 uses semantic versioning, you should use something like + `install_requires=['cmd2 >= 0.9.4, <=2']` to make sure that your plugin + doesn't try and run with a future version of `cmd2` with which it may not be + compatible + + +## 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/template/build-pyenvs.sh b/plugins/template/build-pyenvs.sh new file mode 100644 index 00000000..39c28aa1 --- /dev/null +++ b/plugins/template/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/template/cmd2_myplugin/__init__.py b/plugins/template/cmd2_myplugin/__init__.py new file mode 100644 index 00000000..41f0b9cc --- /dev/null +++ b/plugins/template/cmd2_myplugin/__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 .myplugin import empty_decorator, MyPluginMixin # noqa: F401 + +try: + __version__ = get_distribution(__name__).version +except DistributionNotFound: + __version__ = 'unknown' diff --git a/plugins/template/cmd2_myplugin/myplugin.py b/plugins/template/cmd2_myplugin/myplugin.py new file mode 100644 index 00000000..5fa12caf --- /dev/null +++ b/plugins/template/cmd2_myplugin/myplugin.py @@ -0,0 +1,69 @@ +# +# coding=utf-8 +"""An example cmd2 plugin""" + +import functools +from typing import Callable, TYPE_CHECKING + +import cmd2 + +if TYPE_CHECKING: + _Base = cmd2.Cmd +else: + _Base = object + + +def empty_decorator(func: Callable) -> Callable: + """An empty decorator for myplugin""" + + @functools.wraps(func) + def _empty_decorator(self, *args, **kwargs): + self.poutput("in the empty decorator") + func(self, *args, **kwargs) + + _empty_decorator.__doc__ = func.__doc__ + return _empty_decorator + + +class MyPluginMixin(_Base): + """A mixin class which adds a 'say' command to a cmd2 subclass + + The order in which you add the mixin matters. Say you want to + use this mixin in a class called MyApp. + + class MyApp(cmd2_myplugin.MyPlugin, cmd2.Cmd): + def __init__(self, *args, **kwargs): + # gotta have this or neither the plugin or cmd2 will initialize + super().__init__(*args, **kwargs) + """ + + def __init__(self, *args, **kwargs): + # code placed here runs before cmd2 initializes + super().__init__(*args, **kwargs) + # code placed here runs after cmd2 initializes + # this is where you register any hook functions + self.register_preloop_hook(self.cmd2_myplugin_preloop_hook) + self.register_postloop_hook(self.cmd2_myplugin_postloop_hook) + self.register_postparsing_hook(self.cmd2_myplugin_postparsing_hook) + + def do_say(self, statement): + """Simple say command""" + self.poutput(statement) + + # + # define hooks as functions, not methods + def cmd2_myplugin_preloop_hook(self) -> None: + """Method to be called before the command loop begins""" + self.poutput("preloop hook") + + def cmd2_myplugin_postloop_hook(self) -> None: + """Method to be called after the command loop finishes""" + self.poutput("postloop hook") + + def cmd2_myplugin_postparsing_hook( + self, + data: cmd2.plugin.PostparsingData + ) -> cmd2.plugin.PostparsingData: + """Method to be called after parsing user input, but before running the command""" + self.poutput('in postparsing hook') + return data diff --git a/plugins/template/cmd2_myplugin/pylintrc b/plugins/template/cmd2_myplugin/pylintrc new file mode 100644 index 00000000..2f6d3de2 --- /dev/null +++ b/plugins/template/cmd2_myplugin/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/template/examples/example.py b/plugins/template/examples/example.py new file mode 100644 index 00000000..2c9b8e5c --- /dev/null +++ b/plugins/template/examples/example.py @@ -0,0 +1,21 @@ +# +# coding=utf-8 + +import cmd2 +import cmd2_myplugin + + +class Example(cmd2_myplugin.MyPlugin, 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) + + @cmd2_myplugin.empty_decorator + def do_something(self, arg): + self.poutput('this is the something command') + + +if __name__ == '__main__': + app = Example() + app.cmdloop() diff --git a/plugins/template/setup.py b/plugins/template/setup.py new file mode 100644 index 00000000..17d06fa8 --- /dev/null +++ b/plugins/template/setup.py @@ -0,0 +1,59 @@ +# +# 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-myplugin', + # use_scm_version=True, # use_scm_version doesn't work if setup.py isn't in the repository root + version='1.0.1', + + description='A template used to build plugins for cmd2', + long_description=long_description, + long_description_content_type='text/markdown', + keywords='cmd2 plugin', + + author='Kotfu', + author_email='kotfu@kotfu.net', + url='https://github.com/python-cmd2/cmd2-plugin-template', + license='MIT', + + packages=['cmd2_myplugin'], + + 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={ + 'test': [ + 'codecov', + 'coverage', + 'pytest', + 'pytest-cov', + ], + 'dev': ['setuptools_scm', 'pytest', 'codecov', 'pytest-cov', + 'pylint', 'invoke', 'wheel', 'twine'] + }, +) diff --git a/plugins/template/tasks.py b/plugins/template/tasks.py new file mode 100644 index 00000000..3fcb4cbf --- /dev/null +++ b/plugins/template/tasks.py @@ -0,0 +1,203 @@ +# +# -*- coding: utf-8 -*- +"""Development related tasks to be run with 'invoke'""" + +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("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, 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_myplugin --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', '.coverage'] + rmrf(dirs) + + +namespace_clean.add_task(pytest_clean, 'pytest') + + +@invoke.task +def pylint(context): + """Check code quality using pylint""" + context.run('pylint --rcfile=cmd2_myplugin/pylintrc cmd2_myplugin') + + +namespace.add_task(pylint) + + +@invoke.task +def pylint_tests(context): + """Check code quality of test suite using pylint""" + context.run('pylint --rcfile=tests/pylintrc tests') + + +namespace.add_task(pylint_tests) + + +##### +# +# build and distribute +# +##### +BUILDDIR = 'build' +DISTDIR = 'dist' + + +@invoke.task +def build_clean(context): + """Remove the build directory""" + # pylint: disable=unused-argument + rmrf(BUILDDIR) + + +namespace_clean.add_task(build_clean, 'build') + + +@invoke.task +def dist_clean(context): + """Remove the dist directory""" + # pylint: disable=unused-argument + rmrf(DISTDIR) + + +namespace_clean.add_task(dist_clean, 'dist') + + +@invoke.task +def eggs_clean(context): + """Remove egg directories""" + # pylint: disable=unused-argument + 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 bytecode_clean(context): + """Remove __pycache__ directories and *.pyc files""" + # pylint: disable=unused-argument + dirs = set() + for root, dirnames, files in os.walk(os.curdir): + if '__pycache__' in dirnames: + dirs.add(os.path.join(root, '__pycache__')) + for file in files: + if file.endswith(".pyc"): + dirs.add(os.path.join(root, file)) + print("Removing __pycache__ directories and .pyc files") + rmrf(dirs, verbose=False) + + +namespace_clean.add_task(bytecode_clean, 'bytecode') + +# +# 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""" + context.run('python setup.py sdist') + + +namespace.add_task(sdist) + + +@invoke.task(pre=[clean_all]) +def wheel(context): + """Build a wheel distribution""" + context.run('python setup.py bdist_wheel') + + +namespace.add_task(wheel) + +# +# these two tasks are commented out so you don't +# accidentally run them and upload this template to pypi +# + +# @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) diff --git a/plugins/template/tests/__init__.py b/plugins/template/tests/__init__.py new file mode 100644 index 00000000..eb198dc0 --- /dev/null +++ b/plugins/template/tests/__init__.py @@ -0,0 +1,2 @@ +# +# empty file to create a package diff --git a/plugins/template/tests/pylintrc b/plugins/template/tests/pylintrc new file mode 100644 index 00000000..1dd17c1c --- /dev/null +++ b/plugins/template/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/template/tests/test_myplugin.py b/plugins/template/tests/test_myplugin.py new file mode 100644 index 00000000..4149f7df --- /dev/null +++ b/plugins/template/tests/test_myplugin.py @@ -0,0 +1,67 @@ +# +# coding=utf-8 + +from cmd2 import cmd2 +import cmd2_myplugin + +###### +# +# define a class which uses our plugin and some convenience functions +# +###### + + +class MyApp(cmd2_myplugin.MyPluginMixin, cmd2.Cmd): + """Simple subclass of cmd2.Cmd with our SayMixin plugin included.""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @cmd2_myplugin.empty_decorator + def do_empty(self, args): + self.poutput("running the empty command") + +# +# 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. + + +def init_app(): + app = MyApp() + return app + + +##### +# +# unit tests +# +##### + +def test_say(capsys): + # call our initialization function instead of using a fixture + app = init_app() + # run our mixed in command + app.onecmd_plus_hooks('say hello') + # use the capsys fixture to retrieve the output on stdout and stderr + out, err = capsys.readouterr() + # make our assertions + assert out == 'in postparsing hook\nhello\n' + assert not err + + +def test_decorator(capsys): + # call our initialization function instead of using a fixture + app = init_app() + # run one command in the app + app.onecmd_plus_hooks('empty') + # use the capsys fixture to retrieve the output on stdout and stderr + out, err = capsys.readouterr() + # make our assertions + assert out == 'in postparsing hook\nin the empty decorator\nrunning the empty command\n' + assert not err |