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