Plugin Configuration for Developers

This page describes how to use Limnoria’s config system when developing plugins.

Adding Plugin Configuration (config.py)

As discussed in the Configuration user guide, Limnoria’s configuration is hierarchical. Each plugin will register its config options in a separate group reflecting its name: supybot.plugins.<pluginname>. This design ensures that the every plugin’s options are properly namespaced.

The default template provided by supybot-plugin-create will already set up the plugin’s config group, e.g.:

WorldDom = conf.registerPlugin('WorldDom')

Creating Configuration Variables

Global config values are defined using conf.registerGlobalValue(). The arguments are as follows:

  • The parent group to add the value to.

  • The name of the config variable.

  • The variable type, including the default value and help text as parameters. Supported variable types are listed in a later section.

conf.registerGlobalValue(WorldDom, 'globalWorldDominationRequires',
    registry.String('worldDom', """Determines the capability required to perform world domination."""))

The full name of the above config value will be supybot.plugins.WorldDom.globalWorldDominationRequires, or plugins.WorldDom.globalWorldDominationRequires for short.

Note that all configuration variables are groups, and it is possible to register more variables underneath them. This can be useful as it allows extending existing config variables without changing their type:

conf.registerGlobalValue(WorldDom.globalWorldDominationRequires, 'weekends',
    registry.String('worldDomWeekends', """Determines the capability required to perform world domination on weekends."""))

Nested configuration variables must be declared after their parent.

Creating Configuration Groups

To create a group that is not a config variable itself, use conf.registerGroup():

conf.registerGroup(WorldDom, 'attackTargets')

Adding values to a group is the same as adding one under another config variable:

conf.registerGlobalValue(WorldDom.attackTargets, 'air',
    registry.SpaceSeparatedListOfStrings('', """Contains the list of air targets."""))

Variations

Channel-specific values

Limnoria supports channel-specific variables, which allows bot administrators to set a global value as well as override it on a per-channel basis.

These are defined using conf.registerChannelValue():

conf.registerChannelValue(WorldDom.attackTargets, 'air',
    registry.SpaceSeparatedListOfStrings('', """Contains the list of air targets."""))

Network-specific values

Network-specific variables are defined using conf.registerNetworkValue():

conf.registerNetworkValue(WorldDom, 'exempt',
    registry.Boolean(False, """Determines whether the network will be exempt from world domination (for now...)"""))

Private values

The variable type also takes an optional private argument, for setting a configuration variable to private (useful for passwords, authentication tokens, api keys, …):

conf.registerChannelValue(WorldDom, 'controlRoom',
    registry.Boolean(False, """Whether this channel is the secret control room.""", private=True))

When this is set, the bot will only allow bot owners (in the case of global variables) or channel administrators (in the case of channel-specific variables) to query the config value.

Accessing the config from plugin.py

To read a config variable from the plugin code, use self.registryValue() with the name of the configuration variable. The variable name will include all group names after the plugin name, e.g.:

self.registryValue('globalWorldDominationRequires')
self.registryValue('attackTargets.air')

This will return data in the type that the config variable was declared as (e.g., a list of strings for attackTargets.air, as it has type registry.SpaceSeparatedListOfStrings).

If it is a channel-specific variable, you should pass in additional channel and network arguments like this:

self.registryValue('attackTargets.air', msg.channel, irc.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('attackTargets.air', value=['foo', 'bar'])
self.setRegistryValue('attackTargets.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.attackTargets.air()
conf.supybot.plugins.WorldDom.attackTargets.get('air')()
conf.supybot.plugins.WorldDom.attackTargets.air.get('network').get('#channel')()
conf.supybot.plugins.WorldDom.attackTargets.air.setValue(['foo'])
conf.supybot.plugins.WorldDom.attackTargets.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('attackTargets.air', '#channel', 'network')
self.setRegistryValue('attackTargets.air', value=['foo', 'bar'],
                      channel=channel)
conf.supybot.plugins.WorldDom.attackTargets.air.get('#channel')()
conf.supybot.plugins.WorldDom.attackTargets.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.

The Built-in Registry Types

Limnoria’s registry module defines the following built-in config variable types:

  • 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).

  • registry.String - Accepts any string.

  • registry.NormalizedString - Accepts any string but will normalize sequences of 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.

Custom Registry Types

If your plugin requires a more restrictive set of inputs, we recommend creating a custom registry type so that invalid values can never be configured. This in turn can simplify the code in your actual plugin.

Creating a Custom Registry Type

Creating a custom registry type involves subclassing one of the built-in registry types. For example, this NegativeInteger type only accepts negative integers:

class NegativeInteger(registry.Integer):
    """Value must be a negative integer."""

    def setValue(self, v):
        if v >= 0:
            self.error()
        super().setValue(self, v)

The most important parts here are the setValue() definition and the docstring, which determines the error message when setting an invalid value. Call self.error() on invalid input, and the superclass’ setValue() to actually set the value.

For more detailed examples, see src/registry.py in the source code.

What Subclasses Can I Use?

In addition to the built-in types, the following abstract types can be used for custom registry types:

  • 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. See the following example, or the definitions of registry.SpaceSeparatedListOfStrings and registry.CommaSeparatedListOfStrings in src/registry.py

Using a Custom Registry Type

Custom registry types can be passed in to any of the conf.register...() methods mentioned above:

class CommaSeparatedListOfProbabilities(registry.SeparatedListOf):
    Value = registry.Probability
    def splitter(self, s):
        return re.split(r'\s*,\s*', s)
    joiner = ', '.join

conf.registerGlobalValue(SomePlugin, 'someConfVar',
    CommaSeparatedListOfProbabilities('0.0, 1.0', """Holds the list of
    probabilities for whatever."""))

The default value and config variable description are passed in as with any other registry type.

Using ‘configure’ for supybot-wizard support

Note

This section is mostly for reference. In practice, very few third-party plugins define support for supybot-wizard, as they are often installed after already configuring the bot.

Interactive configuration for plugins is defined in the configure function. The supybot.questions module provides several convenience functions to make implementing these easier:

  • “expect” is the most general prompting mechanism which specifies certain inputs and 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 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 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 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.

Here is a full example:

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)

The first thing this configure function asks for is whether the world domination commands should 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. Otherwise, we prompt them for a specific capability to check for, defaulting to the “admin” capability. This can also be set to any arbitrary capability name, which the bot can automatically check for as well.

Lastly, we ask for which 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 standard cardinal directions are available as well.

Configuration hooks

It is possible to define callbacks for when a configuration variable is changed. This is usually not necessary, but can be used for instance to cache results or pre-fetch 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):
        super().__init__(irc)
        conf.supybot.nick.addCallback(self._configCallback)

    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.