# HG changeset patch
# User cmlenz
# Date 1155768528 0
# Node ID 456039594db97db00570bbd9a31269bdf4559ed2
# Parent 7b1f07496bf761a07d9697ec2b8baa68154d3564
Implement the XPath relational operators and the `round()` function.
diff --git a/ChangeLog b/ChangeLog
--- a/ChangeLog
+++ b/ChangeLog
@@ -21,9 +21,9 @@
* String literals in XPath expressions that contain spaces are now parsed
as expected.
* Added support for the XPath functions boolean(), ceiling(), concat(),
- contains(), false(), floor(), normalize-space(), number(), starts-with(),
- string-length(), substring(), substring-after(), substring-before(),
- translate(), and true().
+ contains(), false(), floor(), normalize-space(), number(), round(),
+ starts-with(), string-length(), substring(), substring-after(),
+ substring-before(), translate(), and true().
Version 0.1
diff --git a/markup/path.py b/markup/path.py
--- a/markup/path.py
+++ b/markup/path.py
@@ -223,7 +223,7 @@
_QUOTES = (("'", "'"), ('"', '"'))
_TOKENS = ('::', ':', '..', '.', '//', '/', '[', ']', '()', '(', ')', '@',
- '=', '!=', '!', '|', ',')
+ '=', '!=', '!', '|', ',', '>=', '>', '<=', '<')
_tokenize = re.compile('("[^"]*")|(\'[^\']*\')|(%s)|([^%s\s]+)|\s+' % (
'|'.join([re.escape(t) for t in _TOKENS]),
''.join([re.escape(t[0]) for t in _TOKENS]))).findall
@@ -381,9 +381,17 @@
return expr
def _equality_expr(self):
+ expr = self._relational_expr()
+ while self.cur_token in ('=', '!='):
+ op = _operator_map[self.cur_token]
+ self.next_token()
+ expr = op(expr, self._relational_expr())
+ return expr
+
+ def _relational_expr(self):
expr = self._primary_expr()
- while self.cur_token in ('=', '!='):
- op = _operator_map.get(self.cur_token)
+ while self.cur_token in ('>', '>=', '<', '>='):
+ op = _operator_map[self.cur_token]
self.next_token()
expr = op(expr, self._primary_expr())
return expr
@@ -675,6 +683,21 @@
def __repr__(self):
return 'number(%r)' % self.expr
+class RoundFunction(Function):
+ """The `round` function, which returns the nearest integer number for the
+ given number.
+ """
+ __slots__ = ['number']
+ def __init__(self, number):
+ self.number = number
+ def __call__(self, kind, data, pos):
+ number = self.number(kind, data, pos)
+ if type(number) is tuple:
+ number = number[1]
+ return round(float(number))
+ def __repr__(self):
+ return 'round(%r)' % self.number
+
class StartsWithFunction(Function):
"""The `starts-with` function that returns whether one string starts with
a given substring.
@@ -822,7 +845,8 @@
'local-name': LocalNameFunction, 'name': NameFunction,
'namespace-uri': NamespaceUriFunction,
'normalize-space': NormalizeSpaceFunction, 'not': NotFunction,
- 'number': NumberFunction, 'starts-with': StartsWithFunction,
+ 'number': NumberFunction, 'round': RoundFunction,
+ 'starts-with': StartsWithFunction,
'string-length': StringLengthFunction,
'substring': SubstringFunction,
'substring-after': SubstringAfterFunction,
@@ -928,4 +952,91 @@
def __repr__(self):
return '%s or %s' % (self.lval, self.rval)
-_operator_map = {'=': EqualsOperator, '!=': NotEqualsOperator}
+class GreaterThanOperator(object):
+ """The relational operator `>` (greater than)."""
+ __slots__ = ['lval', 'rval']
+ def __init__(self, lval, rval):
+ self.lval = lval
+ self.rval = rval
+ def __call__(self, kind, data, pos):
+ lval = self.lval(kind, data, pos)
+ if type(lval) is tuple:
+ lval = lval[1]
+ rval = self.rval(kind, data, pos)
+ if type(rval) is tuple:
+ rval = rval[1]
+ return float(lval) > float(rval)
+ def __repr__(self):
+ return '%s>%s' % (self.lval, self.rval)
+
+class GreaterThanOperator(object):
+ """The relational operator `>` (greater than)."""
+ __slots__ = ['lval', 'rval']
+ def __init__(self, lval, rval):
+ self.lval = lval
+ self.rval = rval
+ def __call__(self, kind, data, pos):
+ lval = self.lval(kind, data, pos)
+ if type(lval) is tuple:
+ lval = lval[1]
+ rval = self.rval(kind, data, pos)
+ if type(rval) is tuple:
+ rval = rval[1]
+ return float(lval) > float(rval)
+ def __repr__(self):
+ return '%s>%s' % (self.lval, self.rval)
+
+class GreaterThanOrEqualOperator(object):
+ """The relational operator `>=` (greater than or equal)."""
+ __slots__ = ['lval', 'rval']
+ def __init__(self, lval, rval):
+ self.lval = lval
+ self.rval = rval
+ def __call__(self, kind, data, pos):
+ lval = self.lval(kind, data, pos)
+ if type(lval) is tuple:
+ lval = lval[1]
+ rval = self.rval(kind, data, pos)
+ if type(rval) is tuple:
+ rval = rval[1]
+ return float(lval) >= float(rval)
+ def __repr__(self):
+ return '%s>=%s' % (self.lval, self.rval)
+
+class LessThanOperator(object):
+ """The relational operator `<` (less than)."""
+ __slots__ = ['lval', 'rval']
+ def __init__(self, lval, rval):
+ self.lval = lval
+ self.rval = rval
+ def __call__(self, kind, data, pos):
+ lval = self.lval(kind, data, pos)
+ if type(lval) is tuple:
+ lval = lval[1]
+ rval = self.rval(kind, data, pos)
+ if type(rval) is tuple:
+ rval = rval[1]
+ return float(lval) < float(rval)
+ def __repr__(self):
+ return '%s<%s' % (self.lval, self.rval)
+
+class LessThanOrEqualOperator(object):
+ """The relational operator `<=` (less than or equal)."""
+ __slots__ = ['lval', 'rval']
+ def __init__(self, lval, rval):
+ self.lval = lval
+ self.rval = rval
+ def __call__(self, kind, data, pos):
+ lval = self.lval(kind, data, pos)
+ if type(lval) is tuple:
+ lval = lval[1]
+ rval = self.rval(kind, data, pos)
+ if type(rval) is tuple:
+ rval = rval[1]
+ return float(lval) <= float(rval)
+ def __repr__(self):
+ return '%s<=%s' % (self.lval, self.rval)
+
+_operator_map = {'=': EqualsOperator, '!=': NotEqualsOperator,
+ '>': GreaterThanOperator, '>=': GreaterThanOrEqualOperator,
+ '<': LessThanOperator, '>=': LessThanOrEqualOperator}
diff --git a/markup/tests/path.py b/markup/tests/path.py
--- a/markup/tests/path.py
+++ b/markup/tests/path.py
@@ -177,8 +177,8 @@
def test_3step(self):
xml = XML('')
- path = Path('root/foo/*')
- self.assertEqual('',
+ path = Path('foo/*')
+ self.assertEqual('',
repr(path))
self.assertEqual('', path.select(xml).render())
@@ -257,29 +257,43 @@
def test_predicate_attr(self):
xml = XML(' ')
self.assertEqual(' ',
- Path('root/item[@important]').select(xml).render())
+ Path('item[@important]').select(xml).render())
self.assertEqual(' ',
- Path('root/item[@important="very"]').select(xml).render())
+ Path('item[@important="very"]').select(xml).render())
def test_predicate_attr_equality(self):
xml = XML(' ')
self.assertEqual('',
- Path('root/item[@important="very"]').select(xml).render())
+ Path('item[@important="very"]').select(xml).render())
self.assertEqual(' ',
- Path('root/item[@important!="very"]').select(xml).render())
+ Path('item[@important!="very"]').select(xml).render())
+
+ def test_predicate_attr_greater_than(self):
+ xml = XML(' ')
+ self.assertEqual('',
+ Path('item[@priority>3]').select(xml).render())
+ self.assertEqual(' ',
+ Path('item[@priority>2]').select(xml).render())
+
+ def test_predicate_attr_less_than(self):
+ xml = XML(' ')
+ self.assertEqual('',
+ Path('item[@priority<3]').select(xml).render())
+ self.assertEqual(' ',
+ Path('item[@priority<4]').select(xml).render())
def test_predicate_attr_and(self):
xml = XML(' ')
- path = Path('root/item[@important and @important="very"]')
+ path = Path('item[@important and @important="very"]')
self.assertEqual(' ', path.select(xml).render())
- path = Path('root/item[@important and @important="notso"]')
+ path = Path('item[@important and @important="notso"]')
self.assertEqual('', path.select(xml).render())
def test_predicate_attr_or(self):
xml = XML(' ')
- path = Path('root/item[@urgent or @important]')
+ path = Path('item[@urgent or @important]')
self.assertEqual(' ', path.select(xml).render())
- path = Path('root/item[@urgent or @notso]')
+ path = Path('item[@urgent or @notso]')
self.assertEqual('', path.select(xml).render())
def test_predicate_boolean_function(self):
@@ -332,6 +346,13 @@
path = Path('*[number("3.0")=3]')
self.assertEqual('bar', path.select(xml).render())
+ def test_predicate_round_function(self):
+ xml = XML('bar')
+ path = Path('*[round("4.4")=4]')
+ self.assertEqual('bar', path.select(xml).render())
+ path = Path('*[round("4.6")=5]')
+ self.assertEqual('bar', path.select(xml).render())
+
def test_predicate_starts_with_function(self):
xml = XML('bar')
path = Path('*[starts-with(name(), "f")]')