diff --git a/.coveragerc b/.coveragerc index ad8b6730a..f74cd869d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,8 +1,8 @@ # .coveragerc to control coverage.py [run] # Source -source = plugins/*/cmd2_*/ - cmd2/ +source = cmd2/ + # (boolean, default False): whether to measure branch coverage in addition to statement coverage. branch = False diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b4b3ad8ef..0e480f9b3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -54,9 +54,6 @@ docs/* @tleonhardt examples/modular* @anselor examples/*.py @kmvanbrunt @tleonhardt -# Plugins -plugins/* @anselor - # Unit and Integration Tests tests/* @kmvanbrunt @tleonhardt @@ -76,5 +73,6 @@ MANIFEST.in @tleonhardt mkdocs.yml @tleonhardt package.json @tleonhardt pyproject.toml @tleonhardt @kmvanbrunt +ruff.toml @tleonhardt README.md @kmvanbrunt @tleonhardt tasks.py @tleonhardt diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index d9d46c479..3467d5828 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -51,7 +51,8 @@ We have a [Makefile](../Makefile) with commands that make it quick and easy for everything set up and perform common development tasks. Nearly all project configuration, including for dependencies and quality tools is in the -[pyproject.toml](../pyproject.toml) file. +[pyproject.toml](../pyproject.toml) file other than for `ruff` which is in +[ruff.toml](../ruff.toml). > _Updating to the latest releases for all prerequisites via `uv` is recommended_. This can be done > with `uv lock --upgrade` followed by `uv sync`. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ad54ce78f..1c003e9d5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,12 +9,12 @@ repos: - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.13.0" + rev: "v0.13.1" hooks: - id: ruff-format - args: [--config=pyproject.toml] + args: [--config=ruff.toml] - id: ruff-check - args: [--config=pyproject.toml, --fix, --exit-non-zero-on-fix] + args: [--config=ruff.toml, --fix, --exit-non-zero-on-fix] - repo: https://github.com/pre-commit/mirrors-prettier rev: "v3.1.0" diff --git a/MANIFEST.in b/MANIFEST.in index 5ff97f8b3..b9fb5f89c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include LICENSE README.md CHANGELOG.md mkdocs.yml pyproject.toml tasks.py +include LICENSE README.md CHANGELOG.md mkdocs.yml pyproject.toml ruff.toml tasks.py recursive-include examples * recursive-include tests * recursive-include docs * diff --git a/cmd2/plugin.py b/cmd2/plugin.py index 9243d232f..9f65824ae 100644 --- a/cmd2/plugin.py +++ b/cmd2/plugin.py @@ -1,4 +1,4 @@ -"""Classes for the cmd2 plugin system.""" +"""Classes for the cmd2 lifecycle hooks that you can register multiple callback functions/methods with.""" from dataclasses import ( dataclass, diff --git a/codecov.yml b/codecov.yml index f3bcec98b..0a775717e 100644 --- a/codecov.yml +++ b/codecov.yml @@ -5,10 +5,6 @@ component_management: name: cmd2 # this is a display name, and can be changed freely paths: - cmd2/** - - component_id: plugins - name: plugins - paths: - - plugins/** # Ignore certain paths, all files under these paths will be skipped during processing ignore: diff --git a/docs/index.md b/docs/index.md index 431ac2983..ccb9dfd99 100644 --- a/docs/index.md +++ b/docs/index.md @@ -53,10 +53,10 @@ app.cmdloop() end="" %} -## Plugins +## Mixins {% - include-markdown "./plugins/index.md" + include-markdown "./mixins/index.md" start="" end="" %} diff --git a/docs/mixins/index.md b/docs/mixins/index.md new file mode 100644 index 000000000..cac8de2b0 --- /dev/null +++ b/docs/mixins/index.md @@ -0,0 +1,7 @@ +# Mixins + + + +- [cmd2 Mixin Template](mixin_template.md) + + diff --git a/docs/mixins/mixin_template.md b/docs/mixins/mixin_template.md new file mode 100644 index 000000000..f51388cb6 --- /dev/null +++ b/docs/mixins/mixin_template.md @@ -0,0 +1,170 @@ +# cmd2 Mixin Template + +## Mixin Classes in General + +In Python, a mixin is a class designed to provide a specific set of functionalities to other classes +through multiple inheritance. Mixins are not intended to be instantiated on their own; rather, they +serve as a way to "mix in" or compose behaviors into a base class without creating a rigid "is-a" +relationship. + +For more information about Mixin Classes, we recommend this `Real Python` article on +[What Are Mixin Classes in Python?](https://realpython.com/python-mixin/). + +## Overview of cmd2 mixins + +If you have some set of re-usable behaviors that you wish to apply to multiple different `cmd2` +applications, then creating a mixin class to encapsulate this behavior can be a great idea. It is +one way to extend `cmd2` by relying on multiple inheritance. It is quick and easy, but there are +some potential pitfalls you should be aware of so you know how to do it correctly. + +The [mixins.py](https://github.com/python-cmd2/cmd2/blob/main/examples/mixins.py) example is a +general example that shows you how you can develop a mixin class for `cmd2` applicaitons. In the +past we have referred to these as "Plugins", but in retrospect that probably isn't the best name for +them. They are generally mixin classes that add some extra functionality to your class which +inherits from [cmd2.Cmd][]. + +## Using this template + +This file provides a very basic template for how you can create your own cmd2 Mixin class to +encapsulate re-usable behavior that can be applied to multiple `cmd2` applications via multiple +inheritance. + +## Naming + +If you decide to publish your Mixin as a Python package, you should consider prefixing the name of +your project with `cmd2-`. If you take this approach, then 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 mixin. A mixin is a class that +encapsulates and injects code into another class. Developers who use a mixin in their `cmd2` +project, will inject the mixin's code into their subclass of [cmd2.Cmd][]. + +### Mixin and Initialization + +The following short example shows how to create a mixin class and how everything gets initialized. + +Here's the mixin: + +```python +class MyMixin: + 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 mixin: + +```python +import cmd2 + + +class Example(MyMixin, cmd2.Cmd): + """A cmd2 application class to show how to use a mixin class.""" + + def __init__(self, *args, **kwargs): + # code placed here runs before cmd2.Cmd or + # any mixins initialize + super().__init__(*args, **kwargs) + # code placed here runs after cmd2.Cmd and + # all mixins have initialized +``` + +Note how the mixin 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 `MyMixin` first, the `MyMixin.__init__()` + method will never be called. +- You may want your mixin to be able to override methods from `cmd2.Cmd`. If you mixin the mixin + class after `cmd2.Cmd`, the python method resolution order will call `cmd2.Cmd` methods before it + calls those in your mixin. + +### Add commands + +Your mixin can add user visible commands. You do it the same way in a mixin that you would in a +`cmd2.Cmd` app: + +```python +class MyMixin: + + def do_say(self, statement): + """Simple say command""" + self.poutput(statement) +``` + +You have all the same capabilities within the mixin that you do inside a `cmd2.Cmd` app, including +argument parsing via decorators and custom help methods. + +### Add (or hide) settings + +A mixin may add user controllable settings to the application. Here's an example: + +```python +class MyMixin: + 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 mixin can provide a decorator which users of your mixin can use to wrap functionality around +their own commands. + +### Override methods + +Your mixin 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 mixins in their +application, and several of the mixins override the same method, only the first mixin to be mixed in +will have the overridden method called. + +Hooks are a much better approach. + +### Hooks + +Mixins can register hooks, which are called by `cmd2.Cmd` during various points in the application +and command processing lifecycle. Mixins should not override any of the legacy `cmd` 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 mixin. Hook methods get mixed +into the `cmd2` application and this naming convention helps avoid unintentional method overriding. + +Here's a simple example: + +```python +class MyMixin: + + 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_mymixin_postparsing_hook) + + def cmd2_mymixin_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 mixins (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 mixin 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 mixin will +know what's available. diff --git a/docs/plugins/index.md b/docs/plugins/index.md deleted file mode 100644 index d0ccbd766..000000000 --- a/docs/plugins/index.md +++ /dev/null @@ -1,7 +0,0 @@ -# Plugins - - - -- [cmd2 Plugin Template](plugin_template.md) - - diff --git a/docs/plugins/plugin_template.md b/docs/plugins/plugin_template.md deleted file mode 100644 index b3560c75a..000000000 --- a/docs/plugins/plugin_template.md +++ /dev/null @@ -1,7 +0,0 @@ -# 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/plugins/template/cmd2_myplugin/myplugin.py b/examples/mixin.py old mode 100644 new mode 100755 similarity index 58% rename from plugins/template/cmd2_myplugin/myplugin.py rename to examples/mixin.py index 37639a5c2..90b2ce56d --- a/plugins/template/cmd2_myplugin/myplugin.py +++ b/examples/mixin.py @@ -1,4 +1,5 @@ -"""An example cmd2 plugin.""" +#!/usr/bin/env python +"""An example cmd2 mixin.""" import functools from collections.abc import Callable @@ -13,7 +14,7 @@ def empty_decorator(func: Callable) -> Callable: - """An empty decorator for myplugin.""" + """An empty decorator for use with your mixin.""" @functools.wraps(func) def _empty_decorator(self, *args, **kwargs) -> None: @@ -24,15 +25,15 @@ def _empty_decorator(self, *args, **kwargs) -> None: return _empty_decorator -class MyPluginMixin(_Base): +class MyMixin(_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): + class MyApp(MyMixin, cmd2.Cmd): def __init__(self, *args, **kwargs): - # gotta have this or neither the plugin or cmd2 will initialize + # gotta have this or neither the mixin or cmd2 will initialize super().__init__(*args, **kwargs) """ @@ -41,9 +42,9 @@ def __init__(self, *args, **kwargs) -> None: 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) + self.register_preloop_hook(self.cmd2_mymixin_preloop_hook) + self.register_postloop_hook(self.cmd2_mymixin_postloop_hook) + self.register_postparsing_hook(self.cmd2_mymixin_postparsing_hook) def do_say(self, statement) -> None: """Simple say command.""" @@ -51,15 +52,32 @@ def do_say(self, statement) -> None: # # define hooks as functions, not methods - def cmd2_myplugin_preloop_hook(self) -> None: + def cmd2_mymixin_preloop_hook(self) -> None: """Method to be called before the command loop begins.""" self.poutput("preloop hook") - def cmd2_myplugin_postloop_hook(self) -> None: + def cmd2_mymixin_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: + def cmd2_mymixin_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 + + +class Example(MyMixin, cmd2.Cmd): + """An class to show how to use a mixin.""" + + def __init__(self, *args, **kwargs) -> None: + # gotta have this or neither the mixin or cmd2 will initialize + super().__init__(*args, **kwargs) + + @empty_decorator + def do_something(self, _arg) -> None: + self.poutput('this is the something command') + + +if __name__ == '__main__': + app = Example() + app.cmdloop() diff --git a/mkdocs.yml b/mkdocs.yml index c0f6208b3..5a4304166 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -193,9 +193,9 @@ nav: - examples/getting_started.md - examples/alternate_event_loops.md - examples/examples.md - - Plugins: - - plugins/index.md - - plugins/plugin_template.md + - Mixins: + - mixins/index.md + - mixins/mixin_template.md - Testing: - testing.md - API Reference: diff --git a/plugins/README.txt b/plugins/README.txt deleted file mode 100644 index 6bbf89f24..000000000 --- a/plugins/README.txt +++ /dev/null @@ -1 +0,0 @@ -For information about creating a cmd2 plugin, see template/README.md diff --git a/plugins/tasks.py b/plugins/tasks.py deleted file mode 100644 index d6924edbc..000000000 --- a/plugins/tasks.py +++ /dev/null @@ -1,139 +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 pathlib - -import invoke - -from plugins.template import ( - tasks as template_tasks, -) - -# create namespaces -namespace = invoke.Collection( - template=template_tasks, -) -namespace_clean = invoke.Collection('clean') -namespace.add_collection(namespace_clean, 'clean') - -##### -# -# pytest, pylint, and codecov -# -##### - -TASK_ROOT = pathlib.Path(__file__).resolve().parent -TASK_ROOT_STR = str(TASK_ROOT) - - -@invoke.task() -def pytest(_) -> None: - """Run tests and code coverage using pytest.""" - - -namespace.add_task(pytest) - - -@invoke.task() -def pytest_clean(_) -> None: - """Remove pytest cache and code coverage files and directories.""" - - -namespace_clean.add_task(pytest_clean, 'pytest') - - -@invoke.task() -def mypy(_) -> None: - """Run mypy optional static type checker.""" - - -namespace.add_task(mypy) - - -@invoke.task() -def mypy_clean(_) -> None: - """Remove mypy cache directory.""" - # pylint: disable=unused-argument - - -namespace_clean.add_task(mypy_clean, 'mypy') - - -##### -# -# build and distribute -# -##### -BUILDDIR = 'build' -DISTDIR = 'dist' - - -@invoke.task() -def build_clean(_) -> None: - """Remove the build directory.""" - - -namespace_clean.add_task(build_clean, 'build') - - -@invoke.task() -def dist_clean(_) -> None: - """Remove the dist directory.""" - - -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(_) -> None: - """Run all clean tasks.""" - # pylint: disable=unused-argument - - -namespace_clean.add_task(clean_all, 'all') - - -@invoke.task(pre=[clean_all]) -def sdist(_) -> None: - """Create a source distribution.""" - - -namespace.add_task(sdist) - - -@invoke.task(pre=[clean_all]) -def wheel(_) -> None: - """Build a wheel distribution.""" - - -namespace.add_task(wheel) - - -# ruff linter -@invoke.task() -def lint(context) -> None: - with context.cd(TASK_ROOT_STR): - context.run("ruff check") - - -namespace.add_task(lint) - - -# ruff formatter -@invoke.task() -def format(context) -> None: # noqa: A001 - """Run formatter.""" - with context.cd(TASK_ROOT_STR): - context.run("ruff format --check") - - -namespace.add_task(format) diff --git a/plugins/template/CHANGELOG.md b/plugins/template/CHANGELOG.md deleted file mode 100644 index 74009e6c4..000000000 --- a/plugins/template/CHANGELOG.md +++ /dev/null @@ -1,12 +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). - -## 1.0.0 (2018-07-24) - -### Added - -- Created plugin template and initial documentation diff --git a/plugins/template/LICENSE b/plugins/template/LICENSE deleted file mode 100644 index b1784d5d6..000000000 --- a/plugins/template/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 4ade388f9..000000000 --- a/plugins/template/README.md +++ /dev/null @@ -1,290 +0,0 @@ -# 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 visible 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 superfluous 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 -- [nox](https://nox.thea.codes/en/stable/) runs the unit tests on multiple versions of python -- [GitHub Actions](https://github.com/features/actions) runs the tests on the various supported - platforms - -This plugin template is set up to use the same strategy. - -### Create python environments - -This project uses [nox](https://nox.thea.codes/en/stable/) 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.14.0 -$ pyenv virtualenv -p python3.14 3.14.0 cmd2-3.14 -``` - -Now set pyenv to make both of those available at the same time: - -``` -$ pyenv local cmd2-3.14 -``` - -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 | -| ------------ | ------ | ---------- | -| `python3.14` | 3.14.0 | cmd2-3.14 | -| `pip3.14` | 3.14.0 | cmd2-3.14 | - -## 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.14` virtualenv. If you want to work in -other virtualenvs, you'll need to manually select it, and install again:: - -$ pyenv shell cmd2-3.14 $ 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 nox to run unit tests in multiple versions of python - -The included `noxfile.py` is setup to run the unit tests in 3.10, 3.11, 3.12, 3.13, and 3.14 You can -run your unit tests in all of these versions of python by: - -``` -$ nox -``` - -### Run unit tests on multiple platforms - -[GitHub Actions](https://github.com/features/actions) offers 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/main/LICENSE). We -invite plugin authors to consider doing the same. diff --git a/plugins/template/build-pyenvs.sh b/plugins/template/build-pyenvs.sh deleted file mode 100644 index 9ee27578b..000000000 --- a/plugins/template/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/template/cmd2_myplugin/__init__.py b/plugins/template/cmd2_myplugin/__init__.py deleted file mode 100644 index 3d4703d54..000000000 --- a/plugins/template/cmd2_myplugin/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Description of myplugin. - -An overview of what myplugin does. -""" - -import importlib.metadata as importlib_metadata - -from .myplugin import ( # noqa: F401 - MyPluginMixin, - empty_decorator, -) - -try: - __version__ = importlib_metadata.version(__name__) -except importlib_metadata.PackageNotFoundError: # pragma: no cover - # package is not installed - __version__ = 'unknown' diff --git a/plugins/template/cmd2_myplugin/pylintrc b/plugins/template/cmd2_myplugin/pylintrc deleted file mode 100644 index 2f6d3de24..000000000 --- a/plugins/template/cmd2_myplugin/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/template/examples/example.py b/plugins/template/examples/example.py deleted file mode 100644 index 055970b1e..000000000 --- a/plugins/template/examples/example.py +++ /dev/null @@ -1,20 +0,0 @@ -import cmd2_myplugin - -import cmd2 - - -class Example(cmd2_myplugin.MyPlugin, cmd2.Cmd): - """An class to show how to use a plugin.""" - - def __init__(self, *args, **kwargs) -> None: - # gotta have this or neither the plugin or cmd2 will initialize - super().__init__(*args, **kwargs) - - @cmd2_myplugin.empty_decorator - def do_something(self, _arg) -> None: - self.poutput('this is the something command') - - -if __name__ == '__main__': - app = Example() - app.cmdloop() diff --git a/plugins/template/noxfile.py b/plugins/template/noxfile.py deleted file mode 100644 index d37ed1384..000000000 --- a/plugins/template/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) -> None: - session.install('invoke', './[test]') - session.run('invoke', 'pytest', '--junit', '--no-pty') diff --git a/plugins/template/setup.py b/plugins/template/setup.py deleted file mode 100644 index cc4f63315..000000000 --- a/plugins/template/setup.py +++ /dev/null @@ -1,50 +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() - -setuptools.setup( - name='cmd2-myplugin', - # use_scm_version=True, # use_scm_version doesn't work if setup.py isn't in the repository root # noqa: ERA001 - version='2.0.0', - 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.10', - install_requires=['cmd2 >= 2, <3'], - 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.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/template/tasks.py b/plugins/template/tasks.py deleted file mode 100644 index 93a9c1a1c..000000000 --- a/plugins/template/tasks.py +++ /dev/null @@ -1,198 +0,0 @@ -"""Development related tasks to be run with 'invoke'.""" - -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) -> None: - """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) -> None: - """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) -> None: - """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) -> None: - """Check code quality using pylint.""" - context.run('pylint --rcfile=cmd2_myplugin/pylintrc cmd2_myplugin') - - -namespace.add_task(pylint) - - -@invoke.task -def pylint_tests(context) -> None: - """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) -> None: - """Remove the build directory.""" - # pylint: disable=unused-argument - rmrf(BUILDDIR) - - -namespace_clean.add_task(build_clean, 'build') - - -@invoke.task -def dist_clean(_context) -> None: - """Remove the dist directory.""" - # pylint: disable=unused-argument - rmrf(DISTDIR) - - -namespace_clean.add_task(dist_clean, 'dist') - - -@invoke.task -def eggs_clean(_context) -> None: - """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) -> None: - """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) -> None: - """Run all clean tasks.""" - # pylint: disable=unused-argument - - -namespace_clean.add_task(clean_all, 'all') - - -@invoke.task(pre=[clean_all]) -def sdist(context) -> None: - """Create a source distribution.""" - context.run('python -m build --sdist') - - -namespace.add_task(sdist) - - -@invoke.task(pre=[clean_all]) -def wheel(context) -> None: - """Build a wheel distribution.""" - context.run('python -m build --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/*') # noqa: ERA001 -# namespace.add_task(pypi) # noqa: ERA001 - -# @invoke.task(pre=[sdist, wheel]) -# def pypi_test(context): -# """Build and upload a distribution to https://test.pypi.org""" # noqa: ERA001 -# context.run('twine upload --repository-url https://test.pypi.org/legacy/ dist/*') # noqa: ERA001 -# namespace.add_task(pypi_test) # noqa: ERA001 diff --git a/plugins/template/tests/__init__.py b/plugins/template/tests/__init__.py deleted file mode 100644 index eb198dc04..000000000 --- a/plugins/template/tests/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# -# empty file to create a package diff --git a/plugins/template/tests/pylintrc b/plugins/template/tests/pylintrc deleted file mode 100644 index 1dd17c1c7..000000000 --- a/plugins/template/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/template/tests/test_myplugin.py b/plugins/template/tests/test_myplugin.py deleted file mode 100644 index 54e919f53..000000000 --- a/plugins/template/tests/test_myplugin.py +++ /dev/null @@ -1,69 +0,0 @@ -import cmd2_myplugin - -from cmd2 import ( - cmd2, -) - -###### -# -# 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) -> None: - super().__init__(*args, **kwargs) - - @cmd2_myplugin.empty_decorator - def do_empty(self, _args) -> None: - 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(): - return MyApp() - - -##### -# -# unit tests -# -##### - - -def test_say(capsys) -> None: - # 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) -> None: - # 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 diff --git a/pyproject.toml b/pyproject.toml index 7b7a1e58f..d0e857194 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,7 +87,6 @@ exclude = [ "^docs/", # docs directory "^dist/", "^examples/", # examples directory - "^plugins/*", # plugins directory "^noxfile\\.py$", # nox config file "setup\\.py$", # any files named setup.py "^site/", @@ -113,200 +112,6 @@ addopts = [ "--cov-report=html", ] -[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 -target-version = "py310" # Minimum supported version of Python -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://docs.astral.sh/ruff/rules - "A", # flake8-builtins (variables or arguments shadowing built-ins) - # "AIR", # Airflow specific warnings - "ANN", # flake8-annotations (missing type annotations for arguments or return types) - "ARG", # flake8-unused-arguments (functions or methods with arguments that are never used) - "ASYNC", # flake8-async (async await bugs) - "B", # flake8-bugbear (various likely bugs and design issues) - "BLE", # flake8-blind-except (force more specific exception types than just Exception) - "C4", # flake8-comprehensions (warn about things that could be written as a comprehensions but aren't) - "C90", # McCabe cyclomatic complexity (warn about functions that are too complex) - "COM", # flake8-commas (forces commas at the end of every type of iterable/container - # "CPY", # flake8-copyright (warn about missing copyright notice at top of file - currently in preview) - "D", # pydocstyle (warn about things like missing docstrings) - # "DOC", # pydoclint (docstring warnings - currently in preview) - # "DJ", # flake8-django (Django-specific warnings) - "DTZ", # flake8-datetimez (warn about datetime calls where no timezone is specified) - "E", # pycodestyle errors (warn about major stylistic issues like mixing spaces and tabs) - # "EM", # flake8-errmsg (warn about exceptions that use string literals that aren't assigned to a variable first) - "ERA", # eradicate (warn about commented-out code) - "EXE", # flake8-executable (warn about files with a shebang present that aren't executable or vice versa) - "F", # Pyflakes (a bunch of common warnings for things like unused imports, imports shadowed by variables, etc) - # "FA", # flake8-future-annotations (warn if certain from __future__ imports are used but missing) - # "FAST", # FastAPI specific warnings - # "FBT", # flake8-boolean-trap (force all boolean arguments passed to functions to be keyword arguments and not positional) - "FIX", # flake8-fixme (warn about lines containing FIXME, TODO, XXX, or HACK) - "FLY", # flynt (automatically convert from old school string .format to f-strings) - "FURB", # refurb (A tool for refurbishing and modernizing Python codebases) - "G", # flake8-logging-format (warn about logging statements using outdated string formatting methods) - "I", # isort (sort all import statements in the order established by isort) - "ICN", # flake8-import-conventions (force idiomatic import conventions for certain modules typically imported as something else) - "INP", # flake8-no-pep420 (warn about files in the implicit namespace - i.e. force creation of __init__.py files to make packages) - "INT", # flake8-gettext (warnings that only apply when you are internationalizing your strings) - "ISC", # flake8-implicit-str-concat (warnings related to implicit vs explicit string concatenation) - "LOG", # flake8-logging (warn about potential logger issues, but very pedantic) - "N", # pep8-naming (force idiomatic naming for classes, functions/methods, and variables/arguments) - # "NPY", # NumPy specific rules - # "PD", # pandas-vet (Pandas specific rules) - "PERF", # Perflint (warn about performance issues) - "PGH", # pygrep-hooks (force specific rule codes when ignoring type or linter issues on a line) - "PIE", # flake8-pie (eliminate unnecessary use of pass, range starting at 0, etc.) - "PLC", # Pylint Conventions - "PLE", # Pylint Errors - # "PLR", # Pylint Refactoring suggestions - "PLW", # Pylint Warnings - "PT", # flake8-pytest-style (warnings about unit test best practices) - # "PTH", # flake8-use-pathlib (force use of pathlib instead of os.path) - "PYI", # flake8-pyi (warnings related to type hint best practices) - "Q", # flake8-quotes (force double quotes) - "RET", # flake8-return (various warnings related to implicit vs explicit return statements) - "RSE", # flake8-raise (warn about unnecessary parentheses on raised exceptions) - "RUF", # Ruff-specific rules (miscellaneous grab bag of lint checks specific to Ruff) - "S", # flake8-bandit (security oriented checks, but extremely pedantic - do not attempt to apply to unit test files) - "SIM", # flake8-simplify (rules to attempt to simplify code) - # "SLF", # flake8-self (warn when protected members are accessed outside of a class or file) - "SLOT", # flake8-slots (warn about subclasses that should define __slots__) - "T10", # flake8-debugger (check for pdb traces left in Python code) - # "T20", # flake8-print (warn about use of `print` or `pprint` - force use of loggers) - "TC", # flake8-type-checking (type checking warnings) - "TD", # flake8-todos (force all TODOs to include an author and issue link) - "TID", # flake8-tidy-imports (extra import rules to check) - "TRY", # tryceratops (warnings related to exceptions and try/except) - "UP", # pyupgrade (A tool (and pre-commit hook) to automatically upgrade syntax for newer versions of the language) - "W", # pycodestyle warnings (warn about minor stylistic issues) - "YTT", # flake8-2020 (checks for misuse of sys.version or sys.version_info) -] -ignore = [ - # `uv run ruff rule E501` for a description of that rule - "ANN401", # Dynamically typed expressions (typing.Any) are disallowed (would be good to enable this later) - "COM812", # Conflicts with ruff format (see https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules) - "COM819", # Conflicts with ruff format - "D203", # 1 blank line required before class docstring (conflicts with D211) - "D206", # Conflicts with ruff format - "D213", # Multi-line docstring summary should start at 2nd line (conflicts with D212 which starts at 1st line) - "D300", # Conflicts with ruff format - "E111", # Conflicts with ruff format - "E114", # Conflicts with ruff format - "E117", # Conflicts with ruff format - "ISC002", # Conflicts with ruff format - "PLC0415", # `import` should be at the top-level of a file" - "Q000", # Conflicts with ruff format - "Q001", # Conflicts with ruff format - "Q002", # Conflicts with ruff format - "Q003", # Conflicts with ruff format - "TC006", # Add quotes to type expression in typing.cast() (not needed except for forward references) - "TRY003", # Avoid specifying long messages outside the exception class (force custom exceptions for everything) - "UP017", # Use datetime.UTC alias (requires Python 3.11+) - "W191", # Conflicts with ruff format -] - -# 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 - -[tool.ruff.lint.per-file-ignores] -# Do not call setattr with constant attribute value -"cmd2/argparse_custom.py" = ["B010"] - -# Ignore various varnings in examples/ directory -"examples/*.py" = [ - "ANN", # Ignore all type annotation rules in examples folder - "D", # Ignore all pydocstyle rules in examples folder - "INP001", # Module is part of an implicit namespace - "PLW2901", # loop variable overwritten inside loop - "S", # Ignore all Security rules in examples folder -] -"examples/scripts/*.py" = ["F821"] # Undefined name `app` -"plugins/*.py" = ["INP001"] # Module is part of an implicit namespace - -# Ingore various rulesets in test and plugins directories -"{plugins,tests}/*.py" = [ - "ANN", # Ignore all type annotation rules in test folders - "ARG", # Ignore all unused argument warnings in test folders - "D", # Ignore all pydocstyle rules in test folders - "E501", # Line too long - "S", # Ignore all Security rules in test folders - "SLF", # Ignore all warnings about private or protected member access in test folders -] -# Undefined name `app` and module is part of an implicit namespace -"tests/pyscript/*.py" = ["F821", "INP001"] - -[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" - [tool.setuptools] packages = ["cmd2"] diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 000000000..80995579b --- /dev/null +++ b/ruff.toml @@ -0,0 +1,191 @@ +# 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 +target-version = "py310" # Minimum supported version of Python +output-format = "full" + +[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://docs.astral.sh/ruff/rules + "A", # flake8-builtins (variables or arguments shadowing built-ins) + # "AIR", # Airflow specific warnings + "ANN", # flake8-annotations (missing type annotations for arguments or return types) + "ARG", # flake8-unused-arguments (functions or methods with arguments that are never used) + "ASYNC", # flake8-async (async await bugs) + "B", # flake8-bugbear (various likely bugs and design issues) + "BLE", # flake8-blind-except (force more specific exception types than just Exception) + "C4", # flake8-comprehensions (warn about things that could be written as a comprehensions but aren't) + "C90", # McCabe cyclomatic complexity (warn about functions that are too complex) + "COM", # flake8-commas (forces commas at the end of every type of iterable/container + # "CPY", # flake8-copyright (warn about missing copyright notice at top of file - currently in preview) + "D", # pydocstyle (warn about things like missing docstrings) + # "DOC", # pydoclint (docstring warnings - currently in preview) + # "DJ", # flake8-django (Django-specific warnings) + "DTZ", # flake8-datetimez (warn about datetime calls where no timezone is specified) + "E", # pycodestyle errors (warn about major stylistic issues like mixing spaces and tabs) + # "EM", # flake8-errmsg (warn about exceptions that use string literals that aren't assigned to a variable first) + "ERA", # eradicate (warn about commented-out code) + "EXE", # flake8-executable (warn about files with a shebang present that aren't executable or vice versa) + "F", # Pyflakes (a bunch of common warnings for things like unused imports, imports shadowed by variables, etc) + # "FA", # flake8-future-annotations (warn if certain from __future__ imports are used but missing) + # "FAST", # FastAPI specific warnings + # "FBT", # flake8-boolean-trap (force all boolean arguments passed to functions to be keyword arguments and not positional) + "FIX", # flake8-fixme (warn about lines containing FIXME, TODO, XXX, or HACK) + "FLY", # flynt (automatically convert from old school string .format to f-strings) + "FURB", # refurb (A tool for refurbishing and modernizing Python codebases) + "G", # flake8-logging-format (warn about logging statements using outdated string formatting methods) + "I", # isort (sort all import statements in the order established by isort) + "ICN", # flake8-import-conventions (force idiomatic import conventions for certain modules typically imported as something else) + "INP", # flake8-no-pep420 (warn about files in the implicit namespace - i.e. force creation of __init__.py files to make packages) + "INT", # flake8-gettext (warnings that only apply when you are internationalizing your strings) + "ISC", # flake8-implicit-str-concat (warnings related to implicit vs explicit string concatenation) + "LOG", # flake8-logging (warn about potential logger issues, but very pedantic) + "N", # pep8-naming (force idiomatic naming for classes, functions/methods, and variables/arguments) + # "NPY", # NumPy specific rules + # "PD", # pandas-vet (Pandas specific rules) + "PERF", # Perflint (warn about performance issues) + "PGH", # pygrep-hooks (force specific rule codes when ignoring type or linter issues on a line) + "PIE", # flake8-pie (eliminate unnecessary use of pass, range starting at 0, etc.) + "PLC", # Pylint Conventions + "PLE", # Pylint Errors + # "PLR", # Pylint Refactoring suggestions + "PLW", # Pylint Warnings + "PT", # flake8-pytest-style (warnings about unit test best practices) + # "PTH", # flake8-use-pathlib (force use of pathlib instead of os.path) + "PYI", # flake8-pyi (warnings related to type hint best practices) + "Q", # flake8-quotes (force double quotes) + "RET", # flake8-return (various warnings related to implicit vs explicit return statements) + "RSE", # flake8-raise (warn about unnecessary parentheses on raised exceptions) + "RUF", # Ruff-specific rules (miscellaneous grab bag of lint checks specific to Ruff) + "S", # flake8-bandit (security oriented checks, but extremely pedantic - do not attempt to apply to unit test files) + "SIM", # flake8-simplify (rules to attempt to simplify code) + # "SLF", # flake8-self (warn when protected members are accessed outside of a class or file) + "SLOT", # flake8-slots (warn about subclasses that should define __slots__) + "T10", # flake8-debugger (check for pdb traces left in Python code) + # "T20", # flake8-print (warn about use of `print` or `pprint` - force use of loggers) + "TC", # flake8-type-checking (type checking warnings) + "TD", # flake8-todos (force all TODOs to include an author and issue link) + "TID", # flake8-tidy-imports (extra import rules to check) + "TRY", # tryceratops (warnings related to exceptions and try/except) + "UP", # pyupgrade (A tool (and pre-commit hook) to automatically upgrade syntax for newer versions of the language) + "W", # pycodestyle warnings (warn about minor stylistic issues) + "YTT", # flake8-2020 (checks for misuse of sys.version or sys.version_info) +] +ignore = [ + # `uv run ruff rule E501` for a description of that rule + "ANN401", # Dynamically typed expressions (typing.Any) are disallowed (would be good to enable this later) + "COM812", # Conflicts with ruff format (see https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules) + "COM819", # Conflicts with ruff format + "D203", # 1 blank line required before class docstring (conflicts with D211) + "D206", # Conflicts with ruff format + "D213", # Multi-line docstring summary should start at 2nd line (conflicts with D212 which starts at 1st line) + "D300", # Conflicts with ruff format + "E111", # Conflicts with ruff format + "E114", # Conflicts with ruff format + "E117", # Conflicts with ruff format + "ISC002", # Conflicts with ruff format + "PLC0415", # `import` should be at the top-level of a file" + "Q000", # Conflicts with ruff format + "Q001", # Conflicts with ruff format + "Q002", # Conflicts with ruff format + "Q003", # Conflicts with ruff format + "TC006", # Add quotes to type expression in typing.cast() (not needed except for forward references) + "TRY003", # Avoid specifying long messages outside the exception class (force custom exceptions for everything) + "UP017", # Use datetime.UTC alias (requires Python 3.11+) + "W191", # Conflicts with ruff format +] + +# 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 + +[lint.per-file-ignores] +# Do not call setattr with constant attribute value +"cmd2/argparse_custom.py" = ["B010"] + +# Ignore various warnings in examples/ directory +"examples/*.py" = [ + "ANN", # Ignore all type annotation rules in examples folder + "D", # Ignore all pydocstyle rules in examples folder + "INP001", # Module is part of an implicit namespace + "PLW2901", # loop variable overwritten inside loop + "S", # Ignore all Security rules in examples folder +] +"examples/scripts/*.py" = ["F821"] # Undefined name `app` + +# Ingore various rulesets in test directories +"{tests}/*.py" = [ + "ANN", # Ignore all type annotation rules in test folders + "ARG", # Ignore all unused argument warnings in test folders + "D", # Ignore all pydocstyle rules in test folders + "E501", # Line too long + "S", # Ignore all Security rules in test folders + "SLF", # Ignore all warnings about private or protected member access in test folders +] +# Undefined name `app` and module is part of an implicit namespace +"tests/pyscript/*.py" = ["F821", "INP001"] + +[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/tasks.py b/tasks.py index a74051cd1..1399b6336 100644 --- a/tasks.py +++ b/tasks.py @@ -15,7 +15,6 @@ import invoke from invoke.context import Context - from plugins import ( tasks as plugin_tasks, )