HackSoftware / Django-Styleguide

Django styleguide used in HackSoft projects

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Clarify QuerySetType vs. Iterable[Model]

SHxKM opened this issue · comments

commented

In the docs there is this example:

def get_users(*, fetched_by: User) -> Iterable[User]:
    user_ids = get_visible_users_for(user=fetched_by)

    query = Q(id__in=user_ids)

    return User.objects.filter(query)

But also this example:

def get_items_for_user(
    *,
    user: User
) -> QuerySetType[Item]:
    return Item.objects.filter(payments__user=user)

What's the difference/distinction between the two? when should one prefer one over the other?

If you are talking about the indentation of the function arguments.
And if the question is:
"Should you prefer to keep function signature and arguments on one line or should you expand them like in the second snippet".
The answer is:
We tend to keep the function signatures on a single line like this:

def get_users(*, fetched_by: User) -> Iterable[User]:

We do this unless the line becomes longer than 120 symbols. If it becomes longer - we move the arguments on new lines like in the second snippet:

def get_items_for_user(
    *,
    user: User
) -> QuerySetType[Item]:

I guess the function signature from the second snippet used to have more arguments. Some of them were removed and we forgot to move the function signature back to a single line of code.

In conclusion:
We prefer to keep function signatures on a single line. If the line becomes longer than 120 symbols we indent every function argument on a single line making the code more readable.

commented

@kdkocev - thanks for the detailed message but I’m talking about the different return annotation.

If you are rather asking about the function bodies:

The get_users selector does some preparations before the actual database query. These preparations are written on separate lines to make the function easier to read. It would be much more difficult to understand what this function is doing if the body is:

def get_users(*, fetched_by: User) -> Iterable[User]:
    return User.objects.filter(Q(id__in=get_visible_users_for(user=fetched_by)))

If it's the return type you are asking about:

QuerySetType implements all the functions that Iterable has so it's ok to put it as a return type.
I think that it would be better if both return QuerySetType here because Iterable doesn't have the same interface that the querysets have.
It could be a mistake that we wrote the type of get_users as Iterable rather than QuerySetType

Sometimes we write the type as Iterable when we add dynamic properties to the queryset before returning it. If you add custom annotated properties to the queryset and use .filter afterwards - you will lose the dynamic properties 😕
So if we don't want anybody to use our returned queryset as a queryset but rather as something that can be iterated over - we write the return type as Iterable instead of QuerySetType

commented

@kdkocev - many thanks for this detailed response!