| | import pytest |
| | import types |
| | from unittest.mock import MagicMock |
| |
|
| | from bs4 import ( |
| | CSS, |
| | BeautifulSoup, |
| | ResultSet, |
| | ) |
| |
|
| | from . import ( |
| | SoupTest, |
| | SOUP_SIEVE_PRESENT, |
| | ) |
| |
|
| | if SOUP_SIEVE_PRESENT: |
| | from soupsieve import SelectorSyntaxError |
| |
|
| |
|
| | @pytest.mark.skipif(not SOUP_SIEVE_PRESENT, reason="Soup Sieve not installed") |
| | class TestCSSSelectors(SoupTest): |
| | """Test basic CSS selector functionality. |
| | |
| | This functionality is implemented in soupsieve, which has a much |
| | more comprehensive test suite, so this is basically an extra check |
| | that soupsieve works as expected. |
| | """ |
| |
|
| | HTML = """ |
| | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" |
| | "http://www.w3.org/TR/html4/strict.dtd"> |
| | <html> |
| | <head> |
| | <title>The title</title> |
| | <link rel="stylesheet" href="blah.css" type="text/css" id="l1"> |
| | </head> |
| | <body> |
| | <custom-dashed-tag class="dashed" id="dash1">Hello there.</custom-dashed-tag> |
| | <div id="main" class="fancy"> |
| | <div id="inner"> |
| | <h1 id="header1">An H1</h1> |
| | <p>Some text</p> |
| | <p class="onep" id="p1">Some more text</p> |
| | <h2 id="header2">An H2</h2> |
| | <p class="class1 class2 class3" id="pmulti">Another</p> |
| | <a href="http://bob.example.org/" rel="friend met" id="bob">Bob</a> |
| | <h2 id="header3">Another H2</h2> |
| | <a id="me" href="http://simonwillison.net/" rel="me">me</a> |
| | <span class="s1"> |
| | <a href="#" id="s1a1">span1a1</a> |
| | <a href="#" id="s1a2">span1a2 <span id="s1a2s1">test</span></a> |
| | <span class="span2"> |
| | <a href="#" id="s2a1">span2a1</a> |
| | </span> |
| | <span class="span3"></span> |
| | <custom-dashed-tag class="dashed" id="dash2"/> |
| | <div data-tag="dashedvalue" id="data1"/> |
| | </span> |
| | </div> |
| | <x id="xid"> |
| | <z id="zida"/> |
| | <z id="zidab"/> |
| | <z id="zidac"/> |
| | </x> |
| | <y id="yid"> |
| | <z id="zidb"/> |
| | </y> |
| | <p lang="en" id="lang-en">English</p> |
| | <p lang="en-gb" id="lang-en-gb">English UK</p> |
| | <p lang="en-us" id="lang-en-us">English US</p> |
| | <p lang="fr" id="lang-fr">French</p> |
| | </div> |
| | |
| | <div id="footer"> |
| | </div> |
| | """ |
| |
|
| | def setup_method(self): |
| | self.soup = BeautifulSoup(self.HTML, 'html.parser') |
| |
|
| | def assert_selects(self, selector, expected_ids, **kwargs): |
| | results = self.soup.select(selector, **kwargs) |
| | assert isinstance(results, ResultSet) |
| | el_ids = [el['id'] for el in results] |
| | el_ids.sort() |
| | expected_ids.sort() |
| | assert expected_ids == el_ids, "Selector %s, expected [%s], got [%s]" % ( |
| | selector, ', '.join(expected_ids), ', '.join(el_ids) |
| | ) |
| |
|
| | assertSelect = assert_selects |
| |
|
| | def assert_select_multiple(self, *tests): |
| | for selector, expected_ids in tests: |
| | self.assert_selects(selector, expected_ids) |
| |
|
| | def test_precompiled(self): |
| | sel = self.soup.css.compile('div') |
| |
|
| | els = self.soup.select(sel) |
| | assert len(els) == 4 |
| | for div in els: |
| | assert div.name == 'div' |
| |
|
| | el = self.soup.select_one(sel) |
| | assert 'main' == el['id'] |
| |
|
| | def test_one_tag_one(self): |
| | els = self.soup.select('title') |
| | assert len(els) == 1 |
| | assert els[0].name == 'title' |
| | assert els[0].contents == ['The title'] |
| |
|
| | def test_one_tag_many(self): |
| | els = self.soup.select('div') |
| | assert len(els) == 4 |
| | for div in els: |
| | assert div.name == 'div' |
| |
|
| | el = self.soup.select_one('div') |
| | assert 'main' == el['id'] |
| |
|
| | def test_select_one_returns_none_if_no_match(self): |
| | match = self.soup.select_one('nonexistenttag') |
| | assert None == match |
| |
|
| |
|
| | def test_tag_in_tag_one(self): |
| | els = self.soup.select('div div') |
| | self.assert_selects('div div', ['inner', 'data1']) |
| |
|
| | def test_tag_in_tag_many(self): |
| | for selector in ('html div', 'html body div', 'body div'): |
| | self.assert_selects(selector, ['data1', 'main', 'inner', 'footer']) |
| |
|
| |
|
| | def test_limit(self): |
| | self.assert_selects('html div', ['main'], limit=1) |
| | self.assert_selects('html body div', ['inner', 'main'], limit=2) |
| | self.assert_selects('body div', ['data1', 'main', 'inner', 'footer'], |
| | limit=10) |
| |
|
| | def test_tag_no_match(self): |
| | assert len(self.soup.select('del')) == 0 |
| |
|
| | def test_invalid_tag(self): |
| | with pytest.raises(SelectorSyntaxError): |
| | self.soup.select('tag%t') |
| |
|
| | def test_select_dashed_tag_ids(self): |
| | self.assert_selects('custom-dashed-tag', ['dash1', 'dash2']) |
| |
|
| | def test_select_dashed_by_id(self): |
| | dashed = self.soup.select('custom-dashed-tag[id=\"dash2\"]') |
| | assert dashed[0].name == 'custom-dashed-tag' |
| | assert dashed[0]['id'] == 'dash2' |
| |
|
| | def test_dashed_tag_text(self): |
| | assert self.soup.select('body > custom-dashed-tag')[0].text == 'Hello there.' |
| |
|
| | def test_select_dashed_matches_find_all(self): |
| | assert self.soup.select('custom-dashed-tag') == self.soup.find_all('custom-dashed-tag') |
| |
|
| | def test_header_tags(self): |
| | self.assert_select_multiple( |
| | ('h1', ['header1']), |
| | ('h2', ['header2', 'header3']), |
| | ) |
| |
|
| | def test_class_one(self): |
| | for selector in ('.onep', 'p.onep', 'html p.onep'): |
| | els = self.soup.select(selector) |
| | assert len(els) == 1 |
| | assert els[0].name == 'p' |
| | assert els[0]['class'] == ['onep'] |
| |
|
| | def test_class_mismatched_tag(self): |
| | els = self.soup.select('div.onep') |
| | assert len(els) == 0 |
| |
|
| | def test_one_id(self): |
| | for selector in ('div#inner', '#inner', 'div div#inner'): |
| | self.assert_selects(selector, ['inner']) |
| |
|
| | def test_bad_id(self): |
| | els = self.soup.select('#doesnotexist') |
| | assert len(els) == 0 |
| |
|
| | def test_items_in_id(self): |
| | els = self.soup.select('div#inner p') |
| | assert len(els) == 3 |
| | for el in els: |
| | assert el.name == 'p' |
| | assert els[1]['class'] == ['onep'] |
| | assert not els[0].has_attr('class') |
| |
|
| | def test_a_bunch_of_emptys(self): |
| | for selector in ('div#main del', 'div#main div.oops', 'div div#main'): |
| | assert len(self.soup.select(selector)) == 0 |
| |
|
| | def test_multi_class_support(self): |
| | for selector in ('.class1', 'p.class1', '.class2', 'p.class2', |
| | '.class3', 'p.class3', 'html p.class2', 'div#inner .class2'): |
| | self.assert_selects(selector, ['pmulti']) |
| |
|
| | def test_multi_class_selection(self): |
| | for selector in ('.class1.class3', '.class3.class2', |
| | '.class1.class2.class3'): |
| | self.assert_selects(selector, ['pmulti']) |
| |
|
| | def test_child_selector(self): |
| | self.assert_selects('.s1 > a', ['s1a1', 's1a2']) |
| | self.assert_selects('.s1 > a span', ['s1a2s1']) |
| |
|
| | def test_child_selector_id(self): |
| | self.assert_selects('.s1 > a#s1a2 span', ['s1a2s1']) |
| |
|
| | def test_attribute_equals(self): |
| | self.assert_select_multiple( |
| | ('p[class="onep"]', ['p1']), |
| | ('p[id="p1"]', ['p1']), |
| | ('[class="onep"]', ['p1']), |
| | ('[id="p1"]', ['p1']), |
| | ('link[rel="stylesheet"]', ['l1']), |
| | ('link[type="text/css"]', ['l1']), |
| | ('link[href="blah.css"]', ['l1']), |
| | ('link[href="no-blah.css"]', []), |
| | ('[rel="stylesheet"]', ['l1']), |
| | ('[type="text/css"]', ['l1']), |
| | ('[href="blah.css"]', ['l1']), |
| | ('[href="no-blah.css"]', []), |
| | ('p[href="no-blah.css"]', []), |
| | ('[href="no-blah.css"]', []), |
| | ) |
| |
|
| | def test_attribute_tilde(self): |
| | self.assert_select_multiple( |
| | ('p[class~="class1"]', ['pmulti']), |
| | ('p[class~="class2"]', ['pmulti']), |
| | ('p[class~="class3"]', ['pmulti']), |
| | ('[class~="class1"]', ['pmulti']), |
| | ('[class~="class2"]', ['pmulti']), |
| | ('[class~="class3"]', ['pmulti']), |
| | ('a[rel~="friend"]', ['bob']), |
| | ('a[rel~="met"]', ['bob']), |
| | ('[rel~="friend"]', ['bob']), |
| | ('[rel~="met"]', ['bob']), |
| | ) |
| |
|
| | def test_attribute_startswith(self): |
| | self.assert_select_multiple( |
| | ('[rel^="style"]', ['l1']), |
| | ('link[rel^="style"]', ['l1']), |
| | ('notlink[rel^="notstyle"]', []), |
| | ('[rel^="notstyle"]', []), |
| | ('link[rel^="notstyle"]', []), |
| | ('link[href^="bla"]', ['l1']), |
| | ('a[href^="http://"]', ['bob', 'me']), |
| | ('[href^="http://"]', ['bob', 'me']), |
| | ('[id^="p"]', ['pmulti', 'p1']), |
| | ('[id^="m"]', ['me', 'main']), |
| | ('div[id^="m"]', ['main']), |
| | ('a[id^="m"]', ['me']), |
| | ('div[data-tag^="dashed"]', ['data1']) |
| | ) |
| |
|
| | def test_attribute_endswith(self): |
| | self.assert_select_multiple( |
| | ('[href$=".css"]', ['l1']), |
| | ('link[href$=".css"]', ['l1']), |
| | ('link[id$="1"]', ['l1']), |
| | ('[id$="1"]', ['data1', 'l1', 'p1', 'header1', 's1a1', 's2a1', 's1a2s1', 'dash1']), |
| | ('div[id$="1"]', ['data1']), |
| | ('[id$="noending"]', []), |
| | ) |
| |
|
| | def test_attribute_contains(self): |
| | self.assert_select_multiple( |
| | |
| | ('[rel*="style"]', ['l1']), |
| | ('link[rel*="style"]', ['l1']), |
| | ('notlink[rel*="notstyle"]', []), |
| | ('[rel*="notstyle"]', []), |
| | ('link[rel*="notstyle"]', []), |
| | ('link[href*="bla"]', ['l1']), |
| | ('[href*="http://"]', ['bob', 'me']), |
| | ('[id*="p"]', ['pmulti', 'p1']), |
| | ('div[id*="m"]', ['main']), |
| | ('a[id*="m"]', ['me']), |
| | |
| | ('[href*=".css"]', ['l1']), |
| | ('link[href*=".css"]', ['l1']), |
| | ('link[id*="1"]', ['l1']), |
| | ('[id*="1"]', ['data1', 'l1', 'p1', 'header1', 's1a1', 's1a2', 's2a1', 's1a2s1', 'dash1']), |
| | ('div[id*="1"]', ['data1']), |
| | ('[id*="noending"]', []), |
| | |
| | ('[href*="."]', ['bob', 'me', 'l1']), |
| | ('a[href*="."]', ['bob', 'me']), |
| | ('link[href*="."]', ['l1']), |
| | ('div[id*="n"]', ['main', 'inner']), |
| | ('div[id*="nn"]', ['inner']), |
| | ('div[data-tag*="edval"]', ['data1']) |
| | ) |
| |
|
| | def test_attribute_exact_or_hypen(self): |
| | self.assert_select_multiple( |
| | ('p[lang|="en"]', ['lang-en', 'lang-en-gb', 'lang-en-us']), |
| | ('[lang|="en"]', ['lang-en', 'lang-en-gb', 'lang-en-us']), |
| | ('p[lang|="fr"]', ['lang-fr']), |
| | ('p[lang|="gb"]', []), |
| | ) |
| |
|
| | def test_attribute_exists(self): |
| | self.assert_select_multiple( |
| | ('[rel]', ['l1', 'bob', 'me']), |
| | ('link[rel]', ['l1']), |
| | ('a[rel]', ['bob', 'me']), |
| | ('[lang]', ['lang-en', 'lang-en-gb', 'lang-en-us', 'lang-fr']), |
| | ('p[class]', ['p1', 'pmulti']), |
| | ('[blah]', []), |
| | ('p[blah]', []), |
| | ('div[data-tag]', ['data1']) |
| | ) |
| |
|
| | def test_quoted_space_in_selector_name(self): |
| | html = """<div style="display: wrong">nope</div> |
| | <div style="display: right">yes</div> |
| | """ |
| | soup = BeautifulSoup(html, 'html.parser') |
| | [chosen] = soup.select('div[style="display: right"]') |
| | assert "yes" == chosen.string |
| |
|
| | def test_unsupported_pseudoclass(self): |
| | with pytest.raises(NotImplementedError): |
| | self.soup.select("a:no-such-pseudoclass") |
| |
|
| | with pytest.raises(SelectorSyntaxError): |
| | self.soup.select("a:nth-of-type(a)") |
| |
|
| | def test_nth_of_type(self): |
| | |
| | els = self.soup.select('div#inner p:nth-of-type(1)') |
| | assert len(els) == 1 |
| | assert els[0].string == 'Some text' |
| |
|
| | |
| | els = self.soup.select('div#inner p:nth-of-type(3)') |
| | assert len(els) == 1 |
| | assert els[0].string == 'Another' |
| |
|
| | |
| | els = self.soup.select('div#inner p:nth-of-type(4)') |
| | assert len(els) == 0 |
| |
|
| | |
| | els = self.soup.select('div p:nth-of-type(0)') |
| | assert len(els) == 0 |
| |
|
| | def test_nth_of_type_direct_descendant(self): |
| | els = self.soup.select('div#inner > p:nth-of-type(1)') |
| | assert len(els) == 1 |
| | assert els[0].string == 'Some text' |
| |
|
| | def test_id_child_selector_nth_of_type(self): |
| | self.assert_selects('#inner > p:nth-of-type(2)', ['p1']) |
| |
|
| | def test_select_on_element(self): |
| | |
| | |
| | inner = self.soup.find("div", id="main") |
| | selected = inner.select("div") |
| | |
| | |
| | self.assert_selects_ids(selected, ['inner', 'data1']) |
| |
|
| | def test_overspecified_child_id(self): |
| | self.assert_selects(".fancy #inner", ['inner']) |
| | self.assert_selects(".normal #inner", []) |
| |
|
| | def test_adjacent_sibling_selector(self): |
| | self.assert_selects('#p1 + h2', ['header2']) |
| | self.assert_selects('#p1 + h2 + p', ['pmulti']) |
| | self.assert_selects('#p1 + #header2 + .class1', ['pmulti']) |
| | assert [] == self.soup.select('#p1 + p') |
| |
|
| | def test_general_sibling_selector(self): |
| | self.assert_selects('#p1 ~ h2', ['header2', 'header3']) |
| | self.assert_selects('#p1 ~ #header2', ['header2']) |
| | self.assert_selects('#p1 ~ h2 + a', ['me']) |
| | self.assert_selects('#p1 ~ h2 + [rel="me"]', ['me']) |
| | assert [] == self.soup.select('#inner ~ h2') |
| |
|
| | def test_dangling_combinator(self): |
| | with pytest.raises(SelectorSyntaxError): |
| | self.soup.select('h1 >') |
| |
|
| | def test_sibling_combinator_wont_select_same_tag_twice(self): |
| | self.assert_selects('p[lang] ~ p', ['lang-en-gb', 'lang-en-us', 'lang-fr']) |
| |
|
| | |
| | def test_multiple_select(self): |
| | self.assert_selects('x, y', ['xid', 'yid']) |
| |
|
| | def test_multiple_select_with_no_space(self): |
| | self.assert_selects('x,y', ['xid', 'yid']) |
| |
|
| | def test_multiple_select_with_more_space(self): |
| | self.assert_selects('x, y', ['xid', 'yid']) |
| |
|
| | def test_multiple_select_duplicated(self): |
| | self.assert_selects('x, x', ['xid']) |
| |
|
| | def test_multiple_select_sibling(self): |
| | self.assert_selects('x, y ~ p[lang=fr]', ['xid', 'lang-fr']) |
| |
|
| | def test_multiple_select_tag_and_direct_descendant(self): |
| | self.assert_selects('x, y > z', ['xid', 'zidb']) |
| |
|
| | def test_multiple_select_direct_descendant_and_tags(self): |
| | self.assert_selects('div > x, y, z', ['xid', 'yid', 'zida', 'zidb', 'zidab', 'zidac']) |
| |
|
| | def test_multiple_select_indirect_descendant(self): |
| | self.assert_selects('div x,y, z', ['xid', 'yid', 'zida', 'zidb', 'zidab', 'zidac']) |
| |
|
| | def test_invalid_multiple_select(self): |
| | with pytest.raises(SelectorSyntaxError): |
| | self.soup.select(',x, y') |
| | with pytest.raises(SelectorSyntaxError): |
| | self.soup.select('x,,y') |
| |
|
| | def test_multiple_select_attrs(self): |
| | self.assert_selects('p[lang=en], p[lang=en-gb]', ['lang-en', 'lang-en-gb']) |
| |
|
| | def test_multiple_select_ids(self): |
| | self.assert_selects('x, y > z[id=zida], z[id=zidab], z[id=zidb]', ['xid', 'zidb', 'zidab']) |
| |
|
| | def test_multiple_select_nested(self): |
| | self.assert_selects('body > div > x, y > z', ['xid', 'zidb']) |
| |
|
| | def test_select_duplicate_elements(self): |
| | |
| | |
| | markup = '<div class="c1"/><div class="c2"/><div class="c1"/>' |
| | soup = BeautifulSoup(markup, 'html.parser') |
| | selected = soup.select(".c1, .c2") |
| | assert 3 == len(selected) |
| |
|
| | |
| | |
| | |
| | for element in soup.find_all(class_=['c1', 'c2']): |
| | assert element in selected |
| |
|
| | def test_closest(self): |
| | inner = self.soup.find("div", id="inner") |
| | closest = inner.css.closest("div[id=main]") |
| | assert closest == self.soup.find("div", id="main") |
| |
|
| | def test_match(self): |
| | inner = self.soup.find("div", id="inner") |
| | main = self.soup.find("div", id="main") |
| | assert inner.css.match("div[id=main]") == False |
| | assert main.css.match("div[id=main]") == True |
| |
|
| | def test_iselect(self): |
| | gen = self.soup.css.iselect("h2") |
| | assert isinstance(gen, types.GeneratorType) |
| | [header2, header3] = gen |
| | assert header2['id'] == 'header2' |
| | assert header3['id'] == 'header3' |
| |
|
| | def test_filter(self): |
| | inner = self.soup.find("div", id="inner") |
| | results = inner.css.filter("h2") |
| | assert len(inner.css.filter("h2")) == 2 |
| |
|
| | results = inner.css.filter("h2[id=header3]") |
| | assert isinstance(results, ResultSet) |
| | [result] = results |
| | assert result['id'] == 'header3' |
| |
|
| | def test_escape(self): |
| | m = self.soup.css.escape |
| | assert m(".foo#bar") == '\\.foo\\#bar' |
| | assert m("()[]{}") == '\\(\\)\\[\\]\\{\\}' |
| | assert m(".foo") == self.soup.css.escape(".foo") |
| |
|