diff --git a/Factoids/README.txt b/Factoids/README.txt new file mode 100644 index 000000000..d60b47a97 --- /dev/null +++ b/Factoids/README.txt @@ -0,0 +1 @@ +Insert a description of your plugin here, with any notes, etc. about using it. diff --git a/Factoids/__init__.py b/Factoids/__init__.py new file mode 100644 index 000000000..8511d291d --- /dev/null +++ b/Factoids/__init__.py @@ -0,0 +1,64 @@ +### +# Copyright (c) 2002-2005, Jeremiah Fincher +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +### + +""" +Handles 'factoids,' little tidbits of information held in a database and +available on demand via several commands. +""" + +import supybot +import supybot.world as world + +# Use this for the version of this plugin. You may wish to put a CVS keyword +# in here if you're keeping the plugin in CVS or some similar system. +__version__ = "0.1" + +__author__ = supybot.authors.jemfinch + +# This is a dictionary mapping supybot.Author instances to lists of +# contributions. +__contributors__ = {} + +# This is a url where the most recent plugin package can be downloaded. +__url__ = '' # 'http://supybot.com/Members/yourname/Factoids/download' + +import config +import plugin +reload(plugin) # In case we're being reloaded. +# Add more reloads here if you add third-party modules and want them to be +# reloaded when this plugin is reloaded. Don't forget to import them as well! + +if world.testing: + import test + +Class = plugin.Class +configure = config.configure + + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: diff --git a/Factoids/config.py b/Factoids/config.py new file mode 100644 index 000000000..b4c9b5a1b --- /dev/null +++ b/Factoids/config.py @@ -0,0 +1,61 @@ +### +# Copyright (c) 2002-2005, Jeremiah Fincher +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +### + +import supybot.conf as conf +import supybot.registry as registry + +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 + conf.registerPlugin('Factoids', True) + + +Factoids = conf.registerPlugin('Factoids') +conf.registerChannelValue(Factoids, 'learnSeparator', + registry.String('as', """Determines what separator must be used in the + learn command. Defaults to 'as' -- learn as . Users might + feel more comfortable with 'is' or something else, so it's + configurable.""")) +conf.registerChannelValue(Factoids, 'showFactoidIfOnlyOneMatch', + registry.Boolean(True, """Determines whether the bot will reply with the + single matching factoid if only one factoid matches when using the search + command.""")) +conf.registerChannelValue(Factoids, 'replyWhenInvalidCommand', + registry.Boolean(True, """Determines whether the bot will reply to invalid + commands by searching for a factoid; basically making the whatis + unnecessary when you want all factoids for a given key.""")) +conf.registerChannelValue(Factoids, 'factoidPrefix', + registry.StringWithSpaceOnRight('could be ', """Determines the string that + factoids will be introduced by.""")) + + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: diff --git a/Factoids/plugin.py b/Factoids/plugin.py new file mode 100644 index 000000000..6b383a079 --- /dev/null +++ b/Factoids/plugin.py @@ -0,0 +1,389 @@ +### +# Copyright (c) 2002-2005, Jeremiah Fincher +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +### + +import os +import time +import string + +import supybot.conf as conf +import supybot.ircdb as ircdb +import supybot.utils as utils +from supybot.commands import * +import supybot.plugins as plugins +import supybot.ircutils as ircutils +import supybot.callbacks as callbacks + +try: + import sqlite +except ImportError: + raise callbacks.Error, 'You need to have PySQLite installed to use this ' \ + 'plugin. Download it at ' + +def getFactoid(irc, msg, args, state): + assert not state.channel + callConverter('channel', irc, msg, args, state) + separator = state.cb.registryValue('learnSeparator', state.channel) + try: + i = args.index(separator) + except ValueError: + raise callbacks.ArgumentError + args.pop(i) + key = [] + value = [] + for (j, s) in enumerate(args[:]): + if j < i: + key.append(args.pop(0)) + else: + value.append(args.pop(0)) + state.args.append(' '.join(key)) + state.args.append(' '.join(value)) + +addConverter('factoid', getFactoid) + +class Factoids(callbacks.Plugin, plugins.ChannelDBHandler): + def makeDb(self, filename): + if os.path.exists(filename): + return sqlite.connect(filename) + db = sqlite.connect(filename) + cursor = db.cursor() + cursor.execute("""CREATE TABLE keys ( + id INTEGER PRIMARY KEY, + key TEXT UNIQUE ON CONFLICT IGNORE, + locked BOOLEAN + )""") + cursor.execute("""CREATE TABLE factoids ( + id INTEGER PRIMARY KEY, + key_id INTEGER, + added_by TEXT, + added_at TIMESTAMP, + fact TEXT + )""") + cursor.execute("""CREATE TRIGGER remove_factoids + BEFORE DELETE ON keys + BEGIN + DELETE FROM factoids WHERE key_id = old.id; + END + """) + db.commit() + return db + + def learn(self, irc, msg, args, channel, key, factoid): + """[] as + + Associates with . is only necessary if the + message isn't sent on the channel itself. The word 'as' is necessary + to separate the key from the value. It can be changed to another + word via the learnSeparator registry value. + """ + db = self.getDb(channel) + cursor = db.cursor() + cursor.execute("SELECT id, locked FROM keys WHERE key LIKE %s", key) + if cursor.rowcount == 0: + cursor.execute("""INSERT INTO keys VALUES (NULL, %s, 0)""", key) + db.commit() + cursor.execute("SELECT id, locked FROM keys WHERE key LIKE %s",key) + (id, locked) = map(int, cursor.fetchone()) + capability = ircdb.makeChannelCapability(channel, 'factoids') + if not locked: + if ircdb.users.hasUser(msg.prefix): + name = ircdb.users.getUser(msg.prefix).name + else: + name = msg.nick + cursor.execute("""INSERT INTO factoids VALUES + (NULL, %s, %s, %s, %s)""", + id, name, int(time.time()), factoid) + db.commit() + irc.replySuccess() + else: + irc.error('That factoid is locked.') + learn = wrap(learn, ['factoid']) + + def _lookupFactoid(self, channel, key): + db = self.getDb(channel) + cursor = db.cursor() + cursor.execute("""SELECT factoids.fact FROM factoids, keys + WHERE keys.key LIKE %s AND factoids.key_id=keys.id + ORDER BY factoids.id + LIMIT 20""", key) + return [t[0] for t in cursor.fetchall()] + + def _replyFactoids(self, irc, channel, key, factoids, number=0, error=True): + if factoids: + if number: + try: + irc.reply(factoids[number-1]) + except IndexError: + irc.error('That\'s not a valid number for that key.') + return + else: + intro = self.registryValue('factoidPrefix', channel) + prefix = format('%q %s', key, intro) + if len(factoids) == 1: + irc.reply(prefix + factoids[0]) + else: + factoidsS = [] + counter = 1 + for factoid in factoids: + factoidsS.append(format('(#%i) %s', counter, factoid)) + counter += 1 + irc.replies(factoidsS, prefixer=prefix, + joiner=', or ', onlyPrefixFirst=True) + elif error: + irc.error('No factoid matches that key.') + + def tokenizedCommand(self, irc, msg, tokens): + if irc.isChannel(msg.args[0]): + channel = msg.args[0] + if self.registryValue('replyWhenInvalidCommand', channel): + key = ' '.join(tokens) + factoids = self._lookupFactoid(channel, key) + self._replyFactoids(irc, channel, key, factoids, error=False) + + def whatis(self, irc, msg, args, channel, number, key): + """[] [] + + Looks up the value of in the factoid database. If given a + number, will return only that exact factoid. is only + necessary if the message isn't sent in the channel itself. + """ + factoids = self._lookupFactoid(channel, key) + self._replyFactoids(irc, channel, key, factoids, number) + whatis = wrap(whatis, ['channel', reverse(optional('int', 0)), 'text']) + + def lock(self, irc, msg, args, channel, key): + """[] + + Locks the factoid(s) associated with so that they cannot be + removed or added to. is only necessary if the message isn't + sent in the channel itself. + """ + db = self.getDb(channel) + cursor = db.cursor() + cursor.execute("UPDATE keys SET locked=1 WHERE key LIKE %s", key) + db.commit() + irc.replySuccess() + lock = wrap(lock, ['channel', 'text']) + + def unlock(self, irc, msg, args, channel, key): + """[] + + Unlocks the factoid(s) associated with so that they can be + removed or added to. is only necessary if the message isn't + sent in the channel itself. + """ + db = self.getDb(channel) + cursor = db.cursor() + cursor.execute("UPDATE keys SET locked=0 WHERE key LIKE %s", key) + db.commit() + irc.replySuccess() + unlock = wrap(unlock, ['channel', 'text']) + + def forget(self, irc, msg, args, channel, number, key): + """[] [|*] + + Removes the factoid from the factoids database. If there are + more than one factoid with such a key, a number is necessary to + determine which one should be removed. A * can be used to remove all + factoids associated with a key. is only necessary if + the message isn't sent in the channel itself. + """ + if number == '*': + number = True + db = self.getDb(channel) + cursor = db.cursor() + cursor.execute("""SELECT keys.id, factoids.id + FROM keys, factoids + WHERE key LIKE %s AND + factoids.key_id=keys.id""", key) + if cursor.rowcount == 0: + irc.error('There is no such factoid.') + elif cursor.rowcount == 1 or number is True: + (id, _) = cursor.fetchone() + cursor.execute("""DELETE FROM factoids WHERE key_id=%s""", id) + cursor.execute("""DELETE FROM keys WHERE key LIKE %s""", key) + db.commit() + irc.replySuccess() + else: + if number is not None: + results = cursor.fetchall() + try: + (_, id) = results[number] + except IndexError: + irc.error('Invalid factoid number.') + return + cursor.execute("DELETE FROM factoids WHERE id=%s", id) + db.commit() + irc.replySuccess() + else: + irc.error('%s factoids have that key. ' + 'Please specify which one to remove, ' + 'or use * to designate all of them.' % + cursor.rowcount) + forget = wrap(forget, ['channel', + reverse(optional(first(('literal', '*'), + 'id'))), + 'text']) + + def random(self, irc, msg, args, channel): + """[] + + Returns a random factoid from the database for . + is only necessary if the message isn't sent in the channel itself. + """ + db = self.getDb(channel) + cursor = db.cursor() + cursor.execute("""SELECT fact, key_id FROM factoids + ORDER BY random() + LIMIT 3""") + if cursor.rowcount != 0: + L = [] + for (factoid, id) in cursor.fetchall(): + cursor.execute("""SELECT key FROM keys WHERE id=%s""", id) + (key,) = cursor.fetchone() + L.append('"%s": %s' % (ircutils.bold(key), factoid)) + irc.reply('; '.join(L)) + else: + irc.error('I couldn\'t find a factoid.') + random = wrap(random, ['channel']) + + def info(self, irc, msg, args, channel, key): + """[] + + Gives information about the factoid(s) associated with . + is only necessary if the message isn't sent in the channel + itself. + """ + db = self.getDb(channel) + cursor = db.cursor() + cursor.execute("SELECT id, locked FROM keys WHERE key LIKE %s", key) + if cursor.rowcount == 0: + irc.error('No factoid matches that key.') + return + (id, locked) = map(int, cursor.fetchone()) + cursor.execute("""SELECT added_by, added_at FROM factoids + WHERE key_id=%s + ORDER BY id""", id) + factoids = cursor.fetchall() + L = [] + counter = 0 + for (added_by, added_at) in factoids: + counter += 1 + added_at = time.strftime(conf.supybot.reply.format.time(), + time.localtime(int(added_at))) + L.append(format('#%i was added by %s at %s', + counter, added_by, added_at)) + factoids = '; '.join(L) + s = format('Key %q is %s and has %n associated with it: %s', + key, locked and 'locked' or 'not locked', + (counter, 'factoid'), factoids) + irc.reply(s) + info = wrap(info, ['channel', 'text']) + + def change(self, irc, msg, args, channel, key, number, replacer): + """[] + + Changes the factoid # associated with according to + . + """ + db = self.getDb(channel) + cursor = db.cursor() + cursor.execute("""SELECT factoids.id, factoids.fact + FROM keys, factoids + WHERE keys.key LIKE %s AND + keys.id=factoids.key_id""", key) + if cursor.rowcount == 0: + irc.error(format('I couldn\'t find any key %q', key)) + return + elif cursor.rowcount < number: + irc.errorInvalid('key id') + (id, fact) = cursor.fetchall()[number-1] + newfact = replacer(fact) + cursor.execute("UPDATE factoids SET fact=%s WHERE id=%s", newfact, id) + db.commit() + irc.replySuccess() + change = wrap(change, ['channel', 'something', + ('id', 'key id'), 'regexpReplacer']) + + _sqlTrans = string.maketrans('*?', '%_') + def search(self, irc, msg, args, channel, optlist, globs): + """[] [--values] [--{regexp} ] [ ...] + + Searches the keyspace for keys matching . If --regexp is given, + it associated value is taken as a regexp and matched against the keys. + If --values is given, search the value space instead of the keyspace. + """ + if not optlist and not globs: + raise callbacks.ArgumentError + tables = ['keys'] + formats = [] + criteria = [] + target = 'keys.key' + predicateName = 'p' + db = self.getDb(channel) + for (option, arg) in optlist: + if option == 'values': + target = 'factoids.fact' + if 'factoids' not in tables: + tables.append('factoids') + criteria.append('factoids.key_id=keys.id') + elif option == 'regexp': + criteria.append('%s(TARGET)' % predicateName) + def p(s, r=arg): + return int(bool(r.search(s))) + db.create_function(predicateName, 1, p) + predicateName += 'p' + for glob in globs: + criteria.append('TARGET LIKE %s') + formats.append(glob.translate(self._sqlTrans)) + cursor = db.cursor() + sql = """SELECT keys.key FROM %s WHERE %s""" % \ + (', '.join(tables), ' AND '.join(criteria)) + sql = sql.replace('TARGET', target) + cursor.execute(sql, formats) + if cursor.rowcount == 0: + irc.reply('No keys matched that query.') + elif cursor.rowcount == 1 and \ + self.registryValue('showFactoidIfOnlyOneMatch', channel): + self.whatis(irc, msg, [cursor.fetchone()[0]]) + elif cursor.rowcount > 100: + irc.reply('More than 100 keys matched that query; ' + 'please narrow your query.') + else: + keys = [repr(t[0]) for t in cursor.fetchall()] + s = format('%L', keys) + irc.reply(s) + search = wrap(search, ['channel', + getopts({'values': '', 'regexp': 'regexpMatcher'}), + any('glob')]) + + +Class = Factoids + + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: diff --git a/Factoids/test.py b/Factoids/test.py new file mode 100644 index 000000000..a5dc424d6 --- /dev/null +++ b/Factoids/test.py @@ -0,0 +1,154 @@ +### +# Copyright (c) 2002-2005, Jeremiah Fincher +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +### + +from supybot.test import * + +try: + import sqlite +except ImportError: + sqlite = None + +if sqlite: + class FactoidsTestCase(ChannelPluginTestCase): + plugins = ('Factoids',) + def testRandomfactoid(self): + self.assertError('random') + self.assertNotError('learn jemfinch as my primary author') + self.assertRegexp('random', 'primary author') + + def testLearn(self): + self.assertNotError('learn jemfinch as my primary author') + self.assertNotError('info jemfinch') + self.assertRegexp('whatis jemfinch', 'my primary author') + self.assertRegexp('whatis JEMFINCH', 'my primary author') + self.assertRegexp('whatis JEMFINCH 1', 'my primary author') + self.assertNotError('learn jemfinch as a bad assembly programmer') + self.assertRegexp('whatis jemfinch 2', 'bad assembly') + self.assertNotRegexp('whatis jemfinch 2', 'primary author') + self.assertRegexp('whatis jemfinch', r'.*primary author.*assembly') + self.assertError('forget jemfinch') + self.assertError('forget jemfinch 3') + self.assertError('forget jemfinch 0') + self.assertNotError('forget jemfinch 2') + self.assertNotError('forget jemfinch 1') + self.assertError('whatis jemfinch') + self.assertError('info jemfinch') + + self.assertNotError('learn foo bar as baz') + self.assertNotError('info foo bar') + self.assertRegexp('whatis foo bar', 'baz') + self.assertNotError('learn foo bar as quux') + self.assertRegexp('whatis foo bar', '.*baz.*quux') + self.assertError('forget foo bar') + self.assertNotError('forget foo bar 2') + self.assertNotError('forget foo bar 1') + self.assertError('whatis foo bar') + self.assertError('info foo bar') + + self.assertError('learn foo bar baz') # No 'as' + self.assertError('learn foo bar') # No 'as' + + def testChangeFactoid(self): + self.assertNotError('learn foo as bar') + self.assertNotError('change foo 1 s/bar/baz/') + self.assertRegexp('whatis foo', 'baz') + self.assertError('change foo 2 s/bar/baz/') + self.assertError('change foo 0 s/bar/baz/') + + def testSearchFactoids(self): + self.assertNotError('learn jemfinch as my primary author') + self.assertNotError('learn strike as a cool guy working on me') + self.assertNotError('learn inkedmn as another of my developers') + self.assertNotError('learn jamessan as a developer of much python') + self.assertNotError('learn bwp as author of my weather command') + self.assertRegexp('factoids search --regexp /.w./', 'bwp') + self.assertRegexp('factoids search --regexp /^.+i/', + 'jemfinch.*strike') + self.assertNotRegexp('factoids search --regexp /^.+i/', 'inkedmn') + self.assertRegexp('factoids search --regexp m/j/ --regexp m/ss/', + 'jamessan') + self.assertRegexp('factoids search --regexp m/^j/ *ss*', + 'jamessan') + self.assertRegexp('factoids search --regexp /^j/', + 'jemfinch.*jamessan') + self.assertRegexp('factoids search j*', 'jemfinch.*jamessan') + self.assertRegexp('factoids search *ke*', + 'inkedmn.*strike|strike.*inkedmn') + self.assertRegexp('factoids search ke', + 'inkedmn.*strike|strike.*inkedmn') + self.assertRegexp('factoids search jemfinch', + 'my primary author') + self.assertRegexp('factoids search --values primary author', + 'my primary author') + + def testWhatisOnNumbers(self): + self.assertNotError('learn 911 as emergency number') + self.assertRegexp('whatis 911', 'emergency number') + + def testNotZeroIndexed(self): + self.assertNotError('learn foo as bar') + self.assertNotRegexp('info foo', '#0') + self.assertNotRegexp('whatis foo', '#0') + self.assertNotError('learn foo as baz') + self.assertNotRegexp('info foo', '#0') + self.assertNotRegexp('whatis foo', '#0') + + def testInfoReturnsRightNumber(self): + self.assertNotError('learn foo as bar') + self.assertNotRegexp('info foo', '2 factoids') + + def testLearnSeparator(self): + self.assertError('learn foo is bar') + self.assertNotError('learn foo as bar') + self.assertRegexp('whatis foo', 'bar') + try: + conf.supybot.plugins.Factoids.learnSeparator.setValue('is') + self.assertError('learn bar as baz') + self.assertNotError('learn bar is baz') + self.assertRegexp('whatis bar', 'baz') + finally: + conf.supybot.plugins.Factoids.learnSeparator.setValue('as') + + def testShowFactoidIfOnlyOneMatch(self): + m1 = self.assertNotError('factoids search m/foo|bar/') + try: + conf.supybot.plugins.Factoids. \ + showFactoidIfOnlyOneMatch.setValue(False) + m2 = self.assertNotError('factoids search m/foo/') + self.failUnless(m1.args[1].startswith(m2.args[1])) + finally: + conf.supybot.plugins.Factoids. \ + showFactoidIfOnlyOneMatch.setValue(True) + + def testInvalidCommand(self): + self.assertNotError('learn foo as bar') + self.assertRegexp('foo', 'bar') + + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: