Repository: facelessuser/soupsieve (python)

Issue:
Names of iter functions
Right now we have two iter functions `commentsiter` and `selectiter`. For 1.0, should they be called `comments_iter` and `select_iter`.  Or maybe `icomments` and `iselect`?

We have some flexibility before 1.0 release, so we should really settle on something. `commentsiter` and `selectiter` just seem hard to read.

Pull request description:
Deprecate commentsiter and selectiter
Use iselect and icomments instead as they are shorter and easier to read. Closes #3 .

Patch:
diff --git a/.codecov.yml b/.codecov.yml
new file mode 100644
index 0000000..caaaa71
--- /dev/null
+++ b/.codecov.yml
@@ -0,0 +1,5 @@
+comment: false
+
+coverage:
+  status:
+    patch: false
diff --git a/docs/src/dictionary/en-custom.txt b/docs/src/dictionary/en-custom.txt
index 700f9bd..1c3f456 100644
--- a/docs/src/dictionary/en-custom.txt
+++ b/docs/src/dictionary/en-custom.txt
@@ -19,6 +19,7 @@ WIP
 XHTML
 accessor
 builtin
+deprecations
 html
 iterable
 linter
diff --git a/docs/src/markdown/about/changelog.md b/docs/src/markdown/about/changelog.md
index c403f00..08db752 100644
--- a/docs/src/markdown/about/changelog.md
+++ b/docs/src/markdown/about/changelog.md
@@ -1,5 +1,9 @@
 # Changelog
 
+## 0.5.0
+
+- **NEW**: Deprecate `commentsiter` and `selectiter` in favor of `icomments` and `iselect`. Expect removal in version 1.0.
+
 ## 0.4.0
 
 - **NEW**: Initial prerelease.
diff --git a/docs/src/markdown/api.md b/docs/src/markdown/api.md
index 39cb02e..c2e7f49 100644
--- a/docs/src/markdown/api.md
+++ b/docs/src/markdown/api.md
@@ -37,14 +37,14 @@ def select(select, node, namespaces=None, limit=0, mode=HTML5):
 [<p class="a">Cat</p>, <p class="b">Dog</p>, <p class="c">Mouse</p>]
 ```
 
-### `soupsieve.selectiter()`
+### `soupsieve.iselect()`
 
 ```py3
-def selectiter(select, node, namespaces=None, limit=0, mode=HTML5):
+def iselect(select, node, namespaces=None, limit=0, mode=HTML5):
     """Select the specified tags."""
 ```
 
-`selectiter` is exactly like `select` except that it returns a generator instead of a list.
+`iselect` is exactly like `select` except that it returns a generator instead of a list.
 
 ### `soupsieve.match()`
 
@@ -92,14 +92,14 @@ def comments(node, limit=0, mode=HTML5):
 
 `comments` accepts a `node` or element, a `limit`, and a document mode.
 
-### `soupsieve.commentsiter()`
+### `soupsieve.icomments()`
 
 ```
