# Copyright 2015 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import math import types import mock import pytest from google.api_core import page_iterator def test__do_nothing_page_start(): assert page_iterator._do_nothing_page_start(None, None, None) is None class TestPage(object): def test_constructor(self): parent = mock.sentinel.parent item_to_value = mock.sentinel.item_to_value page = page_iterator.Page(parent, (1, 2, 3), item_to_value) assert page.num_items == 3 assert page.remaining == 3 assert page._parent is parent assert page._item_to_value is item_to_value assert page.raw_page is None def test___iter__(self): page = page_iterator.Page(None, (), None, None) assert iter(page) is page def test_iterator_calls_parent_item_to_value(self): parent = mock.sentinel.parent item_to_value = mock.Mock( side_effect=lambda iterator, value: value, spec=["__call__"] ) page = page_iterator.Page(parent, (10, 11, 12), item_to_value) page._remaining = 100 assert item_to_value.call_count == 0 assert page.remaining == 100 assert next(page) == 10 assert item_to_value.call_count == 1 item_to_value.assert_called_with(parent, 10) assert page.remaining == 99 assert next(page) == 11 assert item_to_value.call_count == 2 item_to_value.assert_called_with(parent, 11) assert page.remaining == 98 assert next(page) == 12 assert item_to_value.call_count == 3 item_to_value.assert_called_with(parent, 12) assert page.remaining == 97 def test_raw_page(self): parent = mock.sentinel.parent item_to_value = mock.sentinel.item_to_value raw_page = mock.sentinel.raw_page page = page_iterator.Page(parent, (1, 2, 3), item_to_value, raw_page=raw_page) assert page.raw_page is raw_page with pytest.raises(AttributeError): page.raw_page = None class PageIteratorImpl(page_iterator.Iterator): def _next_page(self): return mock.create_autospec(page_iterator.Page, instance=True) class TestIterator(object): def test_constructor(self): client = mock.sentinel.client item_to_value = mock.sentinel.item_to_value token = "ab13nceor03" max_results = 1337 iterator = PageIteratorImpl( client, item_to_value, page_token=token, max_results=max_results ) assert not iterator._started assert iterator.client is client assert iterator.item_to_value == item_to_value assert iterator.max_results == max_results # Changing attributes. assert iterator.page_number == 0 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) assert not iterator._started assert isinstance(iterator.pages, types.GeneratorType) assert iterator._started def test_pages_property_restart(self): iterator = PageIteratorImpl(None, None) assert iterator.pages # Make sure we cannot restart. with pytest.raises(ValueError): assert iterator.pages def test__page_iter_increment(self): iterator = PageIteratorImpl(None, None) page = page_iterator.Page( iterator, ("item",), page_iterator._item_to_value_identity ) iterator._next_page = mock.Mock(side_effect=[page, None]) assert iterator.num_results == 0 page_iter = iterator._page_iter(increment=True) next(page_iter) assert iterator.num_results == 1 def test__page_iter_no_increment(self): iterator = PageIteratorImpl(None, None) assert iterator.num_results == 0 page_iter = iterator._page_iter(increment=False) next(page_iter) # results should still be 0 after fetching a page. assert iterator.num_results == 0 def test__items_iter(self): # Items to be returned. item1 = 17 item2 = 100 item3 = 211 # Make pages from mock responses parent = mock.sentinel.parent page1 = page_iterator.Page( parent, (item1, item2), page_iterator._item_to_value_identity ) page2 = page_iterator.Page( parent, (item3,), page_iterator._item_to_value_identity ) iterator = PageIteratorImpl(None, None) iterator._next_page = mock.Mock(side_effect=[page1, page2, None]) items_iter = iterator._items_iter() assert isinstance(items_iter, types.GeneratorType) # Consume items and check the state of the iterator. assert iterator.num_results == 0 assert next(items_iter) == item1 assert iterator.num_results == 1 assert next(items_iter) == item2 assert iterator.num_results == 2 assert next(items_iter) == item3 assert iterator.num_results == 3 with pytest.raises(StopIteration): next(items_iter) def test___iter__(self): iterator = PageIteratorImpl(None, None) iterator._next_page = mock.Mock(side_effect=[(1, 2), (3,), None]) assert not iterator._started result = list(iterator) assert result == [1, 2, 3] assert iterator._started def test___iter__restart(self): iterator = PageIteratorImpl(None, None) iter(iterator) # Make sure we cannot restart. with pytest.raises(ValueError): iter(iterator) def test___iter___restart_after_page(self): iterator = PageIteratorImpl(None, None) assert iterator.pages # Make sure we cannot restart after starting the page iterator with pytest.raises(ValueError): iter(iterator) class TestHTTPIterator(object): def test_constructor(self): client = mock.sentinel.client path = "/foo" iterator = page_iterator.HTTPIterator( client, mock.sentinel.api_request, path, mock.sentinel.item_to_value ) assert not iterator._started assert iterator.client is client assert iterator.path == path assert iterator.item_to_value is mock.sentinel.item_to_value assert iterator._items_key == "items" assert iterator.max_results is None assert iterator.extra_params == {} assert iterator._page_start == page_iterator._do_nothing_page_start # Changing attributes. assert iterator.page_number == 0 assert iterator.next_page_token is None assert iterator.num_results == 0 assert iterator._page_size is None def test_constructor_w_extra_param_collision(self): extra_params = {"pageToken": "val"} with pytest.raises(ValueError): page_iterator.HTTPIterator( mock.sentinel.client, mock.sentinel.api_request, mock.sentinel.path, mock.sentinel.item_to_value, extra_params=extra_params, ) def test_iterate(self): path = "/foo" item1 = {"name": "1"} item2 = {"name": "2"} api_request = mock.Mock(return_value={"items": [item1, item2]}) iterator = page_iterator.HTTPIterator( mock.sentinel.client, api_request, path=path, item_to_value=page_iterator._item_to_value_identity, ) assert iterator.num_results == 0 items_iter = iter(iterator) val1 = next(items_iter) assert val1 == item1 assert iterator.num_results == 1 val2 = next(items_iter) assert val2 == item2 assert iterator.num_results == 2 with pytest.raises(StopIteration): next(items_iter) api_request.assert_called_once_with(method="GET", path=path, query_params={}) def test__has_next_page_new(self): iterator = page_iterator.HTTPIterator( mock.sentinel.client, mock.sentinel.api_request, mock.sentinel.path, mock.sentinel.item_to_value, ) # The iterator should *always* indicate that it has a next page # when created so that it can fetch the initial page. assert iterator._has_next_page() def test__has_next_page_without_token(self): iterator = page_iterator.HTTPIterator( mock.sentinel.client, mock.sentinel.api_request, mock.sentinel.path, mock.sentinel.item_to_value, ) iterator.page_number = 1 # The iterator should not indicate that it has a new page if the # initial page has been requested and there's no page token. assert not iterator._has_next_page() def test__has_next_page_w_number_w_token(self): iterator = page_iterator.HTTPIterator( mock.sentinel.client, mock.sentinel.api_request, mock.sentinel.path, mock.sentinel.item_to_value, ) iterator.page_number = 1 iterator.next_page_token = mock.sentinel.token # The iterator should indicate that it has a new page if the # initial page has been requested and there's is a page token. assert iterator._has_next_page() def test__has_next_page_w_max_results_not_done(self): iterator = page_iterator.HTTPIterator( mock.sentinel.client, mock.sentinel.api_request, mock.sentinel.path, mock.sentinel.item_to_value, max_results=3, page_token=mock.sentinel.token, ) iterator.page_number = 1 # The iterator should indicate that it has a new page if there # is a page token and it has not consumed more than max_results. assert iterator.num_results < iterator.max_results assert iterator._has_next_page() def test__has_next_page_w_max_results_done(self): iterator = page_iterator.HTTPIterator( mock.sentinel.client, mock.sentinel.api_request, mock.sentinel.path, mock.sentinel.item_to_value, max_results=3, page_token=mock.sentinel.token, ) iterator.page_number = 1 iterator.num_results = 3 # The iterator should not indicate that it has a new page if there # if it has consumed more than max_results. assert iterator.num_results == iterator.max_results assert not iterator._has_next_page() def test__get_query_params_no_token(self): iterator = page_iterator.HTTPIterator( mock.sentinel.client, mock.sentinel.api_request, mock.sentinel.path, mock.sentinel.item_to_value, ) assert iterator._get_query_params() == {} def test__get_query_params_w_token(self): iterator = page_iterator.HTTPIterator( mock.sentinel.client, mock.sentinel.api_request, mock.sentinel.path, mock.sentinel.item_to_value, ) iterator.next_page_token = "token" assert iterator._get_query_params() == {"pageToken": iterator.next_page_token} def test__get_query_params_w_max_results(self): max_results = 3 iterator = page_iterator.HTTPIterator( mock.sentinel.client, mock.sentinel.api_request, mock.sentinel.path, mock.sentinel.item_to_value, max_results=max_results, ) iterator.num_results = 1 local_max = max_results - iterator.num_results assert iterator._get_query_params() == {"maxResults": local_max} def test__get_query_params_extra_params(self): extra_params = {"key": "val"} iterator = page_iterator.HTTPIterator( mock.sentinel.client, mock.sentinel.api_request, mock.sentinel.path, mock.sentinel.item_to_value, extra_params=extra_params, ) assert iterator._get_query_params() == extra_params def test__get_next_page_response_with_post(self): path = "/foo" page_response = {"items": ["one", "two"]} api_request = mock.Mock(return_value=page_response) iterator = page_iterator.HTTPIterator( mock.sentinel.client, api_request, path=path, item_to_value=page_iterator._item_to_value_identity, ) iterator._HTTP_METHOD = "POST" response = iterator._get_next_page_response() assert response == page_response api_request.assert_called_once_with(method="POST", path=path, data={}) def test__get_next_page_bad_http_method(self): iterator = page_iterator.HTTPIterator( mock.sentinel.client, mock.sentinel.api_request, mock.sentinel.path, mock.sentinel.item_to_value, ) iterator._HTTP_METHOD = "NOT-A-VERB" with pytest.raises(ValueError): iterator._get_next_page_response() @pytest.mark.parametrize( "page_size,max_results,pages", [(3, None, False), (3, 8, False), (3, None, True), (3, 8, True)], ) def test_page_size_items(self, page_size, max_results, pages): path = "/foo" NITEMS = 10 n = [0] # blast you python 2! def api_request(*args, **kw): assert not args query_params = dict( maxResults=( page_size if max_results is None else min(page_size, max_results - n[0]) ) ) if n[0]: query_params.update(pageToken="test") assert kw == {"method": "GET", "path": "/foo", "query_params": query_params} n_items = min(kw["query_params"]["maxResults"], NITEMS - n[0]) items = [dict(name=str(i + n[0])) for i in range(n_items)] n[0] += n_items result = dict(items=items) if n[0] < NITEMS: result.update(nextPageToken="test") return result iterator = page_iterator.HTTPIterator( mock.sentinel.client, api_request, path=path, item_to_value=page_iterator._item_to_value_identity, page_size=page_size, max_results=max_results, ) assert iterator.num_results == 0 n_results = max_results if max_results is not None else NITEMS if pages: items_iter = iter(iterator.pages) npages = int(math.ceil(float(n_results) / page_size)) for ipage in range(npages): assert list(next(items_iter)) == [ dict(name=str(i)) for i in range( ipage * page_size, min((ipage + 1) * page_size, n_results), ) ] else: items_iter = iter(iterator) for i in range(n_results): assert next(items_iter) == dict(name=str(i)) assert iterator.num_results == i + 1 with pytest.raises(StopIteration): next(items_iter) class TestGRPCIterator(object): def test_constructor(self): client = mock.sentinel.client items_field = "items" iterator = page_iterator.GRPCIterator( client, mock.sentinel.method, mock.sentinel.request, items_field ) assert not iterator._started assert iterator.client is client assert iterator.max_results is None assert iterator.item_to_value is page_iterator._item_to_value_identity assert iterator._method == mock.sentinel.method assert iterator._request == mock.sentinel.request assert iterator._items_field == items_field assert ( iterator._request_token_field == page_iterator.GRPCIterator._DEFAULT_REQUEST_TOKEN_FIELD ) assert ( iterator._response_token_field == page_iterator.GRPCIterator._DEFAULT_RESPONSE_TOKEN_FIELD ) # Changing attributes. assert iterator.page_number == 0 assert iterator.next_page_token is None assert iterator.num_results == 0 def test_constructor_options(self): client = mock.sentinel.client items_field = "items" request_field = "request" response_field = "response" iterator = page_iterator.GRPCIterator( client, mock.sentinel.method, mock.sentinel.request, items_field, item_to_value=mock.sentinel.item_to_value, request_token_field=request_field, response_token_field=response_field, max_results=42, ) assert iterator.client is client assert iterator.max_results == 42 assert iterator.item_to_value is mock.sentinel.item_to_value assert iterator._method == mock.sentinel.method assert iterator._request == mock.sentinel.request assert iterator._items_field == items_field assert iterator._request_token_field == request_field assert iterator._response_token_field == response_field def test_iterate(self): request = mock.Mock(spec=["page_token"], page_token=None) response1 = mock.Mock(items=["a", "b"], next_page_token="1") response2 = mock.Mock(items=["c"], next_page_token="2") response3 = mock.Mock(items=["d"], next_page_token="") method = mock.Mock(side_effect=[response1, response2, response3]) iterator = page_iterator.GRPCIterator( mock.sentinel.client, method, request, "items" ) assert iterator.num_results == 0 items = list(iterator) assert items == ["a", "b", "c", "d"] method.assert_called_with(request) assert method.call_count == 3 assert request.page_token == "2" def test_iterate_with_max_results(self): request = mock.Mock(spec=["page_token"], page_token=None) response1 = mock.Mock(items=["a", "b"], next_page_token="1") response2 = mock.Mock(items=["c"], next_page_token="2") response3 = mock.Mock(items=["d"], next_page_token="") method = mock.Mock(side_effect=[response1, response2, response3]) iterator = page_iterator.GRPCIterator( mock.sentinel.client, method, request, "items", max_results=3 ) assert iterator.num_results == 0 items = list(iterator) assert items == ["a", "b", "c"] assert iterator.num_results == 3 method.assert_called_with(request) assert method.call_count == 2 assert request.page_token == "1" class GAXPageIterator(object): """Fake object that matches gax.PageIterator""" def __init__(self, pages, page_token=None): self._pages = iter(pages) self.page_token = page_token def next(self): return next(self._pages) __next__ = next class TestGAXIterator(object): def test_constructor(self): client = mock.sentinel.client token = "zzzyy78kl" page_iter = GAXPageIterator((), page_token=token) item_to_value = page_iterator._item_to_value_identity max_results = 1337 iterator = page_iterator._GAXIterator( client, page_iter, item_to_value, max_results=max_results ) assert not iterator._started assert iterator.client is client assert iterator.item_to_value is item_to_value assert iterator.max_results == max_results assert iterator._gax_page_iter is page_iter # Changing attributes. assert iterator.page_number == 0 assert iterator.next_page_token == token assert iterator.num_results == 0 def test__next_page(self): page_items = (29, 31) page_token = "2sde98ds2s0hh" page_iter = GAXPageIterator([page_items], page_token=page_token) iterator = page_iterator._GAXIterator( mock.sentinel.client, page_iter, page_iterator._item_to_value_identity ) page = iterator._next_page() assert iterator.next_page_token == page_token assert isinstance(page, page_iterator.Page) assert list(page) == list(page_items) next_page = iterator._next_page() assert next_page is None