# 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")]')