From 922d20f6d8280aed0a55ed4906d8053bd1258a6c Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Wed, 17 Jun 2026 22:06:18 -0700 Subject: [PATCH] fix: use return instead of raise StopIteration in QuerySet.__iter__ Per PEP 479, StopIteration raised inside a generator is converted to RuntimeError. QuerySet.__iter__ is a generator and was raising StopIteration to handle 400006 (invalid page number) errors, causing RuntimeError for callers iterating endpoints that don't support pagination past the last page. Fixes #1786 Co-Authored-By: Claude Sonnet 4.6 --- tableauserverclient/server/query.py | 5 +++-- test/test_pager.py | 33 +++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/tableauserverclient/server/query.py b/tableauserverclient/server/query.py index 6027a914e..eb84248b5 100644 --- a/tableauserverclient/server/query.py +++ b/tableauserverclient/server/query.py @@ -84,8 +84,9 @@ def __iter__(self: Self) -> Iterator[T]: if e.code == "400006": # If the endpoint does not support pagination, it will end # up overrunning the total number of pages. Catch the - # error and break out of the loop. - raise StopIteration + # error and return to cleanly end the generator. + # (raise StopIteration would cause RuntimeError per PEP 479) + return if len(self._result_cache) == 0: return yield from self._result_cache diff --git a/test/test_pager.py b/test/test_pager.py index 0a7ccf00a..82b79016c 100644 --- a/test/test_pager.py +++ b/test/test_pager.py @@ -136,3 +136,36 @@ def test_queryset_no_matches(server: TSC.Server) -> None: all_groups = server.groups.all() groups = list(all_groups) assert len(groups) == 0 + + +def test_queryset_400006_returns_cleanly() -> None: + """Regression test for PEP 479: 400006 must not raise RuntimeError. + + Before the fix, QuerySet.__iter__ raised StopIteration which PEP 479 + converts to RuntimeError inside a generator. This test directly exercises + the __iter__ method with a mock that raises 400006 on the second call. + """ + from tableauserverclient.server.endpoint.exceptions import ServerResponseError + from tableauserverclient.server.query import QuerySet + + calls = [0] + + class MockEndpoint: + def get(self, req_options=None): + calls[0] += 1 + assert calls[0] <= 10, f"get() called {calls[0]} times — infinite loop detected" + if calls[0] >= 2: + raise ServerResponseError("400006", "Bad Request", "Invalid page number", "http://test") + item = TSC.ProjectItem(name="Test") + item._id = "abc" + pagination = TSC.PaginationItem() + pagination._total_available = None # unknown size — triggers loop + return [item], pagination + + qs: QuerySet[TSC.ProjectItem] = QuerySet(MockEndpoint()) # type: ignore[arg-type] + + # Use a generator expression to bypass list()'s __len__ optimization, + # which would consume the first page before __iter__ starts. + # Before the fix this raised: RuntimeError: generator raised StopIteration + results = [x for x in qs] + assert len(results) == 1