Finally refactored to make commands be "lists of strings" rather than just strings. This is preliminary to allowing nested plugins.

This commit is contained in:
Jeremy Fincher 2005-02-18 05:17:23 +00:00
parent 5b3b616671
commit c864836a2f
2 changed files with 100 additions and 189 deletions

View File

@ -51,7 +51,7 @@ import supybot.irclib as irclib
import supybot.ircmsgs as ircmsgs import supybot.ircmsgs as ircmsgs
import supybot.ircutils as ircutils import supybot.ircutils as ircutils
import supybot.registry as registry import supybot.registry as registry
from supybot.utils.iter import any from supybot.utils.iter import any, all
def _addressed(nick, msg, prefixChars=None, nicks=None, def _addressed(nick, msg, prefixChars=None, nicks=None,
prefixStrings=None, whenAddressedByNick=None, prefixStrings=None, whenAddressedByNick=None,
@ -344,17 +344,6 @@ def tokenize(s, channel=None):
except ValueError, e: except ValueError, e:
raise SyntaxError, str(e) raise SyntaxError, str(e)
def findCallbackForCommand(irc, name):
"""Given a command name and an Irc object, returns a list of callbacks that
commandName is in."""
L = []
name = canonicalName(name)
for callback in irc.callbacks:
if hasattr(callback, 'isCommand'):
if callback.isCommand(name):
L.append(callback)
return L
def formatArgumentError(method, name=None): def formatArgumentError(method, name=None):
if name is None: if name is None:
name = method.__name__ name = method.__name__
@ -366,6 +355,9 @@ def formatArgumentError(method, name=None):
else: else:
return 'Invalid arguments for %s.' % method.__name__ return 'Invalid arguments for %s.' % method.__name__
def formatCommand(command):
return ' '.join(command)
def checkCommandCapability(msg, cb, commandName): def checkCommandCapability(msg, cb, commandName):
assert isinstance(commandName, basestring), commandName assert isinstance(commandName, basestring), commandName
plugin = cb.name().lower() plugin = cb.name().lower()
@ -606,22 +598,6 @@ class IrcObjectProxy(RichReplyMethods):
assert all(lambda x: isinstance(x, basestring), self.args) assert all(lambda x: isinstance(x, basestring), self.args)
self.finalEval() self.finalEval()
def _callTokenizedCommands(self):
for cb in self.irc.callbacks:
if hasattr(cb, 'tokenizedCommand'):
self._callTokenizedCommand(cb)
if self.msg.repliedTo:
return
def _callTokenizedCommand(self, cb):
try:
cb.tokenizedCommand(self, self.msg, self.args)
except Error, e:
return self.error(str(e))
except Exception, e:
log.exception('Uncaught exception in %s.tokenizedCommand.' %
cb.name())
def _callInvalidCommands(self): def _callInvalidCommands(self):
log.debug('Calling invalidCommands.') log.debug('Calling invalidCommands.')
for cb in self.irc.callbacks: for cb in self.irc.callbacks:
@ -641,40 +617,40 @@ class IrcObjectProxy(RichReplyMethods):
log.exception('Uncaught exception in %s.invalidCommand.'% log.exception('Uncaught exception in %s.invalidCommand.'%
cb.name()) cb.name())
def findCallbackForCommand(self, command): def findCallbacksForArgs(self, args):
cbs = findCallbackForCommand(self, command) """Returns a two-tuple of (command, plugins) that has the command
if len(cbs) > 1: (a list of strings) and the plugins for which it was a command."""
command = canonicalName(command) cbs = []
# Check for whether it's the name of a callback; their dispatchers maxL = []
# need to get precedence. for cb in self.irc.callbacks:
for cb in cbs: L = cb.getCommand(args)
if canonicalName(cb.name()) == command: if L and L >= maxL:
return [cb] maxL = L
try: cbs.append((cb, L))
# Check if there's a configured defaultPlugin -- the user gets assert isinstance(L, list), \
# precedence in that case. 'getCommand now returns a list, not a bool.'
assert utils.iter.startswith(L, args), \
'getCommand must return a prefix of the args given.'
log.debug('findCallbacksForCommands: %r', cbs)
cbs = [cb for (cb, L) in cbs if L == maxL]
if len(maxL) == 1:
# Special case: one arg determines the callback. In this case, we
# have to check defaultPlugins.
defaultPlugins = conf.supybot.commands.defaultPlugins defaultPlugins = conf.supybot.commands.defaultPlugins
plugin = defaultPlugins.get(command)() try:
if plugin and plugin != '(Unused)': defaultPlugin = defaultPlugins.get(maxL[0])()
cb = self.irc.getCallback(plugin) log.debug('defaultPlugin: %r', defaultPlugin)
if cb is None: if defaultPlugin:
log.warning('%s is set as a default plugin ' cb = self.irc.getCallback(defaultPlugin)
'for %s, but it isn\'t loaded.', if cb in cbs:
plugin, command) # This is just a sanity check, but there's a small
raise registry.NonExistentRegistryEntry # possibility that a default plugin for a command
else: # is configured to point to a plugin that doesn't
return [cb] # actually have that command.
except registry.NonExistentRegistryEntry, e: return (maxL, [cb])
# Check for whether it's a src/ plugin; they get precedence. except registry.NonExistentRegistryEntry:
important = [] pass # No default plugin defined.
importantPlugins = defaultPlugins.importantPlugins() return (maxL, cbs)
for cb in cbs:
if cb.name() in importantPlugins:
# We do this to handle multiple importants matching.
important.append(cb)
if len(important) == 1:
return important
return cbs
def finalEval(self): def finalEval(self):
# Now that we've already iterated through our args and made sure # Now that we've already iterated through our args and made sure
@ -683,54 +659,41 @@ class IrcObjectProxy(RichReplyMethods):
# evaluated our own list of arguments. # evaluated our own list of arguments.
assert not self.finalEvaled, 'finalEval called twice.' assert not self.finalEvaled, 'finalEval called twice.'
self.finalEvaled = True self.finalEvaled = True
command = self.args[0] # Now, the way we call a command is we iterate over the loaded pluings,
cbs = self.findCallbackForCommand(command) # asking each one if the list of args we have interests it. The
# way we do that is by calling getCommand on the plugin.
# The plugin will return a list of args which it considers to be
# "interesting." We will then give our args to the plugin which
# has the *longest* list. The reason we pick the longest list is
# that it seems reasonable that the longest the list, the more
# specific the command is. That is, given a list of length X, a list
# of length X+1 would be even more specific (assuming that both lists
# used the same prefix. Of course, if two plugins return a list of the
# same length, we'll just error out with a message about ambiguity.
(command, cbs) = self.findCallbacksForArgs(self.args)
if not cbs: if not cbs:
# Normal command not found, let's go for the specialties now. # We used to handle addressedRegexps here, but I think we'll let
# First, check for addressedRegexps -- they take precedence over # them handle themselves in getCommand. They can always just
# tokenizedCommands. # return the full list of args as their "command".
for cb in self.irc.callbacks:
if isinstance(cb, PrivmsgCommandAndRegexp):
payload = addressed(self.irc.nick, self.msg)
for (r, name) in cb.addressedRes:
if r.search(payload):
log.debug('Skipping tokenizedCommands: %s.%s',
cb.name(), name)
return
# Now we call tokenizedCommands.
self._callTokenizedCommands()
# Return if we've replied.
if self.msg.repliedTo:
log.debug('Not calling invalidCommands, '
'tokenizedCommands replied.')
return
# Now we check for regexp commands, which override invalidCommand.
for cb in self.irc.callbacks:
if isinstance(cb, PrivmsgCommandAndRegexp):
for (r, name) in cb.res:
if r.search(self.msg.args[1]):
log.debug('Skipping invalidCommand: %s.%s',
cb.name(), name)
return
# No matching regexp commands, now we do invalidCommands.
self._callInvalidCommands() self._callInvalidCommands()
elif len(cbs) > 1: elif len(cbs) > 1:
names = sorted([cb.name() for cb in cbs]) names = sorted([cb.name() for cb in cbs])
return self.error(format('The command %s is available in the %L ' command = formatCommand(command)
self.error(format('The command %q is available in the %L '
'plugins. Please specify the plugin ' 'plugins. Please specify the plugin '
'whose command you wish to call by using ' 'whose command you wish to call by using '
'its name as a command before %s.', 'its name as a command before %q.',
command, names, command)) command, names, command))
else: else:
cb = cbs[0] cb = cbs[0]
del self.args[0] # Remove the command. args = self.args[len(command):]
if world.isMainThread() and \ if world.isMainThread() and \
(cb.threaded or conf.supybot.debug.threadAllCommands()): (cb.threaded or conf.supybot.debug.threadAllCommands()):
t = CommandThread(target=cb._callCommand, t = CommandThread(target=cb._callCommand,
args=(command, self, self.msg, self.args)) args=(command, self, self.msg, args))
t.start() t.start()
else: else:
cb._callCommand(command, self, self.msg, self.args) cb._callCommand(command, self, self.msg, args)
def reply(self, s, noLengthCheck=False, prefixName=None, def reply(self, s, noLengthCheck=False, prefixName=None,
action=None, private=None, notice=None, to=None, msg=None): action=None, private=None, notice=None, to=None, msg=None):
@ -1000,7 +963,7 @@ class Commands(object):
def isDisabled(self, command): def isDisabled(self, command):
return self._disabled.disabled(command, self.name()) return self._disabled.disabled(command, self.name())
def isCommand(self, name): def isCommandMethod(self, name):
"""Returns whether a given method name is a command in this plugin.""" """Returns whether a given method name is a command in this plugin."""
# This function is ugly, but I don't want users to call methods like # This function is ugly, but I don't want users to call methods like
# doPrivmsg or __init__ or whatever, and this is good to stop them. # doPrivmsg or __init__ or whatever, and this is good to stop them.
@ -1019,17 +982,41 @@ class Commands(object):
else: else:
return False return False
def getCommandMethod(self, name): def isCommand(self, command):
if isinstance(command, basestring):
return self.isCommandMethod(command)
else:
# Since we're doing a little type dispatching here, let's not be
# too liberal.
assert isinstance(command, list)
return self.getCommand(command) == command
def getCommand(self, args):
first = canonicalName(args[0])
if len(args) >= 2 and first == self.canonicalName():
second = canonicalName(args[1])
if self.isCommandMethod(second):
return args[:2]
if self.isCommandMethod(first):
return args[:1]
return []
def getCommandMethod(self, command):
"""Gets the given command from this plugin.""" """Gets the given command from this plugin."""
name = canonicalName(name) self.log.debug('*** command: %s', str(command))
assert self.isCommand(name), format('%q is not a command.', name) command = map(canonicalName, command)
return getattr(self, name) try:
return getattr(self, command[0])
except AttributeError:
assert command[0] == self.canonicalName()
assert len(command) >= 2
return getattr(self, command[1])
def listCommands(self): def listCommands(self):
commands = [] commands = []
name = canonicalName(self.name()) name = canonicalName(self.name())
for s in dir(self): for s in dir(self):
if self.isCommand(s) and \ if self.isCommandMethod(s) and \
(s != name or self._original) and \ (s != name or self._original) and \
s == canonicalName(s): s == canonicalName(s):
method = getattr(self, s) method = getattr(self, s)
@ -1043,11 +1030,7 @@ class Commands(object):
method(irc, msg, *args, **kwargs) method(irc, msg, *args, **kwargs)
def _callCommand(self, command, irc, msg, *args, **kwargs): def _callCommand(self, command, irc, msg, *args, **kwargs):
if command is None: self.log.info('%s called by %q.', formatCommand(command), msg.prefix)
assert self.callingCommand, \
'Received command=None without self.callingCommand.'
command = self.callingCommand
self.log.info('%s called by %s.', command, msg.prefix)
try: try:
try: try:
self.callingCommand = command self.callingCommand = command
@ -1056,7 +1039,7 @@ class Commands(object):
self.callingCommand = None self.callingCommand = None
except (getopt.GetoptError, ArgumentError): except (getopt.GetoptError, ArgumentError):
method = self.getCommandMethod(command) method = self.getCommandMethod(command)
irc.reply(formatArgumentError(method, name=command)) irc.reply(formatArgumentError(method, name=formatCommand(command)))
except (SyntaxError, Error), e: except (SyntaxError, Error), e:
self.log.debug('Error return: %s', utils.exnToString(e)) self.log.debug('Error return: %s', utils.exnToString(e))
irc.error(str(e)) irc.error(str(e))
@ -1069,18 +1052,12 @@ class Commands(object):
else: else:
irc.replyError() irc.replyError()
def getCommandHelp(self, name): def getCommandHelp(self, command):
name = canonicalName(name) method = self.getCommandMethod(command)
assert self.isCommand(name), \ if hasattr(method, '__doc__'):
'%s is not a command in %s.' % (name, self.name())
method = self.getCommandMethod(name)
if hasattr(method, 'isDispatcher') and \
method.isDispatcher and self.__doc__:
return utils.str.normalizeWhitespace(self.__doc__)
elif hasattr(method, '__doc__'):
return getHelp(method) return getHelp(method)
else: else:
return format('The %q command has no help.', name) return format('The %q command has no help.',formatCommand(command))
class PluginMixin(irclib.IrcCallback): class PluginMixin(irclib.IrcCallback):
@ -1097,55 +1074,6 @@ class PluginMixin(irclib.IrcCallback):
# I guess plugin authors will have to get the capitalization right. # I guess plugin authors will have to get the capitalization right.
# self.callAfter = map(str.lower, self.callAfter) # self.callAfter = map(str.lower, self.callAfter)
# self.callBefore = map(str.lower, self.callBefore) # self.callBefore = map(str.lower, self.callBefore)
### Setup the dispatcher command.
canonicalname = canonicalName(myName)
self._original = getattr(self, canonicalname, None)
docstring = """<command> [<args> ...]
Command dispatcher for the %s plugin. Use 'list %s' to see the
commands provided by this plugin. Use 'config list plugins.%s' to see
the configuration values for this plugin. In most cases this dispatcher
command is unnecessary; in cases where more than one plugin defines a
given command, use this command to tell the bot which plugin's command
to use.""" % (myName, myName, myName)
def dispatcher(self, irc, msg, args):
def handleBadArgs():
if self._original:
self._original(irc, msg, args)
else:
if args:
irc.error('%s is not a valid command in this plugin.' %
args[0])
else:
irc.error()
if args:
name = canonicalName(args[0])
if name == canonicalName(self.name()):
handleBadArgs()
elif self.isCommand(name):
cap = checkCommandCapability(msg, self, name)
if cap:
irc.errorNoCapability(cap)
return
del args[0]
method = getattr(self, name)
try:
realname = '%s.%s' % (canonicalname, name)
method(irc, msg, args)
except (getopt.GetoptError, ArgumentError):
irc.reply(formatArgumentError(method, name))
else:
handleBadArgs()
else:
handleBadArgs()
dispatcher = utils.changeFunctionName(dispatcher, canonicalname)
if self._original:
dispatcher.__doc__ = self._original.__doc__
dispatcher.isDispatcher = False
else:
dispatcher.__doc__ = docstring
dispatcher.isDispatcher = True
setattr(self.__class__, canonicalname, dispatcher)
def canonicalName(self): def canonicalName(self):
return canonicalName(self.name()) return canonicalName(self.name())
@ -1219,6 +1147,7 @@ class PluginMixin(irclib.IrcCallback):
class Plugin(PluginMixin, Commands): class Plugin(PluginMixin, Commands):
pass pass
Privmsg = Plugin # Backwards compatibility.
class SimpleProxy(RichReplyMethods): class SimpleProxy(RichReplyMethods):
@ -1253,8 +1182,6 @@ class SimpleProxy(RichReplyMethods):
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.irc, attr) return getattr(self.irc, attr)
Privmsg = Plugin # Backwards compatibility.
class PluginRegexp(Plugin): class PluginRegexp(Plugin):
"""Same as Plugin, except allows the user to also include regexp-based """Same as Plugin, except allows the user to also include regexp-based
callbacks. All regexp-based callbacks must be specified in a set (or callbacks. All regexp-based callbacks must be specified in a set (or
@ -1278,21 +1205,6 @@ class PluginRegexp(Plugin):
r = re.compile(method.__doc__, self.flags) r = re.compile(method.__doc__, self.flags)
self.addressedRes.append((r, name)) self.addressedRes.append((r, name))
def isCommand(self, name):
return self.__parent.isCommand(name) or \
name in self.regexps or \
name in self.addressedRegexps
def getCommandMethod(self, command):
try:
# First we tried without canonizing, in case it's a regexp
# command.
return getattr(self, command)
except AttributeError:
# Now we try with canonization (or, rather, we let our parent do
# so) for normal commands.
return self.__parent.getCommandMethod(command)
def doPrivmsg(self, irc, msg): def doPrivmsg(self, irc, msg):
if msg.isError: if msg.isError:
return return
@ -1308,7 +1220,6 @@ class PluginRegexp(Plugin):
for m in r.finditer(msg.args[1]): for m in r.finditer(msg.args[1]):
proxy = self.Proxy(irc, msg) proxy = self.Proxy(irc, msg)
self._callCommand(name, proxy, msg, m) self._callCommand(name, proxy, msg, m)
PrivmsgCommandAndRegexp = PluginRegexp PrivmsgCommandAndRegexp = PluginRegexp

View File

@ -363,9 +363,9 @@ class PluginTestCase(SupyTestCase):
continue continue
self.failUnless(sys.modules[cb.__class__.__name__].__doc__, self.failUnless(sys.modules[cb.__class__.__name__].__doc__,
'%s has no module documentation.' % name) '%s has no module documentation.' % name)
if hasattr(cb, 'isCommand'): if hasattr(cb, 'isCommandMethod'):
for attr in dir(cb): for attr in dir(cb):
if cb.isCommand(attr) and \ if cb.isCommandMethod(attr) and \
attr == callbacks.canonicalName(attr): attr == callbacks.canonicalName(attr):
self.failUnless(getattr(cb, attr, None).__doc__, self.failUnless(getattr(cb, attr, None).__doc__,
'%s.%s has no help.' % (name, attr)) '%s.%s has no help.' % (name, attr))