Skip to content

Commit 97ff850

Browse files
author
Andrew Cheng
committed
Address function naming concerns and clarify EMPTY_VALUES
1 parent 1d6a98f commit 97ff850

1 file changed

Lines changed: 49 additions & 18 deletions

File tree

pulpcore/filters.py

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from django.db import models
1111
from django.forms.utils import ErrorList
1212
from django.urls import Resolver404, resolve
13-
from django_filters.constants import EMPTY_VALUES
13+
from django_filters.constants import EMPTY_VALUES as STANDARD_EMPTY_VALUES
1414
from django_filters.rest_framework import BaseInFilter, DjangoFilterBackend, filters, filterset
1515
from drf_spectacular.contrib.django_filters import DjangoFilterExtension
1616
from drf_spectacular.plumbing import build_basic_type
@@ -20,7 +20,7 @@
2020

2121
from pulpcore.app.util import extract_pk, get_domain_pk, resolve_prn
2222

23-
EMPTY_VALUES = (*EMPTY_VALUES, "null")
23+
EMPTY_VALUES = (*STANDARD_EMPTY_VALUES, "null")
2424
UPMatch = namedtuple("UPMatch", ("model", "pk"))
2525

2626

@@ -185,6 +185,23 @@ class PulpTypeInFilter(BaseInFilter, PulpTypeFilter):
185185

186186

187187
class ExpressionFilterField(forms.CharField):
188+
"""Parse and compile a ``q=`` complex-filter expression into a queryset transform.
189+
190+
Each parsed node (leaf ``_FilterAction`` and the ``NOT``/``AND``/``OR`` combinators) implements
191+
a two-method protocol:
192+
193+
* ``as_flat_q()`` -> ``Q | None``: a pure, side-effect-free attempt to represent the *whole*
194+
subtree as a single flat ``Q`` object. It returns ``None`` if any operand is "opaque" (a
195+
filter with a custom ``.filter()`` override, a ``method``, or ``.distinct()``) and therefore
196+
cannot be expressed as a plain ``WHERE`` condition.
197+
* ``evaluate(qs)`` -> ``QuerySet``: applies the subtree to ``qs``. It uses ``as_flat_q()`` when
198+
the subtree is fully reducible (one flat ``WHERE``) and otherwise falls back to composing the
199+
opaque operands as ``pk__in`` semi-/anti-join subqueries.
200+
201+
``evaluate()`` is the sole external entry point: :meth:`ExpressionFilter.filter` calls
202+
``value.evaluate(qs)`` on the outermost node.
203+
"""
204+
188205
class _FilterAction:
189206
def __init__(self, filterset, tokens):
190207
key = tokens[0].key
@@ -206,25 +223,33 @@ def __init__(self, filterset, tokens):
206223
self.value = form.cleaned_data[key]
207224
self.complexity = 1
208225

209-
def as_q(self):
210-
# A leaf reduces to a plain WHERE condition (Q) only when it is a standard
211-
# django-filter Filter: no custom .filter() override, no method, no .distinct().
212-
# Anything else (href/prn resolvers, repository_version, method filters, ...) is
213-
# opaque and must be applied via its own queryset transform in evaluate().
226+
def as_flat_q(self):
227+
"""Return this leaf as a flat ``Q``, or ``None`` if the filter is opaque.
228+
229+
A leaf reduces to a plain ``WHERE`` condition only when it is a standard django-filter
230+
``Filter``: no custom ``.filter()`` override, no ``method``, and no ``.distinct()``.
231+
Anything else (href/prn resolvers, ``repository_version``, method filters, ...) is
232+
opaque and must be applied via its own queryset transform in :meth:`evaluate`.
233+
"""
214234
f = self.filter
215235
if (
216236
type(f).filter is not filters.Filter.filter
217237
or getattr(f, "method", None) is not None
218238
or getattr(f, "distinct", False)
219239
):
220240
return None
221-
if self.value in EMPTY_VALUES:
241+
# Mirror filters.Filter.filter exactly: an empty/unspecified value means the filter is
242+
# not applied (identity Q), not "IS NULL". Use django-filter's own EMPTY_VALUES rather
243+
# than the module-augmented set so the sentinel "null" is not swallowed here; null-aware
244+
# filters override .filter() and are handled on the opaque evaluate() path above.
245+
if self.value in STANDARD_EMPTY_VALUES:
222246
return models.Q()
223247
q = models.Q(**{f"{f.field_name}__{f.lookup_expr}": self.value})
224248
return ~q if f.exclude else q
225249

