diff options
-rw-r--r-- | bs4/css.py | 49 | ||||
-rw-r--r-- | bs4/tests/test_css.py | 25 |
2 files changed, 56 insertions, 18 deletions
@@ -8,7 +8,7 @@ except ImportError as e: '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. @@ -17,13 +17,12 @@ class CSS(object): 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. - + The main advantage of doing this is that the tag to be selected + against doesn't need to be explicitly specified in the function + calls, since it's already scoped to a tag. """ - def __init__(self, tag): + def __init__(self, tag, api=soupsieve): """Constructor. You don't need to instantiate this class yourself; instead, @@ -35,14 +34,14 @@ class CSS(object): point. """ - if soupsieve is None: + if api is None: raise NotImplementedError( "Cannot execute CSS selectors because the soupsieve package is not installed." ) + self.api = api self.tag = tag - @classmethod - def escape(cls, ident): + def escape(self, ident): """Escape a CSS identifier. This is a simple wrapper around soupselect.escape(). See the @@ -52,8 +51,8 @@ class CSS(object): raise NotImplementedError( "Cannot escape CSS identifiers because the soupsieve package is not installed." ) - return soupsieve.escape(ident) - + return self.api.escape(ident) + def _ns(self, ns): """Normalize a dictionary of namespaces.""" if ns is None: @@ -97,7 +96,7 @@ class CSS(object): :rtype: bs4.element.Tag """ - return soupsieve.select_one( + return self.api.select_one( select, self.tag, self._ns(namespaces), flags, **kwargs ) @@ -131,7 +130,7 @@ class CSS(object): limit = 0 return self._rs( - soupsieve.select( + self.api.select( select, self.tag, self._ns(namespaces), limit, flags, **kwargs ) @@ -163,7 +162,7 @@ class CSS(object): :return: A generator :rtype: types.GeneratorType """ - return soupsieve.iselect( + return self.api.iselect( select, self.tag, self._ns(namespaces), limit, flags, **kwargs ) @@ -191,7 +190,7 @@ class CSS(object): :rtype: bs4.Tag """ - return soupsieve.closest( + return self.api.closest( select, self.tag, self._ns(namespaces), flags, **kwargs ) @@ -218,7 +217,7 @@ class CSS(object): :return: True if this Tag matches the selector; False otherwise. :rtype: bool """ - return soupsieve.match( + return self.api.match( select, self.tag, self._ns(namespaces), flags, **kwargs ) @@ -246,7 +245,23 @@ class CSS(object): """ return self._rs( - soupsieve.filter( + self.api.filter( select, self.tag, self._ns(namespaces), flags, **kwargs ) ) + + def __getattr__(self, __name): + """Catch-all method that has a chance of giving access to future + methods to be added to Soup Sieve without needing a Beautiful Soup + API change. + + Basically, if you call tag.css.somemethod(selector), this code will + turn that into soupsieve.somemethod(selector, tag). + """ + attr = getattr(self.api, __name) + if callable(attr): + return ( + lambda pattern, *args, __tag=self.tag, __attr=attr, **kwargs: + attr(pattern, __tag, *args, **kwargs) + ) + return attr diff --git a/bs4/tests/test_css.py b/bs4/tests/test_css.py index 51662ed..a6c17de 100644 --- a/bs4/tests/test_css.py +++ b/bs4/tests/test_css.py @@ -1,5 +1,6 @@ import pytest import types +from unittest.mock import MagicMock from bs4 import ( CSS, @@ -469,7 +470,29 @@ class TestCSSSelectors(SoupTest): assert result['id'] == 'header3' def test_escape(self): - m = CSS.escape + m = self.soup.css.escape assert m(".foo#bar") == '\\.foo\\#bar' assert m("()[]{}") == '\\(\\)\\[\\]\\{\\}' assert m(".foo") == self.soup.css.escape(".foo") + + def test_fallback(self): + class Mock(): + attribute = "value" + pass + mock_soupsieve = Mock() + mock_soupsieve.some_other_method = MagicMock() + + # If an unknown method turns out to be present in Soup Sieve, + # we may still be able to call it. + css = CSS(self.soup, api=mock_soupsieve) + css.some_other_method("selector", 1, flags=0) + mock_soupsieve.some_other_method.assert_called_with( + "selector", self.soup, 1, flags=0 + ) + + # If the attribute is not callable, getattr is a passthrough. + assert mock_soupsieve.attribute == "value" + + # If the method just isn't there, too bad. + with pytest.raises(AttributeError): + mock_soupsieve.no_such_method() |