Replace .config rather than reconstructing plugins, drivers, and UIs.
[hooke.git] / hooke / command.py
index bc18ffdb903ceade6f4cd950757a4eaa1c96b427..c694bff28c0d7b2ea8625672017fb186074c1ba5 100644 (file)
@@ -1,5 +1,28 @@
+# Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
+#
+# 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
+# <http://www.gnu.org/licenses/>.
+
 """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
@@ -14,21 +37,57 @@ class CommandExit (Exception):
 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):
+    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.exc_string = traceback.format_exc()
-        sys.exc_clear()
-        super(UncaughtException, self).__init__(self.exc_string)
+        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
     <BLANKLINE>
@@ -38,18 +97,21 @@ class Command (object):
     <BLANKLINE>
     An example Command.
     ITEM:
-    Success
+    <BLANKLINE>
     """
-    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.'),
             ] + arguments
         self._help = help
+        self.plugin = plugin
 
     def run(self, hooke, inqueue=None, outqueue=None, **kwargs):
         """`Normalize inputs and handle <Argument help> before punting
@@ -79,7 +141,7 @@ class Command (object):
         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
@@ -103,26 +165,36 @@ class Command (object):
                        '\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:
-                if num_provided > 0:
-                    value = argument.callback(hooke, 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:', '']
             for a in self.arguments:
                 argument_part.append(textwrap.fill(
-                        a.help(),
+                        a.help(name_fn),
                         initial_indent="",
                         subsequent_indent="    "))
             argument_part = '\n'.join(argument_part)
@@ -159,8 +231,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])
@@ -171,9 +250,6 @@ class Argument (object):
         """
         pass # TODO: validation
 
-    # TODO: type conversion
-
-# TODO: type extensions?
 
 # Useful callbacks