Limnoria-doc/develop/advanced_plugin_config.rst

497 lines
21 KiB
ReStructuredText
Raw Permalink Normal View History

.. _configuration-tutorial:
2011-06-30 16:49:28 +02:00
**********************
2011-06-30 16:21:24 +02:00
Advanced Plugin Config
2011-06-30 16:49:28 +02:00
**********************
2011-06-30 16:21:24 +02:00
This tutorial covers some of the more advanced plugin config features available
to Supybot plugin authors.
What's This Tutorial For?
=========================
Brief overview of what this tutorial covers and the target audience.
Want to know the crazy advanced features available to you, the Supybot plugin
author? Well, this is the tutorial for you. This article assumes you've read
the Supybot plugin author tutorial since all the basics of plugin config are
handled there first.
In this tutorial we'll cover:
* Using the configure function more effectively by using the functions
provided in supybot.questions
* Creating config variable groups and config variables underneath those
groups.
* The built-in config variable types ("registry types") for use with config
variables
* Creating custom registry types to handle config variable values more
effectively
Using 'configure' effectively
=============================
How to use 'configure' effectively using the functions from
'supybot.questions'
In the original Supybot plugin author tutorial you'll note that we gloss over
the configure portion of the config.py file for the sake of keeping the
tutorial to a reasonable length. Well, now we're going to cover it in more
detail.
The supybot.questions module is a nice little module coded specifically to help
clean up the configure section of every plugin's config.py. The boilerplate
config.py code imports the four most useful functions from that module:
* "expect" is a very general prompting mechanism which can specify certain
inputs that it will accept and also specify a default response. It takes
the following arguments:
* prompt: The text to be displayed
* possibilities: The list of possible responses (can be the empty
list, [])
* default (optional): Defaults to None. Specifies the default value
to use if the user enters in no input.
* acceptEmpty (optional): Defaults to False. Specifies whether or not
to accept no input as an answer.
* "anything" is basically a special case of expect which takes anything
(including no input) and has no default value specified. It takes only
one argument:
* prompt: The text to be displayed
* "something" is also a special case of expect, requiring some input and
allowing an optional default. It takes the following arguments:
* prompt: The text to be displayed
* default (optional): Defaults to None. The default value to use if
the user doesn't input anything.
* "yn" is for "yes or no" questions and basically forces the user to input
a "y" for yes, or "n" for no. It takes the following arguments:
* prompt: The text to be displayed
* default (optional): Defaults to None. Default value to use if the
user doesn't input anything.
All of these functions, with the exception of "yn", return whatever string
results as the answer whether it be input from the user or specified as the
default when the user inputs nothing. The "yn" function returns True for "yes"
answers and False for "no" answers.
For the most part, the latter three should be sufficient, but we expose expect
to anyone who needs a more specialized configuration.
Let's go through a quick example configure that covers all four of these
functions. First I'll give you the code, and then we'll go through it,
discussing each usage of a supybot.questions function just to make sure you
realize what the code is actually doing. Here it is::
def configure(advanced):
# This will be called by supybot to configure this module. advanced is
# a bool that specifies whether the user identified himself as an advanced
# user or not. You should effect your configuration by manipulating the
# registry as appropriate.
from supybot.questions import expect, anything, something, yn
WorldDom = conf.registerPlugin('WorldDom', True)
if yn("""The WorldDom plugin allows for total world domination
with simple commands. Would you like these commands to
be enabled for everyone?""", default=False):
WorldDom.globalWorldDominationRequires.setValue("")
else:
cap = something("""What capability would you like to require for
this command to be used?""", default="Admin")
WorldDom.globalWorldDominationRequires.setValue(cap)
dir = expect("""What direction would you like to attack from in
your quest for world domination?""",
["north", "south", "east", "west", "ABOVE"],
default="ABOVE")
WorldDom.attackDirection.setValue(dir)
As you can see, this is the WorldDom plugin, which I am currently working on.
The first thing our configure function checks is to see whether or not the bot
owner would like the world domination commands in this plugin to be available
to everyone. If they say yes, we set the globalWorldDominationRequires
configuration variable to the empty string, signifying that no specific
capabilities are necessary. If they say no, we prompt them for a specific
capability to check for, defaulting to the "Admin" capability. Here they can
create their own custom capability to grant to folks which this plugin will
check for if they want, but luckily for the bot owner they don't really have to
do this since Supybot's capabilities system can be flexed to take care of this.
Lastly, we check to find out what direction they want to attack from as they
venture towards world domination. I prefer "death from above!", so I made that
the default response, but the more boring cardinal directions are available as
choices as well.
Using Config Groups
===================
A brief overview of how to use config groups to organize config variables
Supybot's Hierarchical Configuration
------------------------------------
2011-06-30 16:21:24 +02:00
Supybot's configuration is inherently hierarchical, as you've probably already
figured out in your use of the bot. Naturally, it makes sense to allow plugin
authors to create their own hierarchies to organize their configuration
variables for plugins that have a lot of plugin options. If you've taken a look
at the plugins that Supybot comes with, you've probably noticed that several of
them take advantage of this. In this section of this tutorial we'll go over how
to make your own config hierarchy for your plugin.
Here's the brilliant part about Supybot config values which makes hierarchical
structuring all that much easier - values are groups. That is, any config value
you may already defined in your plugins can already be treated as a group, you
simply need to know how to add items to that group.
Now, if you want to just create a group that doesn't have an inherent value you
can do that as well, but you'd be surprised at how rarely you have to do that.
In fact if you look at most of the plugins that Supybot comes with, you'll only
find that we do this in a handful of spots yet we use the "values as groups"
feature quite a bit.
Creating a Config Group
-----------------------
2011-06-30 16:21:24 +02:00
As stated before, config variables themselves are groups, so you can create a
group simply by creating a configuration variable::
conf.registerGlobalValue(WorldDom, 'globalWorldDominationRequires',
registry.String('', """Determines the capability required to access the
world domination commands in this plugin."""))
As you probably know by now this creates the config variable
supybot.plugins.WorldDom.globalWorldDominationRequires which you can access/set
using the Config plugin directly on the running bot. What you may not have
known prior to this tutorial is that that variable is also a group.
Specifically, it is now the WorldDom.globalWorldDominationRequires group, and
we can add config variables to it! Unfortunately, this particular bit of
configuration doesn't really require anything underneath it, so let's create a
new group which does using the "create only a group, not a value" command.
Let's create a configurable list of targets for different types of attacks
(land, sea, air, etc.). We'll call the group attackTargets. Here's how you
create just a config group alone with no value assigned::
conf.registerGroup(WorldDom, 'attackTargets')
The first argument is just the group under which you want to create your new
group (and we got WorldDom from conf.registerPlugin which was in our
boilerplate code from the plugin creation wizard). The second argument is, of
course, the group name. So now we have WorldDom.attackTargets (or, fully,
supybot.plugins.WorldDom.attackTargets).
Adding Values to a Group
------------------------
2011-06-30 16:21:24 +02:00
Actually, you've already done this several times, just never to a custom group
of your own. You've always added config values to your plugin's config group.
With that in mind, the only slight modification needed is to simply point to
the new group::
conf.registerGlobalValue(WorldDom.attackTargets, 'air',
registry.SpaceSeparatedListOfStrings('', """Contains the list of air
targets."""))
And now we have a nice list of air targets! You'll notice that the first
argument is WorldDom.attackTargets, our new group. Make sure that the
conf.registerGroup call is made before this one or else you'll get a nasty
AttributeError.
Variations
----------
Channel-specific values
^^^^^^^^^^^^^^^^^^^^^^^
A very handy feature is channel-specific variables, which allows bot
administrators to set a global value (as for non-channel-specific values
AND another value for specific channels).
The syntax is pretty much like the previous one, except we use
`registerChannelValue` instead of `registerGlobalValue`::
conf.registerChannelValue(WorldDom.attackTargets, 'air',
registry.SpaceSeparatedListOfStrings('', """Contains the list of air
targets."""))
Private values
^^^^^^^^^^^^^^
Variable type also take an optional argument, for setting a configuration
variable to private (useful for passwords, authentication tokens,
api keys, …)::
conf.registerChannelValue(WorldDom.attackTargets, 'air',
registry.SpaceSeparatedListOfStrings('', """Contains the list of air
targets.""", private=True))
Accessing the configuration registry
------------------------------------
Of course, you can access the variables in your plugins.
If it is a variable created by your plugin, you can do it like this
(if the configuration variable's name is `air`)::
self.registryValue('air')
and it will return data of the right type (in this case, a list of string,
as we declarated it above as a `registry.SpaceSeparatedListOfStrings`).
If it is a channel-specific variable, you can get the value on `#channel`
and `network` like this (if the variable is not defined on this channel,
it defaults to the global one)::
self.registryValue('air', '#channel', 'network')
.. note::
You will typically obtain the current channel name using the `channel`
converter (in commands with a `<channel>` argument) or `msg.channel`
(in other methods); and the network name with `irc.network`.
You can also set configuration variables (either globally or for a single
channel)::
self.setRegistryValue('air', value=['foo', 'bar'])
self.setRegistryValue('air', value=['foo', 'bar'],
channel=channel, network=network)
You can also access other configuration variables (or your own if you want)
via the ``supybot.conf`` module::
conf.supybot.plugins.WorldDom.air()
conf.supybot.plugins.WorldDom.get('air')()
conf.supybot.plugins.WorldDom.air.get('network').get('#channel')()
conf.supybot.plugins.WorldDom.air.setValue(['foo'])
conf.supybot.plugins.WorldDom.air.get('network').get('#channel').setValue(['foo'])
.. warning::
Before version 2019.10.22, Limnoria (and Supybot) did not support
network-specific configuration variables.
If you want to support these versions, you must drop the `network` argument,
and access the configuration variables like this::
self.registryValue('air', '#channel', 'network')
self.setRegistryValue('air', value=['foo', 'bar'],
channel=channel)
conf.supybot.plugins.WorldDom.air.get('#channel')()
conf.supybot.plugins.WorldDom.air.get('#channel').setValue(['foo'])
This will also work in recent versions of Limnoria, but will prevent users
from setting different values for each network.
2011-06-30 16:21:24 +02:00
The Built-in Registry Types
===========================
A rundown of all of the built-in registry types available for use with config
variables.
The "registry" module defines the following config variable types for your use
(I'll include the 'registry.' on each one since that's how you'll refer to it in
code most often). Most of them are fairly self-explanatory, so excuse the
boring descriptions:
* registry.Boolean - A simple true or false value. Also accepts the
following for true: "true", "on" "enable", "enabled", "1", and the
following for false: "false", "off", "disable", "disabled", "0",
* registry.Integer - Accepts any integer value, positive or negative.
* registry.NonNegativeInteger - Will hold any non-negative integer value.
* registry.PositiveInteger - Same as above, except that it doesn't accept 0
as a value.
* registry.Float - Accepts any floating point number.
* registry.PositiveFloat - Accepts any positive floating point number.
* registry.Probability - Accepts any floating point number between 0 and 1
(inclusive, meaning 0 and 1 are also valid).
* registry.String - Accepts any string that is not a valid Python command
* registry.NormalizedString - Accepts any string (with the same exception
above) but will normalize sequential whitespace to a single space..
* registry.StringSurroundedBySpaces - Accepts any string but assures that
it has a space preceding and following it. Useful for configuring a
string that goes in the middle of a response.
* registry.StringWithSpaceOnRight - Also accepts any string but assures
that it has a space after it. Useful for configuring a string that
begins a response.
* registry.Regexp - Accepts only valid (Perl or Python) regular expressions
* registry.SpaceSeparatedListOfStrings - Accepts a space-separated list of
strings.
There are a few other built-in registry types that are available but are not
usable in their current state, only by creating custom registry types, which
we'll go over in the next section.
Custom Registry Types
=====================
How to create and use your own custom registry types for use in customizing
plugin config variables.
Why Create Custom Registry Types?
---------------------------------
2011-06-30 16:21:24 +02:00
For most configuration, the provided types in the registry module are
sufficient. However, for some configuration variables it's not only convenient
to use custom registry types, it's actually recommended. Customizing registry
types allows for tighter restrictions on the values that get set and for
greater error-checking than is possible with the provided types.
What Defines a Registry Type?
-----------------------------
2011-06-30 16:21:24 +02:00
First and foremost, it needs to subclass one of the existing registry types
from the registry module, whether it be one of the ones in the previous section
or one of the other classes in registry specifically designed to be subclassed.
Also it defines a number of other nice things: a custom error message for your
type, customized value-setting (transforming the data you get into something
else if wanted), etc.
Creating Your First Custom Registry Type
----------------------------------------
2011-06-30 16:21:24 +02:00
As stated above, priority number one is that you subclass one of the types in
the registry module. Basically, you just subclass one of those and then
customize whatever you want. Then you can use it all you want in your own
plugins. We'll do a quick example to demonstrate.
We already have registry.Integer and registry.PositiveInteger, but let's say we
want to accept only negative integers. We can create our own NegativeInteger
registry type like so::
class NegativeInteger(registry.Integer):
"""Value must be a negative integer."""
def setValue(self, v):
if v >= 0:
self.error()
registry.Integer.setValue(self, v)
All we need to do is define a new error message for our custom registry type
(specified by the docstring for the class), and customize the setValue
function. Note that all you have to do when you want to signify that you've
gotten an invalid value is to call self.error(). Finally, we call the parent
class's setValue to actually set the value.
What Else Can I Customize?
--------------------------
2011-06-30 16:21:24 +02:00
Well, the error string and the setValue function are the most useful things
that are available for customization, but there are other things. For examples,
look at the actual built-in registry types defined in registry.py (in the src
directory distributed with the bot).
What Subclasses Can I Use?
--------------------------
2011-06-30 16:21:24 +02:00
Chances are one of the built-in types in the previous section will be
sufficient, but there are a few others of note which deserve mention:
* registry.Value - Provides all the core functionality of registry types
(including acting as a group for other config variables to reside
underneath), but nothing more.
* registry.OnlySomeStrings - Allows you to specify only a certain set of
strings as valid values. Simply override validStrings in the inheriting
class and you're ready to go.
* registry.SeparatedListOf - The generic class which is the parent class to
registry.SpaceSeparatedListOfStrings. Allows you to customize four
things: the type of sequence it is (list, set, tuple, etc.), what each
item must be (String, Boolean, etc.), what separates each item in the
sequence (using custom splitter/joiner functions), and whether or not
the sequence is to be sorted. Look at the definitions of
registry.SpaceSeparatedListOfStrings and
registry.CommaSeparatedListOfStrings at the bottom of registry.py for
more information. Also, there will be an example using this in the
section below.
Using My Custom Registry Type
-----------------------------
2011-06-30 16:21:24 +02:00
Using your new registry type is relatively straightforward. Instead of using
whatever registry built-in you might have used before, now use your own custom
class. Let's say we define a registry type to handle a comma-separated list of
probabilities::
class CommaSeparatedListOfProbabilities(registry.SeparatedListOf):
Value = registry.Probability
def splitter(self, s):
return re.split(r'\s*,\s*', s)
joiner = ', '.join
Now, to use that type we simply have to specify it whenever we create a config
variable using it::
conf.registerGlobalValue(SomePlugin, 'someConfVar',
CommaSeparatedListOfProbabilities('0.0, 1.0', """Holds the list of
probabilities for whatever."""))
Note that we initialize it just the same as we do any other registry type, with
two arguments: the default value, and then the description of the config
variable.
2014-01-20 10:46:39 +01:00
.. _configuration-hooks:
Configuration hooks
===================
.. note::
Until stock Supybot or Gribble merge this feature, this section
only applies to Limnoria.
It is possible to get a function called when a configuration variable is
changed. While this is usually not useful (you get the value whenever you
need it), some plugins do use it, for instance for caching results or
for pre-fetching data.
Let's say you want to write a plugin that prints `nick changed` in the logs
when `supybot.nick` is edited. You can do it like this::
class LogNickChange(callbacks.Plugin):
"""Some useless plugin."""
def __init__(self, irc):
self.__parent = super(LogNickChange, self)
self.__parent.__init__(irc)
conf.supybot.nick.addCallback(self._configCallback)
def _configCallback(self, name=None):
self.log.info('nick changed')
As not all Supybot versions support it (yet), it can be a good idea to
show a warning instead of crashing on those versions::
class LogNickChange(callbacks.Plugin):
"""Some useless plugin."""
def __init__(self, irc):
self.__parent = super(LogNickChange, self)
self.__parent.__init__(irc)
try:
conf.supybot.nick.addCallback(self._configCallback)
except registry.NonExistentRegistryEntry:
self.log.error('Your version of Supybot is not compatible '
'with configuration hooks, but this plugin '
'requires them to work.')
def _configCallback(self, name=None):
self.log.info('nick changed')
.. note::
For the moment, the `name` parameter is never given when the callback is
called. However, in the future, it will be set to the name of the variable
that has been changed (useful if you want to use the same callback for
multiple variable), so it is better to allow this parameter.