From b0da014c34902d622aa14fc88eb4932fea248d2a Mon Sep 17 00:00:00 2001 From: Jordan Woods <13803242+jorwoods@users.noreply.github.com> Date: Tue, 15 Oct 2024 06:40:11 -0500 Subject: [PATCH] fix: handle 0 item response in querysets A flaw in the __iter__ logic introduced to handle scenarios where a pagination element is not included in the response xml resulted in an infinite loop. This PR introduces a few changes to protect against this: 1. After running QuerySet._fetch_all(), if the result_cache is empty, return instead of performing other comparisons. 2. Ensure that any non-None total_available is returned from the PaginationItem's object. 3. In _fetch_all, check if there is a PaginationItem that has been populated so as to not call the server side endpoint muliple times before returning. --- tableauserverclient/server/query.py | 6 ++++-- test/test_pager.py | 12 ++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/tableauserverclient/server/query.py b/tableauserverclient/server/query.py index feebc1a7..74f15f5f 100644 --- a/tableauserverclient/server/query.py +++ b/tableauserverclient/server/query.py @@ -85,6 +85,8 @@ def __iter__(self: Self) -> Iterator[T]: # up overrunning the total number of pages. Catch the # error and break out of the loop. raise StopIteration + if len(self._result_cache) == 0: + return yield from self._result_cache # If the length of the QuerySet is unknown, continue fetching until # the result cache is empty. @@ -150,7 +152,7 @@ def _fetch_all(self: Self) -> None: """ Retrieve the data and store result and pagination item in cache """ - if not self._result_cache: + if not self._result_cache and self._pagination_item._page_number is None: response = self.model.get(self.request_options) if isinstance(response, tuple): self._result_cache, self._pagination_item = response @@ -159,7 +161,7 @@ def _fetch_all(self: Self) -> None: self._pagination_item = PaginationItem() def __len__(self: Self) -> int: - return self.total_available or sys.maxsize + return sys.maxsize if self.total_available is None else self.total_available @property def total_available(self: Self) -> int: diff --git a/test/test_pager.py b/test/test_pager.py index c3035280..1836095b 100644 --- a/test/test_pager.py +++ b/test/test_pager.py @@ -1,6 +1,7 @@ import contextlib import os import unittest +import xml.etree.ElementTree as ET import requests_mock @@ -122,3 +123,14 @@ def test_pager_view(self) -> None: m.get(self.server.views.baseurl, text=view_xml) for view in TSC.Pager(self.server.views): assert view.name is not None + + def test_queryset_no_matches(self) -> None: + elem = ET.Element("tsResponse", xmlns="http://tableau.com/api") + ET.SubElement(elem, "pagination", totalAvailable="0") + ET.SubElement(elem, "groups") + xml = ET.tostring(elem).decode("utf-8") + with requests_mock.mock() as m: + m.get(self.server.groups.baseurl, text=xml) + all_groups = self.server.groups.all() + groups = list(all_groups) + assert len(groups) == 0