mirror of
https://github.com/Mikaela/Limnoria.git
synced 2024-12-24 03:33:11 +01:00
Add the ADVANCED_PLUGIN_CONFIG, ADVANCED_PLUGIN_TESTING, CAPABILITIES, and USING_UTILS docs.
This commit is contained in:
parent
9b79f112a0
commit
7c88da9a29
342
docs/ADVANCED_PLUGIN_CONFIG
Normal file
342
docs/ADVANCED_PLUGIN_CONFIG
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
Advanced Plugin Config
|
||||||
|
----------------------
|
||||||
|
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 except 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 except, 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 put 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
|
||||||
|
|
||||||
|
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
|
||||||
|
=======================
|
||||||
|
|
||||||
|
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
|
||||||
|
========================
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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?
|
||||||
|
|
||||||
|
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?
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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?
|
||||||
|
|
||||||
|
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?
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
283
docs/ADVANCED_PLUGIN_TESTING
Normal file
283
docs/ADVANCED_PLUGIN_TESTING
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
Advanced Plugin Testing
|
||||||
|
-----------------------
|
||||||
|
The complete guide to writing tests for your plugins.
|
||||||
|
|
||||||
|
Why Write Tests?
|
||||||
|
================
|
||||||
|
Why should I write tests for my plugin? Here's why.
|
||||||
|
|
||||||
|
For those of you asking "Why should I write tests for my plugin? I tried it
|
||||||
|
out, and it works!", read on. For those of you who already realize that
|
||||||
|
Testing is Good (TM), skip to the next section.
|
||||||
|
|
||||||
|
Here are a few quick reasons why to test your Supybot plugins.
|
||||||
|
|
||||||
|
* When/if we rewrite or change certain features in Supybot, tests make
|
||||||
|
sure your plugin will work with these changes. It's much easier to run
|
||||||
|
supybot-test MyPlugin after upgrading the code and before even reloading
|
||||||
|
the bot with the new code than it is to load the bot with new code and
|
||||||
|
then load the plugin only to realize certain things don't work. You may
|
||||||
|
even ultimately decide you want to stick with an older version for a while
|
||||||
|
as you patch your custom plugin. This way you don't have to rush a patch
|
||||||
|
while restless users complain since you're now using a newer version that
|
||||||
|
doesn't have the plugin they really like.
|
||||||
|
|
||||||
|
* Running the automated tests takes a few seconds, testing plugins in IRC
|
||||||
|
on a live bot generally takes quite a bit longer. We make it so that
|
||||||
|
writing tests generally doesn't take much time, so a small initial
|
||||||
|
investment adds up to lots of long-term gains.
|
||||||
|
|
||||||
|
* If you want your plugin to be included in any of our releases (the core
|
||||||
|
Supybot if you think it's worthy, or our supybot-plugins package), it has
|
||||||
|
to have tests. Period.
|
||||||
|
|
||||||
|
For a bigger list of why to write unit tests, check out this article:
|
||||||
|
|
||||||
|
http://www.onjava.com/pub/a/onjava/2003/04/02/javaxpckbk.html
|
||||||
|
|
||||||
|
and also check out what the Extreme Programming folks have to say about unit
|
||||||
|
tests:
|
||||||
|
|
||||||
|
http://www.extremeprogramming.org/rules/unittests.html
|
||||||
|
|
||||||
|
Plugin Tests
|
||||||
|
============
|
||||||
|
How to write tests for commands in your plugins.
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
|
||||||
|
This tutorial assumes you've read through the plugin author tutorial, and that
|
||||||
|
you used supybot-plugin-create to create your plugin (as everyone should). So,
|
||||||
|
you should already have all the necessary imports and all that boilerplate
|
||||||
|
stuff in test.py already, and you have already seen what a basic plugin test
|
||||||
|
looks like from the plugin author tutorial. Now we'll go into more depth about
|
||||||
|
what plugin tests are available to Supybot plugin authors.
|
||||||
|
|
||||||
|
Plugin Test Case Classes
|
||||||
|
|
||||||
|
Supybot comes with two plugin test case classes, PluginTestCase and
|
||||||
|
ChannelPluginTestCase. The former is used when it doesn't matter whether or
|
||||||
|
not the commands are issued in a channel, and the latter is used for when it
|
||||||
|
does. For the most part their API is the same, so unless there's a distinction
|
||||||
|
between the two we'll treat them as one and the same when discussing their
|
||||||
|
functionality.
|
||||||
|
|
||||||
|
The Most Basic Plugin Test Case
|
||||||
|
|
||||||
|
At the most basic level, a plugin test case requires three things:
|
||||||
|
|
||||||
|
* the class declaration (subclassing PluginTestCase or
|
||||||
|
ChannelPluginTestCase)
|
||||||
|
* a list of plugins that need to be loaded for these tests (does not
|
||||||
|
include Owner, Misc, or Config, those are always automatically loaded) -
|
||||||
|
often this is just the name of the plugin that you are writing tests for
|
||||||
|
* some test methods
|
||||||
|
|
||||||
|
Here's what the most basic plugin test case class looks like (for a plugin
|
||||||
|
named MyPlugin):
|
||||||
|
|
||||||
|
class MyPluginTestCase(PluginTestCase):
|
||||||
|
plugins = ('MyPlugin',)
|
||||||
|
|
||||||
|
def testSomething(self):
|
||||||
|
# assertions and such go here
|
||||||
|
|
||||||
|
Your plugin test case should be named TestCase as you see above, though it
|
||||||
|
doesn't necessarily have to be named that way (supybot-plugin-create puts that
|
||||||
|
in place for you anyway). As you can see we elected to subclass PluginTestCase
|
||||||
|
because this hypothetical plugin apparently doesn't do anything
|
||||||
|
channel-specific.
|
||||||
|
|
||||||
|
As you probably noticed, the plugins attribute of the class is where the list
|
||||||
|
of necessary plugins goes, and in this case just contains the plugin that we
|
||||||
|
are testing. This will be the case for probably the majority of plugins. A lot
|
||||||
|
of the time test writers will use a bot function that performs some function
|
||||||
|
that they don't want to write code for and they will just use command nesting
|
||||||
|
to feed the bot what they need by using that plugin's functionality. If you
|
||||||
|
choose to do this, only do so with core bot plugins as this makes distribution
|
||||||
|
of your plugin simpler. After all, we want people to be able to run your
|
||||||
|
plugin tests without having to have all of your plugins!
|
||||||
|
|
||||||
|
One last thing to note before moving along is that each of the test methods
|
||||||
|
should describe what they are testing. If you want to test that your plugin
|
||||||
|
only responds to registered users, don't be afraid to name your test method
|
||||||
|
testOnlyRespondingToRegisteredUsers or testNotRespondingToUnregisteredUsers.
|
||||||
|
You may have noticed some rather long and seemingly unwieldy test method names
|
||||||
|
in our code, but that's okay because they help us know exactly what's failing
|
||||||
|
when we run our tests. With an ambiguously named test method we may have to
|
||||||
|
crack open test.py after running the tests just to see what it is that failed.
|
||||||
|
For this reason you should also test only one thing per test method. Don't
|
||||||
|
write a test method named testFoobarAndBaz. Just write two test methods,
|
||||||
|
testFoobar and testBaz. Also, it is important to note that test methods must
|
||||||
|
begin with test and that any method within the class that does begin with test
|
||||||
|
will be run as a test by the supybot-test program. If you want to write
|
||||||
|
utility functions in your test class that's fine, but don't name them
|
||||||
|
something that begins with test or they will be executed as tests.
|
||||||
|
|
||||||
|
Including Extra Setup
|
||||||
|
|
||||||
|
Some tests you write may require a little bit of setup. For the most part it's
|
||||||
|
okay just to include that in the individual test method itself, but if you're
|
||||||
|
duplicating a lot of setup code across all or most of your test methods it's
|
||||||
|
best to use the setUp method to perform whatever needs to be done prior to
|
||||||
|
each test method.
|
||||||
|
|
||||||
|
The setUp method is inherited from the whichever plugin test case class you
|
||||||
|
chose for your tests, and you can add whatever functionality you want to it.
|
||||||
|
Note the important distinction, however: you should be adding to it and not
|
||||||
|
overriding it. Just define setUp in your own plugin test case class and it
|
||||||
|
will be run before all the test methods are invoked.
|
||||||
|
|
||||||
|
Let's do a quick example of one. Let's write a setUp method which registers a
|
||||||
|
test user for our test bot:
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
ChannelPluginTestCase.setUp(self) # important!!
|
||||||
|
# Create a valid user to use
|
||||||
|
self.prefix = 'foo!bar@baz'
|
||||||
|
self.feedMsg('register tester moo', to=self.nick, frm=self.prefix))
|
||||||
|
m = self.getMsg() # Response to registration.
|
||||||
|
|
||||||
|
Now notice how the first line calls the parent class's setUp method first?
|
||||||
|
This must be done first. Otherwise several problems are likely to arise. For
|
||||||
|
one, you wouldn't have an irc object at self.irc that we use later on nor
|
||||||
|
would self.nick be set.
|
||||||
|
|
||||||
|
As for the rest of the method, you'll notice a few things that are available
|
||||||
|
to the plugin test author. self.prefix refers to the hostmask of the
|
||||||
|
hypothetical test user which will be "talking" to the bot, issuing commands.
|
||||||
|
We set it to some generically fake hostmask, and then we use feedMsg to feed
|
||||||
|
it a private message (using the bot's nick, accessible via self.nick) to
|
||||||
|
register the username "tester" with the password "moo". We have to do it this
|
||||||
|
way (rather than what you'll find out is the standard way of issuing commands
|
||||||
|
to the bot in test cases a little later) because registration must be done in
|
||||||
|
private. And lastly, since feedMsg doesn't dequeue any messages from the bot
|
||||||
|
after getting fed a message, we perform a getMsg to get the response. You're
|
||||||
|
not expected to know all this yet, but do take note of it since using these
|
||||||
|
methods in test-writing is not uncommon. These utility methods as well as all
|
||||||
|
of the available assertions are covered in the next section.
|
||||||
|
|
||||||
|
So, now in any of the test methods we write, we'll be able to count on the
|
||||||
|
fact that there will be a registered user "tester" with a password of "moo",
|
||||||
|
and since we changed our prefix by altering self.prefix and registered after
|
||||||
|
doing so, we are now identified as this user for all messages we send unless
|
||||||
|
we specify that they are coming from some other prefix.
|
||||||
|
|
||||||
|
The Opposite of Setting-up: Tearing Down
|
||||||
|
|
||||||
|
If you did some things in your setUp that you want to clean up after, then
|
||||||
|
this code belongs in the tearDown method of your test case class. It's
|
||||||
|
essentially the same as setUp except that you probably want to wait to invoke
|
||||||
|
the parent class's tearDown until after you've done all of your tearing down.
|
||||||
|
But do note that you do still have to invoke the parent class's tearDown
|
||||||
|
method if you decide to add in your own tear-down stuff.
|
||||||
|
|
||||||
|
Setting Config Variables for Testing
|
||||||
|
|
||||||
|
Before we delve into all of the fun assertions we can use in our test methods
|
||||||
|
it's worth noting that each plugin test case can set custom values for any
|
||||||
|
Supybot config variable they want rather easily. Much like how we can simply
|
||||||
|
list the plugins we want loaded for our tests in the plugins attribute of our
|
||||||
|
test case class, we can set config variables by creating a mapping of
|
||||||
|
variables to values with the config attribute.
|
||||||
|
|
||||||
|
So if, for example, we wanted to disable nested commands within our plugin
|
||||||
|
testing for some reason, we could just do this:
|
||||||
|
|
||||||
|
class MyPluginTestCase(PluginTestCase):
|
||||||
|
config = {'supybot.commands.nested': False}
|
||||||
|
|
||||||
|
def testThisThing(self):
|
||||||
|
# stuff
|
||||||
|
|
||||||
|
And now you can be assured that supybot.commands.nested is going to be off for
|
||||||
|
all of your test methods in this test case class.
|
||||||
|
|
||||||
|
Plugin Test Methods
|
||||||
|
===================
|
||||||
|
The full list of test methods and how to use them.
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
|
||||||
|
You know how to make plugin test case classes and you know how to do just
|
||||||
|
about everything with them except to actually test stuff. Well, listed below
|
||||||
|
are all of the assertions used in tests. If you're unfamiliar with what an
|
||||||
|
assertion is in code testing, it is basically a requirement of something that
|
||||||
|
must be true in order for that test to pass. It's a necessary condition. If
|
||||||
|
any assertion within a test method fails the entire test method fails and it
|
||||||
|
goes on to the next one.
|
||||||
|
|
||||||
|
Assertions
|
||||||
|
|
||||||
|
All of these are methods of the plugin test classes themselves and hence are
|
||||||
|
accessed by using self.assertWhatever in your test methods. These are sorted
|
||||||
|
in order of relative usefulness.
|
||||||
|
|
||||||
|
* assertResponse(query, expectedResponse) - Feeds query to the bot as a
|
||||||
|
message and checks to make sure the response is expectedResponse. The
|
||||||
|
test fails if they do not match (note that prefixed nicks in the
|
||||||
|
response do not need to be included in the expectedResponse).
|
||||||
|
|
||||||
|
* assertError(query) - Feeds query to the bot and expects an error in
|
||||||
|
return. Fails if the bot doesn't return an error.
|
||||||
|
|
||||||
|
* assertNotError(query) - The opposite of assertError. It doesn't matter
|
||||||
|
what the response to query is, as long as it isn't an error. If it is
|
||||||
|
not an error, this test passes, otherwise it fails.
|
||||||
|
|
||||||
|
* assertRegexp(query, regexp, flags=re.I) - Feeds query to the bot and
|
||||||
|
expects something matching the regexp (no m// required) in regexp with
|
||||||
|
the supplied flags. Fails if the regexp does not match the bot's
|
||||||
|
response.
|
||||||
|
|
||||||
|
* assertNotRegexp(query, regexp, flags=re.I) - The opposite of
|
||||||
|
assertRegexp. Fails if the bot's output matches regexp with the
|
||||||
|
supplied flags.
|
||||||
|
|
||||||
|
* assertHelp(query) - Expects query to return the help for that command.
|
||||||
|
Fails if the command help is not triggered.
|
||||||
|
|
||||||
|
* assertAction(query, expectedResponse=None) - Feeds query to the bot and
|
||||||
|
expects an action in response, specifically expectedResponse if it is
|
||||||
|
supplied. Otherwise, the test passes for any action response.
|
||||||
|
|
||||||
|
* assertActionRegexp(query, regexp, flags=re.I) - Basically like
|
||||||
|
assertRegexp but carries the extra requirement that the response must
|
||||||
|
be an action or the test will fail.
|
||||||
|
|
||||||
|
Utilities
|
||||||
|
|
||||||
|
* feedMsg(query, to=None, frm=None) - Simply feeds query to whoever is
|
||||||
|
specified in to or to the bot itself if no one is specified. Can also
|
||||||
|
optionally specify the hostmask of the sender with the frm keyword.
|
||||||
|
Does not actually perform any assertions.
|
||||||
|
|
||||||
|
* getMsg(query) - Feeds query to the bot and gets the response.
|
||||||
|
|
||||||
|
Other Tests
|
||||||
|
===========
|
||||||
|
If you had to write helper code for a plugin and want to test it, here's
|
||||||
|
how.
|
||||||
|
|
||||||
|
Previously we've only discussed how to test stuff in the plugin that is
|
||||||
|
intended for IRC. Well, we realize that some Supybot plugins will require
|
||||||
|
utility code that doesn't necessarily require all of the overhead of setting
|
||||||
|
up IRC stuff, and so we provide a more lightweight test case class,
|
||||||
|
SupyTestCase, which is a very very light wrapper around unittest.TestCase
|
||||||
|
(from the standard unittest module) that basically just provides a little
|
||||||
|
extra logging. This test case class is what you should use for writing those
|
||||||
|
test cases which test things that are independent of IRC.
|
||||||
|
|
||||||
|
For example, in the MoobotFactoids plugin there is a large chunk of utility
|
||||||
|
code dedicating to parsing out random choices within a factoid using a class
|
||||||
|
called OptionList. So, we wrote the OptionListTestCase as a SupyTestCase for
|
||||||
|
the MoobotFactoids plugin. The setup for test methods is basically the same as
|
||||||
|
before, only you don't have to define plugins since this is independent of
|
||||||
|
IRC.
|
||||||
|
|
||||||
|
You still have the choice of using setUp and tearDown if you wish, since those
|
||||||
|
are inherited from unittest.TestCase. But, the same rules about calling the
|
||||||
|
setUp or tearDown method from the parent class still apply.
|
||||||
|
|
||||||
|
With all this in hand, now you can write great tests for your Supybot plugins!
|
||||||
|
|
132
docs/CAPABILITIES
Normal file
132
docs/CAPABILITIES
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
Introduction
|
||||||
|
------------
|
||||||
|
|
||||||
|
Ok, some explanation of the capabilities system is probably in order. With
|
||||||
|
most IRC bots (including the ones I've written myself prior to this one) "what
|
||||||
|
a user can do" is set in one of two ways. On the *really* simple bots, each
|
||||||
|
user has a numeric "level" and commands check to see if a user has a "high
|
||||||
|
enough level" to perform some operation. On bots that are slightly more
|
||||||
|
complicated, users have a list of "flags" whose meanings are hardcoded, and the
|
||||||
|
bot checks to see if a user possesses the necessary flag before performing some
|
||||||
|
operation. Both methods, IMO, are rather arbitrary, and force the user and the
|
||||||
|
programmer to be unduly confined to less expressive constructs.
|
||||||
|
|
||||||
|
This bot is different. Every user has a set of "capabilities" that is
|
||||||
|
consulted every time they give the bot a command. Commands, rather than
|
||||||
|
checking for a user level of 100, or checking if the user has an 'o' flag, are
|
||||||
|
instead able to check if a user has the 'owner' capability. At this point such
|
||||||
|
a difference might not seem revolutionary, but at least we can already tell
|
||||||
|
that this method is self-documenting, and easier for users and developers to
|
||||||
|
understand what's truly going on.
|
||||||
|
|
||||||
|
User Capabilities
|
||||||
|
-----------------
|
||||||
|
What the heck can these capabilities DO?
|
||||||
|
|
||||||
|
If that was all, well, the capability system would be *cool*, but not many
|
||||||
|
people would say it was *awesome*. But it **is** awesome! Several things are
|
||||||
|
happening behind the scenes that make it awesome, and these are things that
|
||||||
|
couldn't happen if the bot was using numeric userlevels or single-character
|
||||||
|
flags. First, whenever a user issues the bot a command, the command dispatcher
|
||||||
|
checks to make sure the user doesn't have the "anticapability" for that
|
||||||
|
command. An anticapability is a capability that, instead of saying "what a
|
||||||
|
user can do", says what a user *cannot* do. It's formed rather simply by
|
||||||
|
adding a dash ('-') to the beginning of a capability; 'rot13' is a capability,
|
||||||
|
and '-rot13' is an anticapability.
|
||||||
|
|
||||||
|
Anyway, when a user issues the bot a command, perhaps 'calc' or 'help', the bot
|
||||||
|
first checks to make sure the user doesn't have the '-calc' or the '-help'
|
||||||
|
(anti)capabilities before even considering responding to the user. So commands
|
||||||
|
can be turned on or off on a *per user* basis, offering fine-grained control
|
||||||
|
not often (if at all!) seen in other bots. This can be further refined by
|
||||||
|
limiting the (anti)capability to a command in a specific plugin or even an
|
||||||
|
entire plugin. For example, the rot13 command is in the Filter plugin. If a
|
||||||
|
user should be able to use another rot13 command, but not the one in the Format
|
||||||
|
plugin, they would simply need to be given '-Format.rot13' anticapability.
|
||||||
|
Similarly, if a user were to be banned from using the Filter plugin altogether,
|
||||||
|
they would simply need to be given the '-Format' anticapability.
|
||||||
|
|
||||||
|
Channel Capabilities
|
||||||
|
--------------------
|
||||||
|
What if #linux wants completely different capabilities from #windows?
|
||||||
|
|
||||||
|
But that's not all! The capabilities system also supports *channel*
|
||||||
|
capabilities, which are capabilities that only apply to a specific channel;
|
||||||
|
they're of the form '#channel,capability'. Whenever a user issues a command to
|
||||||
|
the bot in a channel, the command dispatcher also checks to make sure the user
|
||||||
|
doesn't have the anticapability for that command *in that channel*, and if the
|
||||||
|
user does, the bot won't respond to the user in the channel. Thus now, in
|
||||||
|
addition to having the ability to turn individual commands on or off for an
|
||||||
|
individual user, we can now turn commands on or off for an individual user on
|
||||||
|
an individual channel!
|
||||||
|
|
||||||
|
So when a user 'foo' sends a command 'bar' to the bot on channel '#baz', first
|
||||||
|
the bot checks to see if the user has the anticapability for the command by
|
||||||
|
itself, '-bar'. If so, it errors right then and there, telling the user that
|
||||||
|
he lacks the 'bar' capability. If the user doesn't have that anticapability,
|
||||||
|
then the bot checks to see if the user issued the command over a channel, and
|
||||||
|
if so, checks to see if the user has the antichannelcapability for that
|
||||||
|
command, '#baz,-bar'. If so, again, he tells the user that he lacks the 'bar'
|
||||||
|
capability. If neither of these anticapabilities are present, then the bot
|
||||||
|
just responds to the user like normal.
|
||||||
|
|
||||||
|
Default Capabilities
|
||||||
|
--------------------
|
||||||
|
So what capabilities am I dealing with already?
|
||||||
|
|
||||||
|
There are several default capabilities the bot uses. The most important of
|
||||||
|
these is the 'owner' capability. This capability allows the person having it
|
||||||
|
to use *any* command. It's best to keep this capability reserved to people who
|
||||||
|
actually have access to the shell the bot is running on. It's so important, in
|
||||||
|
fact, that the bot will not allow you to add it with a command--you'll have you
|
||||||
|
edit the users file directly to give it to someone.
|
||||||
|
|
||||||
|
There is also the 'admin' capability for non-owners that are highly trusted to
|
||||||
|
administer the bot appropriately. They can do things such as change the bot's
|
||||||
|
nick, cause the bot to ignore a given user, make the bot join or part channels,
|
||||||
|
etc. They generally cannot do administration related to channels, which is
|
||||||
|
reserved for people with the next capability.
|
||||||
|
|
||||||
|
People who are to administer channels with the bot should have the
|
||||||
|
'#channel,op' capability--whatever channel they are to administrate, they
|
||||||
|
should have that channel capability for 'op'. For example, since I want
|
||||||
|
inkedmn to be an administrator in #supybot, I'll give him the '#supybot,op'
|
||||||
|
capability. This is in addition to his 'admin' capability, since the 'admin'
|
||||||
|
capability doesn't give the person having it control over channels.
|
||||||
|
'#channel,op' is used for such things as giving/receiving ops, kickbanning
|
||||||
|
people, lobotomizing the bot, ignoring users in the channel, and managing the
|
||||||
|
channel capabilities. The '#channel,op' capability is also basically the
|
||||||
|
equivalent of the 'owner' capability for capabilities involving
|
||||||
|
#channel--basically anyone with the #channel,op capability is considered to
|
||||||
|
have all positive capabilities and no negative capabilities for #channel.
|
||||||
|
|
||||||
|
One other globally important capability exists: 'trusted'. This is a command
|
||||||
|
that basically says "This user can be trusted not to try and crash the bot." It
|
||||||
|
allows users to call commands like 'icalc' in the 'Math' plugin, which can
|
||||||
|
cause the bot to begin a calculation that could potentially never return (a
|
||||||
|
calculation like '10**10**10**10'). Another command that requires the 'trusted'
|
||||||
|
capability is the 're' command in the 'Utilities' plugin, which (due to the
|
||||||
|
regular expression implementation in Python (and any other language that uses
|
||||||
|
NFA regular expressions, like Perl or Ruby or Lua or ...) which can allow a
|
||||||
|
regular expression to take exponential time to process). Consider what would
|
||||||
|
happen if someone gave the bot the command 're [format join "" s/./ [dict go]
|
||||||
|
/] [dict go]' It would basically replace every character in the output of
|
||||||
|
'dict go' (14,896 characters!) with the entire output of 'dict go', resulting
|
||||||
|
in 221MB of memory allocated! And that's not even the worst example!
|
||||||
|
|
||||||
|
Final Word
|
||||||
|
----------
|
||||||
|
|
||||||
|
From a programmer's perspective, capabilties are flexible and easy to use. Any
|
||||||
|
command can check if a user has any capability, even ones not thought of when
|
||||||
|
the bot was originally written. Plugins can easily add their own
|
||||||
|
capabilities--it's as easy as just checking for a capability and documenting
|
||||||
|
somewhere that a user needs that capability to do something.
|
||||||
|
|
||||||
|
From an user's perspective, capabilities remove a lot of the mystery and
|
||||||
|
esotery of bot control, in addition to giving a bot owner absolutely
|
||||||
|
finegrained control over what users are allowed to do with the bot.
|
||||||
|
Additionally, defaults can be set by the bot owner for both individual channels
|
||||||
|
and for the bot as a whole, letting an end-user set the policy he wants the bot
|
||||||
|
to follow for users that haven't yet registered in his user database. It's
|
||||||
|
really a revolution!
|
343
docs/USING_UTILS
Normal file
343
docs/USING_UTILS
Normal file
@ -0,0 +1,343 @@
|
|||||||
|
Using Supybot's utils module
|
||||||
|
----------------------------
|
||||||
|
Supybot provides a wealth of utilities for plugin writers in the supybot.utils
|
||||||
|
module, this tutorial describes these utilities and shows you how to use them.
|
||||||
|
|
||||||
|
str.py
|
||||||
|
======
|
||||||
|
The Format Function
|
||||||
|
|
||||||
|
The supybot.utils.str module provides a bunch of utility functions for
|
||||||
|
handling string values. This section contains a quick rundown of all of the
|
||||||
|
functions available, along with descriptions of the arguments they take. First
|
||||||
|
and foremost is the format function, which provides a lot of capability in
|
||||||
|
just one function that uses string-formatting style to accomplish a lot. So
|
||||||
|
much so that it gets its own section in this tutorial. All other functions
|
||||||
|
will be in other sections. format takes several arguments - first, the format
|
||||||
|
string (using the format characters described below), and then after that,
|
||||||
|
each individual item to be formatted. Do not attempt to use the % operator to
|
||||||
|
do the formatting because that will fall back on the normal string formatting
|
||||||
|
operator. The format function uses the following string formatting characters.
|
||||||
|
|
||||||
|
* % - literal "%"
|
||||||
|
* i - integer
|
||||||
|
* s - string
|
||||||
|
* f - float
|
||||||
|
* r - repr
|
||||||
|
* b - form of the verb "to be" (takes an int)
|
||||||
|
* h - form of the verb "to have" (takes an int)
|
||||||
|
* L - commaAndify (takes a list of strings or a tuple of ([strings], and))
|
||||||
|
* p - pluralize (takes a string)
|
||||||
|
* q - quoted (takes a string)
|
||||||
|
* n - n items (takes a 2-tuple of (n, item) or a 3-tuple of (n, between, item))
|
||||||
|
* t - time, formatted (takes an int)
|
||||||
|
* u - url, wrapped in braces
|
||||||
|
|
||||||
|
Here are a few examples to help elaborate on the above descriptions:
|
||||||
|
|
||||||
|
>>> format("Error %q has been reported %n. For more information, see %u.",
|
||||||
|
"AttributeError", (5, "time"), "http://supybot.com")
|
||||||
|
|
||||||
|
'Error "AttributeError" has been reported 5 times. For more information,
|
||||||
|
see <http://supybot.com>.'
|
||||||
|
|
||||||
|
>>> i = 4
|
||||||
|
>>> format("There %b %n at this time. You are only allowed %n at any given
|
||||||
|
time", i, (i, "active", "thread"), (5, "active", "thread"))
|
||||||
|
'There are 4 active threads at this time. You are only allowed 5 active
|
||||||
|
threads at any given time'
|
||||||
|
|
||||||
|
>>> i = 1
|
||||||
|
>>> format("There %b %n at this time. You are only allowed %n at any given
|
||||||
|
time", i, (i, "active", "thread"), (5, "active", "thread"))
|
||||||
|
'There is 1 active thread at this time. You are only allowed 5 active
|
||||||
|
threads at any given time'
|
||||||
|
|
||||||
|
>>> ops = ["foo", "bar", "baz"]
|
||||||
|
>>> format("The following %n %h the %s capability: %L", (len(ops), "user"),
|
||||||
|
len(ops), "op", ops)
|
||||||
|
'The following 3 users have the op capability: foo, bar, and baz'
|
||||||
|
|
||||||
|
As you can see, you can combine all sorts of combinations of formatting
|
||||||
|
strings into one. In fact, that was the major motivation behind format. We
|
||||||
|
have specific functions that you can use individually for each of those
|
||||||
|
formatting types, but it became much easier just to use special formatting
|
||||||
|
chars and the format function than concatenating a bunch of strings that were
|
||||||
|
the result of other utils.str functions.
|
||||||
|
|
||||||
|
The Other Functions
|
||||||
|
|
||||||
|
These are the functions that can't be handled by format. They are sorted in
|
||||||
|
what I perceive to be the general order of usefulness (and I'm leaving the
|
||||||
|
ones covered by format for the next section).
|
||||||
|
|
||||||
|
* ellipsisify(s, n) - Returns a shortened version of a string. Produces up
|
||||||
|
to the first n chars at the nearest word boundary.
|
||||||
|
- s: the string to be shortened
|
||||||
|
- n: the number of characters to shorten it to
|
||||||
|
|
||||||
|
* perlReToPythonRe(s) - Converts a Perl-style regexp (e.g., "/abcd/i" or
|
||||||
|
"m/abcd/i") to an actual Python regexp (an re object)
|
||||||
|
- s: the regexp string
|
||||||
|
|
||||||
|
* perlReToReplacer(s) - converts a perl-style replacement regexp (eg,
|
||||||
|
"s/foo/bar/g") to a Python function that performs such a replacement
|
||||||
|
- s: the regexp string
|
||||||
|
|
||||||
|
* dqrepr(s) - Returns a repr() of s guaranteed to be in double quotes.
|
||||||
|
(Double Quote Repr)
|
||||||
|
- s: the string to be double-quote repr()'ed
|
||||||
|
|
||||||
|
* toBool(s) - Determines whether or not a string means True or False and
|
||||||
|
returns the appropriate boolean value. True is any of "true", "on",
|
||||||
|
"enable", "enabled", or "1". False is any of "false", "off", "disable",
|
||||||
|
"disabled", or "0".
|
||||||
|
- s: the string to determine the boolean value for
|
||||||
|
|
||||||
|
* rsplit(s, sep=None, maxsplit=-1) - functionally the same as str.split in
|
||||||
|
the Python standard library except splitting from the right instead of the
|
||||||
|
left. Python 2.4 has str.rsplit (which this function defers to for those
|
||||||
|
versions >= 2.4), but Python 2.3 did not.
|
||||||
|
- s: the string to be split
|
||||||
|
- sep: the separator to split on, defaults to whitespace
|
||||||
|
- maxsplit: the maximum number of splits to perform, -1 splits all
|
||||||
|
possible splits.
|
||||||
|
|
||||||
|
* normalizeWhitespace(s) - reduces all multi-spaces in a string to a
|
||||||
|
single space
|
||||||
|
- s: the string to normalize
|
||||||
|
|
||||||
|
* depluralize(s) - the opposite of pluralize
|
||||||
|
- s: the string to depluralize
|
||||||
|
|
||||||
|
* unCommaThe(s) - Takes a string of the form "foo, the" and turns it into
|
||||||
|
"the foo"
|
||||||
|
- s: string, the
|
||||||
|
|
||||||
|
* distance(s, t) - computes the levenshtein distance (or "edit distance")
|
||||||
|
between two strings
|
||||||
|
- s: the first string
|
||||||
|
- t: the second string
|
||||||
|
|
||||||
|
* soundex(s, length=4) - computes the soundex for a given string
|
||||||
|
- s: the string to compute the soundex for
|
||||||
|
- length: the length of the soundex to generate
|
||||||
|
|
||||||
|
* matchCase(s1, s2) - Matches the case of the first string in the second
|
||||||
|
string.
|
||||||
|
- s1: the first string
|
||||||
|
- s2: the string which will be made to match the case of the first
|
||||||
|
|
||||||
|
The Commands Format Already Covers
|
||||||
|
|
||||||
|
These commands aren't necessary because you can achieve them more easily by
|
||||||
|
using the format command, but they exist if you decide you want to use them
|
||||||
|
anyway though it is greatly discouraged for general use.
|
||||||
|
|
||||||
|
* commaAndify(seq, comma=",", And="and") - transforms a list of items into
|
||||||
|
a comma separated list with an "and" preceding the last element. For
|
||||||
|
example, ["foo", "bar", "baz"] becomes "foo, bar, and baz". Is smart
|
||||||
|
enough to convert two-element lists to just "item1 and item2" as well.
|
||||||
|
- seq: the sequence of items (don't have to be strings, but need to
|
||||||
|
be 'str()'-able)
|
||||||
|
- comma: the character to use to separate the list
|
||||||
|
- And: the word to use before the last element
|
||||||
|
|
||||||
|
* pluralize(s) - Returns the plural of a string. Put any exceptions to the
|
||||||
|
general English rules of pluralization in the plurals dictionary in
|
||||||
|
supybot.utils.str.
|
||||||
|
- s: the string to pluralize
|
||||||
|
|
||||||
|
* nItems(n, item, between=None) - returns a string that describes a given
|
||||||
|
number of an item (with any string between the actual number and the item
|
||||||
|
itself), handles pluralization with the pluralize function above. Note
|
||||||
|
that the arguments here are in a different order since between is
|
||||||
|
optional.
|
||||||
|
- n: the number of items
|
||||||
|
- item: the type of item
|
||||||
|
- between: the optional string that goes between the number and the
|
||||||
|
type of item
|
||||||
|
|
||||||
|
* quoted(s) - Returns the string surrounded by double-quotes.
|
||||||
|
- s: the string to quote
|
||||||
|
|
||||||
|
* be(i) - Returns the proper form of the verb "to be" based on the number
|
||||||
|
provided (be(1) is "is", be(anything else) is "are")
|
||||||
|
- i: the number of things that "be"
|
||||||
|
|
||||||
|
* has(i) - Returns the proper form of the verb "to have" based on the
|
||||||
|
number provided (has(1) is "has", has(anything else) is "have")
|
||||||
|
- i: the number of things that "has"
|
||||||
|
|
||||||
|
structures.py
|
||||||
|
=============
|
||||||
|
Intro
|
||||||
|
|
||||||
|
This module provides a number of useful data structures that aren't found in
|
||||||
|
the standard Python library. For the most part they were created as needed for
|
||||||
|
the bot and plugins themselves, but they were created in such a way as to be
|
||||||
|
of general use for anyone who needs a data structure that performs a like
|
||||||
|
duty. As usual in this document, I'll try and order these in order of
|
||||||
|
usefulness, starting with the most useful.
|
||||||
|
|
||||||
|
The queue classes
|
||||||
|
|
||||||
|
The structures module provides two general-purpose queue classes for you to
|
||||||
|
use. The "queue" class is a robust full-featured queue that scales up to
|
||||||
|
larger sized queues. The "smallqueue" class is for queues that will contain
|
||||||
|
fewer (less than 1000 or so) items. Both offer the same common interface,
|
||||||
|
which consists of:
|
||||||
|
|
||||||
|
* a constructor which will optionally accept a sequence to start the queue
|
||||||
|
off with
|
||||||
|
* enqueue(item) - adds an item to the back of the queue
|
||||||
|
* dequeue() - removes (and returns) the item from the front of the queue
|
||||||
|
* peek() - returns the item from the front of the queue without removing
|
||||||
|
it
|
||||||
|
* reset() - empties the queue entirely
|
||||||
|
|
||||||
|
In addition to these general-use queue classes, there are two other more
|
||||||
|
specialized queue classes as well. The first is the "TimeoutQueue" which holds
|
||||||
|
a queue of items until they reach a certain age and then they are removed from
|
||||||
|
the queue. It features the following:
|
||||||
|
|
||||||
|
* TimeoutQueue(timeout, queue=None) - you must specify the timeout (in
|
||||||
|
seconds) in the constructor. Note that you can also optionally pass it a
|
||||||
|
queue which uses any implementation you wish to use whether it be one of
|
||||||
|
the above (queue or smallqueue) or if it's some custom queue you create
|
||||||
|
that implements the same interface. If you don't pass it a queue instance
|
||||||
|
to use, it will build its own using smallqueue.
|
||||||
|
* reset(), enqueue(item), dequeue() - all same as above queue classes
|
||||||
|
* setTimeout(secs) - allows you to change the timeout value
|
||||||
|
|
||||||
|
And for the final queue class, there's the "MaxLengthQueue" class. As you may
|
||||||
|
have guessed, it's a queue that is capped at a certain specified length. It
|
||||||
|
features the following:
|
||||||
|
|
||||||
|
* MaxLengthQueue(length, seq=()) - the constructor naturally requires that
|
||||||
|
you set the max length and it allows you to optionally pass in a sequence
|
||||||
|
to be used as the starting queue. The underlying implementation is
|
||||||
|
actually the queue from before.
|
||||||
|
* enqueue(item) - adds an item onto the back of the queue and if it would
|
||||||
|
push it over the max length, it dequeues the item on the front (it does
|
||||||
|
not return this item to you)
|
||||||
|
* all the standard methods from the queue class are inherited for this class
|
||||||
|
|
||||||
|
The Other Structures
|
||||||
|
|
||||||
|
The most useful of the other structures is actually very similar to the
|
||||||
|
"MaxLengthQueue". It's the "RingBuffer", which is essentially a MaxLengthQueue
|
||||||
|
which fills up to its maximum size and then circularly replaces the old
|
||||||
|
contents as new entries are added instead of dequeuing. It features the
|
||||||
|
following:
|
||||||
|
|
||||||
|
* RingBuffer(size, seq=()) - as with the MaxLengthQueue you specify the
|
||||||
|
size of the RingBuffer and optionally give it a sequence.
|
||||||
|
* append(item) - adds item to the end of the buffer, pushing out an item
|
||||||
|
from the front if necessary
|
||||||
|
* reset() - empties out the buffer entirely
|
||||||
|
* resize(i) - shrinks/expands the RingBuffer to the size provided
|
||||||
|
* extend(seq) - append the items from the provided sequence onto the end
|
||||||
|
of the RingBuffer
|
||||||
|
|
||||||
|
The next data structure is the TwoWayDictionary, which as the name implies is
|
||||||
|
a dictionary in which key-value pairs have mappings going both directions. It
|
||||||
|
features the following:
|
||||||
|
|
||||||
|
* TwoWayDictionary(seq=(), **kwargs) - Takes an optional sequence of (key,
|
||||||
|
value) pairs as well as any key=value pairs specified in the constructor
|
||||||
|
as initial values for the two-way dict.
|
||||||
|
* other than that, no extra features that a normal Python dict doesn't
|
||||||
|
already offer with the exception that any (key, val) pair added to the
|
||||||
|
dict is also added as (val, key) as well, so the mapping goes both ways.
|
||||||
|
Elements are still accessed the same way you always do with Python
|
||||||
|
'dict's.
|
||||||
|
|
||||||
|
There is also a MultiSet class available, but it's very unlikely that it will
|
||||||
|
serve your purpose, so I won't go into it here. The curious coder can go check
|
||||||
|
the source and see what it's all about if they wish (it's only used once in our
|
||||||
|
code, in the Relay plugin).
|
||||||
|
|
||||||
|
web.py
|
||||||
|
======
|
||||||
|
The web portion of Supybot's utils module is mainly used for retrieving data
|
||||||
|
from websites but it also has some utility functions pertaining to HTML and
|
||||||
|
email text as well. The functions in web are listed below, once again in order
|
||||||
|
of usefulness.
|
||||||
|
|
||||||
|
* getUrl(url, size=None, headers=None) - gets the data at the URL provided
|
||||||
|
and returns it as one large string
|
||||||
|
- url: the location of the data to be retrieved or a urllib2.Request
|
||||||
|
object to be used in the retrieval
|
||||||
|
- size: the maximum number of bytes to retrieve, defaults to None,
|
||||||
|
meaning that it is to try to retrieve all data
|
||||||
|
- headers: a dictionary mapping header types to header data
|
||||||
|
|
||||||
|
* getUrlFd(url, headers=None) - returns a file-like object for a url
|
||||||
|
- url: the location of the data to be retrieved or a urllib2.Request
|
||||||
|
object to be used in the retrieval
|
||||||
|
- headers: a dictionary mapping header types to header data
|
||||||
|
|
||||||
|
* htmlToText(s, tagReplace=" ") - strips out all tags in a string of HTML,
|
||||||
|
replacing them with the specified character
|
||||||
|
- s: the HTML text to strip the tags out of
|
||||||
|
- tagReplace: the string to replace tags with
|
||||||
|
|
||||||
|
* strError(e) - pretty-printer for web exceptions, returns a descriptive
|
||||||
|
string given a web-related exception
|
||||||
|
- e: the exception to pretty-print
|
||||||
|
|
||||||
|
* mungeEmail(s) - a naive e-mail obfuscation function, replaces "@" with
|
||||||
|
"AT" and "." with "DOT"
|
||||||
|
- s: the e-mail address to obfuscate
|
||||||
|
|
||||||
|
* getDomain(url) - returns the domain of a URL
|
||||||
|
- url: the URL in question
|
||||||
|
|
||||||
|
The Best of the Rest
|
||||||
|
====================
|
||||||
|
Highlights the most useful of the remaining functionality in supybot.utils
|
||||||
|
|
||||||
|
Intro
|
||||||
|
|
||||||
|
Rather than document each of the remaining portions of the supybot.utils
|
||||||
|
module, I've elected to just pick out the choice bits from specific parts and
|
||||||
|
document those instead. Here they are, broken out by module name.
|
||||||
|
|
||||||
|
supybot.utils.file - file utilities
|
||||||
|
|
||||||
|
* touch(filename) - updates the access time of a file by opening it for
|
||||||
|
writing and immediately closing it
|
||||||
|
* mktemp(suffix="") - creates a decent random string, suitable for a
|
||||||
|
temporary filename with the given suffix, if
|
||||||
|
provided
|
||||||
|
* the AtomicFile class - used for files that need to be atomically
|
||||||
|
written, i.e., if there's a failure the original
|
||||||
|
file remains unmodified. For more info consult
|
||||||
|
file.py in src/utils
|
||||||
|
|
||||||
|
supybot.utils.gen - general utilities
|
||||||
|
|
||||||
|
* timeElapsed(elapsed, [lots of optional args]) - given the number of
|
||||||
|
seconds elapsed, returns a string with the English description of the
|
||||||
|
amount of time passed, consult gen.py in src/utils for the exact
|
||||||
|
argument list and documentation if you feel you could use this
|
||||||
|
function.
|
||||||
|
* exnToString(e) - improved exception-to-string function. Provides nicer
|
||||||
|
output than a simple str(e).
|
||||||
|
* InsensitivePreservingDict class - a dict class that is case-insensitive
|
||||||
|
when accessing keys
|
||||||
|
|
||||||
|
supybot.utils.iter - iterable utilities
|
||||||
|
|
||||||
|
* len(iterable) - returns the length of a given iterable
|
||||||
|
* groupby(key, iterable) - equivalent to the itertools.groupby function
|
||||||
|
available as of Python 2.4. Provided for
|
||||||
|
backwards compatibility.
|
||||||
|
* any(p, iterable) - Returns true if any element in the iterable satisfies
|
||||||
|
the predicate p
|
||||||
|
* all(p, iterable) - Returns true if all elements in the iterable satisfy
|
||||||
|
the predicate p
|
||||||
|
* choice(iterable) - Returns a random element from the iterable
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user