226250
def evaluate(self, qs):
227-
q = self.as_q()
251+
"""Apply the leaf: a flat ``Q`` when reducible, else the filter's own transform."""
252+
q = self.as_flat_q()
228253
if q is not None:
229254
return qs.filter(q)
230255
return self.filter.filter(qs, self.value)
@@ -234,12 +259,14 @@ def __init__(self, tokens):
234259
self.expr = tokens[0][0]
235260
self.complexity = self.expr.complexity + 1
236261

237-
def as_q(self):
238-
child = self.expr.as_q()
262+
def as_flat_q(self):
263+
"""Negate the child's flat ``Q``; ``None`` if the child is opaque."""
264+
child = self.expr.as_flat_q()
239265
return None if child is None else ~child
240266

241267
def evaluate(self, qs):
242-
q = self.as_q()
268+
"""Apply ``NOT``: negate a flat ``Q`` when reducible, else anti-join the operand."""
269+
q = self.as_flat_q()
243270
if q is not None:
244271
return qs.filter(q)
245272
# Opaque operand: anti-join via a NOT IN subquery.
@@ -250,8 +277,9 @@ def __init__(self, tokens):
250277
self.exprs = tokens[0]
251278
self.complexity = sum((expr.complexity for expr in self.exprs)) + 1
252279

253-
def as_q(self):
254-
children = [expr.as_q() for expr in self.exprs]
280+
def as_flat_q(self):
281+
"""AND every child's flat ``Q`` into one; ``None`` if any child is opaque."""
282+
children = [expr.as_flat_q() for expr in self.exprs]
255283
if any(child is None for child in children):
256284
return None
257285
query = models.Q()
@@ -260,13 +288,14 @@ def as_q(self):
260288
return query
261289

262290
def evaluate(self, qs):
291+
"""Apply ``AND``: fold reducible operands into one flat ``WHERE``, semi-join the rest."""
263292
# Fold every Q-reducible operand into a single flat WHERE, and apply only the
264293
# genuinely opaque operands as pk__in semi-join subqueries.
265294
result = qs
266295
combined = models.Q()
267296
has_q = False
268297
for expr in self.exprs:
269-
child = expr.as_q()
298+
child = expr.as_flat_q()
270299
if child is not None:
271300
combined &= child
272301
has_q = True
@@ -281,8 +310,9 @@ def __init__(self, tokens):
281310
self.exprs = tokens[0]
282311
self.complexity = sum((expr.complexity for expr in self.exprs)) + 1
283312

284-
def as_q(self):
285-
children = [expr.as_q() for expr in self.exprs]
313+
def as_flat_q(self):
314+
"""OR every child's flat ``Q`` into one; ``None`` if any child is opaque."""
315+
children = [expr.as_flat_q() for expr in self.exprs]
286316
if any(child is None for child in children):
287317
return None
288318
query = children[0]
@@ -291,11 +321,12 @@ def as_q(self):
291321
return query
292322

293323
def evaluate(self, qs):
324+
"""Apply ``OR``: reducible operands stay flat, opaque ones contribute ``pk__in``."""
294325
# Reducible operands stay as flat OR'd conditions; opaque operands contribute a
295326
# pk__in subquery. Both are combined in a single WHERE clause.
296327
query = models.Q()
297328
for expr in self.exprs:
298-
child = expr.as_q()
329+
child = expr.as_flat_q()
299330
if child is not None:
300331
query |= child
301332
else:

0 commit comments

Comments
 (0)