root/cly/trunk/cly/parser.py

Revision 578, 13.3 kB (checked in by athomas, 2 months ago)

Allow help() to return bare strings. These will be auto-formatted is possible.

Line 
1 # -*- coding: utf-8 -*-
2 #
3 # Copyright (C) 2006-2008 Alec Thomas <alec@swapoff.org>
4 #
5 # This software is licensed as described in the file COPYING, which
6 # you should have received as part of this distribution.
7 #
8
9 """CLY parser classes.
10
11 Constructs for parsing user input with a :class:`~cly.builder.Grammar`.
12 """
13
14
15 __all__ = ['HelpParser', 'Context', 'Parser']
16 __docformat__ = 'restructuredtext en'
17
18
19 from cly.exceptions import *
20
21
22 class HelpParser(object):
23     """Extract the help for children of the specified Node.
24
25     Help is extracted from the Node's children, following branches, and
26     returned ordered by group, order and finally help key and string.
27     """
28     def __init__(self, context, node):
29         self.help = []
30         self.node = node
31
32         def parse_help(node):
33             help = node.help(context)
34             if isinstance(help, basestring):
35                 if node.name == node.pattern:
36                     return [(node.name, help)]
37                 else:
38                     return [('<%s>' % node.name, help)]
39             else:
40                 return help
41
42         def add_help(node):
43             node_help = sorted(parse_help(node))
44             for help in node_help:
45                 self.help.append((node.group, node.order, help[0], help[1]))
46
47         for child in node.children(context, follow=True):
48             if child.visible(context):
49                 add_help(child)
50
51         self.help.sort()
52
53     def __iter__(self):
54         """Iterate over each (key, help) help pair.
55
56         >>> from cly.builder import Grammar, Node, Help
57         >>> context = Context(None, None)
58         >>> class Test(Node):
59         ...   def help(self, context):
60         ...     return 'HELP!'
61         >>> help = HelpParser(context, Grammar(
62         ...     one=Node(help='1'),
63         ...     two=Node(help=Help.pair('<two>', '2'), group=2),
64         ...     three=Test(help='HELP!'),
65         ...     ))
66         >>> list(help)
67         [(0, 'one', '1'), (0, 'three', 'HELP!'), (2, '<two>', '2')]
68         """
69
70         for help in self.help:
71             yield (help[0],) + help[2:]
72
73     def format(self):
74         """Format help into a human readable form.
75
76         Output is formatted for use with ``cly.console``.
77
78         Returns a list of lines of text.
79
80         >>> from cly.builder import Grammar, Node, Help
81         >>> import sys
82         >>> context = Context(None, None)
83         >>> grammar = Grammar(
84         ...     one=Node(help='1'),
85         ...     two=Node(help=Help.pair('<two>', '2'), group=2))
86         >>> help = HelpParser(context, grammar)
87         >>> print '\\n'.join(help.format())
88           ^Bone  ^B 1
89         <BLANKLINE>
90           ^B<two>^B 2
91         """
92         if not self.help:
93             return []
94         last_group = None
95         max_len = max([len(h[2]) for h in self.help])
96         out = []
97         for group, order, command, help in self.help:
98             if last_group is not None and last_group != group:
99                 out.append('')
100             last_group = group
101             out.append('  ^B%-*s^B %s' % (max_len, command, help))
102         return out
103
104
105 class Context(object):
106     """Represents the parsing context for a single command.
107
108     A `Context` is created automatically when input is parsed. It contains all
109     the information needed to maintain state during the parse, including the
110     current cursor position in the input stream, the current node in the
111     grammar, variables collected and a history of nodes traversed.
112
113     Basic usage is::
114
115       parser = Parser(grammar)
116       context = parser.parse('some input text')
117       print context.vars
118
119     If the input is invalid the context will have consumed as much input as
120     possible. The attributes ``parsed`` and ``remaining`` contain how much text has
121     been consumed and remains, respectively.
122
123     Useful attributes:
124
125     .. attribute:: parser
126
127         :class:`Parser` this `Context` is attached to.
128
129     .. attribute:: command
130
131         Command being parsed.
132
133     .. attribute:: cursor
134
135         Position of :class:`Parser` cursor.
136
137     .. attribute:: vars
138
139         :class:`~cly.builder.Variable`\ s collected during the parse.
140
141     """
142     def __init__(self, parser, command, data=None):
143         self.parser = parser
144         self.command = command
145         self.cursor = 0
146         self.data = data
147         self.vars = {}
148         self._traversed = {}
149         self.trail = []
150
151     def _get_remaining_input(self):
152         """Return the current remaining unparsed text in the command.
153
154         >>> context = Context(None, 'one two')
155         >>> context.advance(4)
156         >>> context.remaining
157         'two'
158         """
159         return self.command[self.cursor:]
160     remaining = property(_get_remaining_input, doc=_get_remaining_input.__doc__)
161
162     def _get_parsed(self):
163         """Return command text that has been successfully parsed.
164
165         >>> context = Context(None, 'one two')
166         >>> context.advance(4)
167         >>> context.parsed
168         'one '
169         """
170         return self.command[:self.cursor]
171     parsed = property(_get_parsed, doc=_get_parsed.__doc__)
172
173     def _last_node(self):
174         """Return the last node parsed.
175
176         >>> from cly.builder import Grammar, Node
177         >>> parser = Parser(Grammar(one=Node(two=Node())))
178         >>> context = parser.parse('one two three')
179         >>> context.last_node
180         <Node:/one/two>
181         """
182         if self.trail[-1][1] is None or self.trail[-1][1].group():
183             return self.trail[-1][0]
184         else:
185             return self.trail[-2][0]
186     last_node = property(_last_node, doc=_last_node.__doc__)
187
188     def execute(self):
189         """Execute the current (terminal) node. If there is still input
190         remaining an exception will be thrown.
191
192         >>> from cly.builder import Grammar, Node, Action
193         >>> def test(): print 'OK'
194         >>> parser = Parser(Grammar(one=Node()(Action(callback=test))))
195         >>> context = parser.parse('one')
196         >>> context.execute()
197         OK
198         """
199         if self.remaining.strip():
200             raise InvalidToken(self)
201         node = self.trail[-1][0]
202         return node.terminal(self)
203
204     def advance(self, distance):
205         """Advance cursor.
206
207         >>> context = Context(None, 'one two')
208         >>> context.cursor
209         0
210         >>> context.advance(4)
211         >>> context.cursor
212         4
213         """
214         self.cursor += distance
215
216     def candidates(self, text=None):
217         """Return potential candidates from children of last successfully
218         parsed node.
219
220         Arguments:
221             :text: If provided, return candidates after ``text``, otherwise the
222                    remaining unparsed text in the current command will be used.
223
224         >>> from cly.builder import Grammar, Node
225         >>> parser = Parser(Grammar(one=Node()(two=Node(),
226         ...                 three=Node()), four=Node()))
227         >>> context = parser.parse('one')
228         >>> list(context.candidates())
229         ['three ', 'two ']
230         >>> list(context.candidates('th'))
231         ['three ']
232         """
233         if text is None:
234             text = self.remaining
235         for child in self.last_node.children(self, follow=True):
236             for candidate in child.candidates(self, text):
237                 yield candidate
238
239     def help(self):
240         """Return a HelpParser object describing the last successfully parsed
241         node.
242
243         >>> import sys
244         >>> from cly.builder import Grammar, Node
245         >>> parser = Parser(Grammar(one=Node(help='4', two=Node(help='2'),
246         ...                 three=Node(help='3')), four=Node(help='4')))
247         >>> context = parser.parse('one')
248         >>> help = context.help()
249         >>> print '\\n'.join(help.format())
250           ^Bthree^B 3
251           ^Btwo  ^B 2
252         """
253         return HelpParser(self, self.last_node)
254
255     def selected(self, node):
256         """The given node has been selected and will be followed."""
257         path = node.path()
258         self._traversed.setdefault(path, 0)
259         self._traversed[path] += 1
260
261     def traversed(self, node):
262         """How many times has node been traversed in this context?
263
264         >>> from cly.builder import Grammar, Node, Alias
265         >>> parser = Parser(Grammar(one=Node(traversals=0)(Alias(target='/one'))))
266         >>> node = parser.find('/one')
267         >>> for i in range(4):
268         ...     context = parser.parse('one ' * i)
269         ...     print context.traversed(node), context.parsed # doctest: +NORMALIZE_WHITESPACE
270         0
271         1 one
272         2 one one
273         3 one one one
274         """
275         return self._traversed.get(node.path(), 0)
276
277     def update_locals(self, locals):
278         """Update locals before XML evaluation."""
279         return locals
280
281     def __repr__(self):
282         return "<Context command:'%s' remaining:'%s'>" % (self.command, self.remaining)
283
284
285 class Parser(object):
286     """Parse and execute user input against a :class:`~cly.builder.Grammar`.
287
288     For each parse, the parser creates a :class:`Context` containing the state
289     for the run and parses the input, and executes any callbacks.
290
291     After parsing, the returned :class:`Context` can be interrogated for
292     information or used to execute any :class:`~cly.builder.Action`\ s.
293
294     Arguments:
295         :grammar: Grammar to parse with.
296         :data: User data to attach to Context.
297         :context_factory: A callable used to create new :class:`Context`
298                           objects.
299     """
300     def __init__(self, grammar, data=None, context_factory=Context):
301         """Construct a new Parser."""
302         self.grammar = grammar
303         self.data = data
304         self.labels = self._collect_labels()
305         self.context_factory = context_factory
306
307     def _set_grammar(self, grammar):
308         """Set grammar to parse with."""
309         from cly.builder import Grammar
310         assert isinstance(grammar, Grammar)
311         self._grammar = grammar
312
313     def _get_grammar(self):
314         """The :class:`~cly.builder.Grammar` associated with this parser."""
315         return self._grammar
316
317     grammar = property(_get_grammar, _set_grammar)
318
319     def parse(self, command, data=None):
320         """Parse command using the current :class:`~cly.builder.Grammar`.
321
322         This will return a :class:`Context` object that can be used to inspect
323         the state of the parser.
324
325         Arguments:
326             :command: String to parse.
327             :data: Used to pass user data through to callbacks. The
328                    :class:`Context` object has this as an attribute , available
329                    to any :class:`~cly.builder.Action` node callbacks that have
330                    set ``with_context=True``.
331
332         >>> from cly import *
333         >>> parser = Parser(Grammar(one=Node(), two=Node(three=Node(
334         ...                 action=Action(callback=lambda: "foo bar")))))
335         >>> context = parser.parse('two three')
336         >>> context
337         <Context command:'two three' remaining:''>
338         >>> context.execute()
339         'foo bar'
340         >>> parser.parse('two four')
341         <Context command:'two four' remaining:'four'>
342         """
343         if data is None:
344             data = self.data
345         context = self.context_factory(self, command, data)
346
347         def parse(node, match):
348             context.trail.append((node, match))
349             if match is not None:
350                 node.advance(context)
351             node.selected(context, match)
352
353             for subnode in node.next(context):
354                 if subnode.valid(context):
355                     submatch = subnode.match(context)
356                     if submatch is not None:
357                         return parse(subnode, submatch)
358             else:
359                 return
360             raise InvalidToken(context)
361
362         parse(self.grammar, None)
363         return context
364
365     def merge(self, grammar, where=None):
366         """Merge another grammar into this one.
367
368         Arguments:
369             :where: A label or path to a node.
370             :grammar: Grammar to merge.
371         """
372         if where is None:
373             assert hasattr(grammar, 'graft'), \
374                 'need either an explicit "where" or a "graft" attribute on ' \
375                 'the <grammar> root'
376             where = grammar.graft
377         where = self.find(where)
378         where.update(grammar)
379         self.labels.update(self._collect_labels())
380
381     def execute(self, command, data=None):
382         """Parse and execute the given command.
383
384         This is a convenience function that calls :meth:`~Context.execute` on
385         the :class:`Context` object returned by :meth:`parse`.
386
387         Arguments are the same as for :meth:`parse`.
388
389         >>> from cly.builder import Grammar, Node, Action
390         >>> parser = Parser(Grammar(one=Node(), two=Node(three=Node(
391         ...                 action=Action(callback=lambda: "foo bar")))))
392         >>> parser.execute('two three')
393         'foo bar'
394         """
395         return self.parse(command, data).execute()
396
397     def find(self, path):
398         """Find a node by its absolute path.
399
400         >>> from cly.builder import Grammar, Node, Action
401         >>> parser = Parser(Grammar(one=Node(), two=Node(three=Node())))
402         >>> parser.find('/two/three')
403         <Node:/two/three>
404         """
405         return self.grammar.find(path)
406
407     def _collect_labels(self):
408         """Collect labels from grammar."""
409         labels = {}
410         for node in self.grammar.walk():
411             if node.label is not None:
412                 labels[node.label] = node
413         return labels
414
415
416 if __name__ == '__main__':
417     import doctest
418     doctest.testmod()
Note: See TracBrowser for help on using the browser.