diff options
author | Leonard Richardson <leonardr@segfault.org> | 2023-02-03 21:19:24 -0500 |
---|---|---|
committer | Leonard Richardson <leonardr@segfault.org> | 2023-02-03 21:19:24 -0500 |
commit | 1f0f15f9b3620f75b30b700c4764a4f09eca8376 (patch) | |
tree | 9aef3ae2f4ad8c489a92b68b9a393b288751744e | |
parent | e41b7c97b0d85f2a81519c8b756d4b830c3234ca (diff) |
Move the Soup Sieve proxy and its tests into separate files.
-rw-r--r-- | bs4/__init__.py | 1 | ||||
-rw-r--r-- | bs4/css.py | 251 | ||||
-rw-r--r-- | bs4/element.py | 233 | ||||
-rw-r--r-- | bs4/tests/test_css.py | 468 | ||||
-rw-r--r-- | bs4/tests/test_pageelement.py | 458 |
5 files changed, 723 insertions, 688 deletions
diff --git a/bs4/__init__.py b/bs4/__init__.py index db71cc7..a5128d2 100644 --- a/bs4/__init__.py +++ b/bs4/__init__.py @@ -43,6 +43,7 @@ from .dammit import UnicodeDammit from .element import ( CData, Comment, + CSS, DEFAULT_OUTPUT_ENCODING, Declaration, Doctype, diff --git a/bs4/css.py b/bs4/css.py new file mode 100644 index 0000000..5d60267 --- /dev/null +++ b/bs4/css.py @@ -0,0 +1,251 @@ +"""Integration code for CSS selectors using Soup Sieve (pypi: soupsieve).""" + +try: + import soupsieve +except ImportError as e: + soupsieve = None + warnings.warn( + 'The soupsieve package is not installed. CSS selectors cannot be used.' + ) + + +class CSS(object): + """A proxy object against the soupsieve library, to simplify its + CSS selector API. + + Acquire this object through the .css attribute on the + BeautifulSoup object, or on the Tag you want to use as the + starting point for a CSS selector. + + Specifically, the element to be selected against doesn't need to + be explicitly specified in the function call, since you access + this object through a specific tag. + + """ + + def __init__(self, tag): + """Constructor. + + You don't need to instantiate this class yourself; instead, + access the .css attribute on the BeautifulSoup object, or on + the Tag you want to use as the starting point for your CSS + selector. + + :param tag: All CSS selectors will use this as their starting + point. + + """ + if soupsieve is None: + raise NotImplementedError( + "Cannot execute CSS selectors because the soupsieve package is not installed." + ) + self.tag = tag + + @classmethod + def escape(cls, ident): + """Escape a CSS selector. + + This is a simple wrapper around soupselect.escape(). + """ + if soupsieve is None: + raise NotImplementedError( + "Cannot escape CSS selectors because the soupsieve package is not installed." + ) + return soupsieve.escape(ident) + + def _ns(self, ns): + """Normalize a dictionary of namespaces.""" + if ns is None: + ns = self.tag._namespaces + return ns + + def _rs(self, results): + """Normalize a list of results to a Resultset. + + A ResultSet is more consistent with the rest of Beautiful + Soup, and ResultSet.__getattr__ has a helpful error message if + you try to treat a list of results as a single result (a + common mistake). + """ + # Import here to avoid circular import + from bs4.element import ResultSet + return ResultSet(None, results) + + def select_one(self, select, namespaces=None, flags=0, **kwargs): + """Perform a CSS selection operation on the current Tag and return the + first result. + + This uses the Soup Sieve library. For more information, see + that library's documentation for the soupsieve.select_one() + method. + + :param selector: A CSS selector. + + :param namespaces: A dictionary mapping namespace prefixes + used in the CSS selector to namespace URIs. By default, + Beautiful Soup will use the prefixes it encountered while + parsing the document. + + :param flags: Flags to be passed into Soup Sieve's + soupsieve.select_one() method. + + :param kwargs: Keyword arguments to be passed into SoupSieve's + soupsieve.select_one() method. + + :return: A Tag, or None if the selector has no match. + :rtype: bs4.element.Tag + + """ + return soupsieve.select_one( + select, self.tag, self._ns(namespaces), flags, **kwargs + ) + + def select(self, select, namespaces=None, limit=0, flags=0, **kwargs): + """Perform a CSS selection operation on the current Tag. + + This uses the Soup Sieve library. For more information, see + that library's documentation for the soupsieve.select() + method. + + :param selector: A string containing a CSS selector. + + :param namespaces: A dictionary mapping namespace prefixes + used in the CSS selector to namespace URIs. By default, + Beautiful Soup will pass in the prefixes it encountered while + parsing the document. + + :param limit: After finding this number of results, stop looking. + + :param flags: Flags to be passed into Soup Sieve's + soupsieve.select() method. + + :param kwargs: Keyword arguments to be passed into SoupSieve's + soupsieve.select() method. + + :return: A ResultSet of Tag objects. + :rtype: bs4.element.ResultSet + + """ + if limit is None: + limit = 0 + + return self._rs( + soupsieve.select( + select, self.tag, self._ns(namespaces), limit, flags, + **kwargs + ) + ) + + def iselect(self, select, namespaces=None, limit=0, flags=0, **kwargs): + """Perform a CSS selection operation on the current Tag. + + This uses the Soup Sieve library. For more information, see + that library's documentation for the soupsieve.iselect() + method. It is the same as select(), but it returns a generator + instead of a list. + + :param selector: A string containing a CSS selector. + + :param namespaces: A dictionary mapping namespace prefixes + used in the CSS selector to namespace URIs. By default, + Beautiful Soup will pass in the prefixes it encountered while + parsing the document. + + :param limit: After finding this number of results, stop looking. + + :param flags: Flags to be passed into Soup Sieve's + soupsieve.iselect() method. + + :param kwargs: Keyword arguments to be passed into SoupSieve's + soupsieve.iselect() method. + + :return: A generator + :rtype: types.GeneratorType + """ + return soupsieve.iselect( + select, self.tag, self._ns(namespaces), limit, flags, **kwargs + ) + + def closest(self, select, namespaces=None, flags=0, **kwargs): + """Find the Tag closest to this one that matches the given selector. + + This uses the Soup Sieve library. For more information, see + that library's documentation for the soupsieve.closest() + method. + + :param selector: A string containing a CSS selector. + + :param namespaces: A dictionary mapping namespace prefixes + used in the CSS selector to namespace URIs. By default, + Beautiful Soup will pass in the prefixes it encountered while + parsing the document. + + :param flags: Flags to be passed into Soup Sieve's + soupsieve.closest() method. + + :param kwargs: Keyword arguments to be passed into SoupSieve's + soupsieve.closest() method. + + :return: A Tag, or None if there is no match. + :rtype: bs4.Tag + + """ + return soupsieve.closest( + select, self.tag, self._ns(namespaces), flags, **kwargs + ) + + def match(self, select, namespaces=None, flags=0, **kwargs): + """Check whether this Tag matches the given CSS selector. + + This uses the Soup Sieve library. For more information, see + that library's documentation for the soupsieve.match() + method. + + :param: a CSS selector. + + :param namespaces: A dictionary mapping namespace prefixes + used in the CSS selector to namespace URIs. By default, + Beautiful Soup will pass in the prefixes it encountered while + parsing the document. + + :param flags: Flags to be passed into Soup Sieve's + soupsieve.match() method. + + :param kwargs: Keyword arguments to be passed into SoupSieve's + soupsieve.match() method. + + :return: True if this Tag matches the selector; False otherwise. + :rtype: bool + """ + return soupsieve.match( + select, self.tag, self._ns(namespaces), flags, **kwargs + ) + + def filter(self, select, namespaces=None, flags=0, **kwargs): + """Filter this Tag's direct children based on the given CSS selector. + + This uses the Soup Sieve library. It works the same way as + passing this Tag into that library's soupsieve.filter() + method. More information, for more information see the + documentation for soupsieve.filter(). + + :param namespaces: A dictionary mapping namespace prefixes + used in the CSS selector to namespace URIs. By default, + Beautiful Soup will pass in the prefixes it encountered while + parsing the document. + + :param flags: Flags to be passed into Soup Sieve's + soupsieve.filter() method. + + :param kwargs: Keyword arguments to be passed into SoupSieve's + soupsieve.filter() method. + + :return: A ResultSet of Tag objects. + :rtype: bs4.element.ResultSet + + """ + return self._rs( + soupsieve.filter( + select, self.tag, self._ns(namespaces), flags, **kwargs + ) + ) diff --git a/bs4/element.py b/bs4/element.py index a96593c..619fb73 100644 --- a/bs4/element.py +++ b/bs4/element.py @@ -8,14 +8,8 @@ except ImportError as e: import re import sys import warnings -try: - import soupsieve -except ImportError as e: - soupsieve = None - warnings.warn( - 'The soupsieve package is not installed. CSS selectors cannot be used.' - ) +from bs4.css import CSS from bs4.formatter import ( Formatter, HTMLFormatter, @@ -1948,7 +1942,7 @@ class Tag(PageElement): Beautiful Soup will use the prefixes it encountered while parsing the document. - :param kwargs: Keyword arguments to be passed into SoupSieve's + :param kwargs: Keyword arguments to be passed into Soup Sieve's soupsieve.select() method. :return: A Tag. @@ -1981,7 +1975,7 @@ class Tag(PageElement): @property def css(self): """Return an interface to the CSS selector API.""" - return SoupSieveProxy(self) + return CSS(self) # Old names for backwards compatibility def childGenerator(self): @@ -2296,224 +2290,3 @@ class ResultSet(list): raise AttributeError( "ResultSet object has no attribute '%s'. You're probably treating a list of elements like a single element. Did you call find_all() when you meant to call find()?" % key ) - -class SoupSieveProxy(object): - """A proxy object against the soupsieve library, to simplify its - CSS selector API. - - Specifically, the element to be selected against doesn't need to - be explicitly specified in the function call, since you access - this object through a specific tag. - """ - - def __init__(self, tag): - """Constructor. - - You don't need to instantiate this class yourself; instead, - access the .css attribute on the Tag you want to use as the - starting point for your CSS selector, or on the BeautifulSoup - object itself. - - :param tag: All CSS selectors will use this as their starting - point. - """ - if soupsieve is None: - raise NotImplementedError( - "Cannot execute CSS selectors because the soupsieve package is not installed." - ) - self.tag = tag - - def _ns(self, ns): - """Normalize a dictionary of namespaces.""" - if ns is None: - ns = self.tag._namespaces - return ns - - def _rs(self, results): - """Normalize a list of results to a Resultset. - - A ResultSet is more consistent with the rest of Beautiful - Soup, and ResultSet.__getattr__ has a helpful error message if - you try to treat a list of results as a single result (a - common mistake). - """ - return ResultSet(None, results) - - def select_one(self, select, namespaces=None, flags=0, **kwargs): - """Perform a CSS selection operation on the current Tag and return the - first result. - - This uses the Soup Sieve library. For more information, see - that library's documentation for the soupsieve.select_one() - method. - - :param selector: A CSS selector. - - :param namespaces: A dictionary mapping namespace prefixes - used in the CSS selector to namespace URIs. By default, - Beautiful Soup will use the prefixes it encountered while - parsing the document. - - :param flags: Flags to be passed into Soup Sieve's - soupsieve.select_one() method. - - :param kwargs: Keyword arguments to be passed into SoupSieve's - soupsieve.select_one() method. - - :return: A Tag, or None if the selector has no match. - :rtype: bs4.element.Tag - - """ - return soupsieve.select_one( - select, self.tag, self._ns(namespaces), flags, **kwargs - ) - - def select(self, select, namespaces=None, limit=0, flags=0, **kwargs): - """Perform a CSS selection operation on the current Tag. - - This uses the Soup Sieve library. For more information, see - that library's documentation for the soupsieve.select() - method. - - :param selector: A string containing a CSS selector. - - :param namespaces: A dictionary mapping namespace prefixes - used in the CSS selector to namespace URIs. By default, - Beautiful Soup will pass in the prefixes it encountered while - parsing the document. - - :param limit: After finding this number of results, stop looking. - - :param flags: Flags to be passed into Soup Sieve's - soupsieve.select() method. - - :param kwargs: Keyword arguments to be passed into SoupSieve's - soupsieve.select() method. - - :return: A ResultSet of Tag objects. - :rtype: bs4.element.ResultSet - - """ - if limit is None: - limit = 0 - - return self._rs( - soupsieve.select( - select, self.tag, self._ns(namespaces), limit, flags, - **kwargs - ) - ) - - def iselect(self, select, namespaces=None, limit=0, flags=0, **kwargs): - """Perform a CSS selection operation on the current Tag. - - This uses the Soup Sieve library. For more information, see - that library's documentation for the soupsieve.iselect() - method. It is the same as select(), but it returns a generator - instead of a list. - - :param selector: A string containing a CSS selector. - - :param namespaces: A dictionary mapping namespace prefixes - used in the CSS selector to namespace URIs. By default, - Beautiful Soup will pass in the prefixes it encountered while - parsing the document. - - :param limit: After finding this number of results, stop looking. - - :param flags: Flags to be passed into Soup Sieve's - soupsieve.iselect() method. - - :param kwargs: Keyword arguments to be passed into SoupSieve's - soupsieve.iselect() method. - - :return: A generator - :rtype: types.GeneratorType - """ - return soupsieve.iselect( - select, self.tag, self._ns(namespaces), limit, flags, **kwargs - ) - - def closest(self, select, namespaces=None, flags=0, **kwargs): - """Find the Tag closest to this one that matches the given selector. - - This uses the Soup Sieve library. For more information, see - that library's documentation for the soupsieve.closest() - method. - - :param selector: A string containing a CSS selector. - - :param namespaces: A dictionary mapping namespace prefixes - used in the CSS selector to namespace URIs. By default, - Beautiful Soup will pass in the prefixes it encountered while - parsing the document. - - :param flags: Flags to be passed into Soup Sieve's - soupsieve.closest() method. - - :param kwargs: Keyword arguments to be passed into SoupSieve's - soupsieve.closest() method. - - :return: A Tag, or None if there is no match. - :rtype: bs4.Tag - - """ - return soupsieve.closest( - select, self.tag, self._ns(namespaces), flags, **kwargs - ) - - def match(self, select, namespaces=None, flags=0, **kwargs): - """Check whether this Tag matches the given CSS selector. - - This uses the Soup Sieve library. For more information, see - that library's documentation for the soupsieve.match() - method. - - :param: a CSS selector. - - :param namespaces: A dictionary mapping namespace prefixes - used in the CSS selector to namespace URIs. By default, - Beautiful Soup will pass in the prefixes it encountered while - parsing the document. - - :param flags: Flags to be passed into Soup Sieve's - soupsieve.match() method. - - :param kwargs: Keyword arguments to be passed into SoupSieve's - soupsieve.match() method. - - :return: True if this Tag matches the selector; False otherwise. - :rtype: bool - """ - return soupsieve.match( - select, self.tag, self._ns(namespaces), flags, **kwargs - ) - - def filter(self, select, namespaces=None, flags=0, **kwargs): - """Filter this Tag's direct children based on the given CSS selector. - - This uses the Soup Sieve library. It works the same way as - passing this Tag into that library's soupsieve.filter() - method. More information, for more information see the - documentation for soupsieve.filter(). - - :param namespaces: A dictionary mapping namespace prefixes - used in the CSS selector to namespace URIs. By default, - Beautiful Soup will pass in the prefixes it encountered while - parsing the document. - - :param flags: Flags to be passed into Soup Sieve's - soupsieve.filter() method. - - :param kwargs: Keyword arguments to be passed into SoupSieve's - soupsieve.filter() method. - - :return: A ResultSet of Tag objects. - :rtype: bs4.element.ResultSet - - """ - return self._rs( - soupsieve.filter( - select, self.tag, self._ns(namespaces), flags, **kwargs - ) - ) diff --git a/bs4/tests/test_css.py b/bs4/tests/test_css.py new file mode 100644 index 0000000..3da9aed --- /dev/null +++ b/bs4/tests/test_css.py @@ -0,0 +1,468 @@ +import pytest +import types + +from bs4 import ( + 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_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( + # From test_attribute_startswith + ('[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']), + # From test_attribute_endswith + ('[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"]', []), + # New for this test + ('[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): + # Try to select first paragraph + els = self.soup.select('div#inner p:nth-of-type(1)') + assert len(els) == 1 + assert els[0].string == 'Some text' + + # Try to select third paragraph + els = self.soup.select('div#inner p:nth-of-type(3)') + assert len(els) == 1 + assert els[0].string == 'Another' + + # Try to select (non-existent!) fourth paragraph + els = self.soup.select('div#inner p:nth-of-type(4)') + assert len(els) == 0 + + # Zero will select no tags. + 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): + # Other tests operate on the tree; this operates on an element + # within the tree. + inner = self.soup.find("div", id="main") + selected = inner.select("div") + # The <div id="inner"> tag was selected. The <div id="footer"> + # tag was not. + 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']) + + # Test the selector grouping operator (the comma) + 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): + # When markup contains duplicate elements, a multiple select + # will find all of them. + markup = '<div class="c1"/><div class="c2"/><div class="c1"/>' + soup = BeautifulSoup(markup, 'html.parser') + selected = soup.select(".c1, .c2") + assert 3 == len(selected) + + # Verify that find_all finds the same elements, though because + # of an implementation detail it finds them in a different + # order. + 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' diff --git a/bs4/tests/test_pageelement.py b/bs4/tests/test_pageelement.py index 6ce2573..a94280f 100644 --- a/bs4/tests/test_pageelement.py +++ b/bs4/tests/test_pageelement.py @@ -2,7 +2,6 @@ import copy import pickle import pytest -import types from bs4 import BeautifulSoup from bs4.element import ( @@ -12,12 +11,8 @@ from bs4.element import ( ) from . import ( SoupTest, - SOUP_SIEVE_PRESENT, ) -if SOUP_SIEVE_PRESENT: - from soupsieve import SelectorSyntaxError - class TestEncoding(SoupTest): """Test the ability to encode objects into strings.""" @@ -218,459 +213,6 @@ class TestFormatters(SoupTest): assert soup.contents[0].name == 'pre' -@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_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( - # From test_attribute_startswith - ('[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']), - # From test_attribute_endswith - ('[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"]', []), - # New for this test - ('[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): - # Try to select first paragraph - els = self.soup.select('div#inner p:nth-of-type(1)') - assert len(els) == 1 - assert els[0].string == 'Some text' - - # Try to select third paragraph - els = self.soup.select('div#inner p:nth-of-type(3)') - assert len(els) == 1 - assert els[0].string == 'Another' - - # Try to select (non-existent!) fourth paragraph - els = self.soup.select('div#inner p:nth-of-type(4)') - assert len(els) == 0 - - # Zero will select no tags. - 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): - # Other tests operate on the tree; this operates on an element - # within the tree. - inner = self.soup.find("div", id="main") - selected = inner.select("div") - # The <div id="inner"> tag was selected. The <div id="footer"> - # tag was not. - 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']) - - # Test the selector grouping operator (the comma) - 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): - # When markup contains duplicate elements, a multiple select - # will find all of them. - markup = '<div class="c1"/><div class="c2"/><div class="c1"/>' - soup = BeautifulSoup(markup, 'html.parser') - selected = soup.select(".c1, .c2") - assert 3 == len(selected) - - # Verify that find_all finds the same elements, though because - # of an implementation detail it finds them in a different - # order. - 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' - - class TestPersistence(SoupTest): "Testing features like pickle and deepcopy." |