Repository: crsmithdev/arrow
Pull request #458 (2017-09-23 16:21:27)

Return generator instead of list in Arrow.range() et al
Based off of @rominf's PR https://github.com/crsmithdev/arrow/pull/440, implementing https://github.com/crsmithdev/arrow/issues/251, and using an idea from @pganssle.


Patch:
diff --git a/.gitignore b/.gitignore
index 3276dce..ae9b3e3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,7 +10,9 @@
 
 .coverage
 nosetests.xml
+tests/.noseids
 
+.venv/
 local/
 build/
 dist/
@@ -20,3 +22,6 @@ docs/_build/
 
 # tox
 .tox
+
+# VS Code
+.vscode/
diff --git a/arrow/arrow.py b/arrow/arrow.py
index de880bb..7dba85b 100644
--- a/arrow/arrow.py
+++ b/arrow/arrow.py
@@ -195,11 +195,12 @@ class Arrow(object):
     # factories: ranges and spans
 
     @classmethod
+    @util.list_to_iter_deprecation
     def range(cls, frame, start, end=None, tz=None, limit=None):
-        ''' Returns a list of :class:`Arrow <arrow.arrow.Arrow>` objects, representing
-        an iteration of time between two inputs.
+        ''' Returns an iterator of :class:`Arrow <arrow.arrow.Arrow>` objects, representing
+        points in time between two inputs.
 
-        :param frame: the timeframe.  Can be any ``datetime`` property (day, hour, minute...).
+        :param frame: The timeframe.  Can be any ``datetime`` property (day, hour, minute...).
         :param start: A datetime expression, the start of the range.
         :param end: (optional) A datetime expression, the end of the range.
         :param tz: (optional) A :ref:`timezone expression <tz-expr>`.  Defaults to
@@ -234,7 +235,7 @@ class Arrow(object):
             <Arrow [2013-05-05T15:30:00+00:00]>
             <Arrow [2013-05-05T16:30:00+00:00]>
 
-        **NOTE**: Unlike Python's ``range``, ``end`` *may* be included in the returned list::
+        **NOTE**: Unlike Python's ``range``, ``end`` *may* be included in the returned iterator::
 
             >>> start = datetime(2013, 5, 5, 12, 30)
             >>> end = datetime(2013, 5, 5, 13, 30)
@@ -255,23 +256,23 @@ class Arrow(object):
         end = cls._get_datetime(end).replace(tzinfo=tzinfo)
 
         current = cls.fromdatetime(start)
-        results = []
+        i = 0
 
-        while current <= end and len(results) < limit:
-            results.append(current)
+        while current <= end and i < limit:
+            i += 1
+            yield current
 
             values = [getattr(current, f) for f in cls._ATTRS]
             current = cls(*values, tzinfo=tzinfo) + relativedelta(**{frame_relative: relative_steps})
 
-        return results
-
 
     @classmethod
+    @util.list_to_iter_deprecation
     def span_range(cls, frame, start, end, tz=None, limit=None):
-        ''' Returns a list of tuples, each :class:`Arrow <arrow.arrow.Arrow>` objects,
+        ''' Returns an iterator of tuples, each :class:`Arrow <arrow.arrow.Arrow>` objects,
         representing a series of timespans between two inputs.
 
-        :param frame: the timeframe.  Can be any ``datetime`` property (day, hour, minute...).
+        :param frame: The timeframe.  Can be any ``datetime`` property (day, hour, minute...).
         :param start: A datetime expression, the start of the range.
         :param end: (optional) A datetime expression, the end of the range.
         :param tz: (optional) A :ref:`timezone expression <tz-expr>`.  Defaults to
@@ -293,8 +294,8 @@ class Arrow(object):
             - An :class:`Arrow <arrow.arrow.Arrow>` object.
             - A ``datetime`` object.
 
-        **NOTE**: Unlike Python's ``range``, ``end`` will *always* be included in the returned list
-        of timespans.
+        **NOTE**: Unlike Python's ``range``, ``end`` will *always* be included in the returned
+        iterator of timespans.
 
         Usage:
 
@@ -315,19 +316,20 @@ class Arrow(object):
         tzinfo = cls._get_tzinfo(start.tzinfo if tz is None else tz)
         start = cls.fromdatetime(start, tzinfo).span(frame)[0]
         _range = cls.range(frame, start, end, tz, limit)
