Skip to content

Commit 86f60f7

Browse files
authored
Merge pull request #2558 from dhermes/no-more-iterator-subclasses
Removing Iterator and Page subclasses.
2 parents d22d58c + 5c468ae commit 86f60f7

File tree

2 files changed

+149
-118
lines changed

2 files changed

+149
-118
lines changed

packages/google-cloud-core/google/cloud/iterator.py

Lines changed: 93 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -17,37 +17,35 @@
1717
These iterators simplify the process of paging through API responses
1818
where the response is a list of results with a ``nextPageToken``.
1919
20-
To make an iterator work, you may need to override the
21-
``ITEMS_KEY`` class attribute so that a given response (containing a page of
22-
results) can be parsed into an iterable page of the actual objects you want::
23-
24-
class MyIterator(Iterator):
25-
26-
ITEMS_KEY = 'blocks'
27-
28-
def _item_to_value(self, item):
29-
my_item = MyItemClass(other_arg=True)
30-
my_item._set_properties(item)
31-
return my_item
32-
33-
You then can use this to get **all** the results from a resource::
34-
35-
>>> iterator = MyIterator(...)
20+
To make an iterator work, you'll need to provide a way to convert a JSON
21+
item returned from the API into the object of your choice (via
22+
``item_to_value``). You also may need to specify a custom ``items_key`` so
23+
that a given response (containing a page of results) can be parsed into an
24+
iterable page of the actual objects you want. You then can use this to get
25+
**all** the results from a resource::
26+
27+
>>> def item_to_value(iterator, item):
28+
... my_item = MyItemClass(iterator.client, other_arg=True)
29+
... my_item._set_properties(item)
30+
... return my_item
31+
...
32+
>>> iterator = Iterator(..., items_key='blocks',
33+
... item_to_value=item_to_value)
3634
>>> list(iterator) # Convert to a list (consumes all values).
3735
3836
Or you can walk your way through items and call off the search early if
3937
you find what you're looking for (resulting in possibly fewer
4038
requests)::
4139
42-
>>> for my_item in MyIterator(...):
40+
>>> for my_item in Iterator(...):
4341
... print(my_item.name)
4442
... if not my_item.is_valid:
4543
... break
4644
4745
When iterating, not every new item will send a request to the server.
4846
To monitor these requests, track the current page of the iterator::
4947
50-
>>> iterator = MyIterator(...)
48+
>>> iterator = Iterator(...)
5149
>>> iterator.page_number
5250
0
5351
>>> next(iterator)
@@ -58,6 +56,8 @@ def _item_to_value(self, item):
5856
1
5957
>>> next(iterator)
6058
<MyItemClass at 0x7f1d3cccfe90>
59+
>>> iterator.page_number
60+
1
6161
>>> iterator.page.remaining
6262
0
6363
>>> next(iterator)
@@ -70,7 +70,7 @@ def _item_to_value(self, item):
7070
It's also possible to consume an entire page and handle the paging process
7171
manually::
7272
73-
>>> iterator = MyIterator(...)
73+
>>> iterator = Iterator(...)
7474
>>> # Manually pull down the first page.
7575
>>> iterator.update_page()
7676
>>> items = list(iterator.page)
@@ -96,6 +96,8 @@ def _item_to_value(self, item):
9696
]
9797
>>>
9898
>>> # When there are no more results
99+
>>> iterator.next_page_token is None
100+
True
99101
>>> iterator.update_page()
100102
>>> iterator.page is None
101103
True
@@ -113,6 +115,26 @@ def _item_to_value(self, item):
113115
_PAGE_ERR_TEMPLATE = (
114116
'Tried to update the page while current page (%r) still has %d '
115117
'items remaining.')
118+
DEFAULT_ITEMS_KEY = 'items'
119+
"""The dictionary key used to retrieve items from each response."""
120+
121+
122+
# pylint: disable=unused-argument
123+
def _do_nothing_page_start(iterator, page, response):
124+
"""Helper to provide custom behavior after a :class:`Page` is started.
125+
126+
This is a do-nothing stand-in as the default value.
127+
128+
:type iterator: :class:`Iterator`
129+
:param iterator: An iterator that holds some request info.
130+
131+
:type page: :class:`Page`
132+
:param page: The page that was just created.
133+
134+
:type response: dict
135+
:param response: The JSON API response for a page.
136+
"""
137+
# pylint: enable=unused-argument
116138

117139

