Skip to content

Commit 17f5074

Browse files
author
Francesco Faraone
authored
Merge pull request #46 from cloudblue/LITE-24467_fix_slicing_for_resourceset
LITE-24467 Fix slicing for ResourceSet
2 parents b61ca64 + cd1bd48 commit 17f5074

File tree

4 files changed

+115
-51
lines changed

4 files changed

+115
-51
lines changed

connect/client/models/iterators.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def _load(self):
4040
self._results_iterator = iter(self._rs._results)
4141
self._loaded = True
4242

43-
def __next__(self):
43+
def __next__(self): # noqa: CCR001
4444
self._load()
4545

4646
if not self._rs._results:
@@ -51,9 +51,14 @@ def __next__(self):
5151
if (
5252
self._rs._content_range is None
5353
or self._rs._content_range.last >= self._rs._content_range.count - 1
54+
or (self._rs._slice and self._rs._content_range.last >= self._rs._slice.stop - 1)
5455
):
5556
raise
5657
self._config['params']['offset'] += self._config['params']['limit']
58+
if self._rs._slice:
59+
items_to_fetch = self._rs._slice.stop - self._rs._content_range.last - 1
60+
if items_to_fetch < self._config['params']['limit']:
61+
self._config['params']['limit'] = items_to_fetch
5762
results, cr = self._execute_request()
5863
if not results:
5964
raise
@@ -86,7 +91,7 @@ async def _load(self):
8691
self._results_iterator = iter(self._rs._results)
8792
self._loaded = True
8893

89-
async def __anext__(self):
94+
async def __anext__(self): # noqa: CCR001
9095
await self._load()
9196

9297
if not self._rs._results:
@@ -97,9 +102,14 @@ async def __anext__(self):
97102
if (
98103
self._rs._content_range is None
99104
or self._rs._content_range.last >= self._rs._content_range.count - 1
105+
or (self._rs._slice and self._rs._content_range.last >= self._rs._slice.stop - 1)
100106
):
101107
raise StopAsyncIteration
102108
self._config['params']['offset'] += self._config['params']['limit']
109+
if self._rs._slice:
110+
items_to_fetch = self._rs._slice.stop - self._rs._content_range.last - 1
111+
if items_to_fetch < self._config['params']['limit']:
112+
self._config['params']['limit'] = items_to_fetch
103113
results, cr = await self._execute_request()
104114
if not results:
105115
raise StopAsyncIteration

connect/client/models/resourceset.py

