Changeset 481

Show
Ignore:
Timestamp:
12/06/07 10:20:15 (1 year ago)
Author:
athomas
Message:

cly:

  • Fixed some issues in rlext.py.
  • Added an XML grammar definition and parser: Grammar.from_xml().
  • Some doc updates.
  • Help strings are now optional.
Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • cly/trunk/cly/builder.py

    r470 r481  
    1414import os 
    1515import posixpath 
     16from xml.dom import minidom 
     17from inspect import isclass 
     18from cly.exceptions import * 
    1619 
    1720 
     
    2225 
    2326 
    24 from cly.exceptions import * 
    25  
    26  
    2727class Node(object): 
    2828    """The base class for all grammar nodes. 
     
    3434        pairs. There is a useful class called Help which can be used for 
    3535        this purpose. 
    36  
    37         Help strings are required. 
    38  
    39         >>> Node() 
    40         Traceback (most recent call last): 
    41         ... 
    42         TypeError: __init__() takes at least 2 arguments (1 given) 
    43         >>> Node("Something") 
    44         <Node:/> 
    4536 
    4637    ``name=None``: string 
     
    9687    traversals = 1 
    9788 
    98     def __init__(self, help, *args, **kwargs): 
     89    def __init__(self, help='', *args, **kwargs): 
    9990        self._children = {} 
    10091        if isinstance(help, basestring): 
     
    475466 
    476467    def __init__(self, target, *args, **kwargs): 
    477         Node.__init__(self, '<alias for "%s">' % target, *args, **kwargs) 
     468        Node.__init__(self, '<alias for "%s">' % target, 
     469                      *args, **kwargs) 
    478470        self._target = target 
    479471 
     
    543535    with_user_context = None 
    544536 
    545     def __init__(self, help, callback=None, *args, **kwargs): 
     537    def __init__(self, help='', callback=None, *args, **kwargs): 
    546538        if isinstance(help, basestring): 
    547539            help_string = help 
     
    658650    def terminal(self, context): 
    659651        """Null-op for empty lines.""" 
     652 
     653    def from_xml(cls, xml, extra_nodes=None, **locals): 
     654        """Build a CLY Grammar from XML. 
     655 
     656        ``xml``: string 
     657            XML source 
     658        ``extra_nodes``: dictionary 
     659            Dictionary of Node subclasses. 
     660        ``locals``: 
     661            Valid locals() when evaluating XML grammar node attributes. 
     662 
     663        Returns a new Grammar object. 
     664        """ 
     665        try: 
     666            dom = minidom.parseString(xml) 
     667        except Exception, e: 
     668            raise XMLParseError(str(e)) 
     669 
     670        extra_nodes = extra_nodes or {} 
     671 
     672        def boolean(v): 
     673            return v in ('True', 'true') 
     674 
     675        def evaluate(v): 
     676            return eval(v, globals(), locals) 
     677 
     678        arg_types = { 
     679            'traversals': int, 
     680            'group': int, 
     681            'order': int, 
     682            'match_candidates': boolean, 
     683        } 
     684 
     685        arg_types.update(dict.fromkeys( 
     686            'children follow selected next match advance visible ' \ 
     687            'terminal depth path candidates find valid callback'.split(), 
     688            evaluate 
     689            )) 
     690 
     691        node_types = dict([(v.__name__.lower(), v) 
     692                          for v in globals().values() 
     693                          if isclass(v) and issubclass(v, Node)]) 
     694 
     695        node_types.update(extra_nodes) 
     696 
     697        def parse(parent, xnode): 
     698            if not xnode: 
     699                return 
     700 
     701            if xnode.nodeType == minidom.Node.ELEMENT_NODE: 
     702                cls = node_types.get(xnode.localName.lower()) 
     703                if not cls: 
     704                    raise XMLParseError('Invalid node type "%s"' % name) 
     705 
     706                attributes = dict([(str(k), v) for k, v 
     707                                   in xnode.attributes.items()]) 
     708 
     709                for k, v in attributes.items(): 
     710                    if k.startswith('eval:'): 
     711                        attributes.pop(k) 
     712                        k = k[5:] 
     713                        v = evaluate(v) 
     714                    else: 
     715                        v = arg_types.get(k, str)(v) 
     716                    attributes[k] = v 
     717 
     718                name = attributes.pop('name', None) 
     719                node = cls(**attributes) 
     720                if name: 
     721                    path = parent.path() + '/' + name 
     722                    parent(**{str(name): node}) 
     723                else: 
     724                    parent(node) 
     725            else: 
     726                node = parent 
     727 
     728            parse(node, xnode.firstChild) 
     729            parse(parent, xnode.nextSibling) 
     730 
     731        grammar = Grammar() 
     732        if dom.firstChild.localName != 'grammar': 
     733            raise XMLParseError('Invalid root element "%s", expected "grammar"' 
     734                                % dom.firstChild.localName) 
     735        parse(grammar, dom.firstChild.firstChild) 
     736        return grammar 
     737 
     738    from_xml = classmethod(from_xml) 
    660739 
    661740 
  • cly/trunk/cly/exceptions.py

    r434 r481  
    1414 
    1515__all__ = ['Error', 'InvalidHelp', 'InvalidNodePath', 'InvalidAnonymousNode', 
    16            'ParseError', 'UnexpectedEOL', 'InvalidToken', 'ValidationError'] 
     16           'ParseError', 'UnexpectedEOL', 'InvalidToken', 'ValidationError', 
     17           'XMLParseError'] 
    1718__docformat__ = 'restructuredtext en' 
    1819 
     
    3738 
    3839 
     40class XMLParseError(Error): 
     41    """Report an XML grammar parsing error.""" 
     42 
     43 
    3944class ParseError(Error): 
    4045    """Report a parse error. Output is formatted using string templates, 
     
    4853 
    4954    def __init__(self, context, message=None, **kwargs): 
    50         Error.__init__(self) 
     55        template = string.Template(message or self.message) 
     56        message = template.safe_substitute(remaining=context.remaining, 
     57                                           **kwargs) 
     58        Error.__init__(self, message) 
    5159        self.context = context 
    52         self.template_args = kwargs 
    53         if message is not None: 
    54             self.message = message 
    5560 
    56     def __str__(self): 
    57         template = string.Template(self.message) 
    58         return template.safe_substitute(remaining=self.context.remaining, 
    59                                         **self.template_args) 
    6061 
    6162class UnexpectedEOL(ParseError): 
  • cly/trunk/cly/interactive.py

    r468 r481  
    213213        from StringIO import StringIO 
    214214        out = StringIO() 
    215         traceback.print_exc(file = out) 
     215        traceback.print_exc(file=out) 
    216216        print >>sys.stderr, str(exception) 
    217217        print >>sys.stderr, out.getvalue() 
     
    265265            Interact._dump_traceback(e) 
    266266            cly.rlext.force_redisplay() 
    267             raise 
     267            return 0 
    268268 
    269269 
  • cly/trunk/cly/rlext.py

    r468 r481  
    55 
    66# Type declarations 
    7 # int rl_bind_key (int KEY, rl_command_func_t *FUNCTION);' 
    87rl_command_func_t = CFUNCTYPE(c_int, c_int, c_int) 
    98 
    109# Cursor variables 
    11 rl_point = cast(readline.rl_point, POINTER(c_int)
    12 rl_end = cast(readline.rl_end, POINTER(c_int)
     10rl_point = c_int.in_dll(readline, 'rl_point'
     11rl_end = c_int.in_dll(readline, 'rl_end'
    1312 
    1413rl_forced_update_display = readline.rl_forced_update_display 
     
    3231    """Set or get the cursor location.""" 
    3332    if pos is None: 
    34         return rl_point.contents.value 
    35     elif rl_point.contents.value > rl_end.contents.value: 
    36         rl_point.contents = rl_end.contents 
    37     elif rl_point.contents.value < 0: 
    38         rl_point.contents = c_int(0) 
     33        return rl_point.value 
     34    elif rl_point.value > rl_end.value: 
     35        rl_point.value = rl_end.value 
     36    elif rl_point.value < 0: 
     37        rl_point.value = 0 
    3938    else: 
    40         rl_point.contents = c_int(pos) 
     39        rl_point.value = pos 
     40    return 0 
  • cly/trunk/cly/test.py

    r434 r481  
    99import unittest 
    1010import doctest 
     11from cly import Grammar, Parser 
     12 
     13 
     14class TestXMLGrammar(unittest.TestCase): 
     15    """Test XML grammar parser.""" 
     16    def setUp(self): 
     17        self._output = None 
     18 
     19    def _echo(self, **kwargs): 
     20        self._output = kwargs 
     21 
     22    def test_basic(self): 
     23        xml = """<?xml version="1.0"?> 
     24        <grammar> 
     25            <node name='echo'> 
     26                <variable name='text'> 
     27                    <action callback='echo'/> 
     28                </variable> 
     29            </node> 
     30        </grammar> 
     31        """ 
     32 
     33        grammar = Grammar.from_xml(xml, echo=self._echo) 
     34        parser = Parser(grammar) 
     35        parser.execute('echo magic') 
     36        self.assertEqual(self._output, {'text': 'magic'}) 
     37 
     38    def test_integer_types(self): 
     39        xml = """<?xml version="1.0"?> 
     40        <grammar> 
     41            <node name='echo'> 
     42                <variable name='text' traversals='0'> 
     43                    <alias target='/echo/*'/> 
     44                    <action callback='echo'/> 
     45                </variable> 
     46            </node> 
     47        </grammar> 
     48        """ 
     49 
     50        grammar = Grammar.from_xml(xml, echo=self._echo) 
     51        parser = Parser(grammar) 
     52        parser.execute('echo magic monkey') 
     53        self.assertEqual(self._output, {'text': ['magic', 'monkey']}) 
     54 
     55    def test_group(self): 
     56        xml = """<?xml version="1.0"?> 
     57        <grammar> 
     58            <node name='echo'> 
     59                <group traversals='0'> 
     60                    <variable name='text'> 
     61                        <alias target='../../*'/> 
     62                        <action callback='echo'/> 
     63                    </variable> 
     64                </group> 
     65            </node> 
     66        </grammar> 
     67        """ 
     68 
     69        grammar = Grammar.from_xml(xml, echo=self._echo) 
     70        parser = Parser(grammar) 
     71        parser.execute('echo magic monkey') 
     72        self.assertEqual(self._output, {'text': ['magic', 'monkey']}) 
     73 
     74    def test_completion(self): 
     75        xml = """<?xml version="1.0"?> 
     76        <grammar> 
     77            <node name='echo'> 
     78                <variable name='text' candidates='candidates'> 
     79                    <action callback='echo'/> 
     80                </variable> 
     81            </node> 
     82        </grammar> 
     83        """ 
     84 
     85        def candidates(context, text): 
     86            return ['monkey', 'muppet'] 
     87 
     88        grammar = Grammar.from_xml(xml, echo=self._echo, candidates=candidates) 
     89        parser = Parser(grammar) 
     90        context = parser.parse('echo ') 
     91        self.assertEqual(list(context.candidates()), ['monkey', 'muppet']) 
    1192 
    1293 
     
    21102 
    22103    suite = unittest.TestSuite() 
     104    suite.addTest(unittest.makeSuite(TestXMLGrammar, 'test')) 
    23105    suite.addTest(doctest.DocTestSuite(cly)) 
    24106    suite.addTest(doctest.DocTestSuite(cly.interactive)) 
  • cly/trunk/doc/developers-guide.rst

    r470 r481  
    1010 
    1111  1. `Defining the grammar`_. 
    12   2. `Creating a parser`_ based on that grammar. 
    13   3. Parsing input: 
    14  
    15     1. Create a context in which to store parse state. 
    16     2. Traverse grammar, tokenising and matching input stream. 
    17     3. Collect variables from input stream. 
    18  
    19   4. Execute actions, passing collected variables to callback. 
     12  2. `Parsing`_ input text. 
    2013 
    2114Defining the Grammar 
     
    301294already been collected. 
    302295 
    303 Creating a Parser 
    304 ----------------- 
     296Parsing 
     297------- 
    305298 
    306299To actually utilise a grammar you need to bind it to a ``Parser`` 
    307300object. The parser takes care of creating a context for each parse run, 
    308 and parsing the input. The actual collection of variables is taken care 
    309 of by the ``Context`` object. 
    310  
    311 Sometimes it's useful to pass an arbitrary object through to the grammar 
    312 callbacks. This can be achieved with the ``user_context`` parameter to 
    313 the ``parse()`` and ``execute()`` methods of ``Parser``.  This object is 
    314 available to callbacks as the first parameter if the corresponding 
    315 ``Action`` has ``with_user_context=True`` (this flag can also be set 
    316 parser-wide by passing the same parameter to the ``Parser`` 
    317 constructor). It is also available to ``Node`` subclasses as the 
    318 ``context`` parameter to most methods. 
    319  
    320 You can also parse the input without executing the callback, by calling 
    321 the ``Parser.parse()`` method. This returns a ``Context`` object which 
    322 can be inspected if desired. Normally though, just call 
    323 ``Parser.execute()``. 
    324  
    325 Here's an example of normal parser use: 
    326  
    327 .. code-block:: python 
    328  
    329  
    330   def one(one): 
    331     print one 
    332  
    333   grammar = Grammar( 
    334     one=Number('One')( 
    335       Action('Execute one', one), 
    336     ), 
    337   ) 
     301parsing the input, and usually executing any callbacks. 
     302 
     303Basic usage is: 
     304 
     305.. code-block:: python 
    338306 
    339307  parser = Parser(grammar) 
    340   context = parser.parse 
    341   parser.execute('1234') 
    342  
    343  
    344 And here's an example of how to pass a user-defined object through to 
    345 your callbacks: 
    346  
    347 .. code-block:: python 
    348  
    349   def one(context, one): 
    350     print 'One:', context, one 
    351  
    352   grammar = Grammar( 
    353     one=Number('One')( 
    354       Action('Execute one', one, with_user_context=True), 
    355     ), 
    356   ) 
    357  
    358   parser = Parser(grammar) 
    359   context = parser.parse 
    360   parser.execute('1234', user_context='Moo') 
    361  
    362 This will print:: 
    363  
    364   One: Moo 1234 
     308  parser.execute('some input text') 
     309 
     310This will parse the input text and execute any callbacks. If a parse 
     311error occurs a ``cly.exceptions.ParseError`` will be raised. 
     312 
     313Passing User-defined Objects to Callbacks 
     314~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
     315 
     316Sometimes it's useful to be able to pass an arbitrary object through to 
     317the grammar callbacks. This is achieved by first enabling this behaviour 
     318on the desired action nodes by either passing ``with_user_context=True`` 
     319to the constructor or enabled across the entire grammar by doing the 
     320same on the ``Parser`` constructor. Once enabled, simply pass the object 
     321to the ``execute()`` method via the ``user_context`` parameter. That 
     322object will then be provided to all action callbacks as the first 
     323parameter. 
     324 
     325One way of applying this is by binding all callbacks to methods on a 
     326single *class*, then passing an instance of that class as the context: 
     327 
     328.. code-block:: python 
     329 
     330  class A(object): 
     331    def __init__(self, name): 
     332      self.name = name 
     333 
     334    def one(self, one): 
     335      print "One:", self.name, one 
     336 
     337    def two(self, two): 
     338      print "Two:", self.name, two 
     339 
     340  grammar = Grammar( 
     341    one=Variable('One')( 
     342      Action('Execute one', A.one), 
     343      ), 
     344    two=Variable('Two')( 
     345      Action('Execute two', A.two), 
     346      ), 
     347  ) 
     348 
     349  a = A('a') 
     350  b = A('b') 
     351 
     352  parser = Parser(grammar, with_user_context=True) 
     353 
     354  parser.execute('one', user_context=a) 
     355  parser.execute('two', user_context=b) 
     356 
     357This will output:: 
     358 
     359  One: a one 
     360  One: b two 
     361 
    365362 
    366363.. _API documentation: http://swapoff.org/cly/docs 
  • cly/trunk/.todo

    r434 r481  
    1 <todo version="0.1.19"> 
     1<?xml version="1.0"?> 
     2<todo version="0.1.20"> 
    23    <title> 
    34        CLY 
     
    1516        Allow with_context to be specified per-parser. 
    1617    </note> 
     18    <note priority="medium" time="1196955863"> 
     19        Figure out why rlext.py dies when the parser is used... 
     20    </note> 
    1721</todo>