Skip to content

Instantly share code, notes, and snippets.

@hynekcer
Last active November 4, 2020 14:58
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hynekcer/a90895f3a869dc8ba9f64c40c5f1bac4 to your computer and use it in GitHub Desktop.
Save hynekcer/a90895f3a869dc8ba9f64c40c5f1bac4 to your computer and use it in GitHub Desktop.
Django filters with relational operators and dot to related fields instead of double underscores
from django.db.models import Q
from django.utils import six
class MetaV(type):
# metaclass to automatically create an instance by . dot
def __getattr__(self, name):
if name.startswith('_'):
return super(self, MetaV).__getattr__(name)
else:
return V(name)
class V(six.with_metaclass(MetaV, object)):
"""Syntax suger for more readable queryset filters with "." instead "__"
The name "V" can be understand like "variable", because a shortcut for
"field" is occupied yet.
The syntax is very similar to SQLAlchemy or Pandas.
Operators < <= == != >= > are supperted in filters.
>>> from django_dot_filter import V
>>>
>>> qs = Product.objects.filter(V.category.name == 'books',
>>> V.name >= 'B', V.name < 'F',
>>> (V.price < 15) | ~(V.date_created == today),
>>> V.option.in_(['ABC', 'XYZ'])
>>> )
This is the same as
>>> qs = Product.objects.filter(category__name='books',
>>> name__gte='B', name__lt='F',
>>> Q(price__lt=15) | ~Q(date_created=today)
>>> option__in=['ABC', 'XYZ']
>>> )
"""
def __init__(self, name=None):
self._names = []
if name is not None:
self._names.append(name)
def __eq__(self, other):
return self._final(None, other)
def __ne__(self, other):
return ~self._final(None, other)
def __lt__(self, other):
return self._final('lt', other)
def __le__(self, other):
return self._final('lte', other)
def __gt__(self, other):
return self._final('gt', other)
def __ge__(self, other):
return self._final('gte', other)
def in_(self, other):
return self._final('in', other)
def __getattr__(self, name):
if name.startswith('_'):
return super(self, V).__getattr__(name)
else:
self._names.append(name)
return self
def __call__(self, *args):
this_name = self._names[-1]
if len(args) == 2 and this_name == 'range':
return self._final(None, args)
elif len(args) == 1:
# 'iexact', 'exact', 'contains', 'icontains',
# 'startswith', 'istartswith', 'endswith', 'iendswith'
# 'regex', 'iregex'
# 'range'
return self._final(None, args[0])
elif len(args) == 0:
# 'date', 'year', 'month', 'day', 'week_day', 'hour', 'minute', 'second'
return self
else:
raise Exception(
"Invalid number of arguments {} to function {}".format(len(args), this_name)
)
def _final(self, name, other):
if name is not None:
self._names.append(name)
return Q(**{'__'.join(self._names): other})
# --------- TEST ----
rom django.test import TestCase
from django.db.models import Q
from django_dot_filter import V
class VTest(TestCase):
def test_v_compile(self):
def test_eq(obj, obj2):
assert repr(obj) == repr(obj2)
# test that these expressions are compiled correctly to Q expressions
test_eq(V.a == 1, Q(a=1))
test_eq(V.a != 1, ~Q(a=1))
test_eq(V.a < 2, Q(a__lt=2))
test_eq(V.a <= 3, Q(a__lte=3))
test_eq(V.a > 'abc', Q(a__gt='abc'))
test_eq(V.a >= 3.14, Q(a__gte=3.14))
test_eq(V.a.b.c == 1, Q(a__b__c=1))
test_eq((V.a == 1) & (V.b == 2), Q(a=1) & Q(b=2))
test_eq((V.a == 1) | (V.b == 2), Q(a=1) | Q(b=2))
test_eq((V.a == 1) | ~(V.b == 2), Q(a=1) | ~Q(b=2))
test_eq(V.first_name.in_([1, 2]), Q(first_name__in=[1, 2]))
test_eq(~V.a.in_(('Tim', 'Joe')), ~Q(a__in=('Tim', 'Joe')))
test_eq(V.a.year > 2016, Q(a__year__gt=2016))
test_eq(V.a.year() > 2016, Q(a__year__gt=2016))
test_eq(V.a.year() == 2016, Q(a__year=2016))
test_eq(V.a.startswith('Will'), Q(a__startswith='Will'))
test_eq(V.a.regex(r'^(An?|The) +'), Q(a__regex=r'^(An?|The) +'))
test_eq(V.a.range(10, 100), Q(a__range=(10, 100)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment