Skip to content

Commit

Permalink
Merge pull request more-itertools#213 from tmshn/last
Browse files Browse the repository at this point in the history
Implement last()
  • Loading branch information
bbayles authored Jun 9, 2018
2 parents 35c5812 + b6a6b98 commit a882f49
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ These tools return summarized or aggregated data from an iterable.

.. autofunction:: ilen
.. autofunction:: first(iterable[, default])
.. autofunction:: last(iterable[, default])
.. autofunction:: one
.. autofunction:: unique_to_each
.. autofunction:: locate(iterable, pred=bool)
Expand Down
27 changes: 27 additions & 0 deletions more_itertools/more.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
'intersperse',
'islice_extended',
'iterate',
'last',
'locate',
'lstrip',
'make_decorator',
Expand Down Expand Up @@ -137,6 +138,32 @@ def first(iterable, default=_marker):
return default


def last(iterable, default=_marker):
"""Return the last item of *iterable*, or *default* if *iterable* is
empty.
>>> last([0, 1, 2, 3])
3
>>> last([], 'some default')
'some default'
If *default* is not provided and there are no items in the iterable,
raise ``ValueError``.
"""
try:
try:
# Try to access the last item directly
return iterable[-1]
except (TypeError, AttributeError, KeyError):
# If not slice-able, iterate entirely using length-1 deque
return deque(iterable, maxlen=1)[0]
except IndexError: # If the iterable was empty
if default is _marker:
raise ValueError('last() was called on an empty iterable, and no '
'default value was provided.')
return default


class peekable(object):
"""Wrap an iterator to allow lookahead and prepending elements.
Expand Down
85 changes: 85 additions & 0 deletions more_itertools/tests/test_more.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import division, print_function, unicode_literals

from collections import OrderedDict
from decimal import Decimal
from doctest import DocTestSuite
from fractions import Fraction
Expand Down Expand Up @@ -114,6 +115,90 @@ def test_default(self):
self.assertEqual(mi.first([], 'boo'), 'boo')


class IterOnlyRange:
"""User-defined iterable class which only support __iter__.
It is not specified to inherit ``object``, so indexing on a instance will
raise an ``AttributeError`` rather than ``TypeError`` in Python 2.
>>> r = IterOnlyRange(5)
>>> r[0]
AttributeError: IterOnlyRange instance has no attribute '__getitem__'
Note: In Python 3, ``TypeError`` will be raised because ``object`` is
inherited implicitly by default.
>>> r[0]
TypeError: 'IterOnlyRange' object does not support indexing
"""
def __init__(self, n):
"""Set the length of the range."""
self.n = n

def __iter__(self):
"""Works same as range()."""
return iter(range(self.n))


class LastTests(TestCase):
"""Tests for ``last()``"""

def test_many_nonsliceable(self):
"""Test that it works on many-item non-slice-able iterables."""
# Also try it on a generator expression to make sure it works on
# whatever those return, across Python versions.
self.assertEqual(mi.last(x for x in range(4)), 3)

def test_one_nonsliceable(self):
"""Test that it doesn't raise StopIteration prematurely."""
self.assertEqual(mi.last(x for x in range(1)), 0)

def test_empty_stop_iteration_nonsliceable(self):
"""It should raise ValueError for empty non-slice-able iterables."""
self.assertRaises(ValueError, lambda: mi.last(x for x in range(0)))

def test_default_nonsliceable(self):
"""It should return the provided default arg for empty non-slice-able
iterables.
"""
self.assertEqual(mi.last((x for x in range(0)), 'boo'), 'boo')

def test_many_sliceable(self):
"""Test that it works on many-item slice-able iterables."""
self.assertEqual(mi.last([0, 1, 2, 3]), 3)

def test_one_sliceable(self):
"""Test that it doesn't raise StopIteration prematurely."""
self.assertEqual(mi.last([3]), 3)

def test_empty_stop_iteration_sliceable(self):
"""It should raise ValueError for empty slice-able iterables."""
self.assertRaises(ValueError, lambda: mi.last([]))

def test_default_sliceable(self):
"""It should return the provided default arg for empty slice-able
iterables.
"""
self.assertEqual(mi.last([], 'boo'), 'boo')

def test_dict(self):
"""last(dic) and last(dic.keys()) should return same result."""
dic = {'a': 1, 'b': 2, 'c': 3}
self.assertEqual(mi.last(dic), mi.last(dic.keys()))

def test_ordereddict(self):
"""last(dic) should return the last key."""
od = OrderedDict()
od['a'] = 1
od['b'] = 2
od['c'] = 3
self.assertEqual(mi.last(od), 'c')

def test_customrange(self):
"""It should work on custom class where [] raises AttributeError."""
self.assertEqual(mi.last(IterOnlyRange(5)), 4)


class PeekableTests(TestCase):
"""Tests for ``peekable()`` behavor not incidentally covered by testing
``collate()``
Expand Down

0 comments on commit a882f49

Please sign in to comment.