Skip to content

Commit 885b3a5

Browse files
author
Ryan P Kilby
committed
Merge pull request #80 from rpkilby/prevent-transform-recursion
Fix #79, add transform recursion prevention
2 parents 2b79a7c + 32276c4 commit 885b3a5

File tree

6 files changed

+79
-4
lines changed

6 files changed

+79
-4
lines changed

CHANGELOG.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
Unreleased
22
----------
33

4+
* Fixes #79, enabling compatibility with ``django.contrib.postgres``
5+
* Adds basic infinite recursion prevention for chainable transforms
6+
47
v0.8.0
58
------
69

rest_framework_filters/utils.py

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11

22
from collections import OrderedDict
33

4+
import django
45
from django.db.models.constants import LOOKUP_SEP
6+
from django.db.models.expressions import Expression
57
from django.db.models.lookups import Transform
68
from django.utils import six
79

@@ -13,11 +15,47 @@ def lookups_for_field(model_field):
1315
lookups = []
1416

1517
for expr, lookup in six.iteritems(class_lookups(model_field)):
18+
if issubclass(lookup, Transform) and django.VERSION >= (1, 9):
19+
transform = lookup(Expression(model_field))
20+
lookups += [
21+
LOOKUP_SEP.join([expr, sub_expr]) for sub_expr
22+
in lookups_for_transform(transform)
23+
]
24+
25+
else:
26+
lookups.append(expr)
27+
28+
return lookups
29+
30+
31+
def lookups_for_transform(transform):
32+
"""
33+
Generates a list of subsequent lookup expressions for a transform.
34+
35+
Note:
36+
Infinite transform recursion is only prevented when the subsequent and
37+
passed in transforms are the same class. For example, the ``Unaccent``
38+
transform from ``django.contrib.postgres``.
39+
There is no cycle detection across multiple transforms. For example,
40+
``a__b__a__b`` would continue to recurse. However, this is not currently
41+
a problem (no builtin transforms exhibit this behavior).
42+
43+
"""
44+
lookups = []
45+
46+
for expr, lookup in six.iteritems(class_lookups(transform.output_field)):
1647
if issubclass(lookup, Transform):
48+
49+
# type match indicates recursion.
50+
if type(transform) == lookup:
51+
continue
52+
53+
sub_transform = lookup(transform)
1754
lookups += [
18-
LOOKUP_SEP.join([expr, transform]) for transform
19-
in lookups_for_field(lookup(model_field).output_field)
55+
LOOKUP_SEP.join([expr, sub_expr]) for sub_expr
56+
in lookups_for_transform(sub_transform)
2057
]
58+
2159
else:
2260
lookups.append(expr)
2361

@@ -28,12 +66,12 @@ def class_lookups(model_field):
2866
"""
2967
Get a compiled set of class_lookups for a model field.
3068
"""
31-
field_class = model_field.__class__
69+
field_class = type(model_field)
3270
class_lookups = OrderedDict()
3371

3472
# traverse MRO in reverse, as this puts standard
3573
# lookups before subclass transforms/lookups
36-
for cls in field_class.mro()[::-1]:
74+
for cls in reversed(field_class.mro()):
3775
if hasattr(cls, 'class_lookups'):
3876
class_lookups.update(getattr(cls, 'class_lookups'))
3977

tests/test_utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@ def test_transformed_field(self):
3030
self.assertIn('date__year__exact', lookups)
3131

3232

33+
@unittest.skipIf(django.VERSION < (1, 9), "version does not support transformed lookup expressions")
34+
class LookupsForTransformTests(TestCase):
35+
def test_recursion_prevention(self):
36+
model_field = Person._meta.get_field('name')
37+
lookups = utils.lookups_for_field(model_field)
38+
39+
self.assertIn('unaccent__exact', lookups)
40+
self.assertNotIn('unaccent__unaccent__exact', lookups)
41+
42+
3343
class ClassLookupsTests(TestCase):
3444
def test_standard_field(self):
3545
model_field = Person._meta.get_field('name')

tests/testapp/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
2+
default_app_config = 'tests.testapp.apps.TestappConfig'

tests/testapp/apps.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
from django.apps import AppConfig
3+
from django.db.models import CharField, TextField
4+
5+
from .lookups import Unaccent
6+
7+
8+
class TestappConfig(AppConfig):
9+
name = 'tests.testapp'
10+
11+
def ready(self):
12+
CharField.register_lookup(Unaccent)
13+
TextField.register_lookup(Unaccent)

tests/testapp/lookups.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from django.db.models import Transform
2+
3+
4+
# This is a copy of the `Unaccent` transform from `django.contrib.postgres`.
5+
# This is necessary as the postgres app requires psycopg2 to be installed.
6+
class Unaccent(Transform):
7+
bilateral = True
8+
lookup_name = 'unaccent'
9+
function = 'UNACCENT'

0 commit comments

Comments
 (0)