Edgewall Software

source: branches/stable/0.5.x/genshi/path.py

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

Ported [914], [970], and [971] to 0.5.x branch.

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