Skip to content

Commit

Permalink
Update one() for merging
Browse files Browse the repository at this point in the history
  • Loading branch information
bbayles committed Dec 2, 2017
1 parent 97df8d3 commit dfcd00a
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 36 deletions.
2 changes: 1 addition & 1 deletion docs/api.rst
Expand Up @@ -122,7 +122,7 @@ These tools return summarized or aggregated data from an iterable.

.. autofunction:: ilen
.. autofunction:: first(iterable[, default])
.. autofunction:: one
.. autofunction:: one(iterable, too_short=ValueError, too_long=ValueError)
.. autofunction:: unique_to_each
.. autofunction:: locate
.. autofunction:: consecutive_groups
Expand Down
57 changes: 34 additions & 23 deletions more_itertools/more.py
Expand Up @@ -420,50 +420,61 @@ def with_iter(context_manager):
yield item


def one(iterable, too_short=None, too_long=None):
"""Return the only element from the iterable.
def one(iterable, too_short=ValueError, too_long=ValueError):
"""Return the first item from *iterable*, which is expected to contain only
that item. Raise an exception *iterable* is empty or has more than one
item.
Raise an exception if the iterable is empty or longer than 1 element. For
example, assert that a DB query returns a single, unique result.
:func:`one` is useful for ensuring that an iterable contains only one item.
For example, it can be used to retrieve the result of a database query
that is expected to return only a single row.
>>> one(['val'])
'val'
If *iterable* is empty, ``ValueError`` will be raised. You may specify a
different exception type with the *too_short* keyword:
>>> one(['val', 'other']) # doctest: +IGNORE_EXCEPTION_DETAIL
>>> it = []
>>> one(it) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ValueError: too many values to unpack (expected 1)
ValueError: too many items in iterable (expected 1)'
>>> one(it, too_short=IndexError) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
IndexError: too few items in iterable (expected 1)'
Similarly, if *iterable* contains more than one item, ``ValueError`` will
be raised. You may specify a different exception type with the *too_long*
keyword:
>>> one([]) # doctest: +IGNORE_EXCEPTION_DETAIL
>>> it = ['too', 'many']
>>> one(it) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ValueError: not enough values to unpack (expected 1, got 0)
By default, ``one()`` will raise a ValueError if the iterable has the wrong
number of elements. However, you can also provide custom exceptions via
the ``too_short`` and ``too_long`` arguments to raise if the iterable is
either too short (i.e. empty) or too long (i.e. more than one element).
``one()`` attempts to advance the iterable twice in order to ensure there
aren't further items. Because this discards any second item, ``one()`` is
not suitable in situations where you want to catch its exception and then
try an alternative treatment of the iterable. It should be used only when a
iterable longer than 1 item is, in fact, an error.
ValueError: too many items in iterable (expected 1)'
>>> one(it, too_long=RuntimeError) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
RuntimeError: too many items in iterable (expected 1)'
Note that :func:`one` attempts to advance *iterable* twice to ensure there
is only one item. If there is more than one, both items will be discarded.
See :func:`spy` or :func:`peekable` to check iterable contents less
destructively.
"""
it = iter(iterable)

try:
value = next(it)
except StopIteration:
raise too_short or ValueError("not enough values to unpack (expected 1, got 0)")
raise too_short('two few items in iterable (expected 1)')

try:
next(it)
except StopIteration:
pass
else:
raise too_long or ValueError("too many values to unpack (expected 1)")
raise too_long('too many items in iterable (expected 1)')

return value

Expand Down
28 changes: 16 additions & 12 deletions more_itertools/tests/test_more.py
Expand Up @@ -415,18 +415,22 @@ def test_with_iter(self):


class OneTests(TestCase):
def test_one(self):
"""Test the ``one()`` cases that aren't covered by its doctests."""
# Infinite iterables
numbers = count()
self.assertRaises(ValueError, lambda: mi.one(numbers)) # burn 0 and 1
self.assertEqual(next(numbers), 2)

# Custom exceptions
self.assertRaises(ZeroDivisionError,
lambda: mi.one([], ZeroDivisionError, OverflowError))
self.assertRaises(OverflowError,
lambda: mi.one([1,2], ZeroDivisionError, OverflowError))
def test_basic(self):
it = iter(['item'])
self.assertEqual(mi.one(it), 'item')

def test_too_short(self):
it = iter([])
self.assertRaises(ValueError, lambda: mi.one(it))
self.assertRaises(IndexError, lambda: mi.one(it, too_short=IndexError))

def test_too_long(self):
it = count()
self.assertRaises(ValueError, lambda: mi.one(it)) # burn 0 and 1
self.assertEqual(next(it), 2)
self.assertRaises(
OverflowError, lambda: mi.one(it, too_long=OverflowError)
)


class IntersperseTest(TestCase):
Expand Down

0 comments on commit dfcd00a

Please sign in to comment.