summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLeonard Richardson <leonardr@segfault.org>2023-02-03 21:19:24 -0500
committerLeonard Richardson <leonardr@segfault.org>2023-02-03 21:19:24 -0500
commit1f0f15f9b3620f75b30b700c4764a4f09eca8376 (patch)
tree9aef3ae2f4ad8c489a92b68b9a393b288751744e
parente41b7c97b0d85f2a81519c8b756d4b830c3234ca (diff)
Move the Soup Sieve proxy and its tests into separate files.
-rw-r--r--bs4/__init__.py1
-rw-r--r--bs4/css.py251
-rw-r--r--bs4/element.py233
-rw-r--r--bs4/tests/test_css.py468
-rw-r--r--bs4/tests/test_pageelement.py458
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."