-        return [r.span(frame) for r in _range]
+        return (r.span(frame) for r in _range)
 
     @classmethod
+    @util.list_to_iter_deprecation
     def interval(cls, frame, start, end, interval=1, tz=None):
-        ''' Returns an array of tuples, each :class:`Arrow <arrow.arrow.Arrow>` objects,
+        ''' Returns an iterator of tuples, each :class:`Arrow <arrow.arrow.Arrow>` objects,
         representing a series of intervals between two inputs.
 
-        :param frame: the timeframe.  Can be any ``datetime`` property (day, hour, minute...).
+        :param frame: The timeframe.  Can be any ``datetime`` property (day, hour, minute...).
         :param start: A datetime expression, the start of the range.
         :param end: (optional) A datetime expression, the end of the range.
         :param interval: (optional) Time interval for the given time frame.
         :param tz: (optional) A timezone expression.  Defaults to UTC.
-        
+
         Supported frame values: year, quarter, month, week, day, hour, minute, second
 
         Recognized datetime expressions:
@@ -356,10 +358,12 @@ class Arrow(object):
         if interval < 1:
             raise ValueError("interval has to be a positive integer")
 
-        spanRange = cls.span_range(frame,start,end,tz)
-
-        bound = (len(spanRange) // interval) * interval
-        return [ (spanRange[i][0],spanRange[i+ interval - 1][1]) for i in range(0,bound, interval) ]
+        spanRange = iter(cls.span_range(frame, start, end, tz))
+        while True:
+            intvlStart, intvlEnd = next(spanRange)  # StopIteration when exhausted
+            for _ in range(interval-1):
+                _, intvlEnd = next(spanRange)  # StopIteration when exhausted
+            yield intvlStart, intvlEnd
 
     # representations
 
@@ -746,43 +750,43 @@ class Arrow(object):
         sign = -1 if delta < 0 else 1
         diff = abs(delta)
         delta = diff
-        
+
         if granularity=='auto':
             if diff < 10:
                 return locale.describe('now', only_distance=only_distance)
-    
+
             if diff < 45:
                 seconds = sign * delta
                 return locale.describe('seconds', seconds, only_distance=only_distance)
-    
+
             elif diff < 90:
                 return locale.describe('minute', sign, only_distance=only_distance)
             elif diff < 2700:
                 minutes = sign * int(max(delta / 60, 2))
                 return locale.describe('minutes', minutes, only_distance=only_distance)
-    
+
             elif diff < 5400:
                 return locale.describe('hour', sign, only_distance=only_distance)
             elif diff < 79200:
                 hours = sign * int(max(delta / 3600, 2))
                 return locale.describe('hours', hours, only_distance=only_distance)
-    
+
             elif diff < 129600:
                 return locale.describe('day', sign, only_distance=only_distance)
             elif diff < 2160000:
                 days = sign * int(max(delta / 86400, 2))
                 return locale.describe('days', days, only_distance=only_distance)
-    
+
             elif diff < 3888000:
                 return locale.describe('month', sign, only_distance=only_distance)
             elif diff < 29808000:
                 self_months = self._datetime.year * 12 + self._datetime.month
                 other_months = dt.year * 12 + dt.month
-    
+
                 months = sign * int(max(abs(other_months - self_months), 2))
-    
+
                 return locale.describe('months', months, only_distance=only_distance)
-    
+
             elif diff < 47260800:
                 return locale.describe('year', sign, only_distance=only_distance)
             else:
@@ -806,8 +810,8 @@ class Arrow(object):
                 delta = sign * delta / float(60*60*24*365.25)
             else:
                 raise AttributeError('Error. Could not understand your level of granularity. Please select between \
-                "second", "minute", "hour", "day", "week", "month" or "year"') 
-            
+                "second", "minute", "hour", "day", "week", "month" or "year"')
+
             if(trunc(abs(delta)) != 1):
                 granularity += 's'
             return locale.describe(granularity, delta, only_distance=only_distance)
@@ -833,7 +837,7 @@ class Arrow(object):
 
         elif isinstance(other, Arrow):
             return self._datetime - other._datetime
-        
+
         raise TypeError()
 
     def __rsub__(self, other):
diff --git a/arrow/util.py b/arrow/util.py
index 3eed4fa..088e5df 100644
--- a/arrow/util.py
+++ b/arrow/util.py
@@ -1,7 +1,9 @@
 # -*- coding: utf-8 -*-
 from __future__ import absolute_import
 
+import functools
 import sys
+import warnings
 
 # python 2.6 / 2.7 definitions for total_seconds function.
 
@@ -44,4 +46,65 @@ except NameError: #pragma: no cover
         return isinstance(s, str)
 
 
-__all__ = ['total_seconds', 'is_timestamp', 'isstr']
+class list_to_iter_shim(list):
+    ''' A temporary shim for functions that currently return a list but that will, after a
+    deprecation period, return an iteratator.
+    '''
+
+    def __init__(self, iterable=(), **kwargs):
+        ''' Equivalent to list(iterable).  warn_text will be emitted on all non-iterator operations.
+        '''
+        self._warn_text = kwargs.pop('warn_text', None) or 'this object will be converted to an iterator in a future release'
+        self._iter_count = 0
+        list.__init__(self, iterable, **kwargs)
+
+    def _warn(self):
+        warnings.warn(self._warn_text, DeprecationWarning)
+
+    def __iter__(self):
+        self._iter_count += 1
+        if self._iter_count > 1:
+            self._warn()
+        return list.__iter__(self)
+
+    def _wrap_method(name):
+        list_func = getattr(list, name)
+        def wrapper(self, *args, **kwargs):
+            self._warn()
+            return list_func(self, *args, **kwargs)
+        return wrapper
+
+    __contains__ = _wrap_method('__contains__')
+    __add__ = _wrap_method('__add__')
+    __mul__ = _wrap_method('__mul__')
+    __getitem__ = _wrap_method('__getitem__')
+    # Ideally, we would throw warnings from  __len__, but list(x) calls len(x)
+    index = _wrap_method('index')
+    count = _wrap_method('count')
+    __setitem__ = _wrap_method('__setitem__')
+    __delitem__ = _wrap_method('__delitem__')
+    append = _wrap_method('append')
+    if version >= '3.0':  # pragma: no cover
+        clear = _wrap_method('clear')
+        copy = _wrap_method('copy')
+    extend = _wrap_method('extend')
+    __iadd__ = _wrap_method('__iadd__')
+    __imul__ = _wrap_method('__imul__')
+    insert = _wrap_method('insert')
+    pop = _wrap_method('pop')
+    remove = _wrap_method('remove')
+    reverse = _wrap_method('reverse')
+    sort = _wrap_method('sort')
+
+    del _wrap_method
+
+
+def list_to_iter_deprecation(f):
+    warn_text = '{0}() will return an iterator in a future release, convert to list({0}())'.format(f.__name__)
+    @functools.wraps(f)
+    def wrapper(*args, **kwargs):
+        return list_to_iter_shim(f(*args, **kwargs), warn_text=warn_text)
+    return wrapper
+
+
+__all__ = ['total_seconds', 'is_timestamp', 'isstr', 'list_to_iter_shim', 'list_to_iter_deprecation']


Test patch:
diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py
index 6b7bf2e..b49b731 100644
--- a/tests/arrow_tests.py
+++ b/tests/arrow_tests.py
@@ -691,8 +691,8 @@ class ArrowRangeTests(Chai):
 
     def test_year(self):
 
-        result = arrow.Arrow.range('year', datetime(2013, 1, 2, 3, 4, 5),
-            datetime(2016, 4, 5, 6, 7, 8))
+        result = list(arrow.Arrow.range('year', datetime(2013, 1, 2, 3, 4, 5),
+            datetime(2016, 4, 5, 6, 7, 8)))
 
         assertEqual(result, [
             arrow.Arrow(2013, 1, 2, 3, 4, 5),
@@ -703,8 +703,8 @@ class ArrowRangeTests(Chai):
 
     def test_quarter(self):
 
-        result = arrow.Arrow.range('quarter', datetime(2013, 2, 3, 4, 5, 6),
-            datetime(2013, 5, 6, 7, 8, 9))
+        result = list(arrow.Arrow.range('quarter', datetime(2013, 2, 3, 4, 5, 6),
+            datetime(2013, 5, 6, 7, 8, 9)))
 
         assertEqual(result, [
             arrow.Arrow(2013, 2, 3, 4, 5, 6),
@@ -713,8 +713,8 @@ class ArrowRangeTests(Chai):
 
     def test_month(self):
 
-        result = arrow.Arrow.range('month', datetime(2013, 2, 3, 4, 5, 6),
-            datetime(2013, 5, 6, 7, 8, 9))
+        result = list(arrow.Arrow.range('month', datetime(2013, 2, 3, 4, 5, 6),
+            datetime(2013, 5, 6, 7, 8, 9)))
 
         assertEqual(result, [
             arrow.Arrow(2013, 2, 3, 4, 5, 6),
@@ -725,8 +725,8 @@ class ArrowRangeTests(Chai):
 
     def test_week(self):
 
-        result = arrow.Arrow.range('week', datetime(2013, 9, 1, 2, 3, 4),
-            datetime(2013, 10, 1, 2, 3, 4))
+        result = list(arrow.Arrow.range('week', datetime(2013, 9, 1, 2, 3, 4),
+            datetime(2013, 10, 1, 2, 3, 4)))
 
         assertEqual(result, [
             arrow.Arrow(2013, 9, 1, 2, 3, 4),
@@ -738,8 +738,8 @@ class ArrowRangeTests(Chai):
 
     def test_day(self):
 
-        result = arrow.Arrow.range('day', datetime(2013, 1, 2, 3, 4, 5),
-            datetime(2013, 1, 5, 6, 7, 8))
+        result = list(arrow.Arrow.range('day', datetime(2013, 1, 2, 3, 4, 5),
+            datetime(2013, 1, 5, 6, 7, 8)))
 
         assertEqual(result, [
             arrow.Arrow(2013, 1, 2, 3, 4, 5),
@@ -750,8 +750,8 @@ class ArrowRangeTests(Chai):
 
     def test_hour(self):
 
-        result = arrow.Arrow.range('hour', datetime(2013, 1, 2, 3, 4, 5),
-            datetime(2013, 1, 2, 6, 7, 8))
+        result = list(arrow.Arrow.range('hour', datetime(2013, 1, 2, 3, 4, 5),
+            datetime(2013, 1, 2, 6, 7, 8)))
 
         assertEqual(result, [
             arrow.Arrow(2013, 1, 2, 3, 4, 5),
@@ -760,8 +760,8 @@ class ArrowRangeTests(Chai):
             arrow.Arrow(2013, 1, 2, 6, 4, 5),
         ])
 
-        result = arrow.Arrow.range('hour', datetime(2013, 1, 2, 3, 4, 5),
-            datetime(2013, 1, 2, 3, 4, 5))
+        result = list(arrow.Arrow.range('hour', datetime(2013, 1, 2, 3, 4, 5),
+            datetime(2013, 1, 2, 3, 4, 5)))
 
         assertEqual(result, [
             arrow.Arrow(2013, 1, 2, 3, 4, 5),
@@ -769,8 +769,8 @@ class ArrowRangeTests(Chai):
 
     def test_minute(self):
 
-        result = arrow.Arrow.range('minute', datetime(2013, 1, 2, 3, 4, 5),
-            datetime(2013, 1, 2, 3, 7, 8))
+        result = list(arrow.Arrow.range('minute', datetime(2013, 1, 2, 3, 4, 5),
+            datetime(2013, 1, 2, 3, 7, 8)))
 
         assertEqual(result, [
             arrow.Arrow(2013, 1, 2, 3, 4, 5),
@@ -781,8 +781,8 @@ class ArrowRangeTests(Chai):
 
     def test_second(self):
 
-        result = arrow.Arrow.range('second', datetime(2013, 1, 2, 3, 4, 5),
-            datetime(2013, 1, 2, 3, 4, 8))
+        result = list(arrow.Arrow.range('second', datetime(2013, 1, 2, 3, 4, 5),
+            datetime(2013, 1, 2, 3, 4, 8)))
 
         assertEqual(result, [
             arrow.Arrow(2013, 1, 2, 3, 4, 5),
@@ -793,8 +793,8 @@ class ArrowRangeTests(Chai):
 
     def test_arrow(self):
 
-        result = arrow.Arrow.range('day', arrow.Arrow(2013, 1, 2, 3, 4, 5),
-            arrow.Arrow(2013, 1, 5, 6, 7, 8))
+        result = list(arrow.Arrow.range('day', arrow.Arrow(2013, 1, 2, 3, 4, 5),
+            arrow.Arrow(2013, 1, 5, 6, 7, 8)))
 
         assertEqual(result, [
             arrow.Arrow(2013, 1, 2, 3, 4, 5),
@@ -838,14 +838,14 @@ class ArrowRangeTests(Chai):
     def test_unsupported(self):
 
         with assertRaises(AttributeError):
-            arrow.Arrow.range('abc', datetime.utcnow(), datetime.utcnow())
+            next(arrow.Arrow.range('abc', datetime.utcnow(), datetime.utcnow()))
 
 
 class ArrowSpanRangeTests(Chai):
 
     def test_year(self):
 
-        result = arrow.Arrow.span_range('year', datetime(2013, 2, 1), datetime(2016, 3, 31))
+        result = list(arrow.Arrow.span_range('year', datetime(2013, 2, 1), datetime(2016, 3, 31)))
 
         assertEqual(result, [
             (arrow.Arrow(2013, 1, 1), arrow.Arrow(2013, 12, 31, 23, 59, 59, 999999)),
@@ -856,7 +856,7 @@ class ArrowSpanRangeTests(Chai):
 
     def test_quarter(self):
 
-        result = arrow.Arrow.span_range('quarter', datetime(2013, 2, 2), datetime(2013, 5, 15))
+        result = list(arrow.Arrow.span_range('quarter', datetime(2013, 2, 2), datetime(2013, 5, 15)))
 
         assertEqual(result, [
             (arrow.Arrow(2013, 1, 1), arrow.Arrow(2013, 3, 31, 23, 59, 59, 999999)),
@@ -865,7 +865,7 @@ class ArrowSpanRangeTests(Chai):
 
     def test_month(self):
 
-        result = arrow.Arrow.span_range('month', datetime(2013, 1, 2), datetime(2013, 4, 15))
+        result = list(arrow.Arrow.span_range('month', datetime(2013, 1, 2), datetime(2013, 4, 15)))
 
         assertEqual(result, [
             (arrow.Arrow(2013, 1, 1), arrow.Arrow(2013, 1, 31, 23, 59, 59, 999999)),
@@ -876,7 +876,7 @@ class ArrowSpanRangeTests(Chai):
 
     def test_week(self):
 
-        result = arrow.Arrow.span_range('week', datetime(2013, 2, 2), datetime(2013, 2, 28))
+        result = list(arrow.Arrow.span_range('week', datetime(2013, 2, 2), datetime(2013, 2, 28)))
 
         assertEqual(result, [
             (arrow.Arrow(2013, 1, 28), arrow.Arrow(2013, 2, 3, 23, 59, 59, 999999)),
@@ -889,8 +889,8 @@ class ArrowSpanRangeTests(Chai):
 
     def test_day(self):
 
-        result = arrow.Arrow.span_range('day', datetime(2013, 1, 1, 12),
-            datetime(2013, 1, 4, 12))
+        result = list(arrow.Arrow.span_range('day', datetime(2013, 1, 1, 12),
+            datetime(2013, 1, 4, 12)))
 
         assertEqual(result, [
             (arrow.Arrow(2013, 1, 1, 0), arrow.Arrow(2013, 1, 1, 23, 59, 59, 999999)),
@@ -901,8 +901,8 @@ class ArrowSpanRangeTests(Chai):
 
     def test_hour(self):
 
-        result = arrow.Arrow.span_range('hour', datetime(2013, 1, 1, 0, 30),
-            datetime(2013, 1, 1, 3, 30))
+        result = list(arrow.Arrow.span_range('hour', datetime(2013, 1, 1, 0, 30),
+            datetime(2013, 1, 1, 3, 30)))
 
         assertEqual(result, [
             (arrow.Arrow(2013, 1, 1, 0), arrow.Arrow(2013, 1, 1, 0, 59, 59, 999999)),
@@ -911,8 +911,8 @@ class ArrowSpanRangeTests(Chai):
             (arrow.Arrow(2013, 1, 1, 3), arrow.Arrow(2013, 1, 1, 3, 59, 59, 999999)),
         ])
 
-        result = arrow.Arrow.span_range('hour', datetime(2013, 1, 1, 3, 30),
-            datetime(2013, 1, 1, 3, 30))
+        result = list(arrow.Arrow.span_range('hour', datetime(2013, 1, 1, 3, 30),
+            datetime(2013, 1, 1, 3, 30)))
 
         assertEqual(result, [
             (arrow.Arrow(2013, 1, 1, 3), arrow.Arrow(2013, 1, 1, 3, 59, 59, 999999)),
@@ -920,8 +920,8 @@ class ArrowSpanRangeTests(Chai):
 
     def test_minute(self):
 
-        result = arrow.Arrow.span_range('minute', datetime(2013, 1, 1, 0, 0, 30),
-            datetime(2013, 1, 1, 0, 3, 30))
+        result = list(arrow.Arrow.span_range('minute', datetime(2013, 1, 1, 0, 0, 30),
+            datetime(2013, 1, 1, 0, 3, 30)))
 
         assertEqual(result, [
             (arrow.Arrow(2013, 1, 1, 0, 0), arrow.Arrow(2013, 1, 1, 0, 0, 59, 999999)),
@@ -932,8 +932,8 @@ class ArrowSpanRangeTests(Chai):
 
     def test_second(self):
 
-        result = arrow.Arrow.span_range('second', datetime(2013, 1, 1),
-            datetime(2013, 1, 1, 0, 0, 3))
+        result = list(arrow.Arrow.span_range('second', datetime(2013, 1, 1),
+            datetime(2013, 1, 1, 0, 0, 3)))
 
         assertEqual(result, [
             (arrow.Arrow(2013, 1, 1, 0, 0, 0), arrow.Arrow(2013, 1, 1, 0, 0, 0, 999999)),
@@ -992,14 +992,14 @@ class ArrowIntervalTests(Chai):
     def test_incorrect_input(self):
         correct = True
         try:
-            result = arrow.Arrow.interval('month', datetime(2013, 1, 2), datetime(2013, 4, 15),0)
+            result = list(arrow.Arrow.interval('month', datetime(2013, 1, 2), datetime(2013, 4, 15), 0))
         except:
             correct = False
 
         assertEqual(correct,False)
 
     def test_correct(self):
-        result = arrow.Arrow.interval('hour', datetime(2013, 5, 5, 12, 30), datetime(2013, 5, 5, 17, 15),2)
+        result = list(arrow.Arrow.interval('hour', datetime(2013, 5, 5, 12, 30), datetime(2013, 5, 5, 17, 15), 2))
 
         assertEqual(result,[(arrow.Arrow(2013, 5, 5, 12), arrow.Arrow(2013, 5, 5, 13, 59, 59, 999999)),
             (arrow.Arrow(2013, 5, 5, 14), arrow.Arrow(2013, 5, 5, 15, 59, 59, 999999)),
@@ -1116,15 +1116,15 @@ class ArrowHumanizeTests(Chai):
         self.now = arrow.Arrow.utcnow()
 
     def test_granularity(self):
-    
+
         assertEqual(self.now.humanize(granularity = 'second'), 'just now')
-        
+
         later1 = self.now.shift(seconds=1)
         assertEqual(self.now.humanize(later1, granularity = 'second'), 'just now')
         assertEqual(later1.humanize(self.now, granularity = 'second'), 'just now')
         assertEqual(self.now.humanize(later1, granularity = 'minute'), '0 minutes ago')
         assertEqual(later1.humanize(self.now, granularity = 'minute'), 'in 0 minutes')
-        
+
         later100 = self.now.shift(seconds=100)
         assertEqual(self.now.humanize(later100, granularity = 'second'), 'seconds ago')
         assertEqual(later100.humanize(self.now, granularity = 'second'), 'in seconds')
@@ -1132,7 +1132,7 @@ class ArrowHumanizeTests(Chai):
         assertEqual(later100.humanize(self.now, granularity = 'minute'), 'in a minute')
         assertEqual(self.now.humanize(later100, granularity = 'hour'), '0 hours ago')
         assertEqual(later100.humanize(self.now, granularity = 'hour'), 'in 0 hours')
-        
+
         later4000 = self.now.shift(seconds=4000)
         assertEqual(self.now.humanize(later4000, granularity = 'minute'), '66 minutes ago')
         assertEqual(later4000.humanize(self.now, granularity = 'minute'), 'in 66 minutes')
@@ -1140,7 +1140,7 @@ class ArrowHumanizeTests(Chai):
         assertEqual(later4000.humanize(self.now, granularity = 'hour'), 'in an hour')
         assertEqual(self.now.humanize(later4000, granularity = 'day'), '0 days ago')
         assertEqual(later4000.humanize(self.now, granularity = 'day'), 'in 0 days')
-        
+
         later105 = self.now.shift(seconds=10 ** 5)
         assertEqual(self.now.humanize(later105, granularity = 'hour'), '27 hours ago')
         assertEqual(later105.humanize(self.now, granularity = 'hour'), 'in 27 hours')
@@ -1148,7 +1148,7 @@ class ArrowHumanizeTests(Chai):
         assertEqual(later105.humanize(self.now, granularity = 'day'), 'in a day')
         assertEqual(self.now.humanize(later105, granularity = 'month'), '0 months ago')
         assertEqual(later105.humanize(self.now, granularity = 'month'), 'in 0 months')
-        
+
         later106 = self.now.shift(seconds=3 * 10 ** 6)
         assertEqual(self.now.humanize(later106, granularity = 'day'), '34 days ago')
         assertEqual(later106.humanize(self.now, granularity = 'day'), 'in 34 days')
@@ -1156,13 +1156,13 @@ class ArrowHumanizeTests(Chai):
         assertEqual(later106.humanize(self.now, granularity = 'month'), 'in a month')
         assertEqual(self.now.humanize(later106, granularity = 'year'), '0 years ago')
         assertEqual(later106.humanize(self.now, granularity = 'year'), 'in 0 years')
-        
+
         later506 = self.now.shift(seconds=50 * 10 ** 6)
         assertEqual(self.now.humanize(later506, granularity = 'month'), '18 months ago')
         assertEqual(later506.humanize(self.now, granularity = 'month'), 'in 18 months')
         assertEqual(self.now.humanize(later506, granularity = 'year'), 'a year ago')
         assertEqual(later506.humanize(self.now, granularity = 'year'), 'in a year')
-        
+
         later108 = self.now.shift(seconds=10 ** 8)
         assertEqual(self.now.humanize(later108, granularity = 'year'), '3 years ago')
         assertEqual(later108.humanize(self.now, granularity = 'year'), 'in 3 years')
@@ -1172,7 +1172,7 @@ class ArrowHumanizeTests(Chai):
         assertEqual(later108onlydistance .humanize(self.now, only_distance=True, granularity = 'year'), '3 years')
         with assertRaises(AttributeError):
             self.now.humanize(later108, granularity = 'years')
-    
+
     def test_seconds(self):
 
         later = self.now.shift(seconds=10)
@@ -1405,3 +1405,64 @@ class ArrowUtilTests(Chai):
 
         with assertRaises(Exception):
             arrow.Arrow._get_iteration_params(None, None)
+
+    def test_list_to_iter_shim(self):
+        def newshim():
+            return util.list_to_iter_shim(range(5), warn_text='testing')
+
+        # Iterating over a shim once should not throw a warning
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter("always")
+
+            iter(newshim())
+            list(newshim())
+            for _ in newshim(): pass
+            len(newshim())  # ...because it's called by `list(x)`
+
+            assertEqual([], w)
+
+        # Iterating over a shim twice (or more) should throw a warning
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter("always")
+            shim = newshim()
+
+            for _ in shim: pass
+            for _ in shim: pass
+
+            assertEqual(1, len(w))
+            assertEqual(w[0].category, DeprecationWarning)
+            assertEqual("testing", w[0].message.args[0])
+
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter("always")
+            shim = newshim()
+
+            0 in shim
+            shim + []
+            shim * 1
+            shim[0]
+            shim.index(0)
+            shim.count(0)
+
+            shim[0:0] = []  # doesn't warn on py2
+            del shim[0:0]  # doesn't warn on py2
+            newshim().append(6)
+            if util.version >= '3.0':  # pragma: no cover
+                newshim().clear()
+                shim.copy()
+            shim.extend([])
+            shim += []
+            shim *= 1
+            newshim().insert(0, 6)
+            shim.pop(-1)
+            newshim().remove(0)
+            newshim().reverse()
+            newshim().sort()
+
+            if util.version >= '3.0':  # pragma: no cover
+                assertEqual(19, len(w))
+            else:  # pragma: no cover
+                assertEqual(15, len(w))
+            for warn in w:
+                assertEqual(warn.category, DeprecationWarning)
+                assertEqual("testing", warn.message.args[0])