Repository: googleapis/python-api-core (python)

Issue:
paged iterators are not iterators
 **Is your feature request related to a problem? Please describe.**

The naming of the "iterator" classes & modules is misleading.

 **Describe the solution you'd like**

Add a `__next__` method so that a paged iterator can be used as an iterator.

 **Describe alternatives you've considered**

Rename classes to "iterable" instead of iterator. This would be a breaking change, though.

 **Additional context**

* Internal issue 186097906
* https://github.com/googleapis/google-cloud-python/issues/9259


Pull request description:
feat: add iterator capability to paged iterators
Closes #175.

This PR transforms *Iterator classes to actual iterators (before they were just _iterables_).

**PR checklist:**
- [x] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/python-api-core/issues/new/choose) before writing your code!  That way we can discuss the change, evaluate designs, and agree on the general idea
- [x] Ensure the tests and linter pass
- [x] Code coverage does not decrease (if any source code was changed)
- [x] Appropriate docs were updated (if necessary)




Patch:
diff --git a/google/api_core/page_iterator.py b/google/api_core/page_iterator.py
index fff3b55..49879bc 100644
--- a/google/api_core/page_iterator.py
+++ b/google/api_core/page_iterator.py
@@ -170,6 +170,8 @@ class Iterator(object):
         max_results=None,
     ):
         self._started = False
+        self.__active_iterator = None
+
         self.client = client
         """Optional[Any]: The client that created this iterator."""
         self.item_to_value = item_to_value
@@ -228,6 +230,14 @@ class Iterator(object):
         self._started = True
         return self._items_iter()
 
+    def __next__(self):
+        if self.__active_iterator is None:
+            self.__active_iterator = iter(self)
+        return next(self.__active_iterator)
+
+    # Preserve Python 2 compatibility.
+    next = __next__
+
     def _page_iter(self, increment):
         """Generator of pages of API responses.
 
diff --git a/google/api_core/page_iterator_async.py b/google/api_core/page_iterator_async.py
index a0aa41a..c072575 100644
--- a/google/api_core/page_iterator_async.py
+++ b/google/api_core/page_iterator_async.py
@@ -101,6 +101,8 @@ class AsyncIterator(abc.ABC):
         max_results=None,
     ):
         self._started = False
+        self.__active_aiterator = None
+
         self.client = client
         """Optional[Any]: The client that created this iterator."""
         self.item_to_value = item_to_value
@@ -159,6 +161,11 @@ class AsyncIterator(abc.ABC):
         self._started = True
         return self._items_aiter()
 
+    async def __anext__(self):
+        if self.__active_aiterator is None:
+            self.__active_aiterator = self.__aiter__()
+        return await self.__active_aiterator.__anext__()
+
     async def _page_aiter(self, increment):
         """Generator of pages of API responses.
 


Test patch:
diff --git a/tests/asyncio/test_page_iterator_async.py b/tests/asyncio/test_page_iterator_async.py
index 42fac2a..4abacc6 100644
--- a/tests/asyncio/test_page_iterator_async.py
+++ b/tests/asyncio/test_page_iterator_async.py
@@ -47,6 +47,33 @@ class TestAsyncIterator:
         assert iterator.next_page_token == token
         assert iterator.num_results == 0
 
+    @pytest.mark.asyncio
+    async def test_anext(self):
+        parent = mock.sentinel.parent
+        page_1 = page_iterator_async.Page(
+            parent, ("item 1.1", "item 1.2"), page_iterator_async._item_to_value_identity
+        )
+        page_2 = page_iterator_async.Page(
+            parent, ("item 2.1",), page_iterator_async._item_to_value_identity
+        )
+
+        async_iterator = PageAsyncIteratorImpl(None, None)
+        async_iterator._next_page = mock.AsyncMock(side_effect=[page_1, page_2, None])
+
+        # Consume items and check the state of the async_iterator.
+        assert async_iterator.num_results == 0
+        assert await async_iterator.__anext__() == "item 1.1"
+        assert async_iterator.num_results == 1
+
+        assert await async_iterator.__anext__() == "item 1.2"
+        assert async_iterator.num_results == 2
+
+        assert await async_iterator.__anext__() == "item 2.1"
+        assert async_iterator.num_results == 3
+
+        with pytest.raises(StopAsyncIteration):
+            await async_iterator.__anext__()
+
     def test_pages_property_starts(self):
         iterator = PageAsyncIteratorImpl(None, None)
 
diff --git a/tests/unit/test_page_iterator.py b/tests/unit/test_page_iterator.py
index 8359537..97b0657 100644
--- a/tests/unit/test_page_iterator.py
+++ b/tests/unit/test_page_iterator.py
@@ -109,6 +109,26 @@ class TestIterator(object):
         assert iterator.next_page_token == token
         assert iterator.num_results == 0
 
+    def test_next(self):
+        iterator = PageIteratorImpl(None, None)
+        page_1 = page_iterator.Page(
+            iterator, ("item 1.1", "item 1.2"), page_iterator._item_to_value_identity
+        )
+        page_2 = page_iterator.Page(
+            iterator, ("item 2.1",), page_iterator._item_to_value_identity
+        )
+        iterator._next_page = mock.Mock(side_effect=[page_1, page_2, None])
+
+        result = next(iterator)
+        assert result == "item 1.1"
+        result = next(iterator)
+        assert result == "item 1.2"
+        result = next(iterator)
+        assert result == "item 2.1"
+
+        with pytest.raises(StopIteration):
+            next(iterator)
+
     def test_pages_property_starts(self):
         iterator = PageIteratorImpl(None, None)