Skip to content

Commit 1305d49

Browse files
authored
Merge pull request #98 from Gallaecio/condition-precedence
Condition precedence
2 parents b26932d + 754b701 commit 1305d49

File tree

2 files changed

+48
-11
lines changed

2 files changed

+48
-11
lines changed

cssselect/xpath.py

+11-6
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def __repr__(self):
5656

5757
def add_condition(self, condition):
5858
if self.condition:
59-
self.condition = '%s and (%s)' % (self.condition, condition)
59+
self.condition = '(%s) and (%s)' % (self.condition, condition)
6060
else:
6161
self.condition = condition
6262
return self
@@ -457,19 +457,19 @@ def xpath_nth_child_function(self, xpath, function, last=False,
457457
if a == 0:
458458
return xpath.add_condition('%s = %s' % (siblings_count, b_min_1))
459459

460-
expr = []
460+
expressions = []
461461

462462
if a > 0:
463463
# siblings count, an+b-1, is always >= 0,
464464
# so if a>0, and (b-1)<=0, an "n" exists to satisfy this,
465465
# therefore, the predicate is only interesting if (b-1)>0
466466
if b_min_1 > 0:
467-
expr.append('%s >= %s' % (siblings_count, b_min_1))
467+
expressions.append('%s >= %s' % (siblings_count, b_min_1))
468468
else:
469469
# if a<0, and (b-1)<0, no "n" satisfies this,
470470
# this is tested above as an early exist condition
471471
# otherwise,
472-
expr.append('%s <= %s' % (siblings_count, b_min_1))
472+
expressions.append('%s <= %s' % (siblings_count, b_min_1))
473473

474474
# operations modulo 1 or -1 are simpler, one only needs to verify:
475475
#
@@ -495,9 +495,14 @@ def xpath_nth_child_function(self, xpath, function, last=False,
495495
b_neg = '+%s' % b_neg
496496
left = '(%s %s)' % (left, b_neg)
497497

498-
expr.append('%s mod %s = 0' % (left, a))
498+
expressions.append('%s mod %s = 0' % (left, a))
499499

500-
xpath.add_condition(' and '.join(expr))
500+
if len(expressions) > 1:
501+
template = '(%s)'
502+
else:
503+
template = '%s'
504+
xpath.add_condition(' and '.join(template % expression
505+
for expression in expressions))
501506
return xpath
502507

503508
def xpath_nth_last_child_function(self, xpath, function):

tests/test_cssselect.py

+37-5
Original file line numberDiff line numberDiff line change
@@ -428,8 +428,8 @@ def xpath(css):
428428
"e[count(preceding-sibling::*) <= 0]")
429429

430430
assert xpath('e:nth-child(3n+2)') == (
431-
"e[count(preceding-sibling::*) >= 1 and "
432-
"(count(preceding-sibling::*) +2) mod 3 = 0]")
431+
"e[(count(preceding-sibling::*) >= 1) and "
432+
"((count(preceding-sibling::*) +2) mod 3 = 0)]")
433433
assert xpath('e:nth-child(3n-2)') == (
434434
"e[count(preceding-sibling::*) mod 3 = 0]")
435435
assert xpath('e:nth-child(-n+6)') == (
@@ -442,8 +442,8 @@ def xpath(css):
442442
assert xpath('e:nth-last-child(2n+1)') == (
443443
"e[count(following-sibling::*) mod 2 = 0]")
444444
assert xpath('e:nth-last-child(2n+2)') == (
445-
"e[count(following-sibling::*) >= 1 and "
446-
"(count(following-sibling::*) +1) mod 2 = 0]")
445+
"e[(count(following-sibling::*) >= 1) and "
446+
"((count(following-sibling::*) +1) mod 2 = 0)]")
447447
assert xpath('e:nth-last-child(3n+1)') == (
448448
"e[count(following-sibling::*) mod 3 = 0]")
449449
# represents the two last e elements
@@ -497,7 +497,7 @@ def xpath(css):
497497
assert xpath('e > f') == (
498498
"e/f")
499499
assert xpath('e + f') == (
500-
"e/following-sibling::*[name() = 'f' and (position() = 1)]")
500+
"e/following-sibling::*[(name() = 'f') and (position() = 1)]")
501501
assert xpath('e ~ f') == (
502502
"e/following-sibling::f")
503503
assert xpath('e ~ f:nth-child(3)') == (
@@ -622,6 +622,11 @@ def xpath_attr_href_simple_pseudo_element(self, xpath):
622622
other = XPathExpr('@href', '', )
623623
return xpath.join('/', other)
624624

625+
# pseudo-element:
626+
# used to demonstrate operator precedence
627+
def xpath_first_or_second_pseudo(self, xpath):
628+
return xpath.add_condition("@id = 'first' or @id = 'second'")
629+
625630
def xpath(css):
626631
return _unicode(CustomTranslator().css_to_xpath(css))
627632

@@ -633,6 +638,25 @@ def xpath(css):
633638
assert xpath('p img::attr(src)') == (
634639
"descendant-or-self::p/descendant-or-self::*/img/@src")
635640
assert xpath(':scope') == "descendant-or-self::*[1]"
641+
assert xpath(':first-or-second[href]') == (
642+
"descendant-or-self::*[(@id = 'first' or @id = 'second') "
643+
"and (@href)]")
644+
645+
assert str(XPathExpr('', '', condition='@href')) == "[@href]"
646+
647+
document = etree.fromstring(OPERATOR_PRECEDENCE_IDS)
648+
sort_key = dict(
649+
(el, count) for count, el in enumerate(document.getiterator())
650+
).__getitem__
651+
def operator_id(selector):
652+
xpath = CustomTranslator().css_to_xpath(selector)
653+
items = document.xpath(xpath)
654+
items.sort(key=sort_key)
655+
return [element.get('id', 'nil') for element in items]
656+
657+
assert operator_id(':first-or-second') == ['first', 'second']
658+
assert operator_id(':first-or-second[href]') == ['second']
659+
assert operator_id('[href]:first-or-second') == ['second']
636660

637661
def test_series(self):
638662
def series(css):
@@ -935,6 +959,14 @@ def count(selector):
935959
assert count(':scope > div > div[class=dialog]') == 1
936960
assert count(':scope > div div') == 242
937961

962+
OPERATOR_PRECEDENCE_IDS = '''
963+
<html>
964+
<a id="first"></a>
965+
<a id="second" href="#"></a>
966+
<a id="third" href="#"></a>
967+
</html>
968+
'''
969+
938970
XMLLANG_IDS = '''
939971
<test>
940972
<a id="first" xml:lang="en">a</a>

0 commit comments

Comments
 (0)