Edgewall Software

source: tags/0.5.0/genshi/path.py

Last change on this file was 835, checked in by cmlenz, 15 years ago

Fix copyright years.

  • Property svn:eol-style set to native
File size: 43.7 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2006-2008 Edgewall Software
4# All rights reserved.
5#
6# This software is licensed as described in the file COPYING, which
7# you should have received as part of this distribution. The terms
8# are also available at http://genshi.edgewall.org/wiki/License.
9#
10# This software consists of voluntary contributions made by many
11# individuals. For the exact contribution history, see the revision
12# history and logs, available at http://genshi.edgewall.org/log/.
13
14"""Basic support for evaluating XPath expressions against streams.
15
16>>> from genshi.input import XML
17>>> doc = XML('''<doc>
18...  <items count="4">
19...       <item status="new">
20...         <summary>Foo</summary>
21...       </item>
22...       <item status="closed">
23...         <summary>Bar</summary>
24...       </item>
25...       <item status="closed" resolution="invalid">
26...         <summary>Baz</summary>
27...       </item>
28...       <item status="closed" resolution="fixed">
29...         <summary>Waz</summary>
30...       </item>
31...   </items>
32... </doc>''')
33>>> print doc.select('items/item[@status="closed" and '
34...     '(@resolution="invalid" or not(@resolution))]/summary/text()')
35BarBaz
36
37Because the XPath engine operates on markup streams (as opposed to tree
38structures), it only implements a subset of the full XPath 1.0 language.
39"""
40
41from math import ceil, floor
42import operator
43import re
44
45from genshi.core import Stream, Attrs, Namespace, QName
46from genshi.core import START, END, TEXT, START_NS, END_NS, COMMENT, PI, \
47                        START_CDATA, END_CDATA
48
49__all__ = ['Path', 'PathSyntaxError']
50__docformat__ = 'restructuredtext en'
51
52
53class Axis(object):
54    """Defines constants for the various supported XPath axes."""
55
56    ATTRIBUTE = 'attribute'
57    CHILD = 'child'
58    DESCENDANT = 'descendant'
59    DESCENDANT_OR_SELF = 'descendant-or-self'
60    SELF = 'self'
61
62    def forname(cls, name):
63        """Return the axis constant for the given name, or `None` if no such
64        axis was defined.
65        """
66        return getattr(cls, name.upper().replace('-', '_'), None)
67    forname = classmethod(forname)
68
69
70ATTRIBUTE = Axis.ATTRIBUTE
71CHILD = Axis.CHILD
72DESCENDANT = Axis.DESCENDANT
73DESCENDANT_OR_SELF = Axis.DESCENDANT_OR_SELF
74SELF = Axis.SELF
75
76
77class Path(object):
78    """Implements basic XPath support on streams.
79   
80    Instances of this class represent a "compiled" XPath expression, and provide
81    methods for testing the path against a stream, as well as extracting a
82    substream matching that path.
83    """
84
85    def __init__(self, text, filename=None, lineno=-1):
86        """Create the path object from a string.
87       
88        :param text: the path expression
89        :param filename: the name of the file in which the path expression was
90                         found (used in error messages)
91        :param lineno: the line on which the expression was found
92        """
93        self.source = text
94        self.paths = PathParser(text, filename, lineno).parse()
95
96    def __repr__(self):
97        paths = []
98        for path in self.paths:
99            steps = []
100            for axis, nodetest, predicates in path:
101                steps.append('%s::%s' % (axis, nodetest))
102                for predicate in predicates:
103                    steps[-1] += '[%s]' % predicate
104            paths.append('/'.join(steps))
105        return '<%s "%s">' % (self.__class__.__name__, '|'.join(paths))
106
107    def select(self, stream, namespaces=None, variables=None):
108        """Returns a substream of the given stream that matches the path.
109       
110        If there are no matches, this method returns an empty stream.
111       
112        >>> from genshi.input import XML
113        >>> xml = XML('<root><elem><child>Text</child></elem></root>')
114       
115        >>> print Path('.//child').select(xml)
116        <child>Text</child>
117       
118        >>> print Path('.//child/text()').select(xml)
119        Text
120       
121        :param stream: the stream to select from
122        :param namespaces: (optional) a mapping of namespace prefixes to URIs
123        :param variables: (optional) a mapping of variable names to values
124        :return: the substream matching the path, or an empty stream
125        :rtype: `Stream`
126        """
127        if namespaces is None:
128            namespaces = {}
129        if variables is None:
130            variables = {}
131        stream = iter(stream)
132        def _generate():
133            test = self.test()
134            for event in stream:
135                result = test(event, namespaces, variables)
136                if result is True:
137                    yield event
138                    if event[0] is START:
139                        depth = 1
140                        while depth > 0:
141                            subevent = stream.next()
142                            if subevent[0] is START:
143                                depth += 1
144                            elif subevent[0] is END:
145                                depth -= 1
146                            yield subevent
147                            test(subevent, namespaces, variables,
148                                 updateonly=True)
149                elif result:
150                    yield result
151        return Stream(_generate(),
152                      serializer=getattr(stream, 'serializer', None))
153
154    def test(self, ignore_context=False):
155        """Returns a function that can be used to track whether the path matches
156        a specific stream event.
157       
158        The function returned expects the positional arguments ``event``,
159        ``namespaces`` and ``variables``. The first is a stream event, while the
160        latter two are a mapping of namespace prefixes to URIs, and a mapping
161        of variable names to values, respectively. In addition, the function
162        accepts an ``updateonly`` keyword argument that default to ``False``. If
163        it is set to ``True``, the function only updates its internal state,
164        but does not perform any tests or return a result.
165       
166        If the path matches the event, the function returns the match (for
167        example, a `START` or `TEXT` event.) Otherwise, it returns ``None``.
168       
169        >>> from genshi.input import XML
170        >>> xml = XML('<root><elem><child id="1"/></elem><child id="2"/></root>')
171        >>> test = Path('child').test()
172        >>> for event in xml:
173        ...     if test(event, {}, {}):
174        ...         print event[0], repr(event[1])
175        START (QName(u'child'), Attrs([(QName(u'id'), u'2')]))
176       
177        :param ignore_context: if `True`, the path is interpreted like a pattern
178                               in XSLT, meaning for example that it will match
179                               at any depth
180        :return: a function that can be used to test individual events in a
181                 stream against the path
182        :rtype: ``function``
183        """
184        paths = [(p, len(p), [0], [], [0] * len(p)) for p in [
185            (ignore_context and [_DOTSLASHSLASH] or []) + p for p in self.paths
186        ]]
187
188        def _test(event, namespaces, variables, updateonly=False):
189            kind, data, pos = event[:3]
190            retval = None
191            for steps, size, cursors, cutoff, counter in paths:
192                # Manage the stack that tells us "where we are" in the stream
193                if kind is END:
194                    if cursors:
195                        cursors.pop()
196                    continue
197                elif kind is START:
198                    cursors.append(cursors and cursors[-1] or 0)
199                elif kind is START_NS or kind is END_NS \
200                        or kind is START_CDATA or kind is END_CDATA:
201                    continue
202
203                if updateonly or retval or not cursors:
204                    continue
205                cursor = cursors[-1]
206                depth = len(cursors)
207
208                if cutoff and depth + int(kind is not START) > cutoff[0]:
209                    continue
210
211                ctxtnode = not ignore_context and kind is START \
212                                              and depth == 2
213                matched = None
214                while 1:
215                    # Fetch the next location step
216                    axis, nodetest, predicates = steps[cursor]
217
218                    # If this is the start event for the context node, and the
219                    # axis of the location step doesn't include the current
220                    # element, skip the test
221                    if ctxtnode and (axis is CHILD or axis is DESCENDANT):
222                        break
223
224                    # Is this the last step of the location path?
225                    last_step = cursor + 1 == size
226
227                    # Perform the actual node test
228                    matched = nodetest(kind, data, pos, namespaces, variables)
229
230                    # The node test matched
231                    if matched:
232
233                        # Check all the predicates for this step
234                        if predicates:
235                            for predicate in predicates:
236                                pretval = predicate(kind, data, pos, namespaces,
237                                                    variables)
238                                if type(pretval) is float: # FIXME <- need to
239                                                           # check this for
240                                                           # other types that
241                                                           # can be coerced to
242                                                           # float
243                                    counter[cursor] += 1
244                                    if counter[cursor] != int(pretval):
245                                        pretval = False
246                                if not pretval:
247                                    matched = None
248                                    break
249
250                        # Both the node test and the predicates matched
251                        if matched:
252                            if last_step:
253                                if not ctxtnode or kind is not START \
254                                        or axis is ATTRIBUTE or axis is SELF:
255                                    retval = matched
256                            elif not ctxtnode or axis is SELF \
257                                              or axis is DESCENDANT_OR_SELF:
258                                cursor += 1
259                                cursors[-1] = cursor
260                            cutoff[:] = []
261
262                    if kind is START:
263                        if last_step and not (axis is DESCENDANT or
264                                              axis is DESCENDANT_OR_SELF):
265                            cutoff[:] = [depth]
266
267                        elif steps[cursor][0] is ATTRIBUTE:
268                            # If the axis of the next location step is the
269                            # attribute axis, we need to move on to processing
270                            # that step without waiting for the next markup
271                            # event
272                            continue
273
274                    # We're done with this step if it's the last step or the
275                    # axis isn't "self"
276                    if not matched or last_step or not (
277                            axis is SELF or axis is DESCENDANT_OR_SELF):
278                        break
279                    if ctxtnode and axis is DESCENDANT_OR_SELF:
280                        ctxtnode = False
281
282                if (retval or not matched) and kind is START and \
283                        not (axis is DESCENDANT or axis is DESCENDANT_OR_SELF):
284                    # If this step is not a closure, it cannot be matched until
285                    # the current element is closed... so we need to move the
286                    # cursor back to the previous closure and retest that
287                    # against the current element
288                    backsteps = [(i, k, d, p) for i, (k, d, p)
289                                 in enumerate(steps[:cursor])
290                                 if k is DESCENDANT or k is DESCENDANT_OR_SELF]
291                    backsteps.reverse()
292                    for cursor, axis, nodetest, predicates in backsteps:
293                        if nodetest(kind, data, pos, namespaces, variables):
294                            cutoff[:] = []
295                            break
296                    cursors[-1] = cursor
297
298            return retval
299
300        return _test
301
302
303class PathSyntaxError(Exception):
304    """Exception raised when an XPath expression is syntactically incorrect."""
305
306    def __init__(self, message, filename=None, lineno=-1, offset=-1):
307        if filename:
308            message = '%s (%s, line %d)' % (message, filename, lineno)
309        Exception.__init__(self, message)
310        self.filename = filename
311        self.lineno = lineno
312        self.offset = offset
313
314
315class PathParser(object):
316    """Tokenizes and parses an XPath expression."""
317
318    _QUOTES = (("'", "'"), ('"', '"'))
319    _TOKENS = ('::', ':', '..', '.', '//', '/', '[', ']', '()', '(', ')', '@',
320               '=', '!=', '!', '|', ',', '>=', '>', '<=', '<', '$')
321    _tokenize = re.compile('("[^"]*")|(\'[^\']*\')|((?:\d+)?\.\d+)|(%s)|([^%s\s]+)|\s+' % (
322                           '|'.join([re.escape(t) for t in _TOKENS]),
323                           ''.join([re.escape(t[0]) for t in _TOKENS]))).findall
324
325    def __init__(self, text, filename=None, lineno=-1):
326        self.filename = filename
327        self.lineno = lineno
328        self.tokens = filter(None, [dqstr or sqstr or number or token or name
329                                    for dqstr, sqstr, number, token, name in
330                                    self._tokenize(text)])
331        self.pos = 0
332
333    # Tokenizer
334
335    at_end = property(lambda self: self.pos == len(self.tokens) - 1)
336    cur_token = property(lambda self: self.tokens[self.pos])
337
338    def next_token(self):
339        self.pos += 1
340        return self.tokens[self.pos]
341
342    def peek_token(self):
343        if not self.at_end:
344            return self.tokens[self.pos + 1]
345        return None
346
347    # Recursive descent parser
348
349    def parse(self):
350        """Parses the XPath expression and returns a list of location path
351        tests.
352       
353        For union expressions (such as `*|text()`), this function returns one
354        test for each operand in the union. For patch expressions that don't
355        use the union operator, the function always returns a list of size 1.
356       
357        Each path test in turn is a sequence of tests that correspond to the
358        location steps, each tuples of the form `(axis, testfunc, predicates)`
359        """
360        paths = [self._location_path()]
361        while self.cur_token == '|':
362            self.next_token()
363            paths.append(self._location_path())
364        if not self.at_end:
365            raise PathSyntaxError('Unexpected token %r after end of expression'
366                                  % self.cur_token, self.filename, self.lineno)
367        return paths
368
369    def _location_path(self):
370        steps = []
371        while True:
372            if self.cur_token.startswith('/'):
373                if self.cur_token == '//':
374                    steps.append((DESCENDANT_OR_SELF, NodeTest(), []))
375                elif not steps:
376                    raise PathSyntaxError('Absolute location paths not '
377                                          'supported', self.filename,
378                                          self.lineno)
379                self.next_token()
380
381            axis, nodetest, predicates = self._location_step()
382            if not axis:
383                axis = CHILD
384            steps.append((axis, nodetest, predicates))
385
386            if self.at_end or not self.cur_token.startswith('/'):
387                break
388
389        return steps
390
391    def _location_step(self):
392        if self.cur_token == '@':
393            axis = ATTRIBUTE
394            self.next_token()
395        elif self.cur_token == '.':
396            axis = SELF
397        elif self.cur_token == '..':
398            raise PathSyntaxError('Unsupported axis "parent"', self.filename,
399                                  self.lineno)
400        elif self.peek_token() == '::':
401            axis = Axis.forname(self.cur_token)
402            if axis is None:
403                raise PathSyntaxError('Unsupport axis "%s"' % axis,
404                                      self.filename, self.lineno)
405            self.next_token()
406            self.next_token()
407        else:
408            axis = None
409        nodetest = self._node_test(axis or CHILD)
410        predicates = []
411        while self.cur_token == '[':
412            predicates.append(self._predicate())
413        return axis, nodetest, predicates
414
415    def _node_test(self, axis=None):
416        test = prefix = None
417        next_token = self.peek_token()
418        if next_token in ('(', '()'): # Node type test
419            test = self._node_type()
420
421        elif next_token == ':': # Namespace prefix
422            prefix = self.cur_token
423            self.next_token()
424            localname = self.next_token()
425            if localname == '*':
426                test = QualifiedPrincipalTypeTest(axis, prefix)
427            else:
428                test = QualifiedNameTest(axis, prefix, localname)
429
430        else: # Name test
431            if self.cur_token == '*':
432                test = PrincipalTypeTest(axis)
433            elif self.cur_token == '.':
434                test = NodeTest()
435            else:
436                test = LocalNameTest(axis, self.cur_token)
437
438        if not self.at_end:
439            self.next_token()
440        return test
441
442    def _node_type(self):
443        name = self.cur_token
444        self.next_token()
445
446        args = []
447        if self.cur_token != '()':
448            # The processing-instruction() function optionally accepts the
449            # name of the PI as argument, which must be a literal string
450            self.next_token() # (
451            if self.cur_token != ')':
452                string = self.cur_token
453                if (string[0], string[-1]) in self._QUOTES:
454                    string = string[1:-1]
455                args.append(string)
456
457        cls = _nodetest_map.get(name)
458        if not cls:
459            raise PathSyntaxError('%s() not allowed here' % name, self.filename,
460                                  self.lineno)
461        return cls(*args)
462
463    def _predicate(self):
464        assert self.cur_token == '['
465        self.next_token()
466        expr = self._or_expr()
467        if self.cur_token != ']':
468            raise PathSyntaxError('Expected "]" to close predicate, '
469                                  'but found "%s"' % self.cur_token,
470                                  self.filename, self.lineno)
471        if not self.at_end:
472            self.next_token()
473        return expr
474
475    def _or_expr(self):
476        expr = self._and_expr()
477        while self.cur_token == 'or':
478            self.next_token()
479            expr = OrOperator(expr, self._and_expr())
480        return expr
481
482    def _and_expr(self):
483        expr = self._equality_expr()
484        while self.cur_token == 'and':
485            self.next_token()
486            expr = AndOperator(expr, self._equality_expr())
487        return expr
488
489    def _equality_expr(self):
490        expr = self._relational_expr()
491        while self.cur_token in ('=', '!='):
492            op = _operator_map[self.cur_token]
493            self.next_token()
494            expr = op(expr, self._relational_expr())
495        return expr
496
497    def _relational_expr(self):
498        expr = self._sub_expr()
499        while self.cur_token in ('>', '>=', '<', '>='):
500            op = _operator_map[self.cur_token]
501            self.next_token()
502            expr = op(expr, self._sub_expr())
503        return expr
504
505    def _sub_expr(self):
506        token = self.cur_token
507        if token != '(':
508            return self._primary_expr()
509        self.next_token()
510        expr = self._or_expr()
511        if self.cur_token != ')':
512            raise PathSyntaxError('Expected ")" to close sub-expression, '
513                                  'but found "%s"' % self.cur_token,
514                                  self.filename, self.lineno)
515        self.next_token()
516        return expr
517
518    def _primary_expr(self):
519        token = self.cur_token
520        if len(token) > 1 and (token[0], token[-1]) in self._QUOTES:
521            self.next_token()
522            return StringLiteral(token[1:-1])
523        elif token[0].isdigit() or token[0] == '.':
524            self.next_token()
525            return NumberLiteral(as_float(token))
526        elif token == '$':
527            token = self.next_token()
528            self.next_token()
529            return VariableReference(token)
530        elif not self.at_end and self.peek_token().startswith('('):
531            return self._function_call()
532        else:
533            axis = None
534            if token == '@':
535                axis = ATTRIBUTE
536                self.next_token()
537            return self._node_test(axis)
538
539    def _function_call(self):
540        name = self.cur_token
541        if self.next_token() == '()':
542            args = []
543        else:
544            assert self.cur_token == '('
545            self.next_token()
546            args = [self._or_expr()]
547            while self.cur_token == ',':
548                self.next_token()
549                args.append(self._or_expr())
550            if not self.cur_token == ')':
551                raise PathSyntaxError('Expected ")" to close function argument '
552                                      'list, but found "%s"' % self.cur_token,
553                                      self.filename, self.lineno)
554        self.next_token()
555        cls = _function_map.get(name)
556        if not cls:
557            raise PathSyntaxError('Unsupported function "%s"' % name,
558                                  self.filename, self.lineno)
559        return cls(*args)
560
561
562# Type coercion
563
564def as_scalar(value):
565    """Convert value to a scalar. If a single element Attrs() object is passed
566    the value of the single attribute will be returned."""
567    if isinstance(value, Attrs):
568        assert len(value) == 1
569        return value[0][1]
570    else:
571        return value
572
573def as_float(value):
574    # FIXME - if value is a bool it will be coerced to 0.0 and consequently
575    # compared as a float. This is probably not ideal.
576    return float(as_scalar(value))
577
578def as_long(value):
579    return long(as_scalar(value))
580
581def as_string(value):
582    value = as_scalar(value)
583    if value is False:
584        return u''
585    return unicode(value)
586
587def as_bool(value):
588    return bool(as_scalar(value))
589
590
591# Node tests
592
593class PrincipalTypeTest(object):
594    """Node test that matches any event with the given principal type."""
595    __slots__ = ['principal_type']
596    def __init__(self, principal_type):
597        self.principal_type = principal_type
598    def __call__(self, kind, data, pos, namespaces, variables):
599        if kind is START:
600            if self.principal_type is ATTRIBUTE:
601                return data[1] or None
602            else:
603                return True
604    def __repr__(self):
605        return '*'
606
607class QualifiedPrincipalTypeTest(object):
608    """Node test that matches any event with the given principal type in a
609    specific namespace."""
610    __slots__ = ['principal_type', 'prefix']
611    def __init__(self, principal_type, prefix):
612        self.principal_type = principal_type
613        self.prefix = prefix
614    def __call__(self, kind, data, pos, namespaces, variables):
615        namespace = Namespace(namespaces.get(self.prefix))
616        if kind is START:
617            if self.principal_type is ATTRIBUTE and data[1]:
618                return Attrs([(name, value) for name, value in data[1]
619                              if name in namespace]) or None
620            else:
621                return data[0] in namespace
622    def __repr__(self):
623        return '%s:*' % self.prefix
624
625class LocalNameTest(object):
626    """Node test that matches any event with the given principal type and
627    local name.
628    """
629    __slots__ = ['principal_type', 'name']
630    def __init__(self, principal_type, name):
631        self.principal_type = principal_type
632        self.name = name
633    def __call__(self, kind, data, pos, namespaces, variables):
634        if kind is START:
635            if self.principal_type is ATTRIBUTE and self.name in data[1]:
636                return Attrs([(self.name, data[1].get(self.name))])
637            else:
638                return data[0].localname == self.name
639    def __repr__(self):
640        return self.name
641
642class QualifiedNameTest(object):
643    """Node test that matches any event with the given principal type and
644    qualified name.
645    """
646    __slots__ = ['principal_type', 'prefix', 'name']
647    def __init__(self, principal_type, prefix, name):
648        self.principal_type = principal_type
649        self.prefix = prefix
650        self.name = name
651    def __call__(self, kind, data, pos, namespaces, variables):
652        qname = QName('%s}%s' % (namespaces.get(self.prefix), self.name))
653        if kind is START:
654            if self.principal_type is ATTRIBUTE and qname in data[1]:
655                return Attrs([(self.name, data[1].get(self.name))])
656            else:
657                return data[0] == qname
658    def __repr__(self):
659        return '%s:%s' % (self.prefix, self.name)
660
661class CommentNodeTest(object):
662    """Node test that matches any comment events."""
663    __slots__ = []
664    def __call__(self, kind, data, pos, namespaces, variables):
665        return kind is COMMENT
666    def __repr__(self):
667        return 'comment()'
668
669class NodeTest(object):
670    """Node test that matches any node."""
671    __slots__ = []
672    def __call__(self, kind, data, pos, namespaces, variables):
673        if kind is START:
674            return True
675        return kind, data, pos
676    def __repr__(self):
677        return 'node()'
678
679class ProcessingInstructionNodeTest(object):
680    """Node test that matches any processing instruction event."""
681    __slots__ = ['target']
682    def __init__(self, target=None):
683        self.target = target
684    def __call__(self, kind, data, pos, namespaces, variables):
685        return kind is PI and (not self.target or data[0] == self.target)
686    def __repr__(self):
687        arg = ''
688        if self.target:
689            arg = '"' + self.target + '"'
690        return 'processing-instruction(%s)' % arg
691
692class TextNodeTest(object):
693    """Node test that matches any text event."""
694    __slots__ = []
695    def __call__(self, kind, data, pos, namespaces, variables):
696        return kind is TEXT
697    def __repr__(self):
698        return 'text()'
699
700_nodetest_map = {'comment': CommentNodeTest, 'node': NodeTest,
701                 'processing-instruction': ProcessingInstructionNodeTest,
702                 'text': TextNodeTest}
703
704# Functions
705
706class Function(object):
707    """Base class for function nodes in XPath expressions."""
708
709class BooleanFunction(Function):
710    """The `boolean` function, which converts its argument to a boolean
711    value.
712    """
713    __slots__ = ['expr']
714    def __init__(self, expr):
715        self.expr = expr
716    def __call__(self, kind, data, pos, namespaces, variables):
717        val = self.expr(kind, data, pos, namespaces, variables)
718        return as_bool(val)
719    def __repr__(self):
720        return 'boolean(%r)' % self.expr
721
722class CeilingFunction(Function):
723    """The `ceiling` function, which returns the nearest lower integer number
724    for the given number.
725    """
726    __slots__ = ['number']
727    def __init__(self, number):
728        self.number = number
729    def __call__(self, kind, data, pos, namespaces, variables):
730        number = self.number(kind, data, pos, namespaces, variables)
731        return ceil(as_float(number))
732    def __repr__(self):
733        return 'ceiling(%r)' % self.number
734
735class ConcatFunction(Function):
736    """The `concat` function, which concatenates (joins) the variable number of
737    strings it gets as arguments.
738    """
739    __slots__ = ['exprs']
740    def __init__(self, *exprs):
741        self.exprs = exprs
742    def __call__(self, kind, data, pos, namespaces, variables):
743        strings = []
744        for item in [expr(kind, data, pos, namespaces, variables)
745                     for expr in self.exprs]:
746            strings.append(as_string(item))
747        return u''.join(strings)
748    def __repr__(self):
749        return 'concat(%s)' % ', '.join([repr(expr) for expr in self.exprs])
750
751class ContainsFunction(Function):
752    """The `contains` function, which returns whether a string contains a given
753    substring.
754    """
755    __slots__ = ['string1', 'string2']
756    def __init__(self, string1, string2):
757        self.string1 = string1
758        self.string2 = string2
759    def __call__(self, kind, data, pos, namespaces, variables):
760        string1 = self.string1(kind, data, pos, namespaces, variables)
761        string2 = self.string2(kind, data, pos, namespaces, variables)
762        return as_string(string2) in as_string(string1)
763    def __repr__(self):
764        return 'contains(%r, %r)' % (self.string1, self.string2)
765
766class MatchesFunction(Function):
767    """The `matches` function, which returns whether a string matches a regular
768    expression.
769    """
770    __slots__ = ['string1', 'string2']
771    flag_mapping = {'s': re.S, 'm': re.M, 'i': re.I, 'x': re.X}
772
773    def __init__(self, string1, string2, flags=''):
774        self.string1 = string1
775        self.string2 = string2
776        self.flags = self._map_flags(flags)
777    def __call__(self, kind, data, pos, namespaces, variables):
778        string1 = as_string(self.string1(kind, data, pos, namespaces, variables))
779        string2 = as_string(self.string2(kind, data, pos, namespaces, variables))
780        return re.search(string2, string1, self.flags)
781    def _map_flags(self, flags):
782        return reduce(operator.or_,
783                      [self.flag_map[flag] for flag in flags], re.U)
784    def __repr__(self):
785        return 'contains(%r, %r)' % (self.string1, self.string2)
786
787class FalseFunction(Function):
788    """The `false` function, which always returns the boolean `false` value."""
789    __slots__ = []
790    def __call__(self, kind, data, pos, namespaces, variables):
791        return False
792    def __repr__(self):
793        return 'false()'
794
795class FloorFunction(Function):
796    """The `ceiling` function, which returns the nearest higher integer number
797    for the given number.
798    """
799    __slots__ = ['number']
800    def __init__(self, number):
801        self.number = number
802    def __call__(self, kind, data, pos, namespaces, variables):
803        number = self.number(kind, data, pos, namespaces, variables)
804        return floor(as_float(number))
805    def __repr__(self):
806        return 'floor(%r)' % self.number
807
808class LocalNameFunction(Function):
809    """The `local-name` function, which returns the local name of the current
810    element.
811    """
812    __slots__ = []
813    def __call__(self, kind, data, pos, namespaces, variables):
814        if kind is START:
815            return data[0].localname
816    def __repr__(self):
817        return 'local-name()'
818
819class NameFunction(Function):
820    """The `name` function, which returns the qualified name of the current
821    element.
822    """
823    __slots__ = []
824    def __call__(self, kind, data, pos, namespaces, variables):
825        if kind is START:
826            return data[0]
827    def __repr__(self):
828        return 'name()'
829
830class NamespaceUriFunction(Function):
831    """The `namespace-uri` function, which returns the namespace URI of the
832    current element.
833    """
834    __slots__ = []
835    def __call__(self, kind, data, pos, namespaces, variables):
836        if kind is START:
837            return data[0].namespace
838    def __repr__(self):
839        return 'namespace-uri()'
840
841class NotFunction(Function):
842    """The `not` function, which returns the negated boolean value of its
843    argument.
844    """
845    __slots__ = ['expr']
846    def __init__(self, expr):
847        self.expr = expr
848    def __call__(self, kind, data, pos, namespaces, variables):
849        return not as_bool(self.expr(kind, data, pos, namespaces, variables))
850    def __repr__(self):
851        return 'not(%s)' % self.expr
852
853class NormalizeSpaceFunction(Function):
854    """The `normalize-space` function, which removes leading and trailing
855    whitespace in the given string, and replaces multiple adjacent whitespace
856    characters inside the string with a single space.
857    """
858    __slots__ = ['expr']
859    _normalize = re.compile(r'\s{2,}').sub
860    def __init__(self, expr):
861        self.expr = expr
862    def __call__(self, kind, data, pos, namespaces, variables):
863        string = self.expr(kind, data, pos, namespaces, variables)
864        return self._normalize(' ', as_string(string).strip())
865    def __repr__(self):
866        return 'normalize-space(%s)' % repr(self.expr)
867
868class NumberFunction(Function):
869    """The `number` function that converts its argument to a number."""
870    __slots__ = ['expr']
871    def __init__(self, expr):
872        self.expr = expr
873    def __call__(self, kind, data, pos, namespaces, variables):
874        val = self.expr(kind, data, pos, namespaces, variables)
875        return as_float(val)
876    def __repr__(self):
877        return 'number(%r)' % self.expr
878
879class RoundFunction(Function):
880    """The `round` function, which returns the nearest integer number for the
881    given number.
882    """
883    __slots__ = ['number']
884    def __init__(self, number):
885        self.number = number
886    def __call__(self, kind, data, pos, namespaces, variables):
887        number = self.number(kind, data, pos, namespaces, variables)
888        return round(as_float(number))
889    def __repr__(self):
890        return 'round(%r)' % self.number
891
892class StartsWithFunction(Function):
893    """The `starts-with` function that returns whether one string starts with
894    a given substring.
895    """
896    __slots__ = ['string1', 'string2']
897    def __init__(self, string1, string2):
898        self.string1 = string1
899        self.string2 = string2
900    def __call__(self, kind, data, pos, namespaces, variables):
901        string1 = self.string1(kind, data, pos, namespaces, variables)
902        string2 = self.string2(kind, data, pos, namespaces, variables)
903        return as_string(string1).startswith(as_string(string2))
904    def __repr__(self):
905        return 'starts-with(%r, %r)' % (self.string1, self.string2)
906
907class StringLengthFunction(Function):
908    """The `string-length` function that returns the length of the given
909    string.
910    """
911    __slots__ = ['expr']
912    def __init__(self, expr):
913        self.expr = expr
914    def __call__(self, kind, data, pos, namespaces, variables):
915        string = self.expr(kind, data, pos, namespaces, variables)
916        return len(as_string(string))
917    def __repr__(self):
918        return 'string-length(%r)' % self.expr
919
920class SubstringFunction(Function):
921    """The `substring` function that returns the part of a string that starts
922    at the given offset, and optionally limited to the given length.
923    """
924    __slots__ = ['string', 'start', 'length']
925    def __init__(self, string, start, length=None):
926        self.string = string
927        self.start = start
928        self.length = length
929    def __call__(self, kind, data, pos, namespaces, variables):
930        string = self.string(kind, data, pos, namespaces, variables)
931        start = self.start(kind, data, pos, namespaces, variables)
932        length = 0
933        if self.length is not None:
934            length = self.length(kind, data, pos, namespaces, variables)
935        return string[as_long(start):len(as_string(string)) - as_long(length)]
936    def __repr__(self):
937        if self.length is not None:
938            return 'substring(%r, %r, %r)' % (self.string, self.start,
939                                              self.length)
940        else:
941            return 'substring(%r, %r)' % (self.string, self.start)
942
943class SubstringAfterFunction(Function):
944    """The `substring-after` function that returns the part of a string that
945    is found after the given substring.
946    """
947    __slots__ = ['string1', 'string2']
948    def __init__(self, string1, string2):
949        self.string1 = string1
950        self.string2 = string2
951    def __call__(self, kind, data, pos, namespaces, variables):
952        string1 = as_string(self.string1(kind, data, pos, namespaces, variables))
953        string2 = as_string(self.string2(kind, data, pos, namespaces, variables))
954        index = string1.find(string2)
955        if index >= 0:
956            return string1[index + len(string2):]
957        return u''
958    def __repr__(self):
959        return 'substring-after(%r, %r)' % (self.string1, self.string2)
960
961class SubstringBeforeFunction(Function):
962    """The `substring-before` function that returns the part of a string that
963    is found before the given substring.
964    """
965    __slots__ = ['string1', 'string2']
966    def __init__(self, string1, string2):
967        self.string1 = string1
968        self.string2 = string2
969    def __call__(self, kind, data, pos, namespaces, variables):
970        string1 = as_string(self.string1(kind, data, pos, namespaces, variables))
971        string2 = as_string(self.string2(kind, data, pos, namespaces, variables))
972        index = string1.find(string2)
973        if index >= 0:
974            return string1[:index]
975        return u''
976    def __repr__(self):
977        return 'substring-after(%r, %r)' % (self.string1, self.string2)
978
979class TranslateFunction(Function):
980    """The `translate` function that translates a set of characters in a
981    string to target set of characters.
982    """
983    __slots__ = ['string', 'fromchars', 'tochars']
984    def __init__(self, string, fromchars, tochars):
985        self.string = string
986        self.fromchars = fromchars
987        self.tochars = tochars
988    def __call__(self, kind, data, pos, namespaces, variables):
989        string = as_string(self.string(kind, data, pos, namespaces, variables))
990        fromchars = as_string(self.fromchars(kind, data, pos, namespaces, variables))
991        tochars = as_string(self.tochars(kind, data, pos, namespaces, variables))
992        table = dict(zip([ord(c) for c in fromchars],
993                         [ord(c) for c in tochars]))
994        return string.translate(table)
995    def __repr__(self):
996        return 'translate(%r, %r, %r)' % (self.string, self.fromchars,
997                                          self.tochars)
998
999class TrueFunction(Function):
1000    """The `true` function, which always returns the boolean `true` value."""
1001    __slots__ = []
1002    def __call__(self, kind, data, pos, namespaces, variables):
1003        return True
1004    def __repr__(self):
1005        return 'true()'
1006
1007_function_map = {'boolean': BooleanFunction, 'ceiling': CeilingFunction,
1008                 'concat': ConcatFunction, 'contains': ContainsFunction,
1009                 'matches': MatchesFunction, 'false': FalseFunction, 'floor':
1010                 FloorFunction, 'local-name': LocalNameFunction, 'name':
1011                 NameFunction, 'namespace-uri': NamespaceUriFunction,
1012                 'normalize-space': NormalizeSpaceFunction, 'not': NotFunction,
1013                 'number': NumberFunction, 'round': RoundFunction,
1014                 'starts-with': StartsWithFunction, 'string-length':
1015                 StringLengthFunction, 'substring': SubstringFunction,
1016                 'substring-after': SubstringAfterFunction, 'substring-before':
1017                 SubstringBeforeFunction, 'translate': TranslateFunction,
1018                 'true': TrueFunction}
1019
1020# Literals & Variables
1021
1022class Literal(object):
1023    """Abstract base class for literal nodes."""
1024
1025class StringLiteral(Literal):
1026    """A string literal node."""
1027    __slots__ = ['text']
1028    def __init__(self, text):
1029        self.text = text
1030    def __call__(self, kind, data, pos, namespaces, variables):
1031        return self.text
1032    def __repr__(self):
1033        return '"%s"' % self.text
1034
1035class NumberLiteral(Literal):
1036    """A number literal node."""
1037    __slots__ = ['number']
1038    def __init__(self, number):
1039        self.number = number
1040    def __call__(self, kind, data, pos, namespaces, variables):
1041        return self.number
1042    def __repr__(self):
1043        return str(self.number)
1044
1045class VariableReference(Literal):
1046    """A variable reference node."""
1047    __slots__ = ['name']
1048    def __init__(self, name):
1049        self.name = name
1050    def __call__(self, kind, data, pos, namespaces, variables):
1051        return variables.get(self.name)
1052    def __repr__(self):
1053        return str(self.name)
1054
1055# Operators
1056
1057class AndOperator(object):
1058    """The boolean operator `and`."""
1059    __slots__ = ['lval', 'rval']
1060    def __init__(self, lval, rval):
1061        self.lval = lval
1062        self.rval = rval
1063    def __call__(self, kind, data, pos, namespaces, variables):
1064        lval = as_bool(self.lval(kind, data, pos, namespaces, variables))
1065        if not lval:
1066            return False
1067        rval = self.rval(kind, data, pos, namespaces, variables)
1068        return as_bool(rval)
1069    def __repr__(self):
1070        return '%s and %s' % (self.lval, self.rval)
1071
1072class EqualsOperator(object):
1073    """The equality operator `=`."""
1074    __slots__ = ['lval', 'rval']
1075    def __init__(self, lval, rval):
1076        self.lval = lval
1077        self.rval = rval
1078    def __call__(self, kind, data, pos, namespaces, variables):
1079        lval = as_scalar(self.lval(kind, data, pos, namespaces, variables))
1080        rval = as_scalar(self.rval(kind, data, pos, namespaces, variables))
1081        return lval == rval
1082    def __repr__(self):
1083        return '%s=%s' % (self.lval, self.rval)
1084
1085class NotEqualsOperator(object):
1086    """The equality operator `!=`."""
1087    __slots__ = ['lval', 'rval']
1088    def __init__(self, lval, rval):
1089        self.lval = lval
1090        self.rval = rval
1091    def __call__(self, kind, data, pos, namespaces, variables):
1092        lval = as_scalar(self.lval(kind, data, pos, namespaces, variables))
1093        rval = as_scalar(self.rval(kind, data, pos, namespaces, variables))
1094        return lval != rval
1095    def __repr__(self):
1096        return '%s!=%s' % (self.lval, self.rval)
1097
1098class OrOperator(object):
1099    """The boolean operator `or`."""
1100    __slots__ = ['lval', 'rval']
1101    def __init__(self, lval, rval):
1102        self.lval = lval
1103        self.rval = rval
1104    def __call__(self, kind, data, pos, namespaces, variables):
1105        lval = as_bool(self.lval(kind, data, pos, namespaces, variables))
1106        if lval:
1107            return True
1108        rval = self.rval(kind, data, pos, namespaces, variables)
1109        return as_bool(rval)
1110    def __repr__(self):
1111        return '%s or %s' % (self.lval, self.rval)
1112
1113class GreaterThanOperator(object):
1114    """The relational operator `>` (greater than)."""
1115    __slots__ = ['lval', 'rval']
1116    def __init__(self, lval, rval):
1117        self.lval = lval
1118        self.rval = rval
1119    def __call__(self, kind, data, pos, namespaces, variables):
1120        lval = self.lval(kind, data, pos, namespaces, variables)
1121        rval = self.rval(kind, data, pos, namespaces, variables)
1122        return as_float(lval) > as_float(rval)
1123    def __repr__(self):
1124        return '%s>%s' % (self.lval, self.rval)
1125
1126class GreaterThanOrEqualOperator(object):
1127    """The relational operator `>=` (greater than or equal)."""
1128    __slots__ = ['lval', 'rval']
1129    def __init__(self, lval, rval):
1130        self.lval = lval
1131        self.rval = rval
1132    def __call__(self, kind, data, pos, namespaces, variables):
1133        lval = self.lval(kind, data, pos, namespaces, variables)
1134        rval = self.rval(kind, data, pos, namespaces, variables)
1135        return as_float(lval) >= as_float(rval)
1136    def __repr__(self):
1137        return '%s>=%s' % (self.lval, self.rval)
1138
1139class LessThanOperator(object):
1140    """The relational operator `<` (less than)."""
1141    __slots__ = ['lval', 'rval']
1142    def __init__(self, lval, rval):
1143        self.lval = lval
1144        self.rval = rval
1145    def __call__(self, kind, data, pos, namespaces, variables):
1146        lval = self.lval(kind, data, pos, namespaces, variables)
1147        rval = self.rval(kind, data, pos, namespaces, variables)
1148        return as_float(lval) < as_float(rval)
1149    def __repr__(self):
1150        return '%s<%s' % (self.lval, self.rval)
1151
1152class LessThanOrEqualOperator(object):
1153    """The relational operator `<=` (less than or equal)."""
1154    __slots__ = ['lval', 'rval']
1155    def __init__(self, lval, rval):
1156        self.lval = lval
1157        self.rval = rval
1158    def __call__(self, kind, data, pos, namespaces, variables):
1159        lval = self.lval(kind, data, pos, namespaces, variables)
1160        rval = self.rval(kind, data, pos, namespaces, variables)
1161        return as_float(lval) <= as_float(rval)
1162    def __repr__(self):
1163        return '%s<=%s' % (self.lval, self.rval)
1164
1165_operator_map = {'=': EqualsOperator, '!=': NotEqualsOperator,
1166                 '>': GreaterThanOperator, '>=': GreaterThanOrEqualOperator,
1167                 '<': LessThanOperator, '>=': LessThanOrEqualOperator}
1168
1169
1170_DOTSLASHSLASH = (DESCENDANT_OR_SELF, PrincipalTypeTest(None), ())
Note: See TracBrowser for help on using the repository browser.