diff options
author | Leonard Richardson <leonardr@segfault.org> | 2023-02-02 09:29:17 -0500 |
---|---|---|
committer | Leonard Richardson <leonardr@segfault.org> | 2023-02-02 09:29:17 -0500 |
commit | 7713145153f143ebe02657232e36b58ca29bef38 (patch) | |
tree | 8df7dbfd4162ea5b75206a74e56b97ce91141861 | |
parent | 1896698334a1b0a281f1c646a5a2017d25a68c9f (diff) |
Test implementation.
-rw-r--r-- | bs4/element.py | 117 | ||||
-rw-r--r-- | bs4/tests/test_pageelement.py | 27 |
2 files changed, 126 insertions, 18 deletions
diff --git a/bs4/element.py b/bs4/element.py index 583d0e8..bc8320d 100644 --- a/bs4/element.py +++ b/bs4/element.py @@ -1954,10 +1954,7 @@ class Tag(PageElement): :return: A Tag. :rtype: bs4.element.Tag """ - value = self.select(selector, namespaces, 1, **kwargs) - if value: - return value[0] - return None + return self.css.select_one(selector, namespaces, **kwargs) def select(self, selector, namespaces=None, limit=None, **kwargs): """Perform a CSS selection operation on the current element. @@ -1979,21 +1976,12 @@ class Tag(PageElement): :return: A ResultSet of Tags. :rtype: bs4.element.ResultSet """ - if namespaces is None: - namespaces = self._namespaces - - if limit is None: - limit = 0 - if soupsieve is None: - raise NotImplementedError( - "Cannot execute CSS selectors because the soupsieve package is not installed." - ) - - results = soupsieve.select(selector, self, namespaces, limit, **kwargs) + return self.css.select(selector, namespaces, limit, **kwargs) - # We do this because it's more consistent and because - # ResultSet.__getattr__ has a helpful error message. - return ResultSet(None, results) + @property + def css(self): + """Return an interface to the CSS selector API.""" + return SoupSieveProxy(self) # Old names for backwards compatibility def childGenerator(self): @@ -2308,3 +2296,96 @@ 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): + + def __init__(self, element): + self.element = element + + def _ns(self, ns): + """Normalize a dictionary of namespaces.""" + + if soupsieve is None: + raise NotImplementedError( + "Cannot execute CSS selectors because the soupsieve package is not installed." + ) + + if ns is None: + ns = self.element._namespaces + return ns + + def _rs(self, results): + """Normalize a return value to a Resultset. + + We do this because it's more consistent and because + ResultSet.__getattr__ has a helpful error message. + """ + return ResultSet(None, results) + + def select_one(self, select, namespaces=None, flags=0, **kwargs): + """Perform a CSS selection operation on the current element. + + :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 kwargs: Keyword arguments to be passed into SoupSieve's + soupsieve.select() method. + + :return: A Tag. + :rtype: bs4.element.Tag + """ + return soupsieve.select_one( + select, self.element, self._ns(namespaces), flags, **kwargs + ) + + def select(self, select, namespaces=None, limit=0, flags=0, **kwargs): + """Perform a CSS selection operation on the current element. + + This uses the SoupSieve library. + + :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 use the prefixes it encountered while + parsing the document. + + :param limit: After finding this number of results, stop looking. + + :param kwargs: Keyword arguments to be passed into SoupSieve's + soupsieve.select() method. + + :return: A ResultSet of Tags. + :rtype: bs4.element.ResultSet + """ + if limit is None: + limit = 0 + + results = soupsieve.select( + select, self.element, self._ns(namespaces), limit, flags, **kwargs + ) + return self._rs(results) + + def iselect(self, select, namespaces=None, flags=0, **kwargs): + return soupsieve.iselect( + select, self.element, self._ns(namespaces), flags, **kwargs + ) + + def closest(self, select, namespaces=None, flags=0, **kwargs): + return soupsieve.closest( + select, self.element, self._ns(namespaces), flags, **kwargs + ) + + def match(self, select, namespaces=None, flags=0, **kwargs): + return soupsieve.match( + select, self.element, self._ns(namespaces), flags, **kwargs + ) + + def filter(self, select, namespaces=None, flags=0, **kwargs): + return soupsieve.filter( + select, self.element, self._ns(namespaces), flags, **kwargs + ) diff --git a/bs4/tests/test_pageelement.py b/bs4/tests/test_pageelement.py index 6674dad..09b0dea 100644 --- a/bs4/tests/test_pageelement.py +++ b/bs4/tests/test_pageelement.py @@ -2,6 +2,7 @@ import copy import pickle import pytest +import types from bs4 import BeautifulSoup from bs4.element import ( @@ -638,6 +639,32 @@ class TestCSSSelectors(SoupTest): 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 + + [result] = inner.css.filter("h2[id=header3]") + assert result['id'] == 'header3' + class TestPersistence(SoupTest): "Testing features like pickle and deepcopy." |