Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/genshi/path.py: 29%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2006-2009 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/.
14"""Basic support for evaluating XPath expressions against streams.
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
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"""
41from collections import deque
42from functools import reduce
43from math import ceil, floor
44import operator
45import re
46from itertools import chain
48from genshi.compat import text_type, IS_PYTHON2
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
53__all__ = ['Path', 'PathSyntaxError']
54__docformat__ = 'restructuredtext en'
57class Axis(object):
58 """Defines constants for the various supported XPath axes."""
60 ATTRIBUTE = 'attribute'
61 CHILD = 'child'
62 DESCENDANT = 'descendant'
63 DESCENDANT_OR_SELF = 'descendant-or-self'
64 SELF = 'self'
66 @classmethod
67 def forname(cls, name):
68 """Return the axis constant for the given name, or `None` if no such
69 axis was defined.
70 """
71 return getattr(cls, name.upper().replace('-', '_'), None)
74ATTRIBUTE = Axis.ATTRIBUTE
75CHILD = Axis.CHILD
76DESCENDANT = Axis.DESCENDANT
77DESCENDANT_OR_SELF = Axis.DESCENDANT_OR_SELF
78SELF = Axis.SELF
81class GenericStrategy(object):
83 @classmethod
84 def supports(cls, path):
85 return True
87 def __init__(self, path):
88 self.path = path
90 def test(self, ignore_context):
91 p = self.path
92 if ignore_context:
93 if p[0][0] is ATTRIBUTE:
94 steps = [_DOTSLASHSLASH] + p
95 else:
96 steps = [(DESCENDANT_OR_SELF, p[0][1], p[0][2])] + p[1:]
97 elif p[0][0] is CHILD or p[0][0] is ATTRIBUTE \
98 or p[0][0] is DESCENDANT:
99 steps = [_DOTSLASH] + p
100 else:
101 steps = p
103 # for node it contains all positions of xpath expression
104 # where its child should start checking for matches
105 # with list of corresponding context counters
106 # there can be many of them, because position that is from
107 # descendant-like axis can be achieved from different nodes
108 # for example <a><a><b/></a></a> should match both //a//b[1]
109 # and //a//b[2]
110 # positions always form increasing sequence (invariant)
111 stack = [[(0, [[]])]]
113 def _test(event, namespaces, variables, updateonly=False):
114 kind, data, pos = event[:3]
115 retval = None
117 # Manage the stack that tells us "where we are" in the stream
118 if kind is END:
119 if stack:
120 stack.pop()
121 return None
122 if kind is START_NS or kind is END_NS \
123 or kind is START_CDATA or kind is END_CDATA:
124 # should we make namespaces work?
125 return None
127 pos_queue = deque([(pos, cou, []) for pos, cou in stack[-1]])
128 next_pos = []
130 # length of real part of path - we omit attribute axis
131 real_len = len(steps) - ((steps[-1][0] == ATTRIBUTE) or 1 and 0)
132 last_checked = -1
134 # places where we have to check for match, are these
135 # provided by parent
136 while pos_queue:
137 x, pcou, mcou = pos_queue.popleft()
138 axis, nodetest, predicates = steps[x]
140 # we need to push descendant-like positions from parent
141 # further
142 if (axis is DESCENDANT or axis is DESCENDANT_OR_SELF) and pcou:
143 if next_pos and next_pos[-1][0] == x:
144 next_pos[-1][1].extend(pcou)
145 else:
146 next_pos.append((x, pcou))
148 # nodetest first
149 if not nodetest(kind, data, pos, namespaces, variables):
150 continue
152 # counters packs that were already bad
153 missed = set()
154 counters_len = len(pcou) + len(mcou)
156 # number of counters - we have to create one
157 # for every context position based predicate
158 cnum = 0
160 # tells if we have match with position x
161 matched = True
163 if predicates:
164 for predicate in predicates:
165 pretval = predicate(kind, data, pos,
166 namespaces,
167 variables)
168 if type(pretval) is float: # FIXME <- need to check
169 # this for other types that
170 # can be coerced to float
172 # each counter pack needs to be checked
173 for i, cou in enumerate(chain(pcou, mcou)):
174 # it was bad before
175 if i in missed:
176 continue
178 if len(cou) < cnum + 1:
179 cou.append(0)
180 cou[cnum] += 1
182 # it is bad now
183 if cou[cnum] != int(pretval):
184 missed.add(i)
186 # none of counters pack was good
187 if len(missed) == counters_len:
188 pretval = False
189 cnum += 1
191 if not pretval:
192 matched = False
193 break
195 if not matched:
196 continue
198 # counter for next position with current node as context node
199 child_counter = []
201 if x + 1 == real_len:
202 # we reached end of expression, because x + 1
203 # is equal to the length of expression
204 matched = True
205 axis, nodetest, predicates = steps[-1]
206 if axis is ATTRIBUTE:
207 matched = nodetest(kind, data, pos, namespaces,
208 variables)
209 if matched:
210 retval = matched
211 else:
212 next_axis = steps[x + 1][0]
214 # if next axis allows matching self we have
215 # to add next position to our queue
216 if next_axis is DESCENDANT_OR_SELF or next_axis is SELF:
217 if not pos_queue or pos_queue[0][0] > x + 1:
218 pos_queue.appendleft((x + 1, [], [child_counter]))
219 else:
220 pos_queue[0][2].append(child_counter)
222 # if axis is not self we have to add it to child's list
223 if next_axis is not SELF:
224 next_pos.append((x + 1, [child_counter]))
226 if kind is START:
227 stack.append(next_pos)
229 return retval
231 return _test
234class SimplePathStrategy(object):
235 """Strategy for path with only local names, attributes and text nodes."""
237 @classmethod
238 def supports(cls, path):
239 if path[0][0] is ATTRIBUTE:
240 return False
241 allowed_tests = (LocalNameTest, CommentNodeTest, TextNodeTest)
242 for _, nodetest, predicates in path:
243 if predicates:
244 return False
245 if not isinstance(nodetest, allowed_tests):
246 return False
247 return True
249 def __init__(self, path):
250 # fragments is list of tuples (fragment, pi, attr, self_beginning)
251 # fragment is list of nodetests for fragment of path with only
252 # child:: axes between
253 # pi is KMP partial match table for this fragment
254 # attr is attribute nodetest if fragment ends with @ and None otherwise
255 # self_beginning is True if axis for first fragment element
256 # was self (first fragment) or descendant-or-self (farther fragment)
257 self.fragments = []
259 self_beginning = False
260 fragment = []
262 def nodes_equal(node1, node2):
263 """Tests if two node tests are equal"""
264 if type(node1) is not type(node2):
265 return False
266 if type(node1) == LocalNameTest:
267 return node1.name == node2.name
268 return True
270 def calculate_pi(f):
271 """KMP prefix calculation for table"""
272 # the indexes in prefix table are shifted by one
273 # in comparison with common implementations
274 # pi[i] = NORMAL_PI[i + 1]
275 if len(f) == 0:
276 return []
277 pi = [0]
278 s = 0
279 for i in range(1, len(f)):
280 while s > 0 and not nodes_equal(f[s], f[i]):
281 s = pi[s-1]
282 if nodes_equal(f[s], f[i]):
283 s += 1
284 pi.append(s)
285 return pi
287 for axis in path:
288 if axis[0] is SELF:
289 if len(fragment) != 0:
290 # if element is not first in fragment it has to be
291 # the same as previous one
292 # for example child::a/self::b is always wrong
293 if axis[1] != fragment[-1][1]:
294 self.fragments = None
295 return
296 else:
297 self_beginning = True
298 fragment.append(axis[1])
299 elif axis[0] is CHILD:
300 fragment.append(axis[1])
301 elif axis[0] is ATTRIBUTE:
302 pi = calculate_pi(fragment)
303 self.fragments.append((fragment, pi, axis[1], self_beginning))
304 # attribute has always to be at the end, so we can jump out
305 return
306 else:
307 pi = calculate_pi(fragment)
308 self.fragments.append((fragment, pi, None, self_beginning))
309 fragment = [axis[1]]
310 if axis[0] is DESCENDANT:
311 self_beginning = False
312 else: # DESCENDANT_OR_SELF
313 self_beginning = True
314 pi = calculate_pi(fragment)
315 self.fragments.append((fragment, pi, None, self_beginning))
317 def test(self, ignore_context):
318 # stack of triples (fid, p, ic)
319 # fid is index of current fragment
320 # p is position in this fragment
321 # ic is if we ignore context in this fragment
322 stack = []
323 stack_push = stack.append
324 stack_pop = stack.pop
325 frags = self.fragments
326 frags_len = len(frags)
328 def _test(event, namespaces, variables, updateonly=False):
329 # expression found impossible during init
330 if frags is None:
331 return None
333 kind, data, pos = event[:3]
335 # skip events we don't care about
336 if kind is END:
337 if stack:
338 stack_pop()
339 return None
340 if kind is START_NS or kind is END_NS \
341 or kind is START_CDATA or kind is END_CDATA:
342 return None
344 if not stack:
345 # root node, nothing on stack, special case
346 fid = 0
347 # skip empty fragments (there can be actually only one)
348 while not frags[fid][0]:
349 fid += 1
350 p = 0
351 # empty fragment means descendant node at beginning
352 ic = ignore_context or (fid > 0)
354 # expression can match first node, if first axis is self::,
355 # descendant-or-self:: or if ignore_context is True and
356 # axis is not descendant::
357 if not frags[fid][3] and (not ignore_context or fid > 0):
358 # axis is not self-beggining, we have to skip this node
359 stack_push((fid, p, ic))
360 return None
361 else:
362 # take position of parent
363 fid, p, ic = stack[-1]
365 if fid is not None and not ic:
366 # fragment not ignoring context - we can't jump back
367 frag, pi, attrib, _ = frags[fid]
368 frag_len = len(frag)
370 if p == frag_len:
371 # that probably means empty first fragment
372 pass
373 elif frag[p](kind, data, pos, namespaces, variables):
374 # match, so we can go further
375 p += 1
376 else:
377 # not matched, so there will be no match in subtree
378 fid, p = None, None
380 if p == frag_len and fid + 1 != frags_len:
381 # we made it to end of fragment, we can go to following
382 fid += 1
383 p = 0
384 ic = True
386 if fid is None:
387 # there was no match in fragment not ignoring context
388 if kind is START:
389 stack_push((fid, p, ic))
390 return None
392 if ic:
393 # we are in fragment ignoring context
394 while True:
395 frag, pi, attrib, _ = frags[fid]
396 frag_len = len(frag)
398 # KMP new "character"
399 while p > 0 and (p >= frag_len or not \
400 frag[p](kind, data, pos, namespaces, variables)):
401 p = pi[p-1]
402 if frag[p](kind, data, pos, namespaces, variables):
403 p += 1
405 if p == frag_len:
406 # end of fragment reached
407 if fid + 1 == frags_len:
408 # that was last fragment
409 break
410 else:
411 fid += 1
412 p = 0
413 ic = True
414 if not frags[fid][3]:
415 # next fragment not self-beginning
416 break
417 else:
418 break
420 if kind is START:
421 # we have to put new position on stack, for children
423 if not ic and fid + 1 == frags_len and p == frag_len:
424 # it is end of the only, not context ignoring fragment
425 # so there will be no matches in subtree
426 stack_push((None, None, ic))
427 else:
428 stack_push((fid, p, ic))
430 # have we reached the end of the last fragment?
431 if fid + 1 == frags_len and p == frag_len:
432 if attrib: # attribute ended path, return value
433 return attrib(kind, data, pos, namespaces, variables)
434 return True
436 return None
438 return _test
441class SingleStepStrategy(object):
443 @classmethod
444 def supports(cls, path):
445 return len(path) == 1
447 def __init__(self, path):
448 self.path = path
450 def test(self, ignore_context):
451 steps = self.path
452 if steps[0][0] is ATTRIBUTE:
453 steps = [_DOTSLASH] + steps
454 select_attr = steps[-1][0] is ATTRIBUTE and steps[-1][1] or None
456 # for every position in expression stores counters' list
457 # it is used for position based predicates
458 counters = []
459 depth = [0]
461 def _test(event, namespaces, variables, updateonly=False):
462 kind, data, pos = event[:3]
464 # Manage the stack that tells us "where we are" in the stream
465 if kind is END:
466 if not ignore_context:
467 depth[0] -= 1
468 return None
469 elif kind is START_NS or kind is END_NS \
470 or kind is START_CDATA or kind is END_CDATA:
471 # should we make namespaces work?
472 return None
474 if not ignore_context:
475 outside = (steps[0][0] is SELF and depth[0] != 0) \
476 or (steps[0][0] is CHILD and depth[0] != 1) \
477 or (steps[0][0] is DESCENDANT and depth[0] < 1)
478 if kind is START:
479 depth[0] += 1
480 if outside:
481 return None
483 axis, nodetest, predicates = steps[0]
484 if not nodetest(kind, data, pos, namespaces, variables):
485 return None
487 if predicates:
488 cnum = 0
489 for predicate in predicates:
490 pretval = predicate(kind, data, pos, namespaces, variables)
491 if type(pretval) is float: # FIXME <- need to check this
492 # for other types that can be
493 # coerced to float
494 if len(counters) < cnum + 1:
495 counters.append(0)
496 counters[cnum] += 1
497 if counters[cnum] != int(pretval):
498 pretval = False
499 cnum += 1
500 if not pretval:
501 return None
503 if select_attr:
504 return select_attr(kind, data, pos, namespaces, variables)
506 return True
508 return _test
511class Path(object):
512 """Implements basic XPath support on streams.
514 Instances of this class represent a "compiled" XPath expression, and
515 provide methods for testing the path against a stream, as well as
516 extracting a substream matching that path.
517 """
519 STRATEGIES = (SingleStepStrategy, SimplePathStrategy, GenericStrategy)
521 def __init__(self, text, filename=None, lineno=-1):
522 """Create the path object from a string.
524 :param text: the path expression
525 :param filename: the name of the file in which the path expression was
526 found (used in error messages)
527 :param lineno: the line on which the expression was found
528 """
529 self.source = text
530 self.paths = PathParser(text, filename, lineno).parse()
531 self.strategies = []
532 for path in self.paths:
533 for strategy_class in self.STRATEGIES:
534 if strategy_class.supports(path):
535 self.strategies.append(strategy_class(path))
536 break
537 else:
538 raise NotImplementedError('No strategy found for path')
540 def __repr__(self):
541 paths = []
542 for path in self.paths:
543 steps = []
544 for axis, nodetest, predicates in path:
545 steps.append('%s::%s' % (axis, nodetest))
546 for predicate in predicates:
547 steps[-1] += '[%s]' % predicate
548 paths.append('/'.join(steps))
549 return '<%s "%s">' % (type(self).__name__, '|'.join(paths))
551 def select(self, stream, namespaces=None, variables=None):
552 """Returns a substream of the given stream that matches the path.
554 If there are no matches, this method returns an empty stream.
556 >>> from genshi.input import XML
557 >>> xml = XML('<root><elem><child>Text</child></elem></root>')
559 >>> print(Path('.//child').select(xml))
560 <child>Text</child>
562 >>> print(Path('.//child/text()').select(xml))
563 Text
565 :param stream: the stream to select from
566 :param namespaces: (optional) a mapping of namespace prefixes to URIs
567 :param variables: (optional) a mapping of variable names to values
568 :return: the substream matching the path, or an empty stream
569 :rtype: `Stream`
570 """
571 if namespaces is None:
572 namespaces = {}
573 if variables is None:
574 variables = {}
575 stream = iter(stream)
576 def _generate(stream=stream, ns=namespaces, vs=variables):
577 _next = lambda: next(stream)
578 test = self.test()
579 for event in stream:
580 result = test(event, ns, vs)
581 if result is True:
582 yield event
583 if event[0] is START:
584 depth = 1
585 while depth > 0:
586 subevent = _next()
587 if subevent[0] is START:
588 depth += 1
589 elif subevent[0] is END:
590 depth -= 1
591 yield subevent
592 test(subevent, ns, vs, updateonly=True)
593 elif result:
594 yield result
595 return Stream(_generate(),
596 serializer=getattr(stream, 'serializer', None))
598 def test(self, ignore_context=False):
599 """Returns a function that can be used to track whether the path matches
600 a specific stream event.
602 The function returned expects the positional arguments ``event``,
603 ``namespaces`` and ``variables``. The first is a stream event, while the
604 latter two are a mapping of namespace prefixes to URIs, and a mapping
605 of variable names to values, respectively. In addition, the function
606 accepts an ``updateonly`` keyword argument that default to ``False``. If
607 it is set to ``True``, the function only updates its internal state,
608 but does not perform any tests or return a result.
610 If the path matches the event, the function returns the match (for
611 example, a `START` or `TEXT` event.) Otherwise, it returns ``None``.
613 >>> from genshi.input import XML
614 >>> xml = XML('<root><elem><child id="1"/></elem><child id="2"/></root>')
615 >>> test = Path('child').test()
616 >>> namespaces, variables = {}, {}
617 >>> for event in xml:
618 ... if test(event, namespaces, variables):
619 ... print('%s %r' % (event[0], event[1]))
620 START (QName('child'), Attrs([(QName('id'), '2')]))
622 :param ignore_context: if `True`, the path is interpreted like a pattern
623 in XSLT, meaning for example that it will match
624 at any depth
625 :return: a function that can be used to test individual events in a
626 stream against the path
627 :rtype: ``function``
628 """
629 tests = [s.test(ignore_context) for s in self.strategies]
630 if len(tests) == 1:
631 return tests[0]
633 def _multi(event, namespaces, variables, updateonly=False):
634 retval = None
635 for test in tests:
636 val = test(event, namespaces, variables, updateonly=updateonly)
637 if retval is None:
638 retval = val
639 return retval
640 return _multi
643class PathSyntaxError(Exception):
644 """Exception raised when an XPath expression is syntactically incorrect."""
646 def __init__(self, message, filename=None, lineno=-1, offset=-1):
647 if filename:
648 message = '%s (%s, line %d)' % (message, filename, lineno)
649 Exception.__init__(self, message)
650 self.filename = filename
651 self.lineno = lineno
652 self.offset = offset
655class PathParser(object):
656 """Tokenizes and parses an XPath expression."""
658 _QUOTES = (("'", "'"), ('"', '"'))
659 _TOKENS = ('::', ':', '..', '.', '//', '/', '[', ']', '()', '(', ')', '@',
660 '=', '!=', '!', '|', ',', '>=', '>', '<=', '<', '$')
661 _tokenize = re.compile(r'("[^"]*")|(\'[^\']*\')|((?:\d+)?\.\d+)|(%s)|([^%s\s]+)|\s+' % (
662 '|'.join([re.escape(t) for t in _TOKENS]),
663 ''.join([re.escape(t[0]) for t in _TOKENS]))).findall
665 def __init__(self, text, filename=None, lineno=-1):
666 self.filename = filename
667 self.lineno = lineno
668 self.tokens = [t for t in [dqstr or sqstr or number or token or name
669 for dqstr, sqstr, number, token, name in
670 self._tokenize(text)] if t]
671 self.pos = 0
673 # Tokenizer
675 @property
676 def at_end(self):
677 return self.pos == len(self.tokens) - 1
679 @property
680 def cur_token(self):
681 return self.tokens[self.pos]
683 def next_token(self):
684 self.pos += 1
685 return self.tokens[self.pos]
687 def peek_token(self):
688 if not self.at_end:
689 return self.tokens[self.pos + 1]
690 return None
692 # Recursive descent parser
694 def parse(self):
695 """Parses the XPath expression and returns a list of location path
696 tests.
698 For union expressions (such as `*|text()`), this function returns one
699 test for each operand in the union. For patch expressions that don't
700 use the union operator, the function always returns a list of size 1.
702 Each path test in turn is a sequence of tests that correspond to the
703 location steps, each tuples of the form `(axis, testfunc, predicates)`
704 """
705 paths = [self._location_path()]
706 while self.cur_token == '|':
707 self.next_token()
708 paths.append(self._location_path())
709 if not self.at_end:
710 raise PathSyntaxError('Unexpected token %r after end of expression'
711 % self.cur_token, self.filename, self.lineno)
712 return paths
714 def _location_path(self):
715 steps = []
716 while True:
717 if self.cur_token.startswith('/'):
718 if not steps:
719 if self.cur_token == '//':
720 # hack to make //* match every node - also root
721 self.next_token()
722 axis, nodetest, predicates = self._location_step()
723 steps.append((DESCENDANT_OR_SELF, nodetest,
724 predicates))
725 if self.at_end or not self.cur_token.startswith('/'):
726 break
727 continue
728 else:
729 raise PathSyntaxError('Absolute location paths not '
730 'supported', self.filename,
731 self.lineno)
732 elif self.cur_token == '//':
733 steps.append((DESCENDANT_OR_SELF, NodeTest(), []))
734 self.next_token()
736 axis, nodetest, predicates = self._location_step()
737 if not axis:
738 axis = CHILD
739 steps.append((axis, nodetest, predicates))
740 if self.at_end or not self.cur_token.startswith('/'):
741 break
743 return steps
745 def _location_step(self):
746 if self.cur_token == '@':
747 axis = ATTRIBUTE
748 self.next_token()
749 elif self.cur_token == '.':
750 axis = SELF
751 elif self.cur_token == '..':
752 raise PathSyntaxError('Unsupported axis "parent"', self.filename,
753 self.lineno)
754 elif self.peek_token() == '::':
755 axis = Axis.forname(self.cur_token)
756 if axis is None:
757 raise PathSyntaxError('Unsupport axis "%s"' % axis,
758 self.filename, self.lineno)
759 self.next_token()
760 self.next_token()
761 else:
762 axis = None
763 nodetest = self._node_test(axis or CHILD)
764 predicates = []
765 while self.cur_token == '[':
766 predicates.append(self._predicate())
767 return axis, nodetest, predicates
769 def _node_test(self, axis=None):
770 test = prefix = None
771 next_token = self.peek_token()
772 if next_token in ('(', '()'): # Node type test
773 test = self._node_type()
775 elif next_token == ':': # Namespace prefix
776 prefix = self.cur_token
777 self.next_token()
778 localname = self.next_token()
779 if localname == '*':
780 test = QualifiedPrincipalTypeTest(axis, prefix)
781 else:
782 test = QualifiedNameTest(axis, prefix, localname)
784 else: # Name test
785 if self.cur_token == '*':
786 test = PrincipalTypeTest(axis)
787 elif self.cur_token == '.':
788 test = NodeTest()
789 else:
790 test = LocalNameTest(axis, self.cur_token)
792 if not self.at_end:
793 self.next_token()
794 return test
796 def _node_type(self):
797 name = self.cur_token
798 self.next_token()
800 args = []
801 if self.cur_token != '()':
802 # The processing-instruction() function optionally accepts the
803 # name of the PI as argument, which must be a literal string
804 self.next_token() # (
805 if self.cur_token != ')':
806 string = self.cur_token
807 if (string[0], string[-1]) in self._QUOTES:
808 string = string[1:-1]
809 args.append(string)
811 cls = _nodetest_map.get(name)
812 if not cls:
813 raise PathSyntaxError('%s() not allowed here' % name, self.filename,
814 self.lineno)
815 return cls(*args)
817 def _predicate(self):
818 assert self.cur_token == '['
819 self.next_token()
820 expr = self._or_expr()
821 if self.cur_token != ']':
822 raise PathSyntaxError('Expected "]" to close predicate, '
823 'but found "%s"' % self.cur_token,
824 self.filename, self.lineno)
825 if not self.at_end:
826 self.next_token()
827 return expr
829 def _or_expr(self):
830 expr = self._and_expr()
831 while self.cur_token == 'or':
832 self.next_token()
833 expr = OrOperator(expr, self._and_expr())
834 return expr
836 def _and_expr(self):
837 expr = self._equality_expr()
838 while self.cur_token == 'and':
839 self.next_token()
840 expr = AndOperator(expr, self._equality_expr())
841 return expr
843 def _equality_expr(self):
844 expr = self._relational_expr()
845 while self.cur_token in ('=', '!='):
846 op = _operator_map[self.cur_token]
847 self.next_token()
848 expr = op(expr, self._relational_expr())
849 return expr
851 def _relational_expr(self):
852 expr = self._sub_expr()
853 while self.cur_token in ('>', '>=', '<', '>='):
854 op = _operator_map[self.cur_token]
855 self.next_token()
856 expr = op(expr, self._sub_expr())
857 return expr
859 def _sub_expr(self):
860 token = self.cur_token
861 if token != '(':
862 return self._primary_expr()
863 self.next_token()
864 expr = self._or_expr()
865 if self.cur_token != ')':
866 raise PathSyntaxError('Expected ")" to close sub-expression, '
867 'but found "%s"' % self.cur_token,
868 self.filename, self.lineno)
869 self.next_token()
870 return expr
872 def _primary_expr(self):
873 token = self.cur_token
874 if len(token) > 1 and (token[0], token[-1]) in self._QUOTES:
875 self.next_token()
876 return StringLiteral(token[1:-1])
877 elif token[0].isdigit() or token[0] == '.':
878 self.next_token()
879 return NumberLiteral(as_float(token))
880 elif token == '$':
881 token = self.next_token()
882 self.next_token()
883 return VariableReference(token)
884 elif not self.at_end and self.peek_token().startswith('('):
885 return self._function_call()
886 else:
887 axis = None
888 if token == '@':
889 axis = ATTRIBUTE
890 self.next_token()
891 return self._node_test(axis)
893 def _function_call(self):
894 name = self.cur_token
895 if self.next_token() == '()':
896 args = []
897 else:
898 assert self.cur_token == '('
899 self.next_token()
900 args = [self._or_expr()]
901 while self.cur_token == ',':
902 self.next_token()
903 args.append(self._or_expr())
904 if not self.cur_token == ')':
905 raise PathSyntaxError('Expected ")" to close function argument '
906 'list, but found "%s"' % self.cur_token,
907 self.filename, self.lineno)
908 self.next_token()
909 cls = _function_map.get(name)
910 if not cls:
911 raise PathSyntaxError('Unsupported function "%s"' % name,
912 self.filename, self.lineno)
913 return cls(*args)
916# Type coercion
918def as_scalar(value):
919 """Convert value to a scalar. If a single element Attrs() object is passed
920 the value of the single attribute will be returned."""
921 if isinstance(value, Attrs):
922 assert len(value) == 1
923 return value[0][1]
924 else:
925 return value
927def as_float(value):
928 # FIXME - if value is a bool it will be coerced to 0.0 and consequently
929 # compared as a float. This is probably not ideal.
930 return float(as_scalar(value))
932def as_long(value):
933 long_cls = long if IS_PYTHON2 else int
934 return long_cls(as_scalar(value))
936def as_string(value):
937 value = as_scalar(value)
938 if value is False:
939 return ''
940 return text_type(value)
942def as_bool(value):
943 return bool(as_scalar(value))
946# Node tests
948class PrincipalTypeTest(object):
949 """Node test that matches any event with the given principal type."""
950 __slots__ = ['principal_type']
951 def __init__(self, principal_type):
952 self.principal_type = principal_type
953 def __call__(self, kind, data, pos, namespaces, variables):
954 if kind is START:
955 if self.principal_type is ATTRIBUTE:
956 return data[1] or None
957 else:
958 return True
959 def __repr__(self):
960 return '*'
962class QualifiedPrincipalTypeTest(object):
963 """Node test that matches any event with the given principal type in a
964 specific namespace."""
965 __slots__ = ['principal_type', 'prefix']
966 def __init__(self, principal_type, prefix):
967 self.principal_type = principal_type
968 self.prefix = prefix
969 def __call__(self, kind, data, pos, namespaces, variables):
970 namespace = Namespace(namespaces.get(self.prefix))
971 if kind is START:
972 if self.principal_type is ATTRIBUTE and data[1]:
973 return Attrs([(name, value) for name, value in data[1]
974 if name in namespace]) or None
975 else:
976 return data[0] in namespace
977 def __repr__(self):
978 return '%s:*' % self.prefix
980class LocalNameTest(object):
981 """Node test that matches any event with the given principal type and
982 local name.
983 """
984 __slots__ = ['principal_type', 'name']
985 def __init__(self, principal_type, name):
986 self.principal_type = principal_type
987 self.name = name
988 def __call__(self, kind, data, pos, namespaces, variables):
989 if kind is START:
990 if self.principal_type is ATTRIBUTE and self.name in data[1]:
991 return Attrs([(self.name, data[1].get(self.name))])
992 else:
993 return data[0].localname == self.name
994 def __repr__(self):
995 return self.name
997class QualifiedNameTest(object):
998 """Node test that matches any event with the given principal type and
999 qualified name.
1000 """
1001 __slots__ = ['principal_type', 'prefix', 'name']
1002 def __init__(self, principal_type, prefix, name):
1003 self.principal_type = principal_type
1004 self.prefix = prefix
1005 self.name = name
1006 def __call__(self, kind, data, pos, namespaces, variables):
1007 qname = QName('%s}%s' % (namespaces.get(self.prefix), self.name))
1008 if kind is START:
1009 if self.principal_type is ATTRIBUTE and qname in data[1]:
1010 return Attrs([(qname, data[1].get(qname))])
1011 else:
1012 return data[0] == qname
1013 def __repr__(self):
1014 return '%s:%s' % (self.prefix, self.name)
1016class CommentNodeTest(object):
1017 """Node test that matches any comment events."""
1018 __slots__ = []
1019 def __call__(self, kind, data, pos, namespaces, variables):
1020 return kind is COMMENT
1021 def __repr__(self):
1022 return 'comment()'
1024class NodeTest(object):
1025 """Node test that matches any node."""
1026 __slots__ = []
1027 def __call__(self, kind, data, pos, namespaces, variables):
1028 if kind is START:
1029 return True
1030 return kind, data, pos
1031 def __repr__(self):
1032 return 'node()'
1034class ProcessingInstructionNodeTest(object):
1035 """Node test that matches any processing instruction event."""
1036 __slots__ = ['target']
1037 def __init__(self, target=None):
1038 self.target = target
1039 def __call__(self, kind, data, pos, namespaces, variables):
1040 return kind is PI and (not self.target or data[0] == self.target)
1041 def __repr__(self):
1042 arg = ''
1043 if self.target:
1044 arg = '"' + self.target + '"'
1045 return 'processing-instruction(%s)' % arg
1047class TextNodeTest(object):
1048 """Node test that matches any text event."""
1049 __slots__ = []
1050 def __call__(self, kind, data, pos, namespaces, variables):
1051 return kind is TEXT
1052 def __repr__(self):
1053 return 'text()'
1055_nodetest_map = {'comment': CommentNodeTest, 'node': NodeTest,
1056 'processing-instruction': ProcessingInstructionNodeTest,
1057 'text': TextNodeTest}
1059# Functions
1061class Function(object):
1062 """Base class for function nodes in XPath expressions."""
1064class BooleanFunction(Function):
1065 """The `boolean` function, which converts its argument to a boolean
1066 value.
1067 """
1068 __slots__ = ['expr']
1069 _return_type = bool
1070 def __init__(self, expr):
1071 self.expr = expr
1072 def __call__(self, kind, data, pos, namespaces, variables):
1073 val = self.expr(kind, data, pos, namespaces, variables)
1074 return as_bool(val)
1075 def __repr__(self):
1076 return 'boolean(%r)' % self.expr
1078class CeilingFunction(Function):
1079 """The `ceiling` function, which returns the nearest lower integer number
1080 for the given number.
1081 """
1082 __slots__ = ['number']
1083 def __init__(self, number):
1084 self.number = number
1085 def __call__(self, kind, data, pos, namespaces, variables):
1086 number = self.number(kind, data, pos, namespaces, variables)
1087 return ceil(as_float(number))
1088 def __repr__(self):
1089 return 'ceiling(%r)' % self.number
1091class ConcatFunction(Function):
1092 """The `concat` function, which concatenates (joins) the variable number of
1093 strings it gets as arguments.
1094 """
1095 __slots__ = ['exprs']
1096 def __init__(self, *exprs):
1097 self.exprs = exprs
1098 def __call__(self, kind, data, pos, namespaces, variables):
1099 strings = []
1100 for item in [expr(kind, data, pos, namespaces, variables)
1101 for expr in self.exprs]:
1102 strings.append(as_string(item))
1103 return ''.join(strings)
1104 def __repr__(self):
1105 return 'concat(%s)' % ', '.join([repr(expr) for expr in self.exprs])
1107class ContainsFunction(Function):
1108 """The `contains` function, which returns whether a string contains a given
1109 substring.
1110 """
1111 __slots__ = ['string1', 'string2']
1112 def __init__(self, string1, string2):
1113 self.string1 = string1
1114 self.string2 = string2
1115 def __call__(self, kind, data, pos, namespaces, variables):
1116 string1 = self.string1(kind, data, pos, namespaces, variables)
1117 string2 = self.string2(kind, data, pos, namespaces, variables)
1118 return as_string(string2) in as_string(string1)
1119 def __repr__(self):
1120 return 'contains(%r, %r)' % (self.string1, self.string2)
1122class MatchesFunction(Function):
1123 """The `matches` function, which returns whether a string matches a regular
1124 expression.
1125 """
1126 __slots__ = ['string1', 'string2']
1127 flag_mapping = {'s': re.S, 'm': re.M, 'i': re.I, 'x': re.X}
1129 def __init__(self, string1, string2, flags=''):
1130 self.string1 = string1
1131 self.string2 = string2
1132 self.flags = self._map_flags(flags)
1133 def __call__(self, kind, data, pos, namespaces, variables):
1134 string1 = as_string(self.string1(kind, data, pos, namespaces, variables))
1135 string2 = as_string(self.string2(kind, data, pos, namespaces, variables))
1136 return re.search(string2, string1, self.flags)
1137 def _map_flags(self, flags):
1138 return reduce(operator.or_,
1139 [self.flag_map[flag] for flag in flags], re.U)
1140 def __repr__(self):
1141 return 'contains(%r, %r)' % (self.string1, self.string2)
1143class FalseFunction(Function):
1144 """The `false` function, which always returns the boolean `false` value."""
1145 __slots__ = []
1146 def __call__(self, kind, data, pos, namespaces, variables):
1147 return False
1148 def __repr__(self):
1149 return 'false()'
1151class FloorFunction(Function):
1152 """The `ceiling` function, which returns the nearest higher integer number
1153 for the given number.
1154 """
1155 __slots__ = ['number']
1156 def __init__(self, number):
1157 self.number = number
1158 def __call__(self, kind, data, pos, namespaces, variables):
1159 number = self.number(kind, data, pos, namespaces, variables)
1160 return floor(as_float(number))
1161 def __repr__(self):
1162 return 'floor(%r)' % self.number
1164class LocalNameFunction(Function):
1165 """The `local-name` function, which returns the local name of the current
1166 element.
1167 """
1168 __slots__ = []
1169 def __call__(self, kind, data, pos, namespaces, variables):
1170 if kind is START:
1171 return data[0].localname
1172 def __repr__(self):
1173 return 'local-name()'
1175class NameFunction(Function):
1176 """The `name` function, which returns the qualified name of the current
1177 element.
1178 """
1179 __slots__ = []
1180 def __call__(self, kind, data, pos, namespaces, variables):
1181 if kind is START:
1182 return data[0]
1183 def __repr__(self):
1184 return 'name()'
1186class NamespaceUriFunction(Function):
1187 """The `namespace-uri` function, which returns the namespace URI of the
1188 current element.
1189 """
1190 __slots__ = []
1191 def __call__(self, kind, data, pos, namespaces, variables):
1192 if kind is START:
1193 return data[0].namespace
1194 def __repr__(self):
1195 return 'namespace-uri()'
1197class NotFunction(Function):
1198 """The `not` function, which returns the negated boolean value of its
1199 argument.
1200 """
1201 __slots__ = ['expr']
1202 def __init__(self, expr):
1203 self.expr = expr
1204 def __call__(self, kind, data, pos, namespaces, variables):
1205 return not as_bool(self.expr(kind, data, pos, namespaces, variables))
1206 def __repr__(self):
1207 return 'not(%s)' % self.expr
1209class NormalizeSpaceFunction(Function):
1210 """The `normalize-space` function, which removes leading and trailing
1211 whitespace in the given string, and replaces multiple adjacent whitespace
1212 characters inside the string with a single space.
1213 """
1214 __slots__ = ['expr']
1215 _normalize = re.compile(r'\s{2,}').sub
1216 def __init__(self, expr):
1217 self.expr = expr
1218 def __call__(self, kind, data, pos, namespaces, variables):
1219 string = self.expr(kind, data, pos, namespaces, variables)
1220 return self._normalize(' ', as_string(string).strip())
1221 def __repr__(self):
1222 return 'normalize-space(%s)' % repr(self.expr)
1224class NumberFunction(Function):
1225 """The `number` function that converts its argument to a number."""
1226 __slots__ = ['expr']
1227 def __init__(self, expr):
1228 self.expr = expr
1229 def __call__(self, kind, data, pos, namespaces, variables):
1230 val = self.expr(kind, data, pos, namespaces, variables)
1231 return as_float(val)
1232 def __repr__(self):
1233 return 'number(%r)' % self.expr
1235class RoundFunction(Function):
1236 """The `round` function, which returns the nearest integer number for the
1237 given number.
1238 """
1239 __slots__ = ['number']
1240 def __init__(self, number):
1241 self.number = number
1242 def __call__(self, kind, data, pos, namespaces, variables):
1243 number = self.number(kind, data, pos, namespaces, variables)
1244 return round(as_float(number))
1245 def __repr__(self):
1246 return 'round(%r)' % self.number
1248class StartsWithFunction(Function):
1249 """The `starts-with` function that returns whether one string starts with
1250 a given substring.
1251 """
1252 __slots__ = ['string1', 'string2']
1253 def __init__(self, string1, string2):
1254 self.string1 = string1
1255 self.string2 = string2
1256 def __call__(self, kind, data, pos, namespaces, variables):
1257 string1 = self.string1(kind, data, pos, namespaces, variables)
1258 string2 = self.string2(kind, data, pos, namespaces, variables)
1259 return as_string(string1).startswith(as_string(string2))
1260 def __repr__(self):
1261 return 'starts-with(%r, %r)' % (self.string1, self.string2)
1263class StringLengthFunction(Function):
1264 """The `string-length` function that returns the length of the given
1265 string.
1266 """
1267 __slots__ = ['expr']
1268 def __init__(self, expr):
1269 self.expr = expr
1270 def __call__(self, kind, data, pos, namespaces, variables):
1271 string = self.expr(kind, data, pos, namespaces, variables)
1272 return len(as_string(string))
1273 def __repr__(self):
1274 return 'string-length(%r)' % self.expr
1276class SubstringFunction(Function):
1277 """The `substring` function that returns the part of a string that starts
1278 at the given offset, and optionally limited to the given length.
1279 """
1280 __slots__ = ['string', 'start', 'length']
1281 def __init__(self, string, start, length=None):
1282 self.string = string
1283 self.start = start
1284 self.length = length
1285 def __call__(self, kind, data, pos, namespaces, variables):
1286 string = self.string(kind, data, pos, namespaces, variables)
1287 start = self.start(kind, data, pos, namespaces, variables)
1288 length = 0
1289 if self.length is not None:
1290 length = self.length(kind, data, pos, namespaces, variables)
1291 return string[as_long(start):len(as_string(string)) - as_long(length)]
1292 def __repr__(self):
1293 if self.length is not None:
1294 return 'substring(%r, %r, %r)' % (self.string, self.start,
1295 self.length)
1296 else:
1297 return 'substring(%r, %r)' % (self.string, self.start)
1299class SubstringAfterFunction(Function):
1300 """The `substring-after` function that returns the part of a string that
1301 is found after the given substring.
1302 """
1303 __slots__ = ['string1', 'string2']
1304 def __init__(self, string1, string2):
1305 self.string1 = string1
1306 self.string2 = string2
1307 def __call__(self, kind, data, pos, namespaces, variables):
1308 string1 = as_string(self.string1(kind, data, pos, namespaces, variables))
1309 string2 = as_string(self.string2(kind, data, pos, namespaces, variables))
1310 index = string1.find(string2)
1311 if index >= 0:
1312 return string1[index + len(string2):]
1313 return ''
1314 def __repr__(self):
1315 return 'substring-after(%r, %r)' % (self.string1, self.string2)
1317class SubstringBeforeFunction(Function):
1318 """The `substring-before` function that returns the part of a string that
1319 is found before the given substring.
1320 """
1321 __slots__ = ['string1', 'string2']
1322 def __init__(self, string1, string2):
1323 self.string1 = string1
1324 self.string2 = string2
1325 def __call__(self, kind, data, pos, namespaces, variables):
1326 string1 = as_string(self.string1(kind, data, pos, namespaces, variables))
1327 string2 = as_string(self.string2(kind, data, pos, namespaces, variables))
1328 index = string1.find(string2)
1329 if index >= 0:
1330 return string1[:index]
1331 return ''
1332 def __repr__(self):
1333 return 'substring-after(%r, %r)' % (self.string1, self.string2)
1335class TranslateFunction(Function):
1336 """The `translate` function that translates a set of characters in a
1337 string to target set of characters.
1338 """
1339 __slots__ = ['string', 'fromchars', 'tochars']
1340 def __init__(self, string, fromchars, tochars):
1341 self.string = string
1342 self.fromchars = fromchars
1343 self.tochars = tochars
1344 def __call__(self, kind, data, pos, namespaces, variables):
1345 string = as_string(self.string(kind, data, pos, namespaces, variables))
1346 fromchars = as_string(self.fromchars(kind, data, pos, namespaces, variables))
1347 tochars = as_string(self.tochars(kind, data, pos, namespaces, variables))
1348 table = dict(zip([ord(c) for c in fromchars],
1349 [ord(c) for c in tochars]))
1350 return string.translate(table)
1351 def __repr__(self):
1352 return 'translate(%r, %r, %r)' % (self.string, self.fromchars,
1353 self.tochars)
1355class TrueFunction(Function):
1356 """The `true` function, which always returns the boolean `true` value."""
1357 __slots__ = []
1358 def __call__(self, kind, data, pos, namespaces, variables):
1359 return True
1360 def __repr__(self):
1361 return 'true()'
1363_function_map = {'boolean': BooleanFunction, 'ceiling': CeilingFunction,
1364 'concat': ConcatFunction, 'contains': ContainsFunction,
1365 'matches': MatchesFunction, 'false': FalseFunction, 'floor':
1366 FloorFunction, 'local-name': LocalNameFunction, 'name':
1367 NameFunction, 'namespace-uri': NamespaceUriFunction,
1368 'normalize-space': NormalizeSpaceFunction, 'not': NotFunction,
1369 'number': NumberFunction, 'round': RoundFunction,
1370 'starts-with': StartsWithFunction, 'string-length':
1371 StringLengthFunction, 'substring': SubstringFunction,
1372 'substring-after': SubstringAfterFunction, 'substring-before':
1373 SubstringBeforeFunction, 'translate': TranslateFunction,
1374 'true': TrueFunction}
1376# Literals & Variables
1378class Literal(object):
1379 """Abstract base class for literal nodes."""
1381class StringLiteral(Literal):
1382 """A string literal node."""
1383 __slots__ = ['text']
1384 def __init__(self, text):
1385 self.text = text
1386 def __call__(self, kind, data, pos, namespaces, variables):
1387 return self.text
1388 def __repr__(self):
1389 return '"%s"' % self.text
1391class NumberLiteral(Literal):
1392 """A number literal node."""
1393 __slots__ = ['number']
1394 def __init__(self, number):
1395 self.number = number
1396 def __call__(self, kind, data, pos, namespaces, variables):
1397 return self.number
1398 def __repr__(self):
1399 return str(self.number)
1401class VariableReference(Literal):
1402 """A variable reference node."""
1403 __slots__ = ['name']
1404 def __init__(self, name):
1405 self.name = name
1406 def __call__(self, kind, data, pos, namespaces, variables):
1407 return variables.get(self.name)
1408 def __repr__(self):
1409 return str(self.name)
1411# Operators
1413class AndOperator(object):
1414 """The boolean operator `and`."""
1415 __slots__ = ['lval', 'rval']
1416 def __init__(self, lval, rval):
1417 self.lval = lval
1418 self.rval = rval
1419 def __call__(self, kind, data, pos, namespaces, variables):
1420 lval = as_bool(self.lval(kind, data, pos, namespaces, variables))
1421 if not lval:
1422 return False
1423 rval = self.rval(kind, data, pos, namespaces, variables)
1424 return as_bool(rval)
1425 def __repr__(self):
1426 return '%s and %s' % (self.lval, self.rval)
1428class EqualsOperator(object):
1429 """The equality operator `=`."""
1430 __slots__ = ['lval', 'rval']
1431 def __init__(self, lval, rval):
1432 self.lval = lval
1433 self.rval = rval
1434 def __call__(self, kind, data, pos, namespaces, variables):
1435 lval = as_scalar(self.lval(kind, data, pos, namespaces, variables))
1436 rval = as_scalar(self.rval(kind, data, pos, namespaces, variables))
1437 return lval == rval
1438 def __repr__(self):
1439 return '%s=%s' % (self.lval, self.rval)
1441class NotEqualsOperator(object):
1442 """The equality operator `!=`."""
1443 __slots__ = ['lval', 'rval']
1444 def __init__(self, lval, rval):
1445 self.lval = lval
1446 self.rval = rval
1447 def __call__(self, kind, data, pos, namespaces, variables):
1448 lval = as_scalar(self.lval(kind, data, pos, namespaces, variables))
1449 rval = as_scalar(self.rval(kind, data, pos, namespaces, variables))
1450 return lval != rval
1451 def __repr__(self):
1452 return '%s!=%s' % (self.lval, self.rval)
1454class OrOperator(object):
1455 """The boolean operator `or`."""
1456 __slots__ = ['lval', 'rval']
1457 def __init__(self, lval, rval):
1458 self.lval = lval
1459 self.rval = rval
1460 def __call__(self, kind, data, pos, namespaces, variables):
1461 lval = as_bool(self.lval(kind, data, pos, namespaces, variables))
1462 if lval:
1463 return True
1464 rval = self.rval(kind, data, pos, namespaces, variables)
1465 return as_bool(rval)
1466 def __repr__(self):
1467 return '%s or %s' % (self.lval, self.rval)
1469class GreaterThanOperator(object):
1470 """The relational operator `>` (greater than)."""
1471 __slots__ = ['lval', 'rval']
1472 def __init__(self, lval, rval):
1473 self.lval = lval
1474 self.rval = rval
1475 def __call__(self, kind, data, pos, namespaces, variables):
1476 lval = self.lval(kind, data, pos, namespaces, variables)
1477 rval = self.rval(kind, data, pos, namespaces, variables)
1478 return as_float(lval) > as_float(rval)
1479 def __repr__(self):
1480 return '%s>%s' % (self.lval, self.rval)
1482class GreaterThanOrEqualOperator(object):
1483 """The relational operator `>=` (greater than or equal)."""
1484 __slots__ = ['lval', 'rval']
1485 def __init__(self, lval, rval):
1486 self.lval = lval
1487 self.rval = rval
1488 def __call__(self, kind, data, pos, namespaces, variables):
1489 lval = self.lval(kind, data, pos, namespaces, variables)
1490 rval = self.rval(kind, data, pos, namespaces, variables)
1491 return as_float(lval) >= as_float(rval)
1492 def __repr__(self):
1493 return '%s>=%s' % (self.lval, self.rval)
1495class LessThanOperator(object):
1496 """The relational operator `<` (less than)."""
1497 __slots__ = ['lval', 'rval']
1498 def __init__(self, lval, rval):
1499 self.lval = lval
1500 self.rval = rval
1501 def __call__(self, kind, data, pos, namespaces, variables):
1502 lval = self.lval(kind, data, pos, namespaces, variables)
1503 rval = self.rval(kind, data, pos, namespaces, variables)
1504 return as_float(lval) < as_float(rval)
1505 def __repr__(self):
1506 return '%s<%s' % (self.lval, self.rval)
1508class LessThanOrEqualOperator(object):
1509 """The relational operator `<=` (less than or equal)."""
1510 __slots__ = ['lval', 'rval']
1511 def __init__(self, lval, rval):
1512 self.lval = lval
1513 self.rval = rval
1514 def __call__(self, kind, data, pos, namespaces, variables):
1515 lval = self.lval(kind, data, pos, namespaces, variables)
1516 rval = self.rval(kind, data, pos, namespaces, variables)
1517 return as_float(lval) <= as_float(rval)
1518 def __repr__(self):
1519 return '%s<=%s' % (self.lval, self.rval)
1521_operator_map = {'=': EqualsOperator, '!=': NotEqualsOperator,
1522 '>': GreaterThanOperator, '>=': GreaterThanOrEqualOperator,
1523 '<': LessThanOperator, '>=': LessThanOrEqualOperator}
1526_DOTSLASHSLASH = (DESCENDANT_OR_SELF, PrincipalTypeTest(None), ())
1527_DOTSLASH = (SELF, PrincipalTypeTest(None), ())