Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions tests/test_project/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,27 @@ def get_related(self, eagle: Eagle) -> dict:
if "previous_locations" in access:
related["previous_locations"] = [loc.id for loc in eagle.previous_locations.all()]

if "previous_locations__climates" in access:
related["previous_locations__climates"] = [
[climate.id for climate in loc.climates.all()] for loc in eagle.previous_locations.all()
]

if "previous_locations__climates__locations" in access:
related["previous_locations__climates__locations"] = [
[[home.id for home in climate.locations.all()] for climate in loc.climates.all()]
for loc in eagle.previous_locations.all()
]

if "previous_locations_raw" in access:
cache = eagle._prefetched_objects_cache
related["previous_locations_raw"] = [loc.id for loc in cache["previous_locations"]]

if "eaglet" in access:
related["eaglet"] = eagle.eaglet.id

if "eaglet__eagle" in access:
related["eaglet__eagle"] = eagle.eaglet.eagle.id

if "burrow" in access:
related["burrow"] = eagle.burrow.depth

Expand Down
16 changes: 15 additions & 1 deletion tests/test_project/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import contextlib

from django.db.models import Prefetch
from django.db.models import Model, Prefetch
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
Expand All @@ -14,6 +14,13 @@ def _csv(value: str | None) -> list[str]:
return [item for item in (value or "").split(",") if item]


def _resolve_related_model(model: type[Model], lookup: str) -> type[Model]:
# Walk a ``__``-separated relation lookup from *model* and return the model at the end.
for part in lookup.split("__"):
model = model._meta.get_field(part).related_model
return model


class EagleView(APIView):
def get(self, request: Request, pk: int) -> Response:
params = request.query_params
Expand All @@ -36,6 +43,13 @@ def get(self, request: Request, pk: int) -> Response:
lookup, _, to_attr_name = to_attr_spec.partition(":")
queryset = queryset.prefetch_related(Prefetch(lookup, to_attr=to_attr_name))

prefetch_select_spec = params.get("prefetch_select")
if prefetch_select_spec:
lookup, _, sr_field = prefetch_select_spec.partition(":")
related_model = _resolve_related_model(Eagle, lookup)
child_queryset = related_model.objects.select_related(sr_field)
queryset = queryset.prefetch_related(Prefetch(lookup, queryset=child_queryset))

eagle = queryset.get(pk=pk)

data = EagleSerializer(eagle, context={"access": _csv(params.get("access")), "to_attr": to_attr_name}).data
Expand Down
46 changes: 45 additions & 1 deletion tests/test_unused.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import pytest

from eagle import unused
from test_project import views
from test_project.models import Eagle
Expand Down Expand Up @@ -78,10 +80,15 @@ def test_to_attr_prefetch_with_rows_accessed_no_warning(self):


class TestWarnUnusedNestedPrefetch(BaseRequestTest):
def test_nested_prefetched_child_unused_warns_with_propagated_location(self):
@pytest.fixture
def previous_location_with_climate(self):
# 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)
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",
Expand All @@ -91,3 +98,40 @@ def test_nested_prefetched_child_unused_warns_with_propagated_location(self):
assert 'prefetch_related("climates")' in str(warns[0])
assert "<Location instance>" in str(warns[0])
assert views.__file__ in str(warns[0])

def test_depth_three_nested_prefetch_unused_warns(self, previous_location_with_climate):
# 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 "<Climate instance>" 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",
)
== []
)


class TestWarnUnusedSelectRelatedWithinPrefetch(BaseRequestTest):
def test_select_related_within_prefetch_queryset_unused_warns(self):
# 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 "<Eaglet instance>" 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") == []
Loading