root/cly/trunk/cly/builder.py

Revision 584, 52.7 kB (checked in by athomas, 1 month ago)

Docstring updates.

Line 
1 # -*- coding: utf-8 -*-
2 #
3 # Copyright (C) 2006-2007 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
10 """Classes for constructing CLY grammars."""
11
12
13 import datetime
14 import os
15 import posixpath
16 import re
17 import warnings
18 from itertools import chain
19 from xml.dom import minidom
20 from inspect import isclass, getargspec
21 from cly.exceptions import *
22 from cly.parser import Context
23
24 try:
25     import pytz
26 except ImportError:
27     pytz = None
28
29
30 __all__ = [
31     'Node', 'Masquerade', 'Defaults', 'Alias', 'Group', 'If', 'Apply', 'Action',
32     'Variable', 'Grammar', 'XMLGrammar', 'Help', 'LazyHelp', 'Word', 'Keyword',
33     'String', 'URI', 'LDAPDN', 'Integer', 'Float', 'IP', 'Hostname', 'Host',
34     'EMail', 'File', 'Boolean', 'KeyValue', 'AbsoluteTime', 'RelativeTime',
35     'Timezone', 'cull_candidates',
36     ]
37 __docformat__ = 'restructuredtext en'
38
39
40 class Node(object):
41     """The base class for all grammar nodes.
42
43     Any :class:`Node` instances passed to the constructor will become children of
44     this node in the grammar hierarchy. :class:`Node`\ s as keyword arguments
45     will be named after their keyword, while positional arguments will be
46     provided auto-generated names. This is generally only useful for "control"
47     nodes, such as :class:`Alias`.
48
49     Supported keyword arguments:
50
51         :help:
52             string or callable returning a list of (key, help) tuples A help
53             string or a callable returning an iterable of (key, help) pairs.
54             There is a useful class called Help which can be used for this
55             purpose.
56
57         :name:
58             The name of the node. If ommitted the keyword argument name used in
59             the parent Node is used. The node name also defines the node path
60             and the default pattern to match against if not explicitly provided:
61
62             >>> Node(name='something')
63             <Node:/something>
64
65     The following constructor arguments are also class variables, and as
66     such can be overridden at the class level by subclasses of Node. Useful If
67     you find yourself using a particular pattern repeatedly.
68
69         :pattern:
70             The regular expression used to match user input. If not provided,
71             the node name is used:
72
73             >>> a = Node(name='something')
74             >>> a.pattern == a.name
75             True
76
77         :separator:
78             A regular expression used to match the text separating this node
79             and the next.
80
81         :group:
82             Nodes with the same group value will be collated visually.
83             Generally a number, but can also be a string or any other
84             comparable object.
85
86         :order:
87             Within a group, nodes are normally ordered alphabetically. This can
88             be overridden by setting this to a value other than 0.
89
90         :match_candidates:
91             Modifies the behaviour of the parser when matching completion
92             candidates.
93
94             The :meth:`candidates` method returns a list of words that match at the
95             current token, which are then used for completion.  If
96             ``match_candidates=True`` the allowed input will be explicitly
97             constrainted to just these candidates.
98
99             ``match_candidates`` will be set automatically if
100             :meth:`candidates` is provided.
101
102             Useful for situations where you have a general regex pattern (eg. a
103             pattern matching files) but a known set of matches at this point
104             (eg.  files in the current directory).
105
106         :cull_candidates:
107             If ``True`` (the default) :meth:`candidates` may return a static
108             list of candidates that is automatically culled based on the text
109             being matched. This avoids a lot of boiler plate code.
110
111             >>> a = Node(candidates=['one', 'two'])
112             >>> print list(a.candidates(None, ''))
113             ['one ', 'two ']
114             >>> print list(a.candidates(None, 'o'))
115             ['one ']
116
117         :traversals:
118             The number of times this node can match in any parse context.
119             :class:`Alias` nodes allow for multiple traversal.
120
121             If ``traversals=0`` the node will match an infinite number of times.
122
123         :label:
124             Specify the global label for this node. This can be used by the
125             :class:`Alias` to refer to nodes by label rather than path.
126     """
127     pattern = None
128     separator = r'\s+|\s*$'
129     order = 0
130     match_candidates = False
131     cull_candidates = True
132     traversals = 1
133     label = None
134
135     def __init__(self, *anonymous, **kwargs):
136         self._children = {}
137         self._group = None
138         help = kwargs.pop('help', '')
139         if isinstance(help, basestring):
140             self._help = help
141         elif callable(help):
142             self.help = help
143         else:
144             raise InvalidHelp('help must be a callable or a string')
145         if 'pattern' in kwargs:
146             self.pattern = kwargs.pop('pattern')
147         if 'separator' in kwargs:
148             self.separator = kwargs.pop('separator')
149         if 'candidates' in kwargs:
150             candidates = kwargs.pop('candidates')
151             if callable(candidates):
152                 self.candidates = candidates
153             else:
154                 self.candidates = lambda c, t: candidates
155             self.match_candidates = True
156         self.cull_candidates = kwargs.pop('cull_candidates', self.cull_candidates)
157         if self.cull_candidates:
158             def cull(context, text):
159                 return cull_candidates(cull.candidates(context, text), text)
160             cull.candidates = self.candidates
161             self.candidates = cull
162         if self.pattern is not None:
163             self._pattern = re.compile(self.pattern)
164         self._separator = re.compile(self.separator)
165         if self.pattern is not None and self.separator is not None:
166             self._full_match = re.compile('(?:%s)(?:%s)' %
167                                           (self.pattern, self.separator))
168         self.name = kwargs.pop('name', None)
169         self.parent = None
170         self.__anonymous_children = 0
171         self(*anonymous, **kwargs)
172
173     def _get_group(self):
174         if self._group is None and self.parent:
175             return self.parent.group
176         return self._group or 0
177
178     def _set_group(self, group):
179         self._group = group
180
181     group = property(lambda self: self._get_group(),
182                      lambda self, value: self._set_group(value))
183
184     def _set_name(self, name):
185         """Set the name of this node.
186
187         If the Node does not have an existing matching pattern associated with
188         it, a pattern will be created using the name.
189         """
190         self._name = name
191         if isinstance(name, basestring) and self.pattern is None:
192             self.pattern = name
193             self._pattern = re.compile(name)
194         if self.pattern is not None and self.separator is not None:
195             self._full_match = re.compile('(?:%s)(?:%s)' %
196                                           (self.pattern, self.separator))
197     name = property(lambda self: self._name,
198                     lambda self, name: self._set_name(name))
199
200     def help(self, context):
201         """Return help for node.
202
203         :returns: A sequence of tuples in the form (lhs, help).
204         """
205         if self.name == self.pattern:
206             yield (self.name, self._help)
207         else:
208             yield ('<%s>' % self.name, self._help)
209
210     def __call__(self, *anonymous, **options):
211         """Update or add options and child nodes.
212
213         Positional arguments are treated as anonymous child nodes, while
214         keyword arguments can either be named child nodes or attribute updates
215         for this node. See __init__ for more information on attributes.
216
217         >>> top = Node(name='top')
218         >>> top(subnode=Node())
219         <Node:/top>
220         >>> top.find('/subnode')
221         <Node:/top/subnode>
222         """
223         for node in anonymous:
224             if not isinstance(node, Node):
225                 raise InvalidAnonymousNode('"%r" must be a Node object' % node)
226             # TODO Convert help to name instead of __anonymous_<n>
227             node.name = '__anonymous_%i' % self.__anonymous_children
228             node.parent = self
229             self._children[node.name] = node
230             self.__anonymous_children += 1
231
232         for k, v in options.iteritems():
233             if isinstance(v, Node):
234                 k = k.rstrip('_')
235                 v.name = k
236                 v.parent = self
237                 self._children[k] = v
238             else:
239                 try:
240                     setattr(self, k, v)
241                 except AttributeError:
242                     raise AttributeError('Can\'t set attribute "%s"' % k)
243         return self
244
245     def __iter__(self):
246         """Iterate over child nodes, ignoring context.
247
248         >>> tree = Node()(two=Node(), three=Node())
249         >>> list(tree)
250         [<Node:/three>, <Node:/two>]
251         """
252         def nat_tokenise(key, splitter=re.compile(r'(\d+)')):
253             def convert(k):
254               if k.isdigit():
255                 return int(k)
256               return k
257             return [convert(el) for el in splitter.split(key)]
258
259         children = sorted(self._children.values(),
260                           key=lambda i: (i.group, i.order, nat_tokenise(i.name)))
261         for child in children:
262             yield child
263
264     def __setitem__(self, key, child):
265         """Emulate dictionary set.
266
267         >>> node = Node()
268         >>> node['two'] = Node()
269         >>> list(node.walk())
270         [<Node:/>, <Node:/two>]
271         """
272         self(**{key: child})
273
274     def __getitem__(self, key):
275         """Emulate dictionary get.
276
277         >>> node = Node()(two=Node())
278         >>> node['two']
279         <Node:/two>
280         """
281         return self._children[key]
282
283     def __delitem__(self, key):
284         """Emulate dictionary delete.
285
286         >>> node = Node(two=Node(), three=Node())
287         >>> list(node.walk())
288         [<Node:/>, <Node:/three>, <Node:/two>]
289         >>> del node['two']
290         >>> list(node.walk())
291         [<Node:/>, <Node:/three>]
292         """
293         child = self._children.pop(key)
294         child.parent = None
295
296     def __contains__(self, key):
297         """Emulate dictionary key existence test.
298
299         >>> node = Node(two=Node(), three=Node())
300         >>> 'two' in node
301         True
302         """
303         return key in self._children
304
305     def walk(self, predicate=None):
306         """Perform a recursive walk of the grammar tree.
307
308         >>> tree = Node(two=Node(three=Node(), four=Node()))
309         >>> list(tree.walk())
310         [<Node:/>, <Node:/two>, <Node:/two/four>, <Node:/two/three>]
311         """
312         if predicate is None:
313             predicate = lambda node: True
314
315         def _walk(root):
316             if not predicate(root):
317                 return
318             yield root
319             for node in root._children.itervalues():
320                 for subnode in _walk(node):
321                     yield subnode
322
323         for node in _walk(self):
324             yield node
325
326     def children(self, context, follow=False):
327         """Iterate over child nodes, optionally follow()ing branches.
328
329         >>> from cly import *
330         >>> grammar = Grammar(two=Node(three=Node(),
331         ...                            four=Node()),
332         ...                            five=Alias(target='../two/*'))
333         >>> parser = Parser(grammar)
334         >>> context = Context(parser, None)
335         >>> list(grammar.children(context))
336         [<Alias:/five for /two/*>, <Node:/two>]
337         >>> list(grammar.children(context, follow=True))
338         [<Node:/two/four>, <Node:/two/three>, <Node:/two>]
339         """
340         for child in self:
341             if child.valid(context):
342                 if follow:
343                     for branch in child.follow(context):
344                         if branch.valid(context):
345                             yield branch
346                 else:
347                     yield child
348
349     def follow(self, context):
350         """Return alternative Nodes to traverse.
351
352         The children() method calls this method when follow=True to expand
353         aliased nodes, although it could be used for other purposes."""
354         yield self
355
356     def selected(self, context, match):
357         """This node was selected by the parser.
358
359         By default, informs the context that the node has been traversed."""
360         context.selected(self)
361
362     def next(self, context):
363         """Return an iterable over the set of next candidate nodes."""
364         for child in self.children(context, follow=True):
365             yield child
366
367     def match(self, context):
368         """Does this node match the current token?
369
370         Must return a regex match object or None for no match. If
371         ``match_candidates`` is true the token must also match one of the values
372         returned by ``candidates()``.
373
374         Must include separator in determining whether a match was
375         successful."""
376         if not self.valid(context):
377             return None
378         match = self._pattern.match(context.command, context.cursor)
379         if match:
380             # Check that separator matches as well
381             if not self._separator.match(context.command, context.cursor +
382                                          len(match.group())):
383                 return None
384             if self.match_candidates and match.group() + ' ' not in \
385                     self.candidates(context, match.group()):
386                 return None
387             return match
388
389     def advance(self, context):
390         """Advance context cursor based on this nodes match."""
391         match = self._full_match.match(context.command, context.cursor)
392         context.advance(len(match.group()))
393
394     def visible(self, context):
395         """Should this node be visible?"""
396         return True
397
398     def terminal(self, context):
399         """This node was selected as a terminal."""
400         raise UnexpectedEOL(context)
401
402     def depth(self):
403         """The depth of this node in the grammar.
404
405         >>> grammar = Grammar(one=Node(), two=Node())
406         >>> grammar.depth()
407         0
408         >>> grammar.find('/two').depth()
409         1
410         """
411         return self.parent and self.parent.depth() + 1 or 0
412
413     def path(self):
414         """The full grammar path to this node. Path components are separated
415         by a forward slash.
416
417         >>> grammar = Grammar(one=Node(), two=Node())
418         >>> grammar.find('/two').path()
419         '/two'
420         """
421         names = []
422         node = self
423         while node is not None:
424             if node.name is not None:
425                 names.insert(0, node.name)
426             node = node.parent
427         return '/' + '/'.join(names)
428
429     def candidates(self, context, text):
430         """Return an iterable of completion candidates for the given text. The
431         default is to use the content of self.help().
432
433         :param text: Text entered so far.
434
435         >>> grammar = Grammar(one=Node(), two=Node())
436         >>> list(grammar.find('/one').candidates(None, 'o'))
437         ['one ']
438         >>> list(grammar.find('/one').candidates(None, 't'))
439         []
440         """
441         for key, help in self.help(context):
442             if key[0] != '<':
443                 yield key
444
445     def find(self, path):
446         """Find a Node by path rooted at this node.
447
448         :param path: "Path" to the node, or a label.
449         :returns: Found node.
450
451         >>> top = Node(name='top', one=Node(),
452         ...            two=Node(three=Node()))
453         >>> top.find('/two/three')
454         <Node:/top/two/three>
455         >>> top.find('/one/bar')
456         Traceback (most recent call last):
457         ...
458         InvalidNodePath: /top/one/bar
459         """
460         if self.label == path:
461             return self
462         components = filter(None, path.split('/'))
463         if not components:
464             return self
465         for child in self:
466             if not path.startswith('/'):
467                 return child.find(path)
468             elif child.name == components[0]:
469                 return child.find('/' + '/'.join(components[1:]))
470         if path.startswith('/'):
471             raise InvalidNodePath(posixpath.join(self.path(), path.strip('/')))
472         else:
473             raise InvalidNodePath(path)
474
475     def valid(self, context):
476         """Is this node valid in the given context?"""
477         # Node is invalid if traversed more than self.traversals times
478         return not self.traversals or \
479             context.traversed(self) < self.traversals
480
481     def _get_anonymous(self):
482         """Is this node anonymous?"""
483         return self.name.startswith('__anonymous_')
484
485     anonymous = property(_get_anonymous, doc=_get_anonymous.__doc__)
486
487     def update(self, node):
488         """Merge another node into this one.
489
490         If a merging node collides with an existing one, the existing node
491         will be preserved and the merging nodes children merged.
492
493         :param node: Node to merge into this.
494         """
495         self.__anonymous_children += node.__anonymous_children
496         for key, child in node._children.iteritems():
497             if key not in self:
498                 self[key] = child
499             else:
500                 self[key].update(child)
501
502     def __repr__(self):
503         return '<%s:%s>' % (self.__class__.__name__, self.path() or '<root>')
504
505     @classmethod
506     def cast_attribute(cls, namespace, name, value):
507         """Define functions for casting attributes to their correct Python type.
508
509         Upcalling is necessary.
510
511         :param namespace: The XML namespace of the attribute. Compare against
512                     XMLGrammar.EVAL_NS to determine if forced evaluation
513                     can/should occur.
514         :param name: Attribute name.
515         :param value: Value to cast.
516
517         :returns: Tuple of (value, options) where options is a dictionary of
518                   extra Node constructor arguments.
519         """
520         casts = {
521             'traversals': int, 'group': group_cast,
522             'order': int, 'match_candidates': boolean_cast,
523             'with_context': boolean_cast,
524             }
525         if name in casts:
526             return casts[name](value), {}
527
528         # Attributes that can be strings but by default have a method.
529         if name == 'help' and namespace != XMLGrammar.EVAL_NS:
530             return value, {}
531
532         # Is the destination Node attribute callable? Do a lazy eval.
533         function = getattr(cls, name, None)
534         if callable(function):
535             args, _, _, _ = getargspec(function)
536             if args and args[0] == 'self':
537                 args.pop(0)
538             value = lazy_attr_evaluator(value, args)
539         return value, {}
540
541     @classmethod
542     def attribute_aliases(cls):
543         """Define attribute aliases for this node.
544
545         Parent classes are automatically merged, so upcalling is unnecessary.
546
547         :returns: Mapping of old to new keys.
548         """
549         return {'if': 'valid'}
550
551
552 class Masquerade(Node):
553     """A node that masquerades as other nodes.
554
555     Masquerade is a general-purpose tool for dynamically inserting nodes into
556     the grammar at the current location. Implementations should override the
557     ``masqueraded()`` method.
558
559     Use cases:
560
561       - Conditionally present nodes.
562       - Dynamically generated nodes.
563       - Aliases for other parts of the grammar.
564
565     >>> from cly.parser import Parser
566     >>> class Privileged(Masquerade):
567     ...     authenticated = False
568     ...     def masqueraded(self, context):
569     ...         if not self.authenticated:
570     ...             return []
571     ...         shutdown = Node(Action(self.shutdown), name='shutdown')
572     ...         return [shutdown]
573     ...     def shutdown(self):
574     ...         print 'shutdown()'
575     >>> privileged = Privileged()
576     >>> parser = Parser(Grammar(privileged, one=Node()))
577     >>> parser.execute('shutdown')
578     Traceback (most recent call last):
579     ...
580     InvalidToken: invalid token 'shutdown'
581     >>> privileged.authenticated = True
582     >>> parser.execute('shutdown')
583     shutdown()
584     """
585
586     def masqueraded(self, context):
587         """Return a sequence of all masqueraded nodes.
588
589         Masquerades as child nodes by default.
590         """
591         return super(Masquerade, self).children(context, follow=True)
592
593     def selected(self, context, match):
594         raise SystemError('Masqueraded nodes should never be selected')
595
596     def follow(self, context):
597         return list(self.masqueraded(context))
598
599     def visible(self, context):
600         for node in self.masqueraded(context):
601             if node.visible(context):
602                 return True
603         return False
604
605     def valid(self, context):
606         for node in self.masqueraded(context):
607             if node.valid(context):
608                 return True
609         return False
610
611
612 class Defaults(Masquerade):
613     """Set variables in a branch.
614
615     This is primarily useful in XML grammars.
616
617     >>> from cly import *
618     >>> parser = Parser(Grammar(Defaults(foo=10, bar=20)(baz=Node())))
619     >>> parser.parse('baz').vars
620     {'foo': 10, 'bar': 20}
621
622     In an XML grammar, all attributes are evaluated. This means that strings
623     must be double quoted.
624     """
625     vars = ''
626     unset = ''
627
628     def __init__(self, **kwargs):
629         super(Defaults, self).__init__()
630         self.vars = kwargs
631
632     def follow(self, context):
633         context.vars.update(self.vars)
634         return super(Defaults, self).follow(context)
635
636     @classmethod
637     def cast_attribute(cls, namespace, name, value):
638         return eval(value), {}
639
640
641 class