-
Notifications
You must be signed in to change notification settings - Fork 122
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve docs: introduce roles and add another example #136
Changes from 14 commits
fcdc245
26f8a94
2f6831b
0381424
5b77f41
e51039d
a58b8b0
2b39339
6283b3b
ffe2caa
c2286a9
d2bacee
115f1fc
653a1e9
e84fdfa
a20a667
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
:orphan: | ||
|
||
Api Reference | ||
============= | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import eggsample | ||
|
||
@eggsample.hookimpl | ||
def eggsample_add_ingredients(ingredients): | ||
if len([e for e in ingredients if e == "egg"]) > 2: | ||
ingredients.remove("egg") | ||
spam = ["lovely spam", "wonderous spam", "splendiferous spam"] | ||
print(f"Add {spam}") | ||
return spam | ||
|
||
@eggsample.hookimpl | ||
def eggsample_prep_condiments(condiments): | ||
try: | ||
del condiments["steak sauce"] | ||
except KeyError: | ||
pass |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
from setuptools import setup | ||
|
||
setup(name="eggsample-spam", install_requires="eggsample", | ||
entry_points={'eggsample': ['spam = eggsample_spam']}, | ||
py_modules=['eggsample_spam']) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import pluggy | ||
|
||
hookimpl = pluggy.HookimplMarker("eggsample") | ||
"""Marker to be imported and used in plugins (and for own implementations)""" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import pluggy | ||
|
||
hookspec = pluggy.HookspecMarker("eggsample") | ||
|
||
@hookspec | ||
def eggsample_add_ingredients(ingredients): | ||
"""Change the used ingredients. | ||
|
||
:param list ingredients: the ingredients to be modified | ||
""" | ||
|
||
@hookspec | ||
def eggsample_prep_condiments(condiments): | ||
"""Reorganize the condiments tray. | ||
|
||
:param dict condiments: some sauces and stuff | ||
""" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import random | ||
|
||
import pluggy | ||
|
||
from eggsample import hookspecs, lib | ||
|
||
condiments_tray = {"pickled walnuts": 13, "steak sauce": 4, "mushy peas": 2} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Man, gotta try me some pickled walnuts. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is a Wikipedia approved british condiment, so it must be good! |
||
|
||
class EggsellentCook: | ||
def __init__(self, hook): | ||
self.hook = hook | ||
self.ingredients = [] | ||
|
||
def add_ingredients(self): | ||
more_ingredients = self.hook.eggsample_add_ingredients( | ||
ingredients=self.ingredients) | ||
# each hook implementation returned a list of ingredients | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @obestwalter hmm so The easy way to fix this might be to do: for ingredients in self.hook.eggsample_add_ingredients(ingredients=self.ingredients):
self.ingredients.extend(ingredients) |
||
self.ingredients.extend(more_ingredients) | ||
|
||
def prepare_the_food(self): | ||
random.shuffle(self.ingredients) | ||
|
||
def serve_the_food(self): | ||
self.hook.eggsample_prep_condiments(condiments=condiments_tray) | ||
print(f"Your food: {', '.join(self.ingredients)}") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will now join a bunch of |
||
print(f"Some condiments: {', '.join(condiments_tray.keys())}") | ||
|
||
def main(): | ||
pluginmanager = pluggy.PluginManager("eggsample") | ||
pluginmanager.add_hookspecs(hookspecs) | ||
pluginmanager.load_setuptools_entrypoints("eggsample") | ||
pluginmanager.register(lib) | ||
cook = EggsellentCook(pluginmanager.hook) | ||
cook.add_ingredients() | ||
cook.prepare_the_food() | ||
cook.serve_the_food() | ||
|
||
if __name__ == '__main__': | ||
main() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import eggsample | ||
|
||
@eggsample.hookimpl | ||
def eggsample_add_ingredients(ingredients): | ||
basics = ["egg", "egg", "salt", "pepper"] | ||
print(f"Add {basics}") | ||
return basics | ||
|
||
@eggsample.hookimpl | ||
def eggsample_prep_condiments(condiments): | ||
condiments["mint sauce"] = 1 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
from setuptools import setup, find_packages | ||
|
||
setup(name="eggsample", install_requires="pluggy>=0.3,<1.0", | ||
entry_points={'console_scripts': ['eggsample=eggsample.host:main']}, | ||
packages=find_packages()) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,63 +1,176 @@ | ||
``pluggy`` | ||
========== | ||
**The pytest plugin system** | ||
|
||
The ``pytest`` plugin system | ||
**************************** | ||
What is it? | ||
*********** | ||
``pluggy`` is the crystallized core of `plugin management and hook | ||
calling`_ for `pytest`_. | ||
calling`_ for `pytest`_. It enables `200+ plugins`_ to extend and customize | ||
``pytest``'s default behaviour. Even ``pytest`` itself is composed | ||
as a set of ``pluggy`` plugins which are invoked in sequence according to a | ||
well defined set of protocols. | ||
|
||
It gives users the ability to extend or modify the behaviour of a | ||
``host program`` by installing a ``plugin`` for that program. | ||
The plugin code will run as part of normal program execution, changing or | ||
enhancing certain aspects of it. | ||
|
||
In essence, ``pluggy`` enables function `hooking`_ so you can build | ||
"pluggable" systems. | ||
|
||
Why is it useful? | ||
***************** | ||
There are some established mechanisms for modifying the behavior of other | ||
programs/libraries in Python like | ||
`method overriding <https://en.wikipedia.org/wiki/Method_overriding>`_ | ||
(e.g. Jinja2) or | ||
`monkey patching <https://en.wikipedia.org/wiki/Monkey_patch>`_ (e.g. gevent | ||
or for `writing tests <https://docs.pytest.org/en/latest/monkeypatch.html>`_). | ||
These strategies become problematic though when several parties want to | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bravo on this description; well done! |
||
participate in the modification of the same program. Therefore ``pluggy`` | ||
does not rely on these mechanisms to enable a more structured approach and | ||
avoid unnecessary exposure of state and behaviour. This leads to a more | ||
`loosely coupled <https://en.wikipedia.org/wiki/Loose_coupling>`_ relationship | ||
between ``host`` and ``plugins``. | ||
|
||
The ``pluggy`` approach puts the burden on the designer of the | ||
``host program`` to think carefully about which objects are really | ||
needed in a hook implementation, which gives `plugin` creators a clear | ||
framework for how to extend the ``host`` via a well defined set of functions | ||
and objects to work with. | ||
|
||
How does it work? | ||
***************** | ||
Let us start with a short overview of what is involved: | ||
|
||
* ``host`` or ``host program``: the program offering extensibility | ||
by specifying ``hook functions`` and invoking their implementation(s) as | ||
part of program execution | ||
* ``plugin``: the program implementing (a subset of) the specified hooks and | ||
participating in program execution when the implementations are invoked | ||
by the ``host`` | ||
* ``pluggy``: connects ``host`` and ``plugins`` by using ... | ||
|
||
- the hook :ref:`specifications <specs>` defining call signatures | ||
provided by the ``host`` (a.k.a ``hookspecs`` - see :ref:`marking_hooks`) | ||
- the hook :ref:`implementations <impls>` provided by registered | ||
``plugins`` (a.k.a ``hookimpl`` - see `callbacks`_) | ||
- the hook :ref:`caller <calling>` - a call loop triggered at appropriate | ||
program positions in the ``host`` invoking the implementations and | ||
collecting the results | ||
|
||
... where for each registered hook *specification*, a hook *call* will | ||
invoke up to ``N`` registered hook *implementations*. | ||
* ``user``: the person who installed the ``host program`` and wants to | ||
extend its functionality with ``plugins``. In the simplest case they install | ||
the ``plugin`` in the same environment as the ``host`` and the magic will | ||
happen when the ``host program`` is run the next time. Depending on | ||
the ``plugin``, there might be other things they need to do. For example, | ||
they might have to call the host with an additional commandline parameter | ||
to the host that the ``plugin`` added. | ||
|
||
A toy example | ||
------------- | ||
Let us demonstrate the core functionality in one module and show how you can | ||
start experimenting with pluggy functionality. | ||
|
||
In fact, ``pytest`` is itself composed as a set of ``pluggy`` plugins | ||
which are invoked in sequence according to a well defined set of protocols. | ||
Some `200+ plugins`_ use ``pluggy`` to extend and customize ``pytest``'s default behaviour. | ||
.. literalinclude:: examples/toy-example.py | ||
|
||
In essence, ``pluggy`` enables function `hooking`_ so you can build "pluggable" systems. | ||
Running this directly gets us:: | ||
|
||
How's it work? | ||
-------------- | ||
A `plugin` is a `namespace`_ which defines hook functions. | ||
$ python docs/examples/toy-example.py | ||
|
||
``pluggy`` manages *plugins* by relying on: | ||
inside Plugin_2.myhook() | ||
inside Plugin_1.myhook() | ||
[-1, 3] | ||
|
||
- a hook *specification* - defines a call signature | ||
- a set of hook *implementations* - aka `callbacks`_ | ||
- the hook *caller* - a call loop which collects results | ||
A complete example | ||
------------------ | ||
Now let us demonstrate how this plays together in a vaguely real world scenario. | ||
|
||
where for each registered hook *specification*, a hook *call* will invoke up to ``N`` | ||
registered hook *implementations*. | ||
Let's assume our ``host program`` is called **eggsample** where some eggs will | ||
be prepared and served with a tray containing condiments. As everybody knows: | ||
the more cooks are involved the better the food, so let us make the process | ||
pluggable and write a plugin that improves the meal with some spam and removes | ||
the steak sauce from the condiments tray (nobody likes that anyway). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lolz definitely not on eggs. |
||
|
||
``pluggy`` accomplishes all this by implementing a `request-response pattern`_ using *function* | ||
subscriptions and can be thought of and used as a rudimentary busless `publish-subscribe`_ | ||
event system. | ||
.. note:: | ||
|
||
``pluggy``'s approach is meant to let a designer think carefully about which objects are | ||
explicitly needed by an extension writer. This is in contrast to subclass-based extension | ||
systems which may expose unnecessary state and behaviour or encourage `tight coupling`_ | ||
in overlying frameworks. | ||
**naming markers**: ``HookSpecMarker`` and ``HookImplMarker`` must be | ||
initialized with the name of the ``host`` project (the ``name`` | ||
parameter in ``setup()``) - so **eggsample** in our case. | ||
|
||
**naming plugin projects**: they should be named in the form of | ||
``<host>-<plugin>`` (e.g. ``pytest-xdist``), therefore we call our | ||
plugin *eggsample-spam*. | ||
|
||
A first example | ||
--------------- | ||
The host | ||
^^^^^^^^ | ||
``eggsample/eggsample/__init__.py`` | ||
|
||
.. literalinclude:: examples/firstexample.py | ||
.. literalinclude:: examples/eggsample/eggsample/__init__.py | ||
|
||
Running this directly gets us:: | ||
``eggsample/eggsample/hookspecs.py`` | ||
|
||
$ python docs/examples/firstexample.py | ||
.. literalinclude:: examples/eggsample/eggsample/hookspecs.py | ||
|
||
inside Plugin_2.myhook() | ||
inside Plugin_1.myhook() | ||
[-1, 3] | ||
``eggsample/eggsample/lib.py`` | ||
|
||
.. literalinclude:: examples/eggsample/eggsample/lib.py | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I personally like including line numbers with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah - yes There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added it ... and removed it again. Looks absoulutely horrific due to sphinx-doc/sphinx#2427 and does not really add too much utility IMO. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Meh, not that big of a deal. Yeah just avoid the headache. |
||
|
||
``eggsample/eggsample/host.py`` | ||
|
||
.. literalinclude:: examples/eggsample/eggsample/host.py | ||
|
||
Let's get cooking - we install the host and see what a program run looks like:: | ||
|
||
$ pip install -e pluggy/docs/examples/eggsample | ||
$ eggsample | ||
|
||
Add ['egg', 'egg', 'salt', 'pepper'] | ||
Your food: egg, salt, pepper, egg | ||
Some condiments: pickled walnuts, steak sauce, mushy peas, mint sauce | ||
|
||
The plugin | ||
^^^^^^^^^^ | ||
``eggsample-spam/eggsample_spam.py`` | ||
|
||
.. literalinclude:: examples/eggsample-spam/eggsample_spam.py | ||
|
||
``eggsample-spam/setup.py`` | ||
|
||
.. literalinclude:: examples/eggsample-spam/setup.py | ||
|
||
Let's get cooking with more cooks - we install the plugin and and see what | ||
we get:: | ||
|
||
$ pip install -e pluggy/docs/examples/eggsample-spam | ||
$ eggsample | ||
|
||
Add ['egg', 'egg', 'salt', 'pepper'] | ||
add ['lovely spam', 'wonderous spam', 'splendiferous spam'] | ||
Your food: egg, lovely spam, egg, pepper, salt, wonderous spam, splendiferous spam | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah just double check this with a test to make sure either I'm wrong or this needs to be fixed. |
||
Some condiments: pickled walnuts, mushy peas, mint sauce | ||
|
||
More real world examples | ||
------------------------ | ||
To see how ``pluggy`` is used in the real world, have a look at these projects | ||
documentation and source code: | ||
|
||
* `pytest <https://docs.pytest.org/en/latest/writing_plugins.html>`__ | ||
* `tox <https://tox.readthedocs.io/en/latest/plugins.html>`__ | ||
* `devpi <https://devpi.net/docs/devpi/devpi/stable/+d/devguide/index.html>`__ | ||
|
||
For more details and advanced usage please read on. | ||
|
||
.. _define: | ||
|
||
Defining and Collecting Hooks | ||
***************************** | ||
Define and collect hooks | ||
************************ | ||
A *plugin* is a namespace type (currently one of a ``class`` or module) | ||
which defines a set of *hook* functions. | ||
|
||
As mentioned in :ref:`manage`, all *plugins* which define *hooks* | ||
As mentioned in :ref:`manage`, all *plugins* which specify *hooks* | ||
are managed by an instance of a :py:class:`pluggy.PluginManager` which | ||
defines the primary ``pluggy`` API. | ||
|
||
|
@@ -404,7 +517,7 @@ For more info see :ref:`call_historic`. | |
|
||
.. _manage: | ||
|
||
The Plugin Registry | ||
The Plugin registry | ||
******************* | ||
``pluggy`` manages plugins using instances of the | ||
:py:class:`pluggy.PluginManager`. | ||
|
@@ -487,7 +600,7 @@ You can retrieve the *options* applied to a particular | |
|
||
.. _calling: | ||
|
||
Calling Hooks | ||
Calling hooks | ||
************* | ||
The core functionality of ``pluggy`` enables an extension provider | ||
to override function calls made at certain points throughout a program. | ||
|
@@ -674,7 +787,7 @@ in your project you should thus use a dependency restriction like | |
|
||
.. hyperlinks | ||
.. _pytest: | ||
http://pytest.org | ||
https://pytest.org | ||
.. _request-response pattern: | ||
https://en.wikipedia.org/wiki/Request%E2%80%93response | ||
.. _publish-subscribe: | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
condiments.pop('steak sauce', None)
is maybe a little more succinct?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You think so? I'd say you use
del
if you want to get rid of something andlist.pop
if you want to do something with the value. Here the steak sauce is just removed from the tray to vanish into thin air, so I'd usedel
here.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's
dict.pop
but sure. I don't mind either way too much.