Repository: facelessuser/soupsieve

Issue:
select_one and select
I like the function `soup.select()` and `soup.select_one()`. Maybe you can add `select_one` to this repo? If the result is None, I can easily know that there are no match objects. Just like https://docs.python.org/3/library/re.html#re.search  Infact, I don't know why you use `limit` to control the number of the results. I think people would need one or all of the results.

Discussion:
yjqiang: `return tags[0] if len(tags) else None`
Maybe it is better for `return tags[0] if tags else None`?
facelessuser: > return tags[0] if len(tags) else None
Maybe it is better for return tags[0] if tags else None?

It's kind of a stylistic habit of mine.  I kind of do it that way as a reminder that I'm evaluating an iterable object. I'm not strict about it, but it is often something I do without thinking.
yjqiang: > > return tags[0] if len(tags) else None
> Maybe it is better for return tags[0] if tags else None?
> 
> It's kind of a stylistic habit of mine.  I kind of do it that way as a reminder that I'm evaluating an iterable object. I'm not strict about it, but it is often something I do without thinking.

Ok. Not a big deal.
facelessuser: Was just curious about performance, and someone on StackOverflow took the time to benchmark it. It appears that `len()` is definitely slower than checking its truthiness.

```
>>> import timeit
>>> min(timeit.repeat(lambda: len([]) == 0, repeat=100))
0.13775854044661884
>>> min(timeit.repeat(lambda: [] == [], repeat=100))
0.0984637276455409
>>> min(timeit.repeat(lambda: not [], repeat=100))
0.07878462291455435
```

In most cases you'd never notice, but if doing a ton of operations, the accumulation could be become for noticeable.  Nice to know.

Patch:
diff --git a/docs/src/markdown/about/changelog.md b/docs/src/markdown/about/changelog.md
index 431b37d..fe398a9 100644
--- a/docs/src/markdown/about/changelog.md
+++ b/docs/src/markdown/about/changelog.md
@@ -1,5 +1,9 @@
 # Changelog
 
+## 1.4.1
+
+- **NEW**: Add `select_one` method like Beautiful Soup has.
+
 ## 1.4.0
 
 - **NEW**: Throw `NotImplementedError` for at-rules: `@page`, etc.
diff --git a/docs/src/markdown/api.md b/docs/src/markdown/api.md
index c72b648..46cea96 100644
--- a/docs/src/markdown/api.md
+++ b/docs/src/markdown/api.md
@@ -16,10 +16,31 @@ Soup Sieve will detect the document type being used from the Beautiful Soup obje
 
 - While attribute values are generally treated as case sensitive, HTML5, XHTML, and HTML treat the `type` attribute special. The `type` attribute's value is always case insensitive. This is generally how most browsers treat `type`. If you need `type` to be sensitive, you can use the `s` flag: `#!css [type="submit" s]`.
 
+As far as the API is concerned, Soup Sieve mimics Beautiful Soup's original API at the time of writing this, which is why the names `select` and `select_one` are used. As of today, Beautiful Soup has agreed to include Soup Sieve as the official select library which is slated for the 4.7.0 release.
+
+Soup Sieve will always be available as an external API as well for more controlled tag selection if needed.
+
 ## Flags
 
 Early in development, flags were used to specify document type, but as of 1.0.0, there are no flags used at this time, but the parameter is provided for potential future use.
 
+## `soupsieve.select_one()`
+
+```py3
+def select(select, tag, namespaces=None, flags=0):
+    """Select the specified tags."""
+```
+
+`select_one` will return the first tag under the given tag that matches the given CSS selectors provided, or it will return `None` if a suitable tag was not found.
+
+`select_one` accepts a CSS selector string, a `Tag`/`BeautifulSoup` object, an optional [namespace](#namespaces) dictionary, and `flags`.
+
+```pycon3
+>>> import soupsieve as sv
+>>> sv.select_one('p:is(.a, .b, .c)', soup)
+<p class="a">Cat</p>
+```
+
 ## `soupsieve.select()`
 
 ```py3
@@ -126,6 +147,9 @@ class SoupSieve:
     def icomments(self, tag, limit=0):
         """Iterate comments only."""
 
+    def select_one(self, tag):
+        """Select a single tag."""
+
     def select(self, tag, limit=0):
         """Select the specified tags."""
 
diff --git a/docs/src/markdown/index.md b/docs/src/markdown/index.md
index 20739a0..f669b41 100644
--- a/docs/src/markdown/index.md
+++ b/docs/src/markdown/index.md
@@ -56,7 +56,15 @@ To use Soup Sieve, you must create a `BeautifulSoup` object:
 >>> soup = bs4.BeautifulSoup(text, 'html5lib')
 ```
 
-Then you can begin to use Soup Sieve to select:
+Then you can begin to use Soup Sieve to select a single tag:
+
+```pycon3
+>>> import soupsieve as sv
+>>> sv.select_one('p:is(.a, .b, .c)', soup)
+<p class="a">Cat</p>
+```
+
+To select all tags:
 
 ```pycon3
 >>> import soupsieve as sv
diff --git a/soupsieve/__init__.py b/soupsieve/__init__.py
index 15b4c59..b5f594d 100644
--- a/soupsieve/__init__.py
+++ b/soupsieve/__init__.py
@@ -89,6 +89,12 @@ def icomments(tag, limit=0, flags=0):
         yield comment
 
 
+def select_one(select, tag, namespaces=None, flags=0):
+    """Select a single tag."""
+
+    return compile(select, namespaces, flags).select_one(tag)
+
+
 def select(select, tag, namespaces=None, limit=0, flags=0):
     """Select the specified tags."""
 
diff --git a/soupsieve/css_match.py b/soupsieve/css_match.py
index 2b37cf7..9415b2f 100644
--- a/soupsieve/css_match.py
+++ b/soupsieve/css_match.py
@@ -764,6 +764,12 @@ class SoupSieve(ct.Immutable):
         for comment in self._sieve(tag, capture=False, comments=True, limit=limit):
             yield comment
 
+    def select_one(self, tag):
+        """Select a single tag."""
+
+        tags = self.select(tag, limit=1)
+        return tags[0] if tags else None
+
     def select(self, tag, limit=0):
         """Select the specified tags."""
 


Test patch:
diff --git a/tests/test_soupsieve.py b/tests/test_soupsieve.py
index 091a9b5..a8d0c5c 100644
--- a/tests/test_soupsieve.py
+++ b/tests/test_soupsieve.py
@@ -67,6 +67,19 @@ class TestSoupSieve(unittest.TestCase):
 
         self.assertEqual(sorted(['5', 'some-id']), sorted(ids))
 
+        ids = []
+        for el in sv.select('span[id]', soup, limit=1):
+            ids.append(el.attrs['id'])
+
+        self.assertEqual(sorted(['5']), sorted(ids))
+
+        self.assertEqual(
+            sv.select('span[id]', soup, limit=1)[0].attrs['id'],
+            sv.select_one('span[id]', soup).attrs['id']
+        )
+
+        self.assertEqual(None, sv.select_one('h1', soup))
+
         ids = []
         for el in sv.iselect('span[id]', soup):
             ids.append(el.attrs['id'])