Skip to content

Commit 41a0f7f

Browse files
author
annbgn
committed
add test, expand :has() to accept more complex arguments, remove useless ifs
1 parent de1836a commit 41a0f7f

File tree

3 files changed

+36
-22
lines changed

3 files changed

+36
-22
lines changed

cssselect/parser.py

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -255,8 +255,9 @@ class Relation(object):
255255
Represents selector:has(subselector)
256256
"""
257257

258-
def __init__(self, selector, subselector):
258+
def __init__(self, selector, combinator, subselector):
259259
self.selector = selector
260+
self.combinator = combinator
260261
self.subselector = subselector
261262

262263
def __repr__(self):
@@ -267,19 +268,20 @@ def __repr__(self):
267268
)
268269

269270
def canonical(self):
270-
if not self.subselector:
271-
subsel = "*"
272-
else:
271+
try:
273272
subsel = self.subselector[0].canonical()
273+
except TypeError:
274+
subsel = self.subselector.canonical()
274275
if len(subsel) > 1:
275276
subsel = subsel.lstrip("*")
276277
return "%s:has(%s)" % (self.selector.canonical(), subsel)
277278

278279
def specificity(self):
279280
a1, b1, c1 = self.selector.specificity()
280-
a2 = b2 = c2 = 0
281-
if self.subselector:
281+
try:
282282
a2, b2, c2 = self.subselector[-1].specificity()
283+
except TypeError:
284+
a2, b2, c2 = self.subselector.specificity()
283285
return a1 + a2, b1 + b2, c1 + c2
284286

285287

@@ -600,8 +602,8 @@ def parse_simple_selector(stream, inside_negation=False):
600602
raise SelectorSyntaxError("Expected ')', got %s" % (next,))
601603
result = Negation(result, argument)
602604
elif ident.lower() == "has":
603-
arguments = parse_relative_selector(stream)
604-
result = Relation(result, arguments)
605+
combinator, arguments = parse_relative_selector(stream)
606+
result = Relation(result, combinator, arguments)
605607
elif ident.lower() in ("matches", "is"):
606608
selectors = parse_simple_selector_arguments(stream)
607609
result = Matching(result, selectors)
@@ -631,23 +633,27 @@ def parse_arguments(stream):
631633

632634

633635
def parse_relative_selector(stream):
634-
arguments = []
635636
stream.skip_whitespace()
637+
subselector = ""
636638
next = stream.next()
639+
637640
if next in [("DELIM", "+"), ("DELIM", "-"), ("DELIM", ">"), ("DELIM", "~")]:
638-
arguments.append(next)
639-
elif next.type in ("IDENT", "STRING", "NUMBER"):
640-
arguments.append(Element(element=next.value))
641-
while 1:
641+
combinator = next
642642
stream.skip_whitespace()
643643
next = stream.next()
644-
if next.type in ("IDENT", "STRING", "NUMBER"):
645-
arguments.append(Element(element=next.value))
644+
else:
645+
combinator = Token("DELIM", " ", pos=0)
646+
647+
while 1:
648+
if next.type in ("IDENT", "STRING", "NUMBER") or next in [("DELIM", "."), ("DELIM", "*")]:
649+
subselector += next.value
646650
elif next == ('DELIM', ')'):
647-
return arguments
651+
result = parse(subselector)
652+
return combinator, result[0]
648653
else:
649654
raise SelectorSyntaxError(
650655
"Expected an argument, got %s" % (next,))
656+
next = stream.next()
651657

652658

653659
def parse_simple_selector_arguments(stream):

cssselect/xpath.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -275,12 +275,9 @@ def xpath_negation(self, negation):
275275

276276
def xpath_relation(self, relation):
277277
xpath = self.xpath(relation.selector)
278-
combinator, *subselector = relation.subselector
279-
if not subselector:
280-
combinator.value = " "
281-
right = self.xpath(combinator)
282-
else:
283-
right = self.xpath(subselector[0])
278+
combinator = relation.combinator
279+
subselector = relation.subselector
280+
right = self.xpath(subselector.parsed_tree)
284281
method = getattr(
285282
self,
286283
"xpath_relation_%s_combinator" % self.combinator_mapping[combinator.value],

tests/test_cssselect.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ def parse_many(first, *others):
145145
'Hash[Element[div]#foobar]']
146146
assert parse_many('div:not(div.foo)') == [
147147
'Negation[Element[div]:not(Class[Element[div].foo])]']
148+
assert parse_many('div:has(div.foo)') == [
149+
'Relation[Element[div]:has(Selector[Class[Element[div].foo]])]']
148150
assert parse_many('div:is(.foo, #bar)') == [
149151
'Matching[Element[div]:is(Class[Element[*].foo], Hash[Element[*]#bar])]']
150152
assert parse_many(':is(:hover, :visited)') == [
@@ -272,6 +274,7 @@ def specificity(css):
272274

273275
assert specificity(":has(*)") == (0, 0, 0)
274276
assert specificity(":has(foo)") == (0, 0, 1)
277+
assert specificity(":has(.foo)") == (0, 1, 0)
275278
assert specificity(":has(> foo)") == (0, 0, 1)
276279

277280
assert specificity(':is(.foo, #bar)') == (1, 0, 0)
@@ -313,6 +316,7 @@ def css2css(css, res=None):
313316
css2css(':not(#foo)')
314317
css2css(":has(*)")
315318
css2css(":has(foo)")
319+
css2css(':has(*.foo)', ':has(.foo)')
316320
css2css(':is(#bar, .foo)')
317321
css2css(':is(:focused, :visited)')
318322
css2css('foo:empty')
@@ -400,6 +404,12 @@ def get_error(css):
400404
)
401405
assert get_error('> div p') == ("Expected selector, got <DELIM '>' at 0>")
402406

407+
# Unsupported :has() with several arguments
408+
assert get_error(':has(a, b)') == (
409+
"Expected an argument, got <DELIM ',' at 6>")
410+
assert get_error(':has()') == (
411+
"Expected selector, got <EOF at 0>")
412+
403413
def test_translation(self):
404414
def xpath(css):
405415
return _unicode(GenericTranslator().css_to_xpath(css, prefix=''))
@@ -889,6 +899,7 @@ def pcss(main, *selectors, **kwargs):
889899
assert pcss('ol :Not(li[class])') == [
890900
'first-li', 'second-li', 'li-div',
891901
'fifth-li', 'sixth-li', 'seventh-li']
902+
assert pcss('link:has(*)') == []
892903
assert pcss("ol:has(div)") == ["first-ol"]
893904
assert pcss(':is(#first-li, #second-li)') == [
894905
'first-li', 'second-li']

0 commit comments

Comments
 (0)