From 4eafd9b779e2a2988fec6e8e953e5f8229816495 Mon Sep 17 00:00:00 2001 From: Jeremy Fincher Date: Tue, 12 Oct 2004 00:41:40 +0000 Subject: [PATCH] New tests, and new implementation (though a test still fails). --- src/commands.py | 259 +++++++++++++++++------------------------- test/test_commands.py | 83 ++++++++++++++ 2 files changed, 185 insertions(+), 157 deletions(-) create mode 100644 test/test_commands.py diff --git a/src/commands.py b/src/commands.py index 7f1de23ca..9f9a30d6d 100644 --- a/src/commands.py +++ b/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: diff --git a/test/test_commands.py b/test/test_commands.py new file mode 100644 index 000000000..0a97fcc16 --- /dev/null +++ b/test/test_commands.py @@ -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: +