Issue: QueryResultWrapper iterator broken
Author: wkschwartz
State: closed
Created: 2015-03-17 20:48:37

## Summary

Peewee's `QueryResultWrapper.__next__` does not raise `StopIteration` exceptions on subsequent calls after the first `StopIteration` in contrast to the Python iterator protocol (documentation quoted/linked below). Instead, `QueryResultWrapper` iterators raise a `sqlite3.ProgrammingError`. The following test case demonstrates this.
## Test case

``` python
import peewee

database = peewee.SqliteDatabase(":memory:")
class Table(peewee.Model):
    id = peewee.PrimaryKeyField()
    class Meta(object):
        database = database
database.create_tables([Table])

with database.atomic():
    Table.insert_many(({'id': i} for i in range(5))).execute()

iterator = iter(Table.select())

# The first use of `iterator` works fine
for table_instance in iterator:
    pass
# The next line produces the wrong error type in violation of the iterator protocol.
next(iterator)
```
## Traceback

``` python
---------------------------------------------------------------------------
ProgrammingError                          Traceback (most recent call last)
<ipython-input-9-edd1adac5cd0> in <module>()
----> 1 next(iterable)

venv/lib/python3.4/site-packages/peewee.py in next(self)
   1795             return inst
   1796 
-> 1797         obj = self.iterate()
   1798         self._result_cache.append(obj)
   1799         self.__ct += 1

venv/lib/python3.4/site-packages/peewee.py in iterate(self)
   1774 
   1775     def iterate(self):
-> 1776         row = self.cursor.fetchone()
   1777         if not row:
   1778             self._populated = True

ProgrammingError: Cannot operate on a closed cursor.
```
## Expected result: the iterator protocol

Subsequent calls to `next` (either explicitly or through a `for` loop) should raise `StopIteration` exceptions.

From the documentation for the [Python 3 iterator protocol](https://docs.python.org/3/library/stdtypes.html#iterator.__next__):

> Once an iterator’s [`__next__()`](https://docs.python.org/3/library/stdtypes.html#iterator.__next__) method raises [`StopIteration`](https://docs.python.org/3/library/exceptions.html#StopIteration), it must continue to do so on subsequent calls. Implementations that do not obey this property are deemed broken.

From the [Python 2 iterator protocol](https://docs.python.org/2/library/stdtypes.html#iterator.next):

> The intention of the protocol is that once an iterator’s next() method raises StopIteration, it will continue to do so on subsequent calls. Implementations that do not obey this property are deemed broken. (This constraint was added in Python 2.3; in Python 2.2, various iterators are broken according to this rule.)
## Suggested fix

Wrapping the `row = self.cursor.fetchone()` (see traceback above) line in

``` python
try:
    row = self.cursor.fetchone()
except sqlite3.ProgrammingError:
    raise StopIteration
```

would do the trick. A `try` block is fine since loops won't trigger the exception handling -- it's only if you erroneously keep using the iterator after the `StopIteration` that you'll get an exception, and `try` blocks are [efficient when there's no exception to catch](https://docs.python.org/3/faq/design.html#how-fast-are-exceptions).


This is a more correct fix, but I'll have to think through the edge cases:

``` diff
diff --git a/peewee.py b/peewee.py
index 0a640c3..f630bce 100644
--- a/peewee.py
+++ b/peewee.py
@@ -1837,6 +1837,8 @@ class QueryResultWrapper(object):
             inst = self._result_cache[self.__idx]
             self.__idx += 1
             return inst
+        elif self._populated:
+            raise StopIteration

         obj = self.iterate()
         self._result_cache.append(obj)
```


Just out of curiosity, why are you wrapping the select call in `iter`, e.g.:

``` python
iterator = iter(Table.select())
```

How did you come about this bug in production?


The problem arose when I wrapped a query in `map` on Python 3. I had forgotten to wrap the whole thing in `list` so I could iterate over the object a second time.