Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django breaking long lookup names on queries

Let's assume there is a line of code to perform a query using the Django ORM that contains a very long 'lookup name':

QuerySet.filter(myfk__child__onetoone__another__manytomany__relation__monster__relationship__mycustomlookup=':P')

I'd like to break the line to follow pep8, specially 79 characters limit

I know we can do something like:

   QuerySet.filter(
      **{
        'myfk__child__onetoone__another' 
        '__manytomany__relation__monster' 
        '__relationship__mycustomlookup': ':P'
      }
    )

But I'm wondering if there is another, maybe more pythonic/accepted, way ?

like image 721
trinchet Avatar asked Mar 02 '17 16:03

trinchet


3 Answers

Maybe using LOOKUP_SEP to join the lookup names is a bit more paletable?

from django.db.models.constants import LOOKUP_SEP

lookup = LOOKUP_SEP.join(['myfk', 'child', 'onetoone', 'another', 'manytomany',
                          'relation', 'monster', 'relationship',
                          'mycustomlookup'])

QuerySet.filter(**{lookup:':P'})
like image 144
dinosaurwaltz Avatar answered Oct 13 '22 00:10

dinosaurwaltz


pep8 says:

The preferred way of wrapping long lines is by using Python's implied line continuation inside parentheses, brackets and braces.

That's what you've done, so I think what you've got is the most pythonic (or, at least, most pep8ic) way of doing it.

like image 25
daphtdazz Avatar answered Oct 13 '22 00:10

daphtdazz


EDIT (A simplified more attractive answer is here. The original detailed answer is below under line.)

I wrote a module django_dot_filter.py that helps to write query filters more naturally and readable. An expression starts with symbol V and names separated by dots:

from django_dot_filter import V

QuerySet.filter(V.myfk.child.onetoone.another.manytomany
                .relation.monster.relationship
                .mycustomlookup == ':P')

I read it like "this unknown Variable" with fields... therefore I use the letter V. That class is really only a symbol that can be followed by dots, methods, operators etc., everything separated by . instead of __.

Standard readable relationship operators like <, <=, == or != are supported, also parentheses and boolean operators &, |, ~.

Queryset.filter((V.some_related.my_field >= 10)
                | ~V.field_x.startswith('Y') & (V.date_field.year() == 2017)
                & V.price.range(10, 100))

Every lookup can be written the classic way like an attribute V.date_field.year == 2017 or like a method V.date_field.year() == 2017. Many lookups are much more readable as a method with an argument V.my_field.regex(r'^[abc]') instead of my_field__regex=value. It is much more readable for me to see a convention that .date() is a lookup method, but .date is a field.

It is no magic. Only a method with arguments or a relationship operator is every times the last part of lookup. A method without arguments is only a symbol that it is a lookup. Something with a value follows always. Expressions are compiled to Q expressions, including boolean expressions. They can be easily reused in similar projects, saved to variables etc., while exclude(..) conditions instead of missing != operator are less reusable.

(No unsupported features are currently known. Some tests have been written. If I get enough feedback, it can a package. It is a little more verbose than the classic good name=value, suitable for simple cases.



A different answer, if you like readable filters with possible long chains of related fields even if they are complicated.

I wrote a simple module django_dot_filter.py today, that allows to use dot syntax for fields on related models and use operators ==, !=, <, <=, >, >= for conditions. It can use bitwise operators ~ | & as boolean operators similarly like Q objects use, but due to operators priority, the comparision must be enclosed in parentheses. It is inspired by syntax used in SQLAlchemy and Pandas.

doc string:

class V(...):
    """
    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),
    >>>                             ~Q(option__in=['ABC', 'XYZ'])
    >>>                             )
    """

(The class "V" automatically creates a new instance if used with dot. All elements are compiled to Q expression after relational operator or after .in_(iterable) method and the instance is deleted again.)

Some examples from tests

    #       this is V. syntax         compiled Q syntax
    test_eq(V.a.b.c == 1,             Q(a__b__c=1))
    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 == 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))
    # method "in_(..)" is used because the word "in" is reserved.
    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')))

    # this should be eventually improved to support all lookup
    # functions automatically e.g. by ".contains('abc')" instead of "=="
    test_eq(V.a.contains == 'abc',    Q(a__contains='abc'))

It's a little joke inspired by your question, but it works. I remember some old discussion of (core developers? vague memories) that the syntax name__operator=value wouldn't be used again if Django be a new project. It is very concise, but less readable. It is too late to have two official syntaxes.

like image 25
hynekcer Avatar answered Oct 13 '22 00:10

hynekcer