From 922d20f6d8280aed0a55ed4906d8053bd1258a6c Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Wed, 17 Jun 2026 22:06:18 -0700 Subject: [PATCH 1/2] 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 From 18ceb6e3c8fcb50ba8b421ec12b359ba08b35811 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Thu, 18 Jun 2026 16:51:24 -0700 Subject: [PATCH 2/2] test: remove bare baseurl statements outside requests_mock context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit server.workbooks.baseurl accessed outside a requests_mock context causes DNS resolution of http://test on Linux, failing with a ConnectionError. These statements were no-ops — baseurl is pure string formatting. Co-Authored-By: Claude Sonnet 4.6 --- test/test_workbook.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/test/test_workbook.py b/test/test_workbook.py index c5c4f6662..37012b935 100644 --- a/test/test_workbook.py +++ b/test/test_workbook.py @@ -169,7 +169,6 @@ def test_get_by_id_missing_id(server: TSC.Server) -> None: def test_refresh_id(server: TSC.Server) -> None: server.version = "2.8" - server.workbooks.baseurl response_xml = REFRESH_XML.read_text() with requests_mock.mock() as m: m.post( @@ -182,7 +181,6 @@ def test_refresh_id(server: TSC.Server) -> None: def test_refresh_already_running(server: TSC.Server) -> None: server.version = "2.8" - server.workbooks.baseurl response_xml = WORKBOOK_REFRESH_DUPLICATE_XML.read_text() with requests_mock.mock() as m: m.post( @@ -196,7 +194,6 @@ def test_refresh_already_running(server: TSC.Server) -> None: def test_refresh_object(server: TSC.Server) -> None: server.version = "2.8" - server.workbooks.baseurl workbook = TSC.WorkbookItem("") workbook._id = "3cc6cd06-89ce-4fdc-b935-5294135d6d42" response_xml = REFRESH_XML.read_text() @@ -326,7 +323,6 @@ def test_download_sanitizes_name(server: TSC.Server) -> None: def test_download_extract_only(server: TSC.Server) -> None: # Pretend we're 2.5 for 'extract_only' server.version = "2.5" - server.workbooks.baseurl with requests_mock.mock() as m: m.get( @@ -467,7 +463,6 @@ def test_populate_connections_missing_id(server: TSC.Server) -> None: def test_populate_pdf(server: TSC.Server) -> None: server.version = "3.4" - server.workbooks.baseurl response = POPULATE_PDF.read_bytes() with requests_mock.mock() as m: m.get( @@ -487,7 +482,6 @@ def test_populate_pdf(server: TSC.Server) -> None: def test_populate_pdf_unsupported(server: TSC.Server) -> None: server.version = "3.4" - server.workbooks.baseurl with requests_mock.mock() as m: m.get( server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/pdf?type=a5&orientation=landscape", @@ -507,7 +501,6 @@ def test_populate_pdf_unsupported(server: TSC.Server) -> None: def test_populate_pdf_vf_dims(server: TSC.Server) -> None: server.version = "3.23" - server.workbooks.baseurl response = POPULATE_PDF.read_bytes() with requests_mock.mock() as m: m.get( @@ -531,7 +524,6 @@ def test_populate_pdf_vf_dims(server: TSC.Server) -> None: def test_populate_powerpoint(server: TSC.Server) -> None: server.version = "3.8" - server.workbooks.baseurl response = POPULATE_POWERPOINT.read_bytes() with requests_mock.mock() as m: m.get(server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/powerpoint?maxAge=1", content=response) @@ -881,7 +873,6 @@ def test_synchronous_publish_timeout_error(server: TSC.Server) -> None: def test_delete_extracts_all(server: TSC.Server) -> None: server.version = "3.10" - server.workbooks.baseurl response_xml = PUBLISH_ASYNC_XML.read_text() with requests_mock.mock() as m: @@ -895,7 +886,6 @@ def test_delete_extracts_all(server: TSC.Server) -> None: def test_create_extracts_all(server: TSC.Server) -> None: server.version = "3.10" - server.workbooks.baseurl response_xml = PUBLISH_ASYNC_XML.read_text() with requests_mock.mock() as m: @@ -909,7 +899,6 @@ def test_create_extracts_all(server: TSC.Server) -> None: def test_create_extracts_one(server: TSC.Server) -> None: server.version = "3.10" - server.workbooks.baseurl datasource = TSC.DatasourceItem("test") datasource._id = "1f951daf-4061-451a-9df1-69a8062664f2" @@ -925,7 +914,6 @@ def test_create_extracts_one(server: TSC.Server) -> None: def test_revisions(server: TSC.Server) -> None: - server.workbooks.baseurl workbook = TSC.WorkbookItem("project", "test") workbook._id = "06b944d2-959d-4604-9305-12323c95e70e" @@ -956,7 +944,6 @@ def test_revisions(server: TSC.Server) -> None: def test_delete_revision(server: TSC.Server) -> None: - server.workbooks.baseurl workbook = TSC.WorkbookItem("project", "test") workbook._id = "06b944d2-959d-4604-9305-12323c95e70e" @@ -986,7 +973,6 @@ def test_bad_download_response(server: TSC.Server) -> None: def test_odata_connection(server: TSC.Server) -> None: - server.workbooks.baseurl workbook = TSC.WorkbookItem("project", "test") workbook._id = "06b944d2-959d-4604-9305-12323c95e70e" connection = TSC.ConnectionItem()