Run hooke.command.Argument.callback every time (if defined).
[hooke.git] / hooke / command.py
1 """The `command` module provides :class:`Command`\s and
2 :class:`Argument`\s for defining commands.
3
4 It also provides :class:`CommandExit` and subclasses for communicating
5 command completion information between
6 :class:`hooke.engine.CommandEngine`\s and
7 :class:`hooke.ui.UserInterface`\s.
8 """
9
10 import Queue as queue
11 import sys
12 import textwrap
13 import traceback
14
15
16 class CommandExit (Exception):
17     pass
18
19 class Success (CommandExit):
20     pass
21
22 class Exit (Success):
23     """The command requests an end to the interpreter session.
24     """
25     pass
26
27 class Failure (CommandExit):
28     pass
29
30 class UncaughtException (Failure):
31     def __init__(self, exception, traceback_string=None):
32         super(UncaughtException, self).__init__()
33         if traceback_string == None:
34             traceback_string = traceback.format_exc()
35             sys.exc_clear()
36         self.exception = exception
37         self.traceback = traceback_string
38         self.__setstate__(self.__getstate__())
39
40     def __getstate__(self):
41         """Return a picklable representation of the objects state.
42
43         :mod:`pickle`'s doesn't call a :meth:`__init__` when
44         rebuilding a class instance.  To preserve :attr:`args` through
45         a pickle cycle, we use :meth:`__getstate__` and
46         :meth:`__setstate__`.
47
48         See `pickling class instances`_ and `pickling examples`_.
49
50         .. _pickling class instances:
51           http://docs.python.org/library/pickle.html#pickling-and-unpickling-normal-class-instances
52         .. _pickling examples:
53           http://docs.python.org/library/pickle.html#example
54         """
55         return {'exception':self.exception, 'traceback':self.traceback}
56
57     def __setstate__(self, state):
58         """Apply the picklable state from :meth:`__getstate__` to
59         reconstruct the instance.
60         """
61         for key,value in state.items():
62             setattr(self, key, value)
63         self.args = (self.traceback + str(self.exception),)
64
65
66 class Command (object):
67     """One-line command description here.
68
69     >>> c = Command(name='test', help='An example Command.')
70     >>> hooke = None
71     >>> status = c.run(hooke, NullQueue(), PrintQueue(),
72     ...                help=True) # doctest: +REPORT_UDIFF
73     ITEM:
74     Command: test
75     <BLANKLINE>
76     Arguments:
77     <BLANKLINE>
78     help BOOL (bool) Print a help message.
79     <BLANKLINE>
80     An example Command.
81     ITEM:
82     <BLANKLINE>
83     """
84     def __init__(self, name, aliases=None, arguments=[], help=''):
85         self.name = name
86         if aliases == None:
87             aliases = []
88         self.aliases = aliases
89         self.arguments = [
90             Argument(name='help', type='bool', default=False, count=1,
91                      help='Print a help message.'),
92             ] + arguments
93         self._help = help
94
95     def run(self, hooke, inqueue=None, outqueue=None, **kwargs):
96         """`Normalize inputs and handle <Argument help> before punting
97         to :meth:`_run`.
98         """
99         if inqueue == None:
100             inqueue = NullQueue()
101         if outqueue == None:
102             outqueue = NullQueue()
103         try:
104             params = self.handle_arguments(hooke, inqueue, outqueue, kwargs)
105             if params['help'] == True:
106                 outqueue.put(self.help())
107                 raise(Success())
108             self._run(hooke, inqueue, outqueue, params)
109         except CommandExit, e:
110             if isinstance(e, Failure):
111                 outqueue.put(e)
112                 return 1
113             # other CommandExit subclasses fall through to the end
114         except Exception, e:
115             x = UncaughtException(e)
116             outqueue.put(x)
117             return 1
118         else:
119             e = Success()
120         outqueue.put(e)
121         return 0
122
123     def _run(self, inqueue, outqueue, params):
124         """This is where the command-specific magic will happen.
125         """
126         pass
127
128     def handle_arguments(self, hooke, inqueue, outqueue, params):
129         """Normalize and validate input parameters (:class:`Argument` values).
130         """
131         for argument in self.arguments:
132             names = [argument.name] + argument.aliases
133             settings = [(name,v) for name,v in params.items() if name in names]
134             num_provided = len(settings)
135             if num_provided == 0:
136                 if argument.optional == True or argument.count == 0:
137                     settings = [(argument.name, argument.default)]
138                 else:
139                     raise Failure('Required argument %s not set.'
140                                   % argument.name)
141             if num_provided > 1:
142                 raise Failure('Multiple settings for %s:\n  %s'
143                     % (argument.name,
144                        '\n  '.join(['%s: %s' % (name,value)
145                                     for name,value in sorted(settings)])))
146             name,value = settings[0]
147             if num_provided == 0:
148                 params[argument.name] = value
149             else:
150                 if name != argument.name:
151                     params.remove(name)
152                     params[argument.name] = value
153             if argument.callback != None:
154                 value = argument.callback(hooke, self, argument, value)
155                 params[argument.name] = value
156             argument.validate(value)
157         return params
158
159     def help(self, name_fn=lambda name:name):
160         """Return a help message describing the `Command`.
161
162         `name_fn(internal_name) -> external_name` gives calling
163         :class:`hooke.ui.UserInterface`\s a means of changing the
164         display names if it wants (e.g. to remove spaces from command
165         line tokens).
166         """
167         name_part = 'Command: %s' % name_fn(self.name)
168         if len(self.aliases) > 0:
169             name_part += ' (%s)' % ', '.join(
170                 [name_fn(n) for n in self.aliases])
171         parts = [name_part]
172         if len(self.arguments) > 0:
173             argument_part = ['Arguments:', '']
174             for a in self.arguments:
175                 argument_part.append(textwrap.fill(
176                         a.help(name_fn),
177                         initial_indent="",
178                         subsequent_indent="    "))
179             argument_part = '\n'.join(argument_part)
180             parts.append(argument_part)
181         parts.append(self._help) # help part
182         return '\n\n'.join(parts)
183
184 class Argument (object):
185     """Structured user input for :class:`Command`\s.
186     
187     TODO: ranges for `count`?
188     """
189     def __init__(self, name, aliases=None, type='string', metavar=None,
190                  default=None, optional=True, count=1,
191                  completion_callback=None, callback=None, help=''):
192         self.name = name
193         if aliases == None:
194             aliases = []
195         self.aliases = aliases
196         self.type = type
197         if metavar == None:
198             metavar = type.upper()
199         self.metavar = metavar
200         self.default = default
201         self.optional = optional
202         self.count = count
203         self.completion_callback = completion_callback
204         self.callback = callback
205         self._help = help
206
207     def __str__(self):
208         return '<%s %s>' % (self.__class__.__name__, self.name)
209
210     def __repr__(self):
211         return self.__str__()
212
213     def help(self, name_fn=lambda name:name):
214         """Return a help message describing the `Argument`.
215
216         `name_fn(internal_name) -> external_name` gives calling
217         :class:`hooke.ui.UserInterface`\s a means of changing the
218         display names if it wants (e.g. to remove spaces from command
219         line tokens).
220         """        
221         parts = ['%s ' % name_fn(self.name)]
222         if self.metavar != None:
223             parts.append('%s ' % self.metavar)
224         parts.extend(['(%s) ' % self.type, self._help])
225         return ''.join(parts)
226
227     def validate(self, value):
228         """If `value` is not appropriate, raise `ValueError`.
229         """
230         pass # TODO: validation
231
232     # TODO: type conversion
233
234 # TODO: type extensions?
235
236 # Useful callbacks
237
238 class StoreValue (object):
239     def __init__(self, value):
240         self.value = value
241     def __call__(self, hooke, command, argument, fragment=None):
242         return self.value
243
244 class NullQueue (queue.Queue):
245     """The :class:`queue.Queue` equivalent of `/dev/null`.
246
247     This is a bottomless pit.  Items go in, but never come out.
248     """
249     def get(self, block=True, timeout=None):
250         """Raise queue.Empty.
251         
252         There's really no need to override the base Queue.get, but I
253         want to know if someone tries to read from a NullQueue.  With
254         the default implementation they would just block silently
255         forever :(.
256         """
257         raise queue.Empty
258
259     def put(self, item, block=True, timeout=None):
260         """Dump an item into the void.
261
262         Block and timeout are meaningless, because there is always a
263         free slot available in a bottomless pit.
264         """
265         pass
266
267 class PrintQueue (NullQueue):
268     """Debugging :class:`NullQueue` that prints items before dropping
269     them.
270     """
271     def put(self, item, block=True, timeout=None):
272         """Print `item` and then dump it into the void.
273         """
274         print 'ITEM:\n%s' % item