From 087b5dbe2984ab45871b75e14ac0da4b0c7ce087 Mon Sep 17 00:00:00 2001 From: Cameron Hobbs Date: Tue, 9 Jun 2026 15:53:24 +0100 Subject: [PATCH] Use test fixtures correctly Remove platform-api reference --- pyproject.toml | 5 + tests/base.py | 93 ++++++------- tests/conftest.py | 22 +++ tests/test_enabled.py | 20 ++- tests/test_mark_considered.py | 47 ++++--- tests/test_may_access.py | 79 ++++++----- tests/test_scope_ignore.py | 45 ++++-- tests/test_unused.py | 248 ++++++++++++++++++++-------------- 8 files changed, 334 insertions(+), 225 deletions(-) create mode 100644 tests/conftest.py diff --git a/pyproject.toml b/pyproject.toml index d7362b6..2f1769b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,11 @@ python_files = ["test_*.py"] # "." resolves the `tests` suite package; "tests" resolves the test apps # (test_project, excluded_app) that live inside it as top-level imports. pythonpath = [".", "tests"] +# Treat an unused eager load as a hard failure by default: +# any UnusedRelatedAccess that escapes an explicit capture block fails the test. +filterwarnings = [ + "error::eagle.exceptions.UnusedRelatedAccess", +] [tool.coverage.run] branch = true diff --git a/tests/base.py b/tests/base.py index c0dae70..33b2f84 100644 --- a/tests/base.py +++ b/tests/base.py @@ -1,89 +1,76 @@ -import warnings -from types import SimpleNamespace +from dataclasses import dataclass import pytest -from django.urls import reverse from rest_framework.test import APIClient -from eagle import UnusedRelatedAccess, unused +from eagle import unused +from test_project.models import Climate, Eagle, Eaglet, Location from tests.factories import ClimateFactory, EagleFactory, EagletFactory, LocationFactory +@dataclass(frozen=True) +class EagleGraph: + """A connected set of rows: a location with one climate, an eagle living there, and its eaglet.""" + + location: Location + climate: Climate + eagle: Eagle + eaglet: Eaglet + + +@dataclass(frozen=True) +class EagleWithLocation: + """An eagle paired with the location it lives in.""" + + location: Location + eagle: Eagle + + @pytest.mark.django_db class BaseRequestTest: + """Base for tests that drive the eagle detail endpoint; an unused eager load surfaces as an error.""" + @pytest.fixture - def api_client(self): + def api_client(self) -> APIClient: + """Return a DRF test client for calling the eagle endpoints.""" return APIClient() @pytest.fixture - def eagle_graph(self, db): + def eagle_graph(self, db: None) -> EagleGraph: + """Build a fully connected eagle graph whose relations a request can eager-load.""" location = LocationFactory(climates=[ClimateFactory()]) eagle = EagleFactory(location=location) eaglet = EagletFactory(eagle=eagle) - return SimpleNamespace( + return EagleGraph( location=location, climate=location.climates.get(), eagle=eagle, eaglet=eaglet, ) - @pytest.fixture - def warn_request(self, api_client, eagle_graph): - def _request(pk=None, **params): - target = eagle_graph.eagle.pk if pk is None else pk - - url = reverse("eagle-detail", kwargs={"pk": target}) - - with warnings.catch_warnings(record=True) as caught: - warnings.simplefilter("always", UnusedRelatedAccess) - response = api_client.get(url, params) - - assert response.status_code == 200, response.content - return [w.message for w in caught if issubclass(w.category, UnusedRelatedAccess)] - - return _request - - @pytest.fixture(autouse=True) - def setup(self, warn_request, eagle_graph): - self.request = warn_request - self.graph = eagle_graph - @pytest.mark.django_db class MayAccessHelperTestBase: + """Base for tests of the may_access/mark_considered helpers run inside a tracking request.""" + @pytest.fixture - def eagle_with_location(self, db): + def eagle_with_location(self, db: None) -> EagleWithLocation: + """Create a single eagle and the location it lives in.""" location = LocationFactory() eagle = EagleFactory(location=location) - return SimpleNamespace(location=location, eagle=eagle) - - @pytest.fixture - def active_request(self): - unused.begin_request() - yield - if unused.is_active(): - unused.end_request() - - @pytest.fixture - def flush_warnings(self, active_request): - def _flush(): - with warnings.catch_warnings(record=True) as caught: - warnings.simplefilter("always", UnusedRelatedAccess) - unused.end_request() - unused.begin_request() - return [w.message for w in caught if issubclass(w.category, UnusedRelatedAccess)] - - return _flush + return EagleWithLocation(location=location, eagle=eagle) @pytest.fixture(autouse=True) - def setup(self, eagle_with_location, flush_warnings): - self.data = eagle_with_location - self.flush = flush_warnings + def active_request(self) -> None: + """Open a tracking request before each test; the global reset fixture closes it afterwards.""" + unused.begin_request() class InactiveCollectorTestBase: + """Base for tests that must run with no active tracking request.""" + @pytest.fixture(autouse=True) - def inactive_collector(self): + def inactive_collector(self) -> None: + """Ensure no tracking request is active before the test body runs.""" if unused.is_active(): unused.end_request() - return diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..12f8147 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,22 @@ +import warnings +from collections.abc import Iterator + +import pytest + +from eagle import UnusedRelatedAccess, unused + + +@pytest.fixture(autouse=True) +def reset_eagle_tracking() -> Iterator[None]: + """ + Force Eagle's per-request tracking inactive after every test so state never leaks between tests. + + Warnings are configured as errors (see ``filterwarnings`` in pyproject.toml), so a request that + ends with an unused relation raises ``UnusedRelatedAccess`` part-way through ``end_request`` and + never reaches its own cleanup. This finalizer ignores that warning and resets the collector. + """ + yield + if unused.is_active(): + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UnusedRelatedAccess) + unused.end_request() diff --git a/tests/test_enabled.py b/tests/test_enabled.py index f1005e6..80eb272 100644 --- a/tests/test_enabled.py +++ b/tests/test_enabled.py @@ -1,7 +1,11 @@ +import pytest from django.test import override_settings +from django.urls import reverse +from rest_framework.test import APIClient +from eagle import UnusedRelatedAccess from eagle.config import is_enabled -from tests.base import BaseRequestTest +from tests.base import BaseRequestTest, EagleGraph class TestEnabledDefault: @@ -20,8 +24,12 @@ def test_enabled_when_setting_true(self): class TestDisabledRequest(BaseRequestTest): @override_settings(EAGLE_ENABLED=False) - def test_unused_eager_load_does_not_warn_when_disabled(self): - assert self.request(select_related="location") == [] - - def test_unused_eager_load_warns_when_enabled(self): - assert len(self.request(select_related="location")) == 1 + def test_unused_eager_load_does_not_warn_when_disabled(self, api_client: APIClient, eagle_graph: EagleGraph): + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + response = api_client.get(url, {"select_related": "location"}) + assert response.status_code == 200 + + def test_unused_eager_load_warns_when_enabled(self, api_client: APIClient, eagle_graph: EagleGraph): + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + with pytest.raises(UnusedRelatedAccess): + api_client.get(url, {"select_related": "location"}) diff --git a/tests/test_mark_considered.py b/tests/test_mark_considered.py index 174eff3..5fc9d08 100644 --- a/tests/test_mark_considered.py +++ b/tests/test_mark_considered.py @@ -1,23 +1,38 @@ -from tests.base import BaseRequestTest +import pytest +from django.urls import reverse +from rest_framework.test import APIClient + +from eagle import UnusedRelatedAccess +from tests.base import BaseRequestTest, EagleGraph class TestMarkConsidered(BaseRequestTest): - def test_mark_considered_suppresses_warning(self): - assert self.request(select_related="location", mark_considered="location") == [] + def test_mark_considered_suppresses_warning(self, api_client: APIClient, eagle_graph: EagleGraph): + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + response = api_client.get(url, {"select_related": "location", "mark_considered": "location"}) + assert response.status_code == 200 - def test_mark_considered_before_query_suppresses(self): - warns = self.request(select_related="location", mark_considered="location", mark_before="1") - assert warns == [] + def test_mark_considered_before_query_suppresses(self, api_client: APIClient, eagle_graph: EagleGraph): + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + response = api_client.get( + url, {"select_related": "location", "mark_considered": "location", "mark_before": "1"} + ) + assert response.status_code == 200 - def test_mark_considered_multiple_cache_names(self): - warns = self.request( - select_related="location", - prefetch_related="previous_locations", - mark_considered="location,previous_locations", + def test_mark_considered_multiple_cache_names(self, api_client: APIClient, eagle_graph: EagleGraph): + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + response = api_client.get( + url, + { + "select_related": "location", + "prefetch_related": "previous_locations", + "mark_considered": "location,previous_locations", + }, ) - assert warns == [] + assert response.status_code == 200 - def test_mark_considered_wrong_field_still_warns(self): - warns = self.request(select_related="location", mark_considered="previous_locations") - assert len(warns) == 1 - assert 'select_related("location")' in str(warns[0]) + def test_mark_considered_wrong_field_still_warns(self, api_client: APIClient, eagle_graph: EagleGraph): + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + with pytest.raises(UnusedRelatedAccess) as exc_info: + api_client.get(url, {"select_related": "location", "mark_considered": "previous_locations"}) + assert 'select_related("location")' in str(exc_info.value) diff --git a/tests/test_may_access.py b/tests/test_may_access.py index 2df432e..c5683a6 100644 --- a/tests/test_may_access.py +++ b/tests/test_may_access.py @@ -1,45 +1,59 @@ import asyncio import pytest +from django.urls import reverse +from rest_framework.test import APIClient import eagle -from eagle import mark_considered, may_access +from eagle import UnusedRelatedAccess, mark_considered, may_access, unused from test_project.models import Eagle from tests.base import ( BaseRequestTest, + EagleGraph, + EagleWithLocation, InactiveCollectorTestBase, MayAccessHelperTestBase, ) class TestMayAccess(BaseRequestTest): - def test_may_access_suppresses_when_called(self): - warns = self.request(select_related="location", may_access="location", may_access_call="1") - assert warns == [] - - def test_may_access_does_not_suppress_when_not_called(self): - warns = self.request(select_related="location", may_access="location") - assert len(warns) == 1 - assert 'select_related("location")' in str(warns[0]) - - def test_may_access_multiple_fields(self): - warns = self.request( - select_related="location", - prefetch_related="previous_locations", - may_access="location,previous_locations", - may_access_call="1", + def test_may_access_suppresses_when_called(self, api_client: APIClient, eagle_graph: EagleGraph): + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + response = api_client.get(url, {"select_related": "location", "may_access": "location", "may_access_call": "1"}) + assert response.status_code == 200 + + def test_may_access_does_not_suppress_when_not_called(self, api_client: APIClient, eagle_graph: EagleGraph): + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + with pytest.raises(UnusedRelatedAccess) as exc_info: + api_client.get(url, {"select_related": "location", "may_access": "location"}) + assert 'select_related("location")' in str(exc_info.value) + + def test_may_access_multiple_fields(self, api_client: APIClient, eagle_graph: EagleGraph): + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + response = api_client.get( + url, + { + "select_related": "location", + "prefetch_related": "previous_locations", + "may_access": "location,previous_locations", + "may_access_call": "1", + }, ) - assert warns == [] - - def test_may_access_does_not_mark_when_wrapped_raises(self): - warns = self.request( - select_related="location", - may_access="location", - may_access_call="1", - may_access_raise="1", - ) - assert len(warns) == 1 - assert 'select_related("location")' in str(warns[0]) + assert response.status_code == 200 + + def test_may_access_does_not_mark_when_wrapped_raises(self, api_client: APIClient, eagle_graph: EagleGraph): + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + with pytest.raises(UnusedRelatedAccess) as exc_info: + api_client.get( + url, + { + "select_related": "location", + "may_access": "location", + "may_access_call": "1", + "may_access_raise": "1", + }, + ) + assert 'select_related("location")' in str(exc_info.value) class TestPublicExport: @@ -49,7 +63,7 @@ def test_public_exports(self): class TestMayAccessHelper(MayAccessHelperTestBase): - def test_may_access_async_function_suppresses_after_await(self): + def test_may_access_async_function_suppresses_after_await(self, eagle_with_location: EagleWithLocation): Eagle.objects.select_related("location").get() @may_access(Eagle, "location") @@ -57,9 +71,10 @@ async def consumer(): return 7 assert asyncio.run(consumer()) == 7 - assert self.flush() == [] + # location was marked accessed after the await, so ending the request emits no warning (no error). + unused.end_request() - def test_may_access_async_function_no_mark_when_raises(self): + def test_may_access_async_function_no_mark_when_raises(self, eagle_with_location: EagleWithLocation): Eagle.objects.select_related("location").get() class Boom(Exception): @@ -71,7 +86,9 @@ async def consumer(): with pytest.raises(Boom): asyncio.run(consumer()) - assert len(self.flush()) == 1 + # The wrapped function raised before marking, so the loaded relation is still unused. + with pytest.raises(UnusedRelatedAccess): + unused.end_request() class TestMayAccessInactiveCollector(InactiveCollectorTestBase): diff --git a/tests/test_scope_ignore.py b/tests/test_scope_ignore.py index d9eb49a..bc6475d 100644 --- a/tests/test_scope_ignore.py +++ b/tests/test_scope_ignore.py @@ -1,35 +1,50 @@ +import pytest from django.test import override_settings +from django.urls import reverse +from rest_framework.test import APIClient -from tests.base import BaseRequestTest +from eagle import UnusedRelatedAccess +from tests.base import BaseRequestTest, EagleGraph class TestScopeIgnore(BaseRequestTest): @override_settings(EAGLE_WARN_UNUSED_IGNORE=[{"model": "Eagle", "field": "location"}]) - def test_ignore_by_model_and_field(self): - assert self.request(select_related="location") == [] + def test_ignore_by_model_and_field(self, api_client: APIClient, eagle_graph: EagleGraph): + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + response = api_client.get(url, {"select_related": "location"}) + assert response.status_code == 200 @override_settings(EAGLE_WARN_UNUSED_IGNORE=[{"field": "location"}]) - def test_ignore_by_field_only(self): - assert self.request(select_related="location") == [] + def test_ignore_by_field_only(self, api_client: APIClient, eagle_graph: EagleGraph): + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + response = api_client.get(url, {"select_related": "location"}) + assert response.status_code == 200 @override_settings(EAGLE_WARN_UNUSED_IGNORE=[{"model": "Location", "field": "latitude"}]) - def test_ignore_wrong_model_still_warns(self): - warns = self.request(select_related="location") - assert len(warns) == 1 - assert "location" in str(warns[0]) + def test_ignore_wrong_model_still_warns(self, api_client: APIClient, eagle_graph: EagleGraph): + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + with pytest.raises(UnusedRelatedAccess) as exc_info: + api_client.get(url, {"select_related": "location"}) + assert "location" in str(exc_info.value) @override_settings(EAGLE_WARN_UNUSED_IGNORE=[{"location": "*/test_project/*"}]) - def test_ignore_by_location_glob(self): - assert self.request(select_related="location") == [] + def test_ignore_by_location_glob(self, api_client: APIClient, eagle_graph: EagleGraph): + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + response = api_client.get(url, {"select_related": "location"}) + assert response.status_code == 200 @override_settings(EAGLE_WARN_UNUSED_IGNORE=[{"location": "*/test_project/views.py:*"}]) - def test_ignore_by_file_wildcard_glob(self): - assert self.request(select_related="location") == [] + def test_ignore_by_file_wildcard_glob(self, api_client: APIClient, eagle_graph: EagleGraph): + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + response = api_client.get(url, {"select_related": "location"}) + assert response.status_code == 200 @override_settings( EAGLE_WARN_UNUSED_IGNORE=[ {"model": "Eagle", "field": "location", "location": "*/test_project/*"}, ] ) - def test_ignore_by_all_three_keys_combined(self): - assert self.request(select_related="location") == [] + def test_ignore_by_all_three_keys_combined(self, api_client: APIClient, eagle_graph: EagleGraph): + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + response = api_client.get(url, {"select_related": "location"}) + assert response.status_code == 200 diff --git a/tests/test_unused.py b/tests/test_unused.py index d2748bc..ed47492 100644 --- a/tests/test_unused.py +++ b/tests/test_unused.py @@ -1,137 +1,177 @@ import pytest +from django.urls import reverse +from rest_framework.test import APIClient -from eagle import unused +from eagle import UnusedRelatedAccess, unused from test_project import views -from test_project.models import Eagle -from tests.base import ( - BaseRequestTest, -) +from test_project.models import Eagle, Location +from tests.base import BaseRequestTest, EagleGraph from tests.factories import BurrowFactory, ClimateFactory, EagleFactory, LocationFactory class TestWarnUnusedQuerySet(BaseRequestTest): - def test_select_related_accessed_no_warning(self): - assert self.request(select_related="location", access="location") == [] - - def test_select_related_unused_warns(self): - warns = self.request(select_related="location") - assert len(warns) == 1 - assert 'select_related("location")' in str(warns[0]) - - def test_query_outside_request_not_tracked(self): + def test_select_related_accessed_no_warning(self, api_client: APIClient, eagle_graph: EagleGraph): + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + response = api_client.get(url, {"select_related": "location", "access": "location"}) + assert response.status_code == 200 + + def test_select_related_unused_warns(self, api_client: APIClient, eagle_graph: EagleGraph): + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + with pytest.raises(UnusedRelatedAccess) as exc_info: + api_client.get(url, {"select_related": "location"}) + assert 'select_related("location")' in str(exc_info.value) + + def test_query_outside_request_not_tracked(self, api_client: APIClient, eagle_graph: EagleGraph): assert not unused.is_active() Eagle.objects.select_related("location").get() - assert self.request() == [] - - def test_location_captured_in_warning(self): - warns = self.request(select_related="location") - assert len(warns) == 1 - assert views.__file__ in str(warns[0]) - - def test_prefetch_related_unused_warns(self): - warns = self.request(prefetch_related="previous_locations") - assert len(warns) == 1 - assert 'prefetch_related("previous_locations")' in str(warns[0]) - - def test_prefetch_related_accessed_no_warning(self): - assert self.request(prefetch_related="previous_locations", access="previous_locations") == [] - - def test_prefetch_related_raw_cache_read_no_warning(self): - assert self.request(prefetch_related="previous_locations", access="previous_locations_raw") == [] - - def test_reverse_o2o_cached_none_unused_warns(self): + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + response = api_client.get(url) + assert response.status_code == 200 + + def test_location_captured_in_warning(self, api_client: APIClient, eagle_graph: EagleGraph): + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + with pytest.raises(UnusedRelatedAccess) as exc_info: + api_client.get(url, {"select_related": "location"}) + assert views.__file__ in str(exc_info.value) + + def test_prefetch_related_unused_warns(self, api_client: APIClient, eagle_graph: EagleGraph): + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + with pytest.raises(UnusedRelatedAccess) as exc_info: + api_client.get(url, {"prefetch_related": "previous_locations"}) + assert 'prefetch_related("previous_locations")' in str(exc_info.value) + + def test_prefetch_related_accessed_no_warning(self, api_client: APIClient, eagle_graph: EagleGraph): + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + response = api_client.get(url, {"prefetch_related": "previous_locations", "access": "previous_locations"}) + assert response.status_code == 200 + + def test_prefetch_related_raw_cache_read_no_warning(self, api_client: APIClient, eagle_graph: EagleGraph): + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + response = api_client.get(url, {"prefetch_related": "previous_locations", "access": "previous_locations_raw"}) + assert response.status_code == 200 + + def test_reverse_o2o_cached_none_unused_warns(self, api_client: APIClient): eagle_no_eaglet = EagleFactory(location=None, weight=50) - warns = self.request(pk=eagle_no_eaglet.pk, select_related="eaglet") - assert len(warns) == 1 - assert 'select_related("eaglet")' in str(warns[0]) - - def test_reverse_o2o_exists_accessed_no_warning(self): - assert self.request(select_related="eaglet", access="eaglet") == [] - - def test_reverse_o2o_exists_unused_warns(self): - warns = self.request(select_related="eaglet") - assert len(warns) == 1 - assert 'select_related("eaglet")' in str(warns[0]) - - def test_reverse_o2o_from_excluded_app_accessed_no_warning(self): - BurrowFactory(eagle=self.graph.eagle) - assert self.request(select_related="burrow", access="burrow") == [] - - def test_reverse_o2o_from_excluded_app_unused_warns(self): - BurrowFactory(eagle=self.graph.eagle) - warns = self.request(select_related="burrow") - assert len(warns) == 1 - assert 'select_related("burrow")' in str(warns[0]) + url = reverse("eagle-detail", kwargs={"pk": eagle_no_eaglet.pk}) + with pytest.raises(UnusedRelatedAccess) as exc_info: + api_client.get(url, {"select_related": "eaglet"}) + assert 'select_related("eaglet")' in str(exc_info.value) + + def test_reverse_o2o_exists_accessed_no_warning(self, api_client: APIClient, eagle_graph: EagleGraph): + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + response = api_client.get(url, {"select_related": "eaglet", "access": "eaglet"}) + assert response.status_code == 200 + + def test_reverse_o2o_exists_unused_warns(self, api_client: APIClient, eagle_graph: EagleGraph): + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + with pytest.raises(UnusedRelatedAccess) as exc_info: + api_client.get(url, {"select_related": "eaglet"}) + assert 'select_related("eaglet")' in str(exc_info.value) + + def test_reverse_o2o_from_excluded_app_accessed_no_warning(self, api_client: APIClient, eagle_graph: EagleGraph): + BurrowFactory(eagle=eagle_graph.eagle) + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + response = api_client.get(url, {"select_related": "burrow", "access": "burrow"}) + assert response.status_code == 200 + + def test_reverse_o2o_from_excluded_app_unused_warns(self, api_client: APIClient, eagle_graph: EagleGraph): + BurrowFactory(eagle=eagle_graph.eagle) + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + with pytest.raises(UnusedRelatedAccess) as exc_info: + api_client.get(url, {"select_related": "burrow"}) + assert 'select_related("burrow")' in str(exc_info.value) class TestWarnUnusedToAttrPrefetch(BaseRequestTest): - def test_to_attr_prefetch_accessed_no_warning(self): - assert self.request(prefetch_to_attr="previous_locations:prev", access="to_attr") == [] + def test_to_attr_prefetch_accessed_no_warning(self, api_client: APIClient, eagle_graph: EagleGraph): + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + response = api_client.get(url, {"prefetch_to_attr": "previous_locations:prev", "access": "to_attr"}) + assert response.status_code == 200 - def test_to_attr_prefetch_unused_warns(self): - warns = self.request(prefetch_to_attr="previous_locations:prev") - assert len(warns) == 1 - assert 'prefetch_related("previous_locations")' in str(warns[0]) + def test_to_attr_prefetch_unused_warns(self, api_client: APIClient, eagle_graph: EagleGraph): + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + with pytest.raises(UnusedRelatedAccess) as exc_info: + api_client.get(url, {"prefetch_to_attr": "previous_locations:prev"}) + assert 'prefetch_related("previous_locations")' in str(exc_info.value) - def test_to_attr_prefetch_with_rows_accessed_no_warning(self): - previous = LocationFactory() - self.graph.eagle.previous_locations.add(previous) - assert self.request(prefetch_to_attr="previous_locations:prev", access="to_attr") == [] + def test_to_attr_prefetch_with_rows_accessed_no_warning(self, api_client: APIClient, eagle_graph: EagleGraph): + eagle_graph.eagle.previous_locations.add(LocationFactory()) + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + response = api_client.get(url, {"prefetch_to_attr": "previous_locations:prev", "access": "to_attr"}) + assert response.status_code == 200 class TestWarnUnusedNestedPrefetch(BaseRequestTest): @pytest.fixture - def previous_location_with_climate(self): + def previous_location_with_climate(self, eagle_graph: EagleGraph) -> Location: # Give the eagle a previous location that itself has a climate, so a # ``previous_locations__climates__locations`` chain has rows at every depth. previous = LocationFactory(climates=[ClimateFactory()]) - self.graph.eagle.previous_locations.add(previous) + eagle_graph.eagle.previous_locations.add(previous) return previous - def test_nested_prefetched_child_unused_warns_with_propagated_location(self, previous_location_with_climate): - warns = self.request( - prefetch_related="previous_locations__climates", - access="previous_locations", - ) - - assert len(warns) == 1 - assert 'prefetch_related("climates")' in str(warns[0]) - assert "" in str(warns[0]) - assert views.__file__ in str(warns[0]) - - def test_depth_three_nested_prefetch_unused_warns(self, previous_location_with_climate): + def test_nested_prefetched_child_unused_warns_with_propagated_location( + self, api_client: APIClient, eagle_graph: EagleGraph, previous_location_with_climate: Location + ): + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + with pytest.raises(UnusedRelatedAccess) as exc_info: + api_client.get(url, {"prefetch_related": "previous_locations__climates", "access": "previous_locations"}) + + message = str(exc_info.value) + assert 'prefetch_related("climates")' in message + assert "" in message + assert views.__file__ in message + + def test_depth_three_nested_prefetch_unused_warns( + self, api_client: APIClient, eagle_graph: EagleGraph, previous_location_with_climate: Location + ): # Prefetch three levels deep but stop reading at the climate level: the # deepest reverse-M2M (``Climate.locations``) is loaded but never accessed. - warns = self.request( - prefetch_related="previous_locations__climates__locations", - access="previous_locations__climates", - ) - - assert len(warns) == 1 - assert 'prefetch_related("locations")' in str(warns[0]) - assert "" in str(warns[0]) - assert views.__file__ in str(warns[0]) - - def test_depth_three_nested_prefetch_accessed_no_warning(self, previous_location_with_climate): - assert ( - self.request( - prefetch_related="previous_locations__climates__locations", - access="previous_locations__climates__locations", + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + with pytest.raises(UnusedRelatedAccess) as exc_info: + api_client.get( + url, + { + "prefetch_related": "previous_locations__climates__locations", + "access": "previous_locations__climates", + }, ) - == [] + + message = str(exc_info.value) + assert 'prefetch_related("locations")' in message + assert "" in message + assert views.__file__ in message + + def test_depth_three_nested_prefetch_accessed_no_warning( + self, api_client: APIClient, eagle_graph: EagleGraph, previous_location_with_climate: Location + ): + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + response = api_client.get( + url, + { + "prefetch_related": "previous_locations__climates__locations", + "access": "previous_locations__climates__locations", + }, ) + assert response.status_code == 200 class TestWarnUnusedSelectRelatedWithinPrefetch(BaseRequestTest): - def test_select_related_within_prefetch_queryset_unused_warns(self): + def test_select_related_within_prefetch_queryset_unused_warns(self, api_client: APIClient, eagle_graph: EagleGraph): # Prefetch("eaglet", queryset=Eaglet.objects.select_related("eagle")): the prefetched # eaglet is read, but the select_related FK back to its eagle never is. - warns = self.request(prefetch_select="eaglet:eagle", access="eaglet") - - assert len(warns) == 1 - assert 'select_related("eagle")' in str(warns[0]) - assert "" in str(warns[0]) - assert views.__file__ in str(warns[0]) - - def test_select_related_within_prefetch_queryset_accessed_no_warning(self): - assert self.request(prefetch_select="eaglet:eagle", access="eaglet__eagle") == [] + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + with pytest.raises(UnusedRelatedAccess) as exc_info: + api_client.get(url, {"prefetch_select": "eaglet:eagle", "access": "eaglet"}) + + message = str(exc_info.value) + assert 'select_related("eagle")' in message + assert "" in message + assert views.__file__ in message + + def test_select_related_within_prefetch_queryset_accessed_no_warning( + self, api_client: APIClient, eagle_graph: EagleGraph + ): + url = reverse("eagle-detail", kwargs={"pk": eagle_graph.eagle.pk}) + response = api_client.get(url, {"prefetch_select": "eaglet:eagle", "access": "eaglet__eagle"}) + assert response.status_code == 200