118140
class Page(object):
@@ -127,15 +149,21 @@ class Page(object):
127149
:type items_key: str
128150
:param items_key: The dictionary key used to retrieve items
129151
from the response.
152+
153+
:type item_to_value: callable
154+
:param item_to_value: Callable to convert an item from JSON
155+
into the native object. Assumed signature
156+
takes an :class:`Iterator` and a dictionary
157+
holding a single item.
130158
"""
131159

132-
def __init__(self, parent, response, items_key):
160+
def __init__(self, parent, response, items_key, item_to_value):
133161
self._parent = parent
134162
items = response.get(items_key, ())
135163
self._num_items = len(items)
136164
self._remaining = self._num_items
137165
self._item_iter = iter(items)
138-
self.response = response
166+
self._item_to_value = item_to_value
139167

140168
@property
141169
def num_items(self):
@@ -162,7 +190,7 @@ def __iter__(self):
162190
def next(self):
163191
"""Get the next value in the page."""
164192
item = six.next(self._item_iter)
165-
result = self._parent._item_to_value(item)
193+
result = self._item_to_value(self._parent, item)
166194
# Since we've successfully got the next value from the
167195
# iterator, we update the number of remaining.
168196
self._remaining -= 1
@@ -175,12 +203,23 @@ def next(self):
175203
class Iterator(object):
176204
"""A generic class for iterating through Cloud JSON APIs list responses.
177205
178-
Sub-classes need to over-write :attr:`ITEMS_KEY` and to define
179-
:meth:`_item_to_value`.
180-
181206
:type client: :class:`~google.cloud.client.Client`
182207
:param client: The client, which owns a connection to make requests.
183208
209+
:type path: str
210+
:param path: The path to query for the list of items. Defaults
211+
to :attr:`PATH` on the current iterator class.
212+
213+
:type item_to_value: callable
214+
:param item_to_value: Callable to convert an item from JSON
215+
into the native object. Assumed signature
216+
takes an :class:`Iterator` and a dictionary
217+
holding a single item.
218+
219+
:type items_key: str
220+
:param items_key: (Optional) The key used to grab retrieved items from an
221+
API response. Defaults to :data:`DEFAULT_ITEMS_KEY`.
222+
184223
:type page_token: str
185224
:param page_token: (Optional) A token identifying a page in a result set.
186225
@@ -191,26 +230,32 @@ class Iterator(object):
191230
:param extra_params: (Optional) Extra query string parameters for the
192231
API call.
193232
194-
:type path: str
195-
:param path: (Optional) The path to query for the list of items. Defaults
196-
to :attr:`PATH` on the current iterator class.
233+
:type page_start: callable
234+
:param page_start: (Optional) Callable to provide any special behavior
235+
after a new page has been created. Assumed signature
236+
takes the :class:`Iterator` that started the page,
237+
the :class:`Page` that was started and the dictionary
238+
containing the page response.
197239
"""
198240

199-
PAGE_TOKEN = 'pageToken'
200-
MAX_RESULTS = 'maxResults'
201-
RESERVED_PARAMS = frozenset([PAGE_TOKEN, MAX_RESULTS])
202-
PATH = None
203-
ITEMS_KEY = 'items'
204-
"""The dictionary key used to retrieve items from each response."""
205-
_PAGE_CLASS = Page
206-
207-
def __init__(self, client, page_token=None, max_results=None,
208-
extra_params=None, path=None):
209-
self.extra_params = extra_params or {}
210-
self._verify_params()
211-
self.max_results = max_results
241+
_PAGE_TOKEN = 'pageToken'
242+
_MAX_RESULTS = 'maxResults'
243+
_RESERVED_PARAMS = frozenset([_PAGE_TOKEN, _MAX_RESULTS])
244+
245+
def __init__(self, client, path, item_to_value,
246+
items_key=DEFAULT_ITEMS_KEY,
247+
page_token=None, max_results=None, extra_params=None,
248+
page_start=_do_nothing_page_start):
212249
self.client = client
213-
self.path = path or self.PATH
250+
self.path = path
251+
self._items_key = items_key
252+
self._item_to_value = item_to_value
253+
self.max_results = max_results
254+
self.extra_params = extra_params
255+
self._page_start = page_start
256+
if self.extra_params is None:
257+
self.extra_params = {}
258+
self._verify_params()
214259
# The attributes below will change over the life of the iterator.
215260
self.page_number = 0
216261
self.next_page_token = page_token
@@ -222,7 +267,7 @@ def _verify_params(self):
222267
223268
:raises ValueError: If a reserved parameter is used.
224269
"""
225-
reserved_in_use = self.RESERVED_PARAMS.intersection(
270+
reserved_in_use = self._RESERVED_PARAMS.intersection(
226271
self.extra_params)
227272
if reserved_in_use:
228273
raise ValueError('Using a reserved parameter',
@@ -275,26 +320,16 @@ def update_page(self, require_empty=True):
275320
if page_empty:
276321
if self._has_next_page():
277322
response = self._get_next_page_response()
278-
self._page = self._PAGE_CLASS(self, response, self.ITEMS_KEY)
323+
self._page = Page(self, response, self._items_key,
324+
self._item_to_value)
325+
self._page_start(self, self._page, response)
279326
else:
280327
self._page = None
281328
else:
282329
if require_empty:
283330
msg = _PAGE_ERR_TEMPLATE % (self._page, self.page.remaining)
284331
raise ValueError(msg)
285332

286-
def _item_to_value(self, item):
287-
"""Get the next item in the page.
288-
289-
Subclasses will need to implement this method.
290-
291-
:type item: dict
292-
:param item: An item to be converted to a native object.
293-
294-
:raises NotImplementedError: Always
295-
"""
296-
raise NotImplementedError
297-
298333
def next(self):
299334
"""Get the next item from the request."""
300335
self.update_page(require_empty=False)
@@ -330,9 +365,9 @@ def _get_query_params(self):
330365
"""
331366
result = {}
332367
if self.next_page_token is not None:
333-
result[self.PAGE_TOKEN] = self.next_page_token
368+
result[self._PAGE_TOKEN] = self.next_page_token
334369
if self.max_results is not None:
335-
result[self.MAX_RESULTS] = self.max_results - self.num_results
370+
result[self._MAX_RESULTS] = self.max_results - self.num_results
336371
result.update(self.extra_params)
337372
return result
338373

0 commit comments

Comments
 (0)