Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -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

Expand Down
4 changes: 1 addition & 3 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,6 @@ docs/* @tleonhardt
examples/modular* @anselor
examples/*.py @kmvanbrunt @tleonhardt

# Plugins
plugins/* @anselor

# Unit and Integration Tests
tests/* @kmvanbrunt @tleonhardt

Expand All @@ -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
3 changes: 2 additions & 1 deletion .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -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 *
Expand Down
2 changes: 1 addition & 1 deletion cmd2/plugin.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
4 changes: 0 additions & 4 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ app.cmdloop()
end="<!--intro-end-->"
%}

## Plugins
## Mixins

{%
include-markdown "./plugins/index.md"
include-markdown "./mixins/index.md"
start="<!--intro-start-->"
end="<!--intro-end-->"
%}
Expand Down
7 changes: 7 additions & 0 deletions docs/mixins/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Mixins

<!--intro-start-->

- [cmd2 Mixin Template](mixin_template.md)

<!--intro-end-->
170 changes: 170 additions & 0 deletions docs/mixins/mixin_template.md
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 0 additions & 7 deletions docs/plugins/index.md

This file was deleted.

7 changes: 0 additions & 7 deletions docs/plugins/plugin_template.md

This file was deleted.

40 changes: 29 additions & 11 deletions plugins/template/cmd2_myplugin/myplugin.py → examples/mixin.py
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""An example cmd2 plugin."""
#!/usr/bin/env python
"""An example cmd2 mixin."""

import functools
from collections.abc import Callable
Expand All @@ -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:
Expand All @@ -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)
"""

Expand All @@ -41,25 +42,42 @@ 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."""
self.poutput(statement)

#
# 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()
6 changes: 3 additions & 3 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 0 additions & 1 deletion plugins/README.txt

This file was deleted.

Loading
Loading