-def comments(node, limit=0, mode=HTML5):
+def icomments(node, limit=0, mode=HTML5):
     """Get comments only."""
 ```
 
-`commentsiter` is exactly like `comments` except that it returns a generator instead of a list.
+`icomments` is exactly like `comments` except that it returns a generator instead of a list.
 
 ### `soupsieve.compile()`
 
diff --git a/soupsieve/__init__.py b/soupsieve/__init__.py
index 3e5e123..64e1a79 100644
--- a/soupsieve/__init__.py
+++ b/soupsieve/__init__.py
@@ -27,11 +27,12 @@ SOFTWARE.
 """
 from .__meta__ import __version__, __version_info__  # noqa: F401
 from . import css_parser as cp
-from .util import HTML, HTML5, XHTML, XML
+from .util import HTML, HTML5, XHTML, XML, deprecated
 
 __all__ = (
     'HTML', 'HTML5', 'XHTML', 'XML',
-    'SoupSieve', 'compile', 'purge', 'comments', 'select', 'match', 'filter'
+    'SoupSieve', 'compile', 'purge',
+    'comments', 'icomments', 'select', 'iselect', 'match', 'filter'
 )
 
 SoupSieve = cp.SoupSieve
@@ -79,10 +80,10 @@ def comments(node, limit=0, mode=HTML5):
     return compile("", None, mode).comments(node, limit)
 
 
-def commentsiter(node, limit=0, mode=HTML5):
+def icomments(node, limit=0, mode=HTML5):
     """Iterate comments only."""
 
-    yield from compile("", None, mode).commentsiter(node, limit)
+    yield from compile("", None, mode).icomments(node, limit)
 
 
 def select(select, node, namespaces=None, limit=0, mode=HTML5):
@@ -91,7 +92,22 @@ def select(select, node, namespaces=None, limit=0, mode=HTML5):
     return compile(select, namespaces, mode).select(node, limit)
 
 
+def iselect(select, node, namespaces=None, limit=0, mode=HTML5):
+    """Iterate the specified tags."""
+
+    yield from compile(select, namespaces, mode).iselect(node, limit)
+
+
+# ====== Deprecated ======
+@deprecated("Use 'icomments' instead.")
+def commentsiter(node, limit=0, mode=HTML5):
+    """Iterate comments only."""
+
+    yield from icomments(node, limit, mode)
+
+
+@deprecated("Use 'iselect' instead.")
 def selectiter(select, node, namespaces=None, limit=0, mode=HTML5):
     """Iterate the specified tags."""
 
-    yield from compile(select, namespaces, mode).selectiter(node, limit)
+    yield from iselect(select, node, namespaces, limit, mode)
diff --git a/soupsieve/__meta__.py b/soupsieve/__meta__.py
index eb96ea7..ea09e68 100644
--- a/soupsieve/__meta__.py
+++ b/soupsieve/__meta__.py
@@ -186,5 +186,5 @@ def parse_version(ver, pre=False):
     return Version(major, minor, micro, release, pre, post, dev)
 
 
-__version_info__ = Version(0, 4, 0, "final")
+__version_info__ = Version(0, 5, 0, "final")
 __version__ = __version_info__._get_canonical()
diff --git a/soupsieve/css_parser.py b/soupsieve/css_parser.py
index 18d5cf3..59c4e44 100644
--- a/soupsieve/css_parser.py
+++ b/soupsieve/css_parser.py
@@ -5,6 +5,7 @@ from collections import namedtuple
 from functools import lru_cache
 from . import util
 from . import css_match as cm
+from .util import deprecated
 
 
 # Selector patterns
@@ -590,25 +591,25 @@ class SoupSieve(util.Immutable):
         else:
             return [node for node in nodes if self.match(node)]
 
-    def commentsiter(self, node, limit=0):
-        """Iterate comments only."""
-
-        yield from self._sieve(node, capture=False, comments=True, limit=limit)
-
     def comments(self, node, limit=0):
         """Get comments only."""
 
-        return list(self.commentsiter(node, limit))
+        return list(self.icomments(node, limit))
 
-    def selectiter(self, node, limit=0):
-        """Iterate the specified tags."""
+    def icomments(self, node, limit=0):
+        """Iterate comments only."""
 
-        yield from self._sieve(node, limit=limit)
+        yield from self._sieve(node, capture=False, comments=True, limit=limit)
 
     def select(self, node, limit=0):
         """Select the specified tags."""
 
-        return list(self.selectiter(node, limit))
+        return list(self.iselect(node, limit))
+
+    def iselect(self, node, limit=0):
+        """Iterate the specified tags."""
+
+        yield from self._sieve(node, limit=limit)
 
     def __repr__(self):
         """Representation."""
@@ -617,6 +618,19 @@ class SoupSieve(util.Immutable):
 
     __str__ = __repr__
 
+    # ====== Deprecated ======
+    @deprecated("Use 'SoupSieve.icomments' instead.")
+    def commentsiter(self, node, limit=0):
+        """Iterate comments only."""
+
+        yield from self.icomments(node, limit)
+
+    @deprecated("Use 'SoupSieve.iselect' instead.")
+    def selectiter(self, node, limit=0):
+        """Iterate the specified tags."""
+
+        yield from self.iselect(node, limit)
+
 
 def _pickle(p):
     return SoupSieve, (p.pattern, p.namespaces, p.mode)
diff --git a/soupsieve/util.py b/soupsieve/util.py
index 585c417..70bf9e7 100644
--- a/soupsieve/util.py
+++ b/soupsieve/util.py
@@ -2,6 +2,8 @@
 from collections import Mapping
 from collections.abc import Hashable
 import bs4
+from functools import wraps
+import warnings
 
 HTML5 = 0x1
 HTML = 0x2
@@ -126,3 +128,23 @@ class ImmutableDict(Mapping):
         return "%r" % self._d
 
     __str__ = __repr__
+
+
+def deprecated(message, stacklevel=2):
+    """
+    Raise a `DeprecationWarning` when wrapped function/method is called.
+
+    Borrowed from https://stackoverflow.com/a/48632082/866026
+    """
+
+    def _decorator(func):
+        @wraps(func)
+        def _func(*args, **kwargs):
+            warnings.warn(
+                "'{}' is deprecated. {}".format(func.__name__, message),
+                category=DeprecationWarning,
+                stacklevel=stacklevel
+            )
+            return func(*args, **kwargs)
+        return _func
+    return _decorator


Test patch:
diff --git a/tests/test_level1.py b/tests/test_level1.py
index 99f98f9..6979749 100644
--- a/tests/test_level1.py
+++ b/tests/test_level1.py
@@ -119,3 +119,23 @@ class TestLevel1(util.TestCase):
             ["2"],
             mode=sv.HTML5
         )
+
+    def test_classes(self):
+        """Test classes."""
+
+        markup = """
+        <div>
+        <p>Some text <span id="1" class="foo"> in a paragraph</span>.
+        <a id="2" class="bar" href="http://google.com">Link</a>
+        <a id="3" class="foo" href="http://google.com">Link</a>
+        <a id="4" class="foo bar" href="http://google.com">Link</a>
+        </p>
+        </div>
+        """
+
+        self.assert_selector(
+            markup,
+            "a.foo.bar",
+            ["4"],
+            mode=sv.HTML5
+        )
diff --git a/tests/test_level2.py b/tests/test_level2.py
index d46f60d..f5a089b 100644
--- a/tests/test_level2.py
+++ b/tests/test_level2.py
@@ -32,45 +32,75 @@ class TestLevel2(util.TestCase):
     def test_direct_child(self):
         """Test direct child."""
 
+        markup = """
+        <div>
+        <p id="0">Some text <span id="1"> in a paragraph</span>.</p>
+        <a id="2" href="http://google.com">Link</a>
+        <span id="3">Direct child</span>
+        <pre>
+        <span id="4">Child 1</span>
+        <span id="5">Child 2</span>
+        <span id="6">Child 3</span>
+        </pre>
+        </div>
+        """
+
+        # Spaces
         self.assert_selector(
-            """
-            <div>
-            <p id="0">Some text <span id="1"> in a paragraph</span>.</p>
-            <a id="2" href="http://google.com">Link</a>
-            <span id="3">Direct child</span>
-            <pre>
-            <span id="4">Child 1</span>
-            <span id="5">Child 2</span>
-            <span id="6">Child 3</span>
-            </pre>
-            </div>
-            """,
+            markup,
             "div > span",
             ["3"],
             mode=sv.HTML5
         )
 
+        # No spaces
+        self.assert_selector(
+            markup,
+            "div>span",
+            ["3"],
+            mode=sv.HTML5
+        )
+
     def test_direct_sibling(self):
         """Test direct sibling."""
 
+        markup = """
+        <div>
+        <p id="0">Some text <span id="1"> in a paragraph</span>.</p>
+        <a id="2" href="http://google.com">Link</a>
+        <span id="3">Direct child</span>
+        <pre>
+        <span id="4">Child 1</span>
+        <span id="5">Child 2</span>
+        <span id="6">Child 3</span>
+        </pre>
+        </div>
+        """
+
+        # Spaces
         self.assert_selector(
-            """
-            <div>
-            <p id="0">Some text <span id="1"> in a paragraph</span>.</p>
-            <a id="2" href="http://google.com">Link</a>
-            <span id="3">Direct child</span>
-            <pre>
-            <span id="4">Child 1</span>
-            <span id="5">Child 2</span>
-            <span id="6">Child 3</span>
-            </pre>
-            </div>
-            """,
+            markup,
             "span + span",
             ["5", "6"],
             mode=sv.HTML5
         )
 
+        # No spaces
+        self.assert_selector(
+            markup,
+            "span+span",
+            ["5", "6"],
+            mode=sv.HTML5
+        )
+
+        # Complex
+        self.assert_selector(
+            markup,
+            "span#4 + span#5",
+            ["5"],
+            mode=sv.HTML5
+        )
+
     def test_wild_tag(self):
         """Test wild tag."""
 
@@ -113,6 +143,28 @@ class TestLevel2(util.TestCase):
             mode=sv.HTML5
         )
 
+    def test_multi_attribute(self):
+        """Test multiple attribute."""
+
+        self.assert_selector(
+            """
+            <div id="div">
+            <p id="0">Some text <span id="1"> in a paragraph</span>.</p>
+            <a id="2" href="http://google.com">Link</a>
+            <span id="3">Direct child</span>
+            <pre id="pre">
+            <span id="4" class="test">Child 1</span>
+            <span id="5" class="test" data-test="test">Child 2</span>
+            <span id="6">Child 3</span>
+            <span id="6">Child 3</span>
+            </pre>
+            </div>
+            """,
+            "span[id].test[data-test=test]",
+            ["5"],
+            mode=sv.HTML5
+        )
+
     def test_attribute_equal(self):
         """Test attribute with value that equals specified value."""
 
diff --git a/tests/test_soupsieve.py b/tests/test_soupsieve.py
index 1adbec0..09581e2 100644
--- a/tests/test_soupsieve.py
+++ b/tests/test_soupsieve.py
@@ -5,13 +5,15 @@ import soupsieve as sv
 import copy
 import random
 import pytest
+import warnings
 
 
 class TestSoupSieve(unittest.TestCase):
-    """Test soup sieve."""
+    """Test Soup Sieve."""
 
     def test_comments(self):
         """Test comments."""
+
         markup = """
         <!-- before header -->
         <html>
@@ -33,7 +35,7 @@ class TestSoupSieve(unittest.TestCase):
         comments = [str(c).strip() for c in sv.comments(soup, mode=sv.HTML5)]
         self.assertEqual(sorted(comments), sorted(['before header', 'comment', "don't ignore"]))
 
-        comments = [str(c).strip() for c in sv.commentsiter(soup, limit=2, mode=sv.HTML5)]
+        comments = [str(c).strip() for c in sv.icomments(soup, limit=2, mode=sv.HTML5)]
         self.assertEqual(sorted(comments), sorted(['before header', 'comment']))
 
     def test_select(self):
@@ -64,7 +66,7 @@ class TestSoupSieve(unittest.TestCase):
         self.assertEqual(sorted(['5', 'some-id']), sorted(ids))
 
         ids = []
-        for el in sv.selectiter('span[id]', soup):
+        for el in sv.iselect('span[id]', soup):
             ids.append(el.attrs['id'])
 
         self.assertEqual(sorted(['5', 'some-id']), sorted(ids))
@@ -159,3 +161,91 @@ class TestSoupSieve(unittest.TestCase):
 
         with pytest.raises(ValueError):
             sv.compile(p1, namespaces={"": ""})
+
+
+class TestDeprcations(unittest.TestCase):
+    """Test Soup Sieve deprecations."""
+
+    def test_selectiter_deprecation(self):
+        """Test the deprecated iterator functions."""
+
+        markup = """
+        <!-- before header -->
+        <html>
+        <head>
+        </head>
+        <body>
+        <!-- comment -->
+        <p id="1"><code id="2"></code><img id="3" src="./image.png"/></p>
+        <pre id="4"></pre>
+        <p><span id="5" class="some-class"></span><span id="some-id"></span></p>
+        <pre id="6" class='ignore'>
+            <!-- don't ignore -->
+        </pre>
+        </body>
+        </html>
+        """
+
+        soup = bs4.BeautifulSoup(markup, 'html5lib')
+
+        with warnings.catch_warnings(record=True) as w:
+            # Cause all warnings to always be triggered.
+            warnings.simplefilter("always")
+
+            ids = []
+            for el in sv.selectiter('span[id]', soup):
+                ids.append(el.attrs['id'])
+            self.assertEqual(sorted(['5', 'some-id']), sorted(ids))
+            self.assertTrue(len(w) == 1)
+            self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
+
+        with warnings.catch_warnings(record=True) as w:
+            # Cause all warnings to always be triggered.
+            warnings.simplefilter("always")
+
+            ids = []
+            for el in sv.compile('span[id]').selectiter(soup):
+                ids.append(el.attrs['id'])
+            self.assertEqual(sorted(['5', 'some-id']), sorted(ids))
+            self.assertTrue(len(w) == 1)
+            self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
+
+    def test_commentsiter_deprecation(self):
+        """Test the deprecated iterator functions."""
+
+        markup = """
+        <!-- before header -->
+        <html>
+        <head>
+        </head>
+        <body>
+        <!-- comment -->
+        <p id="1"><code id="2"></code><img id="3" src="./image.png"/></p>
+        <pre id="4"></pre>
+        <p><span id="5" class="some-class"></span><span id="some-id"></span></p>
+        <pre id="6" class='ignore'>
+            <!-- don't ignore -->
+        </pre>
+        </body>
+        </html>
+        """
+
+        soup = bs4.BeautifulSoup(markup, 'html5lib')
+
+        with warnings.catch_warnings(record=True) as w:
+            # Cause all warnings to always be triggered.
+            warnings.simplefilter("always")
+
+            comments = [str(c).strip() for c in sv.commentsiter(soup, limit=2)]
+            self.assertEqual(sorted(comments), sorted(['before header', 'comment']))
+            self.assertTrue(len(w) == 1)
+            self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
+
+        with warnings.catch_warnings(record=True) as w:
+            # Cause all warnings to always be triggered.
+            warnings.simplefilter("always")
+
+            comments = [str(c).strip() for c in sv.compile('').commentsiter(soup, limit=2)]
+            self.assertEqual(sorted(comments), sorted(['before header', 'comment']))
+            self.assertTrue(len(w) == 1)
+            self.assertTrue(issubclass(w[-1].category, DeprecationWarning))