| """ Test Iterator Length Transparency |
| |
| Some functions or methods which accept general iterable arguments have |
| optional, more efficient code paths if they know how many items to expect. |
| For instance, map(func, iterable), will pre-allocate the exact amount of |
| space required whenever the iterable can report its length. |
| |
| The desired invariant is: len(it)==len(list(it)). |
| |
| A complication is that an iterable and iterator can be the same object. To |
| maintain the invariant, an iterator needs to dynamically update its length. |
| For instance, an iterable such as range(10) always reports its length as ten, |
| but it=iter(range(10)) starts at ten, and then goes to nine after next(it). |
| Having this capability means that map() can ignore the distinction between |
| map(func, iterable) and map(func, iter(iterable)). |
| |
| When the iterable is immutable, the implementation can straight-forwardly |
| report the original length minus the cumulative number of calls to next(). |
| This is the case for tuples, range objects, and itertools.repeat(). |
| |
| Some containers become temporarily immutable during iteration. This includes |
| dicts, sets, and collections.deque. Their implementation is equally simple |
| though they need to permanently set their length to zero whenever there is |
| an attempt to iterate after a length mutation. |
| |
| The situation slightly more involved whenever an object allows length mutation |
| during iteration. Lists and sequence iterators are dynamically updatable. |
| So, if a list is extended during iteration, the iterator will continue through |
| the new items. If it shrinks to a point before the most recent iteration, |
| then no further items are available and the length is reported at zero. |
| |
| Reversed objects can also be wrapped around mutable objects; however, any |
| appends after the current position are ignored. Any other approach leads |
| to confusion and possibly returning the same item more than once. |
| |
| The iterators not listed above, such as enumerate and the other itertools, |
| are not length transparent because they have no way to distinguish between |
| iterables that report static length and iterators whose length changes with |
| each call (i.e. the difference between enumerate('abc') and |
| enumerate(iter('abc')). |
| |
| """ |
| |
| import unittest |
| from test import support |
| from itertools import repeat |
| from collections import deque |
| from builtins import len as _len |
| |
| n = 10 |
| |
| def len(obj): |
| try: |
| return _len(obj) |
| except TypeError: |
| try: |
| # note: this is an internal undocumented API, |
| # don't rely on it in your own programs |
| return obj.__length_hint__() |
| except AttributeError: |
| raise TypeError |
| |
| class TestInvariantWithoutMutations: |
| |
| def test_invariant(self): |
| it = self.it |
| for i in reversed(range(1, n+1)): |
| self.assertEqual(len(it), i) |
| next(it) |
| self.assertEqual(len(it), 0) |
| self.assertRaises(StopIteration, next, it) |
| self.assertEqual(len(it), 0) |
| |
| class TestTemporarilyImmutable(TestInvariantWithoutMutations): |
| |
| def test_immutable_during_iteration(self): |
| # objects such as deques, sets, and dictionaries enforce |
| # length immutability during iteration |
| |
| it = self.it |
| self.assertEqual(len(it), n) |
| next(it) |
| self.assertEqual(len(it), n-1) |
| self.mutate() |
| self.assertRaises(RuntimeError, next, it) |
| self.assertEqual(len(it), 0) |
| |
| ## ------- Concrete Type Tests ------- |
| |
| class TestRepeat(TestInvariantWithoutMutations, unittest.TestCase): |
| |
| def setUp(self): |
| self.it = repeat(None, n) |
| |
| def test_no_len_for_infinite_repeat(self): |
| # The repeat() object can also be infinite |
| self.assertRaises(TypeError, len, repeat(None)) |
| |
| class TestXrange(TestInvariantWithoutMutations, unittest.TestCase): |
| |
| def setUp(self): |
| self.it = iter(range(n)) |
| |
| class TestXrangeCustomReversed(TestInvariantWithoutMutations, unittest.TestCase): |
| |
| def setUp(self): |
| self.it = reversed(range(n)) |
| |
| class TestTuple(TestInvariantWithoutMutations, unittest.TestCase): |
| |
| def setUp(self): |
| self.it = iter(tuple(range(n))) |
| |
| ## ------- Types that should not be mutated during iteration ------- |
| |
| class TestDeque(TestTemporarilyImmutable, unittest.TestCase): |
| |
| def setUp(self): |
| d = deque(range(n)) |
| self.it = iter(d) |
| self.mutate = d.pop |
| |
| class TestDequeReversed(TestTemporarilyImmutable, unittest.TestCase): |
| |
| def setUp(self): |
| d = deque(range(n)) |
| self.it = reversed(d) |
| self.mutate = d.pop |
| |
| class TestDictKeys(TestTemporarilyImmutable, unittest.TestCase): |
| |
| def setUp(self): |
| d = dict.fromkeys(range(n)) |
| self.it = iter(d) |
| self.mutate = d.popitem |
| |
| class TestDictItems(TestTemporarilyImmutable, unittest.TestCase): |
| |
| def setUp(self): |
| d = dict.fromkeys(range(n)) |
| self.it = iter(d.items()) |
| self.mutate = d.popitem |
| |
| class TestDictValues(TestTemporarilyImmutable, unittest.TestCase): |
| |
| def setUp(self): |
| d = dict.fromkeys(range(n)) |
| self.it = iter(d.values()) |
| self.mutate = d.popitem |
| |
| class TestSet(TestTemporarilyImmutable, unittest.TestCase): |
| |
| def setUp(self): |
| d = set(range(n)) |
| self.it = iter(d) |
| self.mutate = d.pop |
| |
| ## ------- Types that can mutate during iteration ------- |
| |
| class TestList(TestInvariantWithoutMutations, unittest.TestCase): |
| |
| def setUp(self): |
| self.it = iter(range(n)) |
| |
| def test_mutation(self): |
| d = list(range(n)) |
| it = iter(d) |
| next(it) |
| next(it) |
| self.assertEqual(len(it), n-2) |
| d.append(n) |
| self.assertEqual(len(it), n-1) # grow with append |
| d[1:] = [] |
| self.assertEqual(len(it), 0) |
| self.assertEqual(list(it), []) |
| d.extend(range(20)) |
| self.assertEqual(len(it), 0) |
| |
| class TestListReversed(TestInvariantWithoutMutations, unittest.TestCase): |
| |
| def setUp(self): |
| self.it = reversed(range(n)) |
| |
| def test_mutation(self): |
| d = list(range(n)) |
| it = reversed(d) |
| next(it) |
| next(it) |
| self.assertEqual(len(it), n-2) |
| d.append(n) |
| self.assertEqual(len(it), n-2) # ignore append |
| d[1:] = [] |
| self.assertEqual(len(it), 0) |
| self.assertEqual(list(it), []) # confirm invariant |
| d.extend(range(20)) |
| self.assertEqual(len(it), 0) |
| |
| ## -- Check to make sure exceptions are not suppressed by __length_hint__() |
| |
| |
| class BadLen(object): |
| def __iter__(self): return iter(range(10)) |
| def __len__(self): |
| raise RuntimeError('hello') |
| |
| class BadLengthHint(object): |
| def __iter__(self): return iter(range(10)) |
| def __length_hint__(self): |
| raise RuntimeError('hello') |
| |
| class NoneLengthHint(object): |
| def __iter__(self): return iter(range(10)) |
| def __length_hint__(self): |
| return None |
| |
| class TestLengthHintExceptions(unittest.TestCase): |
| |
| def test_issue1242657(self): |
| self.assertRaises(RuntimeError, list, BadLen()) |
| self.assertRaises(RuntimeError, list, BadLengthHint()) |
| self.assertRaises(RuntimeError, [].extend, BadLen()) |
| self.assertRaises(RuntimeError, [].extend, BadLengthHint()) |
| b = bytearray(range(10)) |
| self.assertRaises(RuntimeError, b.extend, BadLen()) |
| self.assertRaises(RuntimeError, b.extend, BadLengthHint()) |
| |
| def test_invalid_hint(self): |
| # Make sure an invalid result doesn't muck-up the works |
| self.assertEqual(list(NoneLengthHint()), list(range(10))) |
| |
| |
| if __name__ == "__main__": |
| unittest.main() |