@@ -111,6 +111,157 @@ def test_pagination(self):
111111 results .per_page = 10
112112 self .assertTrue (results ._num_pages is None )
113113
114+ def test_pagination_with_short_page (self ):
115+ """Indexing walks actual page sizes when the API under-fills a page."""
116+ client = Client ('ua' )
117+ client ._base_url = ''
118+ client ._fetcher = MemoryFetcher ({
119+ '/artists/1' : (
120+ b'{"id": 1, "name": "Badger", "releases_url": "/artists/1/releases"}' ,
121+ 200 ,
122+ ),
123+ '/artists/1/releases?page=1&per_page=50' : (
124+ b'{"pagination": {"per_page": 50, "pages": 2, "items": 2}, '
125+ b'"releases": [{"id": 101, "type": "release", "title": "First"}]}' ,
126+ 200 ,
127+ ),
128+ '/artists/1/releases?page=2&per_page=50' : (
129+ b'{"pagination": {"per_page": 50, "pages": 2, "items": 2}, '
130+ b'"releases": [{"id": 102, "type": "release", "title": "Second"}]}' ,
131+ 200 ,
132+ ),
133+ })
134+
135+ results = client .artist (1 ).releases
136+
137+ self .assertEqual (results .pages , 2 )
138+ self .assertEqual (len (results .page (1 )), 1 )
139+ self .assertEqual (results [0 ].id , 101 )
140+ self .assertEqual (results [1 ].id , 102 )
141+ self .assertRaises (IndexError , lambda : results [2 ])
142+
143+ def test_pagination_with_varying_page_sizes (self ):
144+ """Indexing works correctly with pages of different actual sizes."""
145+ client = Client ('ua' )
146+ client ._base_url = ''
147+ client ._fetcher = MemoryFetcher ({
148+ '/artists/1' : (
149+ b'{"id": 1, "name": "Badger", "releases_url": "/artists/1/releases"}' ,
150+ 200 ,
151+ ),
152+ '/artists/1/releases?page=1&per_page=50' : (
153+ b'{"pagination": {"per_page": 50, "pages": 3, "items": 8}, '
154+ b'"releases": ['
155+ b'{"id": 101, "type": "release", "title": "First"},'
156+ b'{"id": 102, "type": "release", "title": "Second"},'
157+ b'{"id": 103, "type": "release", "title": "Third"}'
158+ b']}' ,
159+ 200 ,
160+ ),
161+ '/artists/1/releases?page=2&per_page=50' : (
162+ b'{"pagination": {"per_page": 50, "pages": 3, "items": 8}, '
163+ b'"releases": ['
164+ b'{"id": 104, "type": "release", "title": "Fourth"},'
165+ b'{"id": 105, "type": "release", "title": "Fifth"}'
166+ b']}' ,
167+ 200 ,
168+ ),
169+ '/artists/1/releases?page=3&per_page=50' : (
170+ b'{"pagination": {"per_page": 50, "pages": 3, "items": 8}, '
171+ b'"releases": ['
172+ b'{"id": 106, "type": "release", "title": "Sixth"},'
173+ b'{"id": 107, "type": "release", "title": "Seventh"},'
174+ b'{"id": 108, "type": "release", "title": "Eighth"}'
175+ b']}' ,
176+ 200 ,
177+ ),
178+ })
179+
180+ results = client .artist (1 ).releases
181+
182+ # Verify we can access items across all pages
183+ self .assertEqual (results [0 ].id , 101 )
184+ self .assertEqual (results [1 ].id , 102 )
185+ self .assertEqual (results [2 ].id , 103 )
186+ self .assertEqual (results [3 ].id , 104 ) # First item on page 2
187+ self .assertEqual (results [4 ].id , 105 ) # Last item on page 2
188+ self .assertEqual (results [5 ].id , 106 ) # First item on page 3
189+ self .assertEqual (results [7 ].id , 108 ) # Last item
190+
191+ # Verify out-of-bounds access raises IndexError
192+ self .assertRaises (IndexError , lambda : results [8 ])
193+
194+ def test_pagination_with_short_page_sequential_traversal (self ):
195+ """Verify sequential page walking works when all pages are short."""
196+ client = Client ('ua' )
197+ client ._base_url = ''
198+ client ._fetcher = MemoryFetcher ({
199+ '/artists/1' : (
200+ b'{"id": 1, "name": "Badger", "releases_url": "/artists/1/releases"}' ,
201+ 200 ,
202+ ),
203+ '/artists/1/releases?page=1&per_page=50' : (
204+ b'{"pagination": {"per_page": 50, "pages": 4, "items": 4}, '
205+ b'"releases": [{"id": 201, "type": "release", "title": "A"}]}' ,
206+ 200 ,
207+ ),
208+ '/artists/1/releases?page=2&per_page=50' : (
209+ b'{"pagination": {"per_page": 50, "pages": 4, "items": 4}, '
210+ b'"releases": [{"id": 202, "type": "release", "title": "B"}]}' ,
211+ 200 ,
212+ ),
213+ '/artists/1/releases?page=3&per_page=50' : (
214+ b'{"pagination": {"per_page": 50, "pages": 4, "items": 4}, '
215+ b'"releases": [{"id": 203, "type": "release", "title": "C"}]}' ,
216+ 200 ,
217+ ),
218+ '/artists/1/releases?page=4&per_page=50' : (
219+ b'{"pagination": {"per_page": 50, "pages": 4, "items": 4}, '
220+ b'"releases": [{"id": 204, "type": "release", "title": "D"}]}' ,
221+ 200 ,
222+ ),
223+ })
224+
225+ results = client .artist (1 ).releases
226+
227+ # With per_page=50 but only 1 item per actual page,
228+ # math-based indexing would fail. Sequential walking should work.
229+ self .assertEqual (results [0 ].id , 201 )
230+ self .assertEqual (results [1 ].id , 202 )
231+ self .assertEqual (results [2 ].id , 203 )
232+ self .assertEqual (results [3 ].id , 204 )
233+ self .assertRaises (IndexError , lambda : results [4 ])
234+
235+ def test_pagination_404_exception_chaining (self ):
236+ """Verify that HTTPError 404 is properly chained when index is out of bounds."""
237+ client = Client ('ua' )
238+ client ._base_url = ''
239+ client ._fetcher = MemoryFetcher ({
240+ '/artists/1' : (
241+ b'{"id": 1, "name": "Badger", "releases_url": "/artists/1/releases"}' ,
242+ 200 ,
243+ ),
244+ '/artists/1/releases?page=1&per_page=50' : (
245+ b'{"pagination": {"per_page": 50, "pages": 1, "items": 2}, '
246+ b'"releases": ['
247+ b'{"id": 301, "type": "release", "title": "Only"},'
248+ b'{"id": 302, "type": "release", "title": "Two"}'
249+ b']}' ,
250+ 200 ,
251+ ),
252+ })
253+
254+ results = client .artist (1 ).releases
255+
256+ # Access valid items
257+ self .assertEqual (results [0 ].id , 301 )
258+ self .assertEqual (results [1 ].id , 302 )
259+
260+ # Accessing beyond bounds should raise IndexError
261+ # (which is chained from HTTPError 404)
262+ with self .assertRaises (IndexError ):
263+ results [100 ]
264+
114265 def test_timeout_defaults_to_none (self ):
115266 # Need to create client without LoggingDelegator here
116267 # self.d would throw AttributeError trying to access timeout properties on LoggingDelegator
0 commit comments