mirror of
https://github.com/Mikaela/Limnoria.git
synced 2024-11-26 20:59:27 +01:00
New tests, and new implementation (though a test still fails).
This commit is contained in:
parent
c3c5ea71bc
commit
4eafd9b779
259
src/commands.py
259
src/commands.py
@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
###
|
||||
# Copyright (c) 2002-2004, Jeremiah Fincher
|
||||
# All rights reserved.
|
||||
@ -59,6 +57,9 @@ import supybot.structures as structures
|
||||
# Non-arg wrappers -- these just change the behavior of a command without
|
||||
# changing the arguments given to it.
|
||||
###
|
||||
|
||||
# Thread has to be a non-arg wrapper because by the time we're parsing and
|
||||
# validating arguments, we're inside the function we'd want to thread.
|
||||
def thread(f):
|
||||
"""Makes sure a command spawns a thread when called."""
|
||||
def newf(self, irc, msg, args, *L, **kwargs):
|
||||
@ -141,11 +142,8 @@ decorators = ircutils.IrcDict({
|
||||
|
||||
|
||||
###
|
||||
# Arg wrappers, wrappers that add arguments to the command. They accept the
|
||||
# irc, msg, and args, of course, as well as a State object which holds the args
|
||||
# (and kwargs, though none currently take advantage of that) to be given to the
|
||||
# command being decorated, as well as the name of the command, the plugin, the
|
||||
# log, etc.
|
||||
# Converters, which take irc, msg, args, and a state object, and build up the
|
||||
# validated and converted args for the method in state.args.
|
||||
###
|
||||
|
||||
# This is just so we can centralize this, since it may change.
|
||||
@ -396,8 +394,8 @@ def public(irc, msg, args, state, errmsg=None):
|
||||
def checkCapability(irc, msg, args, state, cap):
|
||||
cap = ircdb.canonicalCapability(cap)
|
||||
if not ircdb.checkCapability(msg.prefix, cap):
|
||||
state.log.warning('%s tried %s without %s.',
|
||||
msg.prefix, state.name, cap)
|
||||
## state.log.warning('%s tried %s without %s.',
|
||||
## msg.prefix, state.name, cap)
|
||||
irc.errorNoCapability(cap, Raise=True)
|
||||
|
||||
def anything(irc, msg, args, state):
|
||||
@ -507,16 +505,15 @@ def getConverter(name):
|
||||
def callConverter(name, irc, msg, args, state, *L):
|
||||
getConverter(name)(irc, msg, args, state, *L)
|
||||
|
||||
class State(object):
|
||||
def __init__(self, name=None, logger=None):
|
||||
if logger is None:
|
||||
logger = log
|
||||
self.args = []
|
||||
self.kwargs = {}
|
||||
self.name = name
|
||||
self.log = logger
|
||||
self.getopts = []
|
||||
self.channel = None
|
||||
###
|
||||
# Contexts. These determine what the nature of conversions is; whether they're
|
||||
# defaulted, or many of them are allowed, etc. Contexts should be reusable;
|
||||
# i.e., they should not maintain state between calls.
|
||||
###
|
||||
def contextify(spec):
|
||||
if not isinstance(spec, context):
|
||||
spec = context(spec)
|
||||
return spec
|
||||
|
||||
class context(object):
|
||||
def __init__(self, spec):
|
||||
@ -551,9 +548,9 @@ class additional(context):
|
||||
class optional(additional):
|
||||
def __call__(self, irc, msg, args, state):
|
||||
try:
|
||||
self.__parent.__call__(irc, msg, args, state)
|
||||
super(optional, self).__call__(irc, msg, args, state)
|
||||
except (callbacks.ArgumentError, callbacks.Error), e:
|
||||
log.debug('Got %s, returning default.', utils.exntoString(e))
|
||||
log.debug('Got %s, returning default.', utils.exnToString(e))
|
||||
state.args.append(self.default)
|
||||
|
||||
class any(context):
|
||||
@ -571,153 +568,98 @@ class any(context):
|
||||
|
||||
class many(any):
|
||||
def __call__(self, irc, msg, args, state):
|
||||
context.__call__(self, irc, msg, args, state)
|
||||
super(many, self).__call__(irc, msg, args, state)
|
||||
assert state.args
|
||||
if not state.args[-1]:
|
||||
raise callbacks.ArgumentError
|
||||
|
||||
# getopts: None means "no conversion", '' means "takes no argument"
|
||||
def args(irc,msg,args, types=[], state=None,
|
||||
getopts=None, allowExtra=False, requireExtra=False, combineRest=True):
|
||||
if state is None:
|
||||
state = State(name='unknown', logger=log)
|
||||
if requireExtra:
|
||||
# Implied by requireExtra.
|
||||
allowExtra = True
|
||||
combineRest = False
|
||||
types = types[:] # We're going to destroy this.
|
||||
if getopts is not None:
|
||||
getoptL = []
|
||||
for (key, value) in getopts.iteritems():
|
||||
if value != '': # value can be None, remember.
|
||||
key += '='
|
||||
getoptL.append(key)
|
||||
log.debug('getoptL: %r', getoptL)
|
||||
|
||||
def callWrapper(spec):
|
||||
if isinstance(spec, tuple):
|
||||
assert spec, 'tuple specification cannot be empty.'
|
||||
name = spec[0]
|
||||
specArgs = spec[1:]
|
||||
else:
|
||||
assert isinstance(spec, basestring) or spec is None
|
||||
name = spec
|
||||
specArgs = ()
|
||||
if name is None:
|
||||
name = 'anything'
|
||||
enforce = True
|
||||
optional = False
|
||||
if name.startswith('?'):
|
||||
optional = True
|
||||
name = name[1:]
|
||||
elif name.endswith('?'):
|
||||
optional = True
|
||||
enforce = False
|
||||
name = name[:-1]
|
||||
elif name[-1] in '*+':
|
||||
name = name[:-1]
|
||||
default = ''
|
||||
wrapper = wrappers[name]
|
||||
if optional and specArgs:
|
||||
# First arg is default.
|
||||
default = specArgs[0]
|
||||
specArgs = specArgs[1:]
|
||||
if callable(default):
|
||||
default = default()
|
||||
try:
|
||||
wrapper(irc, msg, args, state, *specArgs)
|
||||
except (callbacks.Error, ValueError, callbacks.ArgumentError), e:
|
||||
state.log.debug('%r when calling wrapper.', utils.exnToString(e))
|
||||
if not enforce:
|
||||
state.args.append(default)
|
||||
class getopts(context):
|
||||
"""The empty string indicates that no argument is taken; None indicates
|
||||
that there is no converter for the argument."""
|
||||
def __init__(self, getopts):
|
||||
self.getopts = {}
|
||||
self.getoptL = []
|
||||
for (name, spec) in getopts.iteritems():
|
||||
if spec == '':
|
||||
self.getoptL.append(name)
|
||||
self.getopts[name] = None
|
||||
else:
|
||||
state.log.debug('Re-raising %s because of enforce.', e)
|
||||
raise
|
||||
except IndexError, e:
|
||||
state.log.debug('%r when calling wrapper.', utils.exnToString(e))
|
||||
if optional:
|
||||
state.args.append(default)
|
||||
else:
|
||||
state.log.debug('Raising ArgumentError because of '
|
||||
'non-optional args: %r', spec)
|
||||
raise callbacks.ArgumentError
|
||||
self.getoptL.append(name + '=')
|
||||
self.getopts[name] = contextify(spec)
|
||||
|
||||
# First, we getopt stuff.
|
||||
if getopts is not None:
|
||||
(optlist, args) = getopt.getopt(args, '', getoptL)
|
||||
def __call__(self, irc, msg, args, state):
|
||||
(optlist, args) = getopt.getopt(args, '', self.getoptL)
|
||||
getopts = []
|
||||
for (opt, arg) in optlist:
|
||||
opt = opt[2:] # Strip --
|
||||
log.debug('getopt %s: %r', opt, arg)
|
||||
if getopts[opt] != '':
|
||||
# This is a MESS. But I can't think of a better way to do it.
|
||||
originalArgs = args
|
||||
args = [arg]
|
||||
originalStateArgsLen = len(state.args)
|
||||
callWrapper(getopts[opt])
|
||||
args = originalArgs
|
||||
if originalStateArgsLen < len(state.args):
|
||||
assert originalStateArgsLen == len(state.args)-1
|
||||
arg = state.args.pop()
|
||||
else:
|
||||
arg = None
|
||||
state.getopts.append((opt, arg))
|
||||
context = self.getopts[opt]
|
||||
if context is not None:
|
||||
st = state.essence()
|
||||
context(irc, msg, args, st)
|
||||
assert len(st.args) == 1
|
||||
getopts.append((opt, st.args[0]))
|
||||
else:
|
||||
state.getopts.append((opt, True))
|
||||
#log.debug('Finished getopts: %s', state.getopts)
|
||||
getopts.append((opt, True))
|
||||
state.args.append(getopts)
|
||||
|
||||
# Second, we get out everything but the last argument (or, if combineRest
|
||||
# is False, we'll clear out all the types).
|
||||
while len(types) > 1 or (types and not combineRest):
|
||||
callWrapper(types.pop(0))
|
||||
# Third, if there is a remaining required or optional argument
|
||||
# (there's a possibility that there were no required or optional
|
||||
# arguments) then we join the remaining args and work convert that.
|
||||
if types:
|
||||
name = types[0]
|
||||
if isinstance(name, tuple):
|
||||
name = name[0]
|
||||
if name[-1] in '*+':
|
||||
originalStateArgs = state.args
|
||||
state.args = []
|
||||
if name.endswith('+'):
|
||||
callWrapper(types[0]) # So it raises an error if no args.
|
||||
while args:
|
||||
callWrapper(types[0])
|
||||
lastArgs = state.args
|
||||
state.args = originalStateArgs
|
||||
state.args.append(lastArgs)
|
||||
else:
|
||||
if args:
|
||||
rest = ' '.join(args)
|
||||
args = [rest]
|
||||
callWrapper(types.pop(0))
|
||||
if args and not allowExtra and isinstance(args, list):
|
||||
# args could be a regexp in a urlSnarfer.
|
||||
log.debug('args but not allowExtra: %r', args)
|
||||
raise callbacks.ArgumentError
|
||||
if requireExtra and not args:
|
||||
log.debug('requireExtra and not args: %r', args)
|
||||
log.debug('command.args args: %r' % args)
|
||||
log.debug('command.args state.args: %r' % state.args)
|
||||
log.debug('command.args state.getopts: %r' % state.getopts)
|
||||
return state
|
||||
|
||||
# These are used below, but we need to rename them so their names aren't
|
||||
|
||||
###
|
||||
# This is our state object, passed to converters along with irc, msg, and args.
|
||||
###
|
||||
class State(object):
|
||||
log = log
|
||||
def __init__(self):
|
||||
self.args = []
|
||||
self.kwargs = {}
|
||||
self.channel = None
|
||||
|
||||
def essence(self):
|
||||
st = State()
|
||||
for (attr, value) in self.__dict__.iteritems():
|
||||
if attr not in ('args', 'kwargs', 'channel'):
|
||||
setattr(st, attr, value)
|
||||
return st
|
||||
|
||||
###
|
||||
# This is a compiled Spec object.
|
||||
###
|
||||
class Spec(object):
|
||||
def _state(self, attrs={}):
|
||||
st = State()
|
||||
st.__dict__.update(attrs)
|
||||
return st
|
||||
|
||||
def __init__(self, types, allowExtra=False, combineRest=True):
|
||||
self.types = types
|
||||
self.allowExtra = allowExtra
|
||||
self.combineRest = combineRest
|
||||
utils.mapinto(contextify, self.types)
|
||||
|
||||
def __call__(self, irc, msg, args, stateAttrs={}):
|
||||
state = self._state(stateAttrs)
|
||||
if self.types:
|
||||
types = self.types[:]
|
||||
while types:
|
||||
if len(types) == 1 and self.combineRest and args:
|
||||
break
|
||||
context = types.pop(0)
|
||||
context(irc, msg, args, state)
|
||||
if types and args:
|
||||
assert self.combineRest
|
||||
args[:] = [' '.join(args)]
|
||||
types[0](irc, msg, args, state)
|
||||
if args and not self.allowExtra:
|
||||
raise callbacks.ArgumentError
|
||||
return state
|
||||
|
||||
# This is used below, but we need to rename it so its name isn't
|
||||
# shadowed by our locals.
|
||||
_args = args
|
||||
_decorators = decorators
|
||||
def wrap(f, *argsArgs, **argsKwargs):
|
||||
def wrap(f, specList, decorators=None, **kw):
|
||||
spec = Spec(specList, **kw)
|
||||
def newf(self, irc, msg, args, **kwargs):
|
||||
state = State('%s.%s' % (self.name(), f.func_name), self.log)
|
||||
state.cb = self # This should probably be in State.__init__.
|
||||
_args(irc,msg,args, state=state, *argsArgs, **argsKwargs)
|
||||
if 'getopts' in argsKwargs:
|
||||
f(self, irc, msg, args, state.getopts, *state.args, **state.kwargs)
|
||||
else:
|
||||
f(self, irc, msg, args, *state.args, **state.kwargs)
|
||||
|
||||
spec(irc, msg, args, stateAttrs={'cb': self, 'log': self.log})
|
||||
f(self, irc, msg, args, *state.args, **state.kwargs)
|
||||
newf = utils.changeFunctionName(newf, f.func_name, f.__doc__)
|
||||
decorators = argsKwargs.pop('decorators', None)
|
||||
if decorators is not None:
|
||||
decorators = map(_decorators.__getitem__, decorators)
|
||||
for decorator in decorators:
|
||||
@ -725,6 +667,9 @@ def wrap(f, *argsArgs, **argsKwargs):
|
||||
return newf
|
||||
|
||||
|
||||
__all__ = ['wrap', 'args',
|
||||
'getConverter', 'addConverter', 'callConverter']
|
||||
__all__ = ['wrap', 'context', 'additional', 'optional', 'any',
|
||||
'many', 'getopts', 'getConverter', 'addConverter', 'callConverter']
|
||||
|
||||
if world.testing:
|
||||
__all__.append('Spec')
|
||||
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:
|
||||
|
83
test/test_commands.py
Normal file
83
test/test_commands.py
Normal file
@ -0,0 +1,83 @@
|
||||
###
|
||||
# Copyright (c) 2002-2004, 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 testsupport import *
|
||||
|
||||
from supybot.commands import *
|
||||
import supybot.ircmsgs as ircmsgs
|
||||
import supybot.callbacks as callbacks
|
||||
|
||||
|
||||
class CommandsTestCase(SupyTestCase):
|
||||
msg = ircmsgs.privmsg('test', 'foo')
|
||||
class irc:
|
||||
nick = 'test'
|
||||
def error(self, s):
|
||||
raise self.failureException, s
|
||||
def assertState(self, spec, given, expected, **kwargs):
|
||||
irc = callbacks.SimpleProxy(self.irc(), self.msg)
|
||||
myspec = Spec(spec, **kwargs)
|
||||
state = myspec(irc, self.msg, given)
|
||||
self.assertEqual(state.args, expected,
|
||||
'Expected %r, got %r' % (expected, state.args))
|
||||
|
||||
def testSpecInt(self):
|
||||
self.assertState(['int'], ['1'], [1])
|
||||
self.assertState(['int', 'int', 'int'], ['1', '2', '3'], [1, 2, 3])
|
||||
|
||||
def testRestHandling(self):
|
||||
self.assertState([None], ['foo', 'bar', 'baz'], ['foo bar baz'])
|
||||
|
||||
def testOptional(self):
|
||||
spec = [optional('int', 999), None]
|
||||
self.assertState(spec, ['12', 'foo'], [12, 'foo'])
|
||||
self.assertState(spec, ['foo'], [999, 'foo'])
|
||||
|
||||
def testAdditional(self):
|
||||
spec = [additional('int', 999)]
|
||||
self.assertState(spec, ['12'], [12])
|
||||
self.assertState(spec, [], [999])
|
||||
self.assertRaises(callbacks.Error,
|
||||
self.assertState, spec, ['foo'], ['asdf'])
|
||||
|
||||
def testGetopts(self):
|
||||
spec = ['int', getopts({'foo': None, 'bar': 'int'}), 'int']
|
||||
self.assertState(spec,
|
||||
['12', '--foo', 'baz', '--bar', '13', '15'],
|
||||
[12, [('foo', 'baz'), ('bar', 13)], 15])
|
||||
|
||||
## def testAny(self):
|
||||
## self.assertState([None, any('int'), None],
|
||||
## ['foo', 'bar'],
|
||||
## ['foo', [], 'bar'])
|
||||
|
||||
|
||||
|
||||
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:
|
||||
|
Loading…
Reference in New Issue
Block a user