mirror of
https://github.com/Mikaela/Limnoria.git
synced 2024-11-27 13:19:24 +01:00
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:
parent
5b3b616671
commit
c864836a2f
279
src/callbacks.py
279
src/callbacks.py
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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))
|
||||||
|
Loading…
Reference in New Issue
Block a user