summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--bs4/element.py117
-rw-r--r--bs4/tests/test_pageelement.py27
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."