mirror of
https://github.com/Limnoria/Limnoria-doc.git
synced 2025-04-06 23:39:46 +00:00
plugin_tutorial: rewrite / condense plugin.py section
- Add additional inline links to related doc pages - Mention additional common uses of irc.reply and irc.error
This commit is contained in:
@ -28,6 +28,8 @@ When a plugin is unloaded (or is to be reloaded), the ``die``
|
|||||||
method is called (with no parameter).
|
method is called (with no parameter).
|
||||||
Also make sure you always call the parent's ``die``.
|
Also make sure you always call the parent's ``die``.
|
||||||
|
|
||||||
|
.. _do-method-handlers:
|
||||||
|
|
||||||
Commands and numerics
|
Commands and numerics
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
|
@ -130,40 +130,30 @@ in the :ref:`Advanced Plugin Config Tutorial <configuration-tutorial>`.
|
|||||||
|
|
||||||
plugin.py
|
plugin.py
|
||||||
=========
|
=========
|
||||||
Here's the moment you've been waiting for, the overview of plugin.py and how to
|
``plugin.py`` includes the core code for the plugin. For most plugins this will
|
||||||
make our plugin actually do stuff.
|
include command handlers, as well as anything else that's relevant to its
|
||||||
|
particular use case (database queries,
|
||||||
|
:ref:`HTTP server endpoints <http_plugins>`,
|
||||||
|
:ref:`IRC command triggers <do-method-handlers>`, etc.)
|
||||||
|
|
||||||
At the top, same as always, is the standard copyright block to be used and
|
As with any Python module, you'll need to import any dependencies you want,
|
||||||
abused at your leisure.
|
in addition to the standard ``supybot`` imports included in the plugin
|
||||||
|
template::
|
||||||
Next, some standard imports. Not all of them are used at the moment, but you
|
|
||||||
probably will use many (if not most) of them, so just let them be. Since
|
|
||||||
we'll be making use of Python's standard 'random' module, you'll need to add
|
|
||||||
the following line to the list of imports::
|
|
||||||
|
|
||||||
import random
|
import random
|
||||||
|
|
||||||
Now, the plugin class itself. What you're given is a skeleton: a simple
|
The bulk of the plugin definition then resides in a subclass of
|
||||||
subclass of :class:`callbacks.Plugin` for you to start with. The only real content it
|
:class:`callbacks.Plugin`. By convention, the class name is equal to the name of
|
||||||
has is the boilerplate docstring, which you should modify to reflect what the
|
the plugin, though this is not strictly required (the actual linkage is done by
|
||||||
boilerplate text says - it should be useful so that when someone uses the
|
the ``Class = Random`` statement at the end of the file). It is helpful to fill
|
||||||
plugin help command to determine how to use this plugin, they'll know what they
|
in the plugin docstring with some more details on how to actually use the plugin
|
||||||
need to do. Ours will read something like::
|
too: this info can be shown on a live bot using the
|
||||||
|
``plugin help <plugin name>`` command.
|
||||||
|
|
||||||
"""This plugin provides a few random number commands and some
|
::
|
||||||
commands for getting random samples. Use the "seed" command to seed
|
|
||||||
the plugin's random number generator if you like, though it is
|
|
||||||
unnecessary as it gets seeded upon loading of the plugin. The
|
|
||||||
"random" command is most likely what you're looking for, though
|
|
||||||
there are a number of other useful commands in this plugin. Use
|
|
||||||
'list random' to check them out. """
|
|
||||||
|
|
||||||
It's basically a "guide to getting started" for the plugin. Now, to make the
|
class Random(callbacks.Plugin):
|
||||||
plugin do something. First of all, to get any random numbers we're going to
|
"""This plugin contains commands relating to random numbers, including random sampling from a list and a simple dice roller."""
|
||||||
need a random number generator (RNG). Pretty much everything in our plugin is
|
|
||||||
going to use it, so we'll define it in the constructor of our plugin, __init__.
|
|
||||||
Here we'll also seed it with the current time (standard practice for RNGs).
|
|
||||||
Here's what our __init__ looks like::
|
|
||||||
|
|
||||||
def __init__(self, irc):
|
def __init__(self, irc):
|
||||||
# Make sure to call the superclass' constructor when you define a custom one
|
# Make sure to call the superclass' constructor when you define a custom one
|
||||||
@ -171,85 +161,71 @@ Here's what our __init__ looks like::
|
|||||||
self.rng = random.Random() # create our rng
|
self.rng = random.Random() # create our rng
|
||||||
self.rng.seed() # automatically seeds with current time
|
self.rng.seed() # automatically seeds with current time
|
||||||
|
|
||||||
Make sure you add it with one indentation level more than the ``class`` line
|
For this sample plugin, we define a custom constructor (``__init__``) that
|
||||||
(ie. with four spaces before the ``def``).
|
instantiates a random number generator instance and pre-seeds it. This isn't
|
||||||
|
technically necessary for Python's ``random`` module, but this helps outline
|
||||||
|
how to write a similar constructor. Notice in particular how you must pass in
|
||||||
|
the ``irc`` argument in addition to ``self``.
|
||||||
|
|
||||||
Now, the first two lines may look a little daunting, but it's just
|
.. note::
|
||||||
administrative stuff required if you want to use a custom ``__init__``. If we
|
TODO(jlu): semantically, what does ``irc`` refer to? Most plugins don't
|
||||||
didn't want to do so, we wouldn't have to, but it's not uncommon so I decided
|
actually reference it on load time.
|
||||||
to use an example plugin that did. For the most part you can just copy/paste
|
|
||||||
those lines into any plugin you override the ``__init__`` for and just change them
|
|
||||||
to use the plugin name that you are working on instead.
|
|
||||||
|
|
||||||
So, now we have a RNG in our plugin, let's write a command to get a random
|
Basic command handler
|
||||||
number. We'll start with a simple command named random that just returns a
|
---------------------
|
||||||
random number from our RNG and takes no arguments. Here's what that looks
|
|
||||||
like::
|
|
||||||
|
|
||||||
|
Our first command definition can immediately follow::
|
||||||
|
|
||||||
|
# dummy comment to indent the below code consistently
|
||||||
|
@wrap
|
||||||
def random(self, irc, msg, args):
|
def random(self, irc, msg, args):
|
||||||
"""takes no arguments
|
"""takes no arguments
|
||||||
|
|
||||||
Returns the next random number from the random number generator.
|
Returns the next random number from the random number generator.
|
||||||
"""
|
"""
|
||||||
irc.reply(str(self.rng.random()))
|
irc.reply(str(self.rng.random()))
|
||||||
random = wrap(random)
|
|
||||||
|
|
||||||
Same as before, you have to past it with one indentation level.
|
.. note::
|
||||||
And that's it. Now here are the important points.
|
All functions used as commands must have an all lowercase name.
|
||||||
|
|
||||||
First and foremost, all plugin commands must have all-lowercase function
|
A command function taking in no arguments from IRC will still require 4
|
||||||
names. If they aren't all lowercase they won't show up in a plugin's list of
|
arguments; they are as follows:
|
||||||
commands (nor will they be useable in general). If you look through a plugin
|
|
||||||
and see a function that's not in all lowercase, it is not a plugin command.
|
|
||||||
Chances are it is a helper function of some sort, and in fact using capital
|
|
||||||
letters is a good way of assuring that you don't accidentally expose helper
|
|
||||||
functions to users as commands.
|
|
||||||
|
|
||||||
You'll note the arguments to this class method are ``(self, irc, msg, args)``. This
|
- ``self``: refers to the class instance. It is common to keep local state
|
||||||
is what the argument list for all methods that are to be used as commands must
|
for the plugin as instance variables within the plugin class.
|
||||||
start with. If you wanted additional arguments, you'd append them onto the end,
|
- ``irc``: refers to the IRC network instance the command was called on
|
||||||
but since we take no arguments we just stop there. I'll explain this in more
|
- ``msg``: a :ref:`supybot.ircmsgs <supybot-ircmsgs>` instance; refers to the
|
||||||
detail with our next command, but it is very important that all plugin commands
|
IRC message that triggered this command.
|
||||||
are class methods that start with those four arguments exactly as named.
|
- ``args``: a raw list of remaining unconverted arguments; new plugins that
|
||||||
|
use :ref:`@wrap <using-wrap>` for automatic argument type conversion should
|
||||||
|
never need to interact with ``args`` directly.
|
||||||
|
|
||||||
Next, in the docstring there are two major components. First, the very first
|
The function docstring is expected to be in a particular format. First, the very
|
||||||
line dictates the argument list to be displayed when someone calls the help
|
first line dictates the argument list to be displayed when someone calls the
|
||||||
command for this command (i.e., help random). Then you leave a blank line and
|
``help`` command on this command (i.e., ``help random``). Then, leave a blank
|
||||||
start the actual help string for the function. Don't worry about the fact that
|
line and start the actual help string for the function. Don't worry about the
|
||||||
it's tabbed in or anything like that, as the help command normalizes it to
|
fact that it's tabbed in or anything like that, as the help command normalizes
|
||||||
make it look nice. This part should be fairly brief but sufficient to explain
|
it to make it look nice. This part should be fairly brief but sufficient to
|
||||||
the function and what (if any) arguments it requires. Remember that this should
|
explain the function and what (if any) arguments it requires. Remember that this
|
||||||
fit in one IRC message which is typically around a 450 character limit.
|
should fit in one IRC message which is typically around a 450 character limit.
|
||||||
|
|
||||||
Then we have the actual code body of the plugin, which consists of a single
|
The :py:meth:`irc.reply <supybot.callbacks.NestedCommandsIrcProxy.reply>` call
|
||||||
line: ``irc.reply(str(self.rng.random()))``.
|
is a bit of magic: it issues a reply the same place as the message that
|
||||||
The :py:meth:`irc.reply <supybot.callbacks.NestedCommandsIrcProxy.reply>`
|
triggered the command. i.e. this may be in a channel or in a private
|
||||||
function issues a reply
|
conversation with the bot.
|
||||||
to wherever the PRIVMSG it received the command from with whatever text is
|
|
||||||
provided. If you're not sure what I mean when I say "wherever the PRIVMSG it
|
|
||||||
received the command from", basically it means: if the command is issued in a
|
|
||||||
channel the response is sent in the channel, and if the command is issued in a
|
|
||||||
private dialog the response is sent in a private dialog. The text we want to
|
|
||||||
display is simply the next number from our RNG (self.rng). We get that number
|
|
||||||
by calling the random function, and then we str it just to make sure it is a
|
|
||||||
nice printable string.
|
|
||||||
|
|
||||||
Lastly, all plugin commands must be 'wrap'ed. What the wrap function does is
|
Lastly, notice that commands go through the :ref:`@wrap <using-wrap>`
|
||||||
handle argument parsing for plugin commands in a very nice and very powerful
|
decorator for automatic argument type conversion. For commands that require no
|
||||||
way. With no arguments, we simply need to just wrap it. For more in-depth
|
parameters, calling ``@wrap`` with no arguments is enough.
|
||||||
information on using wrap check out the wrap tutorial (The astute Python
|
|
||||||
programmer may note that this is very much like a decorator, and that's
|
Command handler with parameters
|
||||||
precisely what it is. However, we developed this before decorators existed and
|
-------------------------------
|
||||||
haven't changed the syntax due to our earlier requirement to stay compatible
|
|
||||||
with Python 2.3. As we now require Python 2.4 or greater, this may eventually
|
|
||||||
change to support work via decorators.)
|
|
||||||
|
|
||||||
Now let's create a command with some arguments and see how we use those in our
|
Now let's create a command with some arguments and see how we use those in our
|
||||||
plugin commands. Let's allow the user to seed our RNG with their own seed
|
plugin commands. This ``seed`` command lets the user pick a specific RNG seed::
|
||||||
value. We'll call the command seed and take just the seed value as the argument
|
|
||||||
(which we'll require be a floating point value of some sort, though technically
|
|
||||||
it can be any hashable object). Here's what this command looks like::
|
|
||||||
|
|
||||||
|
# dummy comment to indent the below code consistently
|
||||||
|
@wrap(['float'])
|
||||||
def seed(self, irc, msg, args, seed):
|
def seed(self, irc, msg, args, seed):
|
||||||
"""<seed>
|
"""<seed>
|
||||||
|
|
||||||
@ -258,84 +234,85 @@ it can be any hashable object). Here's what this command looks like::
|
|||||||
"""
|
"""
|
||||||
self.rng.seed(seed)
|
self.rng.seed(seed)
|
||||||
irc.replySuccess()
|
irc.replySuccess()
|
||||||
seed = wrap(seed, ['float'])
|
|
||||||
|
|
||||||
You'll notice first that argument list now includes an extra argument, seed. If
|
For functions that use ``@wrap`` (described further in the
|
||||||
you read the wrap tutorial mentioned above, you should understand how this arg
|
:ref:`Using commands.wrap tutorial <using-wrap>`), additional command arguments
|
||||||
list gets populated with values. Thanks to wrap we don't have to worry about
|
are handled by:
|
||||||
type-checking or value-checking or anything like that. We just specify that it
|
|
||||||
must be a float in the wrap portion and we can use it in the body of the
|
|
||||||
function.
|
|
||||||
|
|
||||||
Of course, we modify the docstring to document this function. Note the syntax
|
1. Adding :ref:`type converters <wrap-converter-list>`, one for each parameter,
|
||||||
on the first line. Arguments go in <> and optional arguments should be
|
to the list passed into ``@wrap``
|
||||||
surrounded by ``[]`` (we'll demonstrate this later as well).
|
2. Adding one function parameter per argument to the command function
|
||||||
|
definition. (i.e. ``def seed(self, irc, msg, args, seed)`` instead of
|
||||||
|
``def seed(self, irc, msg, args)``)
|
||||||
|
|
||||||
The body of the function should be fairly straightforward to figure out, but it
|
We also modify the docstring to document this function. Note the syntax
|
||||||
introduces a new function -
|
on the first line: by convention, required arguments go in ``<>`` and optional
|
||||||
|
arguments should be surrounded by ``[]``.
|
||||||
|
|
||||||
|
The function body includes a new method
|
||||||
:py:meth:`irc.replySuccess <supybot.callbacks.RichReplyMethods.replySuccess>`.
|
:py:meth:`irc.replySuccess <supybot.callbacks.RichReplyMethods.replySuccess>`.
|
||||||
This is just a generic "I
|
This is just a generic "I succeeded" command which responds with whatever the
|
||||||
succeeded" command which responds with whatever the bot owner has configured to
|
bot owner has configured in ``config supybot.replies.success``.
|
||||||
be the success response (configured in supybot.replies.success). Note that we
|
Also, by using ``@wrap``, we don't need to do any type checking inside the
|
||||||
don't do any error-checking in the plugin, and that's because we simply don't
|
function itself - this is handled separately, and invalid argument values will
|
||||||
have to. We are guaranteed that seed will be a float and so the call to our
|
cause the command to error before it reaches the wrapped function.
|
||||||
RNG's seed is guaranteed to work.
|
|
||||||
|
|
||||||
Lastly, of course, the wrap call. Again, read the wrap tutorial for fuller
|
With this alone you'd be able to make a range of useful plugin commands, but
|
||||||
coverage of its use, but the basic premise is that the second argument to wrap
|
we'll go include some more examples to illustrate common patterns.
|
||||||
is a list of converters that handles argument validation and conversion and it
|
|
||||||
then assigns values to each argument in the arg list after the first four
|
|
||||||
(required) arguments. So, our seed argument gets a float, guaranteed.
|
|
||||||
|
|
||||||
With this alone you'd be able to make some pretty usable plugin commands, but
|
Command handler with list-type arguments
|
||||||
we'll go through two more commands to introduce a few more useful ideas. The
|
----------------------------------------
|
||||||
next command we'll make is a sample command which gets a random sample of items
|
The next sample command is named ``sample`` (no pun intended): it takes a random
|
||||||
from a list provided by the user::
|
sample of arbitrary size from a list provided by the user::
|
||||||
|
|
||||||
|
# dummy comment to indent the below code consistently
|
||||||
def sample(self, irc, msg, args, n, items):
|
def sample(self, irc, msg, args, n, items):
|
||||||
"""<number of items> <item1> [<item2> ...]
|
"""<number of items> <item1> [<item2> ...]
|
||||||
|
|
||||||
Returns a sample of the <number of items> taken from the remaining
|
Returns a sample of the <number of items> taken from the remaining
|
||||||
arguments. Obviously <number of items> must be less than the number
|
arguments. <number of items> must be less than the number
|
||||||
of arguments given.
|
of arguments given.
|
||||||
"""
|
"""
|
||||||
if n > len(items):
|
if n > len(items):
|
||||||
|
# Calling irc.error with Raise=True is an alternative early return
|
||||||
irc.error('<number of items> must be less than the number '
|
irc.error('<number of items> must be less than the number '
|
||||||
'of arguments.')
|
'of arguments.', Raise=True)
|
||||||
return
|
|
||||||
sample = self.rng.sample(items, n)
|
sample = self.rng.sample(items, n)
|
||||||
sample.sort()
|
sample.sort()
|
||||||
irc.reply(utils.str.commaAndify(sample))
|
irc.reply(utils.str.commaAndify(sample))
|
||||||
sample = wrap(sample, ['int', many('anything')])
|
sample = wrap(sample, ['int', many('anything')])
|
||||||
|
|
||||||
This plugin command introduces a few new things, but the general structure
|
The important thing to note is that list type arguments are rolled into one
|
||||||
should look fairly familiar by now. You may wonder why we only have two extra
|
parameter in the command function by the ``many`` filter. Similar "multiplicity"
|
||||||
arguments when obviously this plugin can accept any number of arguments. Well,
|
handlers are documented :ref:`here <wrap-multiplicity-handlers>`.
|
||||||
using wrap we collect all of the remaining arguments after the first one into
|
|
||||||
the items argument. If you haven't caught on yet, wrap is really cool and
|
|
||||||
extremely useful.
|
|
||||||
|
|
||||||
Next of course is the updated docstring. Note the use of ``[]`` to denote the
|
We also update the docstring to use the ``[]`` convention when surrounding
|
||||||
optional items after the first item.
|
optional arguments.
|
||||||
|
|
||||||
The body of the plugin should be relatively easy to read. First we check and
|
For this function's body,
|
||||||
make sure that n (the number of items the user wants to sample) is not larger
|
|
||||||
than the actual number of items they gave. If it does, we call irc.error with
|
|
||||||
the error message you see.
|
|
||||||
:py:meth:`irc.error <supybot.callbacks.NestedCommandsIrcProxy.error>`
|
:py:meth:`irc.error <supybot.callbacks.NestedCommandsIrcProxy.error>`
|
||||||
is kind of like irc.replySuccess only it
|
is like
|
||||||
gives an error message using the configured error format (in
|
:py:meth:`irc.replySuccess <supybot.callbacks.NestedCommandsIrcProxy.replySuccess>`
|
||||||
``supybot.replies.error``). Otherwise, we use the sample function from our RNG to
|
but for error messages. We prefer using this instead of ``irc.reply`` for error
|
||||||
get a sample, then we sort it, and we reply with the 'utils.str.commaAndify'ed
|
signaling because its behaviour can be configured specially. For example, you
|
||||||
version. The utils.str.commaAndify function basically takes a list of strings
|
can force all errors to go in private by setting the ``reply.error.inPrivate``
|
||||||
|
option, and this can help reduce noise on a busy channel.
|
||||||
|
Also, ``irc.error()`` with no text will return a generic error message
|
||||||
|
configured in ``supybot.replies.error``, but this is not a valid call to
|
||||||
|
:py:meth:`irc.reply <supybot.callbacks.NestedCommandsIrcProxy.reply>`.
|
||||||
|
|
||||||
|
``utils.str.commaAndify`` is a simple helper that takes a list of strings
|
||||||
and turns it into "item1, item2, item3, item4, and item5" for an arbitrary
|
and turns it into "item1, item2, item3, item4, and item5" for an arbitrary
|
||||||
length. More details on using the utils module can be found in the utils
|
length. Limnoria has accumulated many such helpers in its lifetime, many of
|
||||||
tutorial.
|
which are described in the :ref:`Using Utils <using-utils>` page.
|
||||||
|
|
||||||
Now for the last command that we will add to our plugin.py. This last command
|
Command handler with optional arguments
|
||||||
will allow the bot users to roll an arbitrary n-sided die, with as many sides
|
---------------------------------------
|
||||||
as they so choose. Here's the code for this command::
|
Now for the last command that we will add to our plugin.py. This ``diceroll``
|
||||||
|
command will allow the bot users to roll an arbitrary n-sided die, with n
|
||||||
|
defaulting to 6::
|
||||||
|
|
||||||
|
# dummy comment to indent the below code consistently
|
||||||
def diceroll(self, irc, msg, args, n):
|
def diceroll(self, irc, msg, args, n):
|
||||||
"""[<number of sides>]
|
"""[<number of sides>]
|
||||||
|
|
||||||
@ -346,20 +323,12 @@ as they so choose. Here's the code for this command::
|
|||||||
irc.reply(s, action=True)
|
irc.reply(s, action=True)
|
||||||
diceroll = wrap(diceroll, [additional(('int', 'number of sides'), 6)])
|
diceroll = wrap(diceroll, [additional(('int', 'number of sides'), 6)])
|
||||||
|
|
||||||
The only new thing learned here really is that the irc.reply method accepts an
|
The only new thing described here is that ``irc.reply(..., action=True)`` makes
|
||||||
optional argument action, which if set to True makes the reply an action
|
the bot perform a `/me`. There are some other flags described in the
|
||||||
instead. So instead of just crudely responding with the number, instead you
|
:py:meth:`irc.reply <supybot.callbacks.NestedCommandsIrcProxy.reply>`
|
||||||
should see something like * supybot rolls a 5. You'll also note that it uses a
|
documentation too: common ones include ``private=True``, which
|
||||||
more advanced wrap line than we have used to this point, but to learn more
|
forces a private message, and ``notice=True``, which forces the reply to use
|
||||||
about wrap, you should refer to the wrap tutorial
|
NOTICE instead of PRIVMSG.
|
||||||
|
|
||||||
And now that we're done adding plugin commands you should see the boilerplate
|
|
||||||
stuff at the bottom, which just consists of::
|
|
||||||
|
|
||||||
Class = Random
|
|
||||||
|
|
||||||
And also some vim modeline stuff. Leave these as is, and we're finally done
|
|
||||||
with plugin.py!
|
|
||||||
|
|
||||||
test.py
|
test.py
|
||||||
=======
|
=======
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
.. _using-utils:
|
||||||
|
|
||||||
****************************
|
****************************
|
||||||
Using Supybot's utils module
|
Using Supybot's utils module
|
||||||
****************************
|
****************************
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
.. _using-wrap:
|
||||||
|
|
||||||
*****************************************************
|
*****************************************************
|
||||||
Using commands.wrap to parse your command's arguments
|
Using commands.wrap to parse your command's arguments
|
||||||
*****************************************************
|
*****************************************************
|
||||||
@ -154,6 +156,8 @@ thing that just "something" would return, but rather a list of "something"s.
|
|||||||
|
|
||||||
Converter List
|
Converter List
|
||||||
==============
|
==============
|
||||||
|
|
||||||
|
.. _wrap-converter-list:
|
||||||
Below is a list of all the available converters to use with wrap. If the
|
Below is a list of all the available converters to use with wrap. If the
|
||||||
converter accepts any arguments, they are listed after it and if they are
|
converter accepts any arguments, they are listed after it and if they are
|
||||||
optional, the default value is shown.
|
optional, the default value is shown.
|
||||||
@ -468,6 +472,8 @@ first
|
|||||||
Tries each of the supplied converters in order and returns the result of
|
Tries each of the supplied converters in order and returns the result of
|
||||||
the first successfully applied converter.
|
the first successfully applied converter.
|
||||||
|
|
||||||
|
.. _wrap-multiplicity-handlers:
|
||||||
|
|
||||||
Multiplicity
|
Multiplicity
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user