258 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			258 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Python
		
	
	
	
| """ 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 xrange(10) always reports its length as ten,
 | |
| but it=iter(xrange(10)) starts at ten, and then goes to nine after it.next().
 | |
| 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, xrange 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 test_support
 | |
| from itertools import repeat
 | |
| from collections import deque
 | |
| from __builtin__ 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(unittest.TestCase):
 | |
| 
 | |
|     def test_invariant(self):
 | |
|         it = self.it
 | |
|         for i in reversed(xrange(1, n+1)):
 | |
|             self.assertEqual(len(it), i)
 | |
|             it.next()
 | |
|         self.assertEqual(len(it), 0)
 | |
|         self.assertRaises(StopIteration, it.next)
 | |
|         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)
 | |
|         it.next()
 | |
|         self.assertEqual(len(it), n-1)
 | |
|         self.mutate()
 | |
|         self.assertRaises(RuntimeError, it.next)
 | |
|         self.assertEqual(len(it), 0)
 | |
| 
 | |
| ## ------- Concrete Type Tests -------
 | |
| 
 | |
| class TestRepeat(TestInvariantWithoutMutations):
 | |
| 
 | |
|     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):
 | |
| 
 | |
|     def setUp(self):
 | |
|         self.it = iter(xrange(n))
 | |
| 
 | |
| class TestXrangeCustomReversed(TestInvariantWithoutMutations):
 | |
| 
 | |
|     def setUp(self):
 | |
|         self.it = reversed(xrange(n))
 | |
| 
 | |
| class TestTuple(TestInvariantWithoutMutations):
 | |
| 
 | |
|     def setUp(self):
 | |
|         self.it = iter(tuple(xrange(n)))
 | |
| 
 | |
| ## ------- Types that should not be mutated during iteration -------
 | |
| 
 | |
| class TestDeque(TestTemporarilyImmutable):
 | |
| 
 | |
|     def setUp(self):
 | |
|         d = deque(xrange(n))
 | |
|         self.it = iter(d)
 | |
|         self.mutate = d.pop
 | |
| 
 | |
| class TestDequeReversed(TestTemporarilyImmutable):
 | |
| 
 | |
|     def setUp(self):
 | |
|         d = deque(xrange(n))
 | |
|         self.it = reversed(d)
 | |
|         self.mutate = d.pop
 | |
| 
 | |
| class TestDictKeys(TestTemporarilyImmutable):
 | |
| 
 | |
|     def setUp(self):
 | |
|         d = dict.fromkeys(xrange(n))
 | |
|         self.it = iter(d)
 | |
|         self.mutate = d.popitem
 | |
| 
 | |
| class TestDictItems(TestTemporarilyImmutable):
 | |
| 
 | |
|     def setUp(self):
 | |
|         d = dict.fromkeys(xrange(n))
 | |
|         self.it = d.iteritems()
 | |
|         self.mutate = d.popitem
 | |
| 
 | |
| class TestDictValues(TestTemporarilyImmutable):
 | |
| 
 | |
|     def setUp(self):
 | |
|         d = dict.fromkeys(xrange(n))
 | |
|         self.it = d.itervalues()
 | |
|         self.mutate = d.popitem
 | |
| 
 | |
| class TestSet(TestTemporarilyImmutable):
 | |
| 
 | |
|     def setUp(self):
 | |
|         d = set(xrange(n))
 | |
|         self.it = iter(d)
 | |
|         self.mutate = d.pop
 | |
| 
 | |
| ## ------- Types that can mutate during iteration -------
 | |
| 
 | |
| class TestList(TestInvariantWithoutMutations):
 | |
| 
 | |
|     def setUp(self):
 | |
|         self.it = iter(range(n))
 | |
| 
 | |
|     def test_mutation(self):
 | |
|         d = range(n)
 | |
|         it = iter(d)
 | |
|         it.next()
 | |
|         it.next()
 | |
|         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(xrange(20))
 | |
|         self.assertEqual(len(it), 0)
 | |
| 
 | |
| class TestListReversed(TestInvariantWithoutMutations):
 | |
| 
 | |
|     def setUp(self):
 | |
|         self.it = reversed(range(n))
 | |
| 
 | |
|     def test_mutation(self):
 | |
|         d = range(n)
 | |
|         it = reversed(d)
 | |
|         it.next()
 | |
|         it.next()
 | |
|         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(xrange(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())
 | |
|         self.assertRaises(RuntimeError, zip, BadLen())
 | |
|         self.assertRaises(RuntimeError, zip, BadLengthHint())
 | |
|         self.assertRaises(RuntimeError, filter, None, BadLen())
 | |
|         self.assertRaises(RuntimeError, filter, None, BadLengthHint())
 | |
|         self.assertRaises(RuntimeError, map, chr, BadLen())
 | |
|         self.assertRaises(RuntimeError, map, chr, 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)))
 | |
| 
 | |
| 
 | |
| def test_main():
 | |
|     unittests = [
 | |
|         TestRepeat,
 | |
|         TestXrange,
 | |
|         TestXrangeCustomReversed,
 | |
|         TestTuple,
 | |
|         TestDeque,
 | |
|         TestDequeReversed,
 | |
|         TestDictKeys,
 | |
|         TestDictItems,
 | |
|         TestDictValues,
 | |
|         TestSet,
 | |
|         TestList,
 | |
|         TestListReversed,
 | |
|         TestLengthHintExceptions,
 | |
|     ]
 | |
|     test_support.run_unittest(*unittests)
 | |
| 
 | |
| if __name__ == "__main__":
 | |
|     test_main()
 |