Lines changed: 28 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def __init__(
3434
self._results = None
3535
self._limit = self._client.default_limit or 100
3636
self._offset = 0
37-
self._slice = False
37+
self._slice = None
3838
self._content_range = None
3939
self._fields = None
4040
self._search = None
@@ -264,6 +264,21 @@ def _copy(self):
264264

265265
return rs
266266

267+
def _validate_key(self, key):
268+
if not isinstance(key, (int, slice)):
269+
raise TypeError('ResourceSet indices must be integers or slices.')
270+
271+
if isinstance(key, slice) and (key.start is None or key.stop is None):
272+
raise ValueError('Both start and stop indexes must be specified.')
273+
274+
if (not isinstance(key, slice) and (key < 0)) or (
275+
isinstance(key, slice) and (key.start < 0 or key.stop < 0)
276+
):
277+
raise ValueError('Negative indexing is not supported.')
278+
279+
if isinstance(key, slice) and not (key.step is None or key.step == 0):
280+
raise ValueError('Indexing with step is not supported.')
281+
267282
def help(self):
268283
"""
269284
Output the ResourceSet documentation to the console.
@@ -301,7 +316,7 @@ def __bool__(self):
301316

302317
def __getitem__(self, key): # noqa: CCR001
303318
"""
304-
If called with and integer index, returns the item
319+
If called with slice and integer index, returns the item
305320
at index ``key``.
306321
307322
If key is a slice, set the pagination limit and offset
@@ -313,19 +328,7 @@ def __getitem__(self, key): # noqa: CCR001
313328
:return: The resource at index ``key`` or self if ``key`` is a slice.
314329
:rtype: dict, ResultSet
315330
"""
316-
if not isinstance(key, (int, slice)):
317-
raise TypeError('ResourceSet indices must be integers or slices.')
318-
319-
if isinstance(key, slice) and (key.start is None or key.stop is None):
320-
raise ValueError('Both start and stop indexes must be specified.')
321-
322-
if (not isinstance(key, slice) and (key < 0)) or (
323-
isinstance(key, slice) and (key.start < 0 or key.stop < 0)
324-
):
325-
raise ValueError('Negative indexing is not supported.')
326-
327-
if isinstance(key, slice) and not (key.step is None or key.step == 0):
328-
raise ValueError('Indexing with step is not supported.')
331+
self._validate_key(key)
329332

330333
if self._results is not None:
331334
return self._results[key]
@@ -339,8 +342,10 @@ def __getitem__(self, key): # noqa: CCR001
339342

340343
copy = self._copy()
341344
copy._offset = key.start
342-
copy._limit = key.stop - key.start
343-
copy._slice = True
345+
copy._slice = key
346+
if copy._slice.stop - copy._slice.start < copy._limit:
347+
copy._limit = copy._slice.stop - copy._slice.start
348+
344349
return copy
345350

346351
def count(self):
@@ -429,7 +434,7 @@ def __bool__(self):
429434

430435
def __getitem__(self, key): # noqa: CCR001
431436
"""
432-
If called with and integer index, returns the item
437+
If called with slice and integer index, returns the item
433438
at index ``key``.
434439
435440
If key is a slice, set the pagination limit and offset
@@ -441,19 +446,7 @@ def __getitem__(self, key): # noqa: CCR001
441446
:return: The resource at index ``key`` or self if ``key`` is a slice.
442447
:rtype: dict, ResultSet
443448
"""
444-
if not isinstance(key, (int, slice)):
445-
raise TypeError('ResourceSet indices must be integers or slices.')
446-
447-
if isinstance(key, slice) and (key.start is None or key.stop is None):
448-
raise ValueError('Both start and stop indexes must be specified.')
449-
450-
if (not isinstance(key, slice) and (key < 0)) or (
451-
isinstance(key, slice) and (key.start < 0 or key.stop < 0)
452-
):
453-
raise ValueError('Negative indexing is not supported.')
454-
455-
if isinstance(key, slice) and not (key.step is None or key.step == 0):
456-
raise ValueError('Indexing with step is not supported.')
449+
self._validate_key(key)
457450

458451
if self._results is not None:
459452
return self._results[key]
@@ -463,8 +456,10 @@ def __getitem__(self, key): # noqa: CCR001
463456

464457
copy = self._copy()
465458
copy._offset = key.start
466-
copy._limit = key.stop - key.start
467-
copy._slice = True
459+
copy._slice = key
460+
if copy._slice.stop - copy._slice.start < copy._limit:
461+
copy._limit = copy._slice.stop - copy._slice.start
462+
468463
return copy
469464

470465
async def count(self):

tests/async_client/test_models.py

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -790,21 +790,41 @@ async def test_rs_getitem_not_evaluated(async_client_mock, async_rs_factory):
790790
rs[0]
791791

792792

793-
def test_rs_getitem_slice(mocker, async_client_mock, async_rs_factory):
793+
@pytest.mark.asyncio
794+
async def test_rs_getitem_slice(mocker, async_client_mock, async_rs_factory):
794795
mocker.patch(
795-
'connect.client.models.resourceset.parse_content_range',
796-
return_value=ContentRange(0, 9, 10),
797-
)
798-
expected = [{'id': i} for i in range(10)]
799-
rs = async_rs_factory(
800-
client=async_client_mock(methods=['get']),
796+
'connect.client.models.iterators.parse_content_range',
797+
return_value=ContentRange(2, 3, 10),
801798
)
799+
rs = async_rs_factory(client=async_client_mock(methods=['get']))
800+
expected = [{'id': 2}, {'id': 3}]
802801
rs._client.get.return_value = expected
803802
sliced = rs[2:4]
803+
results = [item async for item in sliced]
804+
assert results == expected
804805
assert isinstance(sliced, AsyncResourceSet)
805-
assert sliced._limit == 2
806-
assert sliced._offset == 2
807-
rs._client.get.assert_not_awaited()
806+
807+
808+
@pytest.mark.asyncio
809+
async def test_rs_getitem_slice_limit(async_mocker, mocker, async_client_mock, async_rs_factory):
810+
mocker.patch(
811+
'connect.client.models.iterators.parse_content_range',
812+
side_effect=[
813+
ContentRange(0, 9, 25),
814+
ContentRange(10, 19, 25),
815+
ContentRange(20, 21, 25),
816+
],
817+
)
818+
rs = async_rs_factory(client=async_client_mock(methods=['get']))
819+
rs._client.get = async_mocker.AsyncMock(side_effect=[
820+
[{'id': i} for i in range(10)],
821+
[{'id': i + 10} for i in range(10)],
822+
[{'id': 20}, {'id': 21}],
823+
])
824+
rs._limit = 10
825+
sliced = rs[0:22]
826+
results = [item async for item in sliced]
827+
assert results == [{'id': i} for i in range(22)]
808828

809829

810830
def test_rs_getitem_slice_type(async_rs_factory):

tests/client/test_models.py

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -698,16 +698,55 @@ def test_rs_getitem(mocker, rs_factory):
698698

699699
def test_rs_getitem_slice(mocker, rs_factory):
700700
mocker.patch(
701-
'connect.client.models.resourceset.parse_content_range',
702-
return_value=ContentRange(0, 9, 10),
701+
'connect.client.models.iterators.parse_content_range',
702+
return_value=ContentRange(2, 3, 10),
703703
)
704-
expected = [{'id': i} for i in range(10)]
704+
expected = [{'id': 2}, {'id': 3}]
705705
rs = rs_factory()
706706
rs._client.get = mocker.MagicMock(return_value=expected)
707707
sliced = rs[2:4]
708-
assert isinstance(sliced, ResourceSet)
709-
assert sliced._limit == 2
710-
assert sliced._offset == 2
708+
results = [resource for resource in sliced]
709+
assert results == expected
710+
711+
712+
def test_rs_getitem_slice_more_than_limit(mocker, rs_factory):
713+
mocker.patch(
714+
'connect.client.models.iterators.parse_content_range',
715+
side_effect=[
716+
ContentRange(1, 100, 257),
717+
ContentRange(101, 101, 257),
718+
],
719+
)
720+
rs = rs_factory()
721+
rs._client.get = mocker.MagicMock(side_effect=[
722+
[{'id': i + 1} for i in range(100)],
723+
[{'id': 101}],
724+
])
725+
sliced = rs[1:102]
726+
results = [resource for resource in sliced]
727+
assert results == [{'id': i + 1} for i in range(101)]
728+
729+
730+
def test_rs_getitem_slice_more_than_limit_no_append(mocker, rs_factory):
731+
mocker.patch(
732+
'connect.client.models.iterators.parse_content_range',
733+
side_effect=[
734+
ContentRange(0, 9, 25),
735+
ContentRange(10, 19, 25),
736+
ContentRange(20, 21, 25),
737+
],
738+
)
739+
rs = rs_factory()
740+
rs._client.get = mocker.MagicMock(side_effect=[
741+
[{'id': i} for i in range(10)],
742+
[{'id': i + 10} for i in range(10)],
743+
[{'id': 20}, {'id': 21}],
744+
])
745+
rs._limit = 10
746+
rs._client.resourceset_append = False
747+
sliced = rs[0:22]
748+
results = [resource for resource in sliced]
749+
assert results == [{'id': i} for i in range(22)]
711750

712751

713752
def test_rs_getitem_slice_type(mocker, rs_factory):

0 commit comments

Comments
 (0)