X-Git-Url: http://git.tremily.us/?a=blobdiff_plain;f=hooke%2Fcommand.py;h=f685327ec4be1fef6f71073df5215f49a85e7de9;hb=838af47943e082c6e09d120d7a5244943195e9b8;hp=bfa4c9f1611fa414f3ab1c1c673eaa362af65c6b;hpb=0104c445c8dbca784c628ba1a2067296dcd3b5e6;p=hooke.git diff --git a/hooke/command.py b/hooke/command.py index bfa4c9f..f685327 100644 --- a/hooke/command.py +++ b/hooke/command.py @@ -1,47 +1,122 @@ +# Copyright (C) 2010 W. Trevor King +# +# This file is part of Hooke. +# +# Hooke is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# Hooke is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General +# Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with Hooke. If not, see +# . + """The `command` module provides :class:`Command`\s and :class:`Argument`\s for defining commands. + +It also provides :class:`CommandExit` and subclasses for communicating +command completion information between +:class:`hooke.engine.CommandEngine`\s and +:class:`hooke.ui.UserInterface`\s. """ import Queue as queue +import sys +import textwrap +import traceback class CommandExit (Exception): - def __str__(self): - return self.__class__.__name__ + pass class Success (CommandExit): pass +class Exit (Success): + """The command requests an end to the interpreter session. + """ + pass + class Failure (CommandExit): pass +class UncaughtException (Failure): + def __init__(self, exception, traceback_string=None): + super(UncaughtException, self).__init__() + if traceback_string == None: + traceback_string = traceback.format_exc() + sys.exc_clear() + self.exception = exception + self.traceback = traceback_string + self.__setstate__(self.__getstate__()) + + def __getstate__(self): + """Return a picklable representation of the objects state. + + :mod:`pickle`'s doesn't call a :meth:`__init__` when + rebuilding a class instance. To preserve :attr:`args` through + a pickle cycle, we use :meth:`__getstate__` and + :meth:`__setstate__`. + + See `pickling class instances`_ and `pickling examples`_. + + .. _pickling class instances: + http://docs.python.org/library/pickle.html#pickling-and-unpickling-normal-class-instances + .. _pickling examples: + http://docs.python.org/library/pickle.html#example + """ + return {'exception':self.exception, 'traceback':self.traceback} + + def __setstate__(self, state): + """Apply the picklable state from :meth:`__getstate__` to + reconstruct the instance. + """ + for key,value in state.items(): + setattr(self, key, value) + self.args = (self.traceback + str(self.exception),) + + class Command (object): """One-line command description here. >>> c = Command(name='test', help='An example Command.') - >>> status = c.run(NullQueue(), PrintQueue(), help=True) # doctest: +REPORT_UDIFF + >>> hooke = None + >>> status = c.run(hooke, NullQueue(), PrintQueue(), + ... help=True) # doctest: +REPORT_UDIFF ITEM: Command: test Arguments: + help BOOL (bool) Print a help message. + stack BOOL (bool) Add this command to appropriate command stacks. An example Command. ITEM: - Success + """ - def __init__(self, name, aliases=None, arguments=[], help=''): + def __init__(self, name, aliases=None, arguments=[], help='', + plugin=None): + # TODO: see_also=[other,command,instances,...] self.name = name if aliases == None: aliases = [] self.aliases = aliases self.arguments = [ Argument(name='help', type='bool', default=False, count=1, - callback=StoreValue(True), help='Print a help message.'), + help='Print a help message.'), + Argument(name='stack', type='bool', default=True, count=1, + help='Add this command to appropriate command stacks.'), ] + arguments self._help = help + self.plugin = plugin - def run(self, inqueue=None, outqueue=None, **kwargs): + def run(self, hooke, inqueue=None, outqueue=None, **kwargs): """`Normalize inputs and handle before punting to :meth:`_run`. """ @@ -50,58 +125,81 @@ class Command (object): if outqueue == None: outqueue = NullQueue() try: - params = self.handle_arguments(inqueue, outqueue, kwargs) + params = self.handle_arguments(hooke, inqueue, outqueue, kwargs) if params['help'] == True: outqueue.put(self.help()) raise(Success()) - self._run(inqueue, outqueue, params) + self._run(hooke, inqueue, outqueue, params) except CommandExit, e: if isinstance(e, Failure): - outqueue.put(e.message) outqueue.put(e) return 1 + # other CommandExit subclasses fall through to the end + except Exception, e: + x = UncaughtException(e) + outqueue.put(x) + return 1 + else: + e = Success() outqueue.put(e) return 0 - def _run(self, inqueue, outqueue, params): + def _run(self, hooke, inqueue, outqueue, params): """This is where the command-specific magic will happen. """ pass - def handle_arguments(self, inqueue, outqueue, params): + def handle_arguments(self, hooke, inqueue, outqueue, params): """Normalize and validate input parameters (:class:`Argument` values). """ for argument in self.arguments: names = [argument.name] + argument.aliases settings = [(name,v) for name,v in params.items() if name in names] - if len(settings) == 0: + num_provided = len(settings) + if num_provided == 0: if argument.optional == True or argument.count == 0: settings = [(argument.name, argument.default)] else: raise Failure('Required argument %s not set.' % argument.name) - if len(settings) > 1: + if num_provided > 1: raise Failure('Multiple settings for %s:\n %s' % (argument.name, '\n '.join(['%s: %s' % (name,value) for name,value in sorted(settings)]))) name,value = settings[0] - if name != argument.name: - params.remove(name) + if num_provided == 0: params[argument.name] = value + else: + if name != argument.name: + params.remove(name) + params[argument.name] = value if argument.callback != None: - value = argument.callback(self, argument, value) + value = argument.callback(hooke, self, argument, value) params[argument.name] = value argument.validate(value) return params - def help(self, *args): - name_part = 'Command: %s' % self.name + def help(self, name_fn=lambda name:name): + """Return a help message describing the `Command`. + + `name_fn(internal_name) -> external_name` gives calling + :class:`hooke.ui.UserInterface`\s a means of changing the + display names if it wants (e.g. to remove spaces from command + line tokens). + """ + name_part = 'Command: %s' % name_fn(self.name) if len(self.aliases) > 0: - name_part += ' (%s)' % ', '.join(self.aliases) + name_part += ' (%s)' % ', '.join( + [name_fn(n) for n in self.aliases]) parts = [name_part] if len(self.arguments) > 0: - argument_part = ['Arguments:'] + [a.help() for a in self.arguments] + argument_part = ['Arguments:', ''] + for a in self.arguments: + argument_part.append(textwrap.fill( + a.help(name_fn), + initial_indent="", + subsequent_indent=" ")) argument_part = '\n'.join(argument_part) parts.append(argument_part) parts.append(self._help) # help part @@ -136,8 +234,15 @@ class Argument (object): def __repr__(self): return self.__str__() - def help(self): - parts = ['%s ' % self.name] + def help(self, name_fn=lambda name:name): + """Return a help message describing the `Argument`. + + `name_fn(internal_name) -> external_name` gives calling + :class:`hooke.ui.UserInterface`\s a means of changing the + display names if it wants (e.g. to remove spaces from command + line tokens). + """ + parts = ['%s ' % name_fn(self.name)] if self.metavar != None: parts.append('%s ' % self.metavar) parts.extend(['(%s) ' % self.type, self._help]) @@ -148,16 +253,13 @@ class Argument (object): """ pass # TODO: validation - # TODO: type conversion - -# TODO: type extensions? # Useful callbacks class StoreValue (object): def __init__(self, value): self.value = value - def __call__(self, command, argument, fragment=None): + def __call__(self, hooke, command, argument, fragment=None): return self.value class NullQueue (queue.Queue):