Improving Q objects with True, False, and None

It’s been a while, but I’m finally getting around to this.

I think Q.TRUE and Q.FALSE as class property aliases of ~Q(pk__in=[]) and Q(pk__in=[]) makes the most sense for now. It would be nice to just use Q.all() and Q.none(), but there would be confusion around Q.none() not being the same as ~Q.any([]).

It’s a bit weird that bool(Q.FALSE) is True, but that’s because only an empty Q() evaluates to False. ALWAYS/NEVER or EVERYTHING/NOTHING might avoid that, but it’s easier for me to think about the logical combinations using TRUE/FALSE.

I could add optimizations for simplifying combining logic, but the ORM handles that when creating the query anyway:

>>> print(get_user_model().objects.filter(Q(pk__in=[]) & Q(pk=7)).query)
...
django.core.exceptions.EmptyResultSet
>>> print(get_user_model().objects.filter(~Q(pk__in=[]) | Q(pk=7)).query)
SELECT "auth_user"."id", ... "auth_user"."date_joined" FROM "auth_user"

Q.any(iterable) and Q.all(iterable) are fairly simple. There’s a weird edge-case with iterables that only contain empty Q() objects. Those iterables will also be considered “empty”. That way any and all NEVER return an empty Q() object. For example, Q.any([Q(), Q()]) will still return Q(pk__in=[]).

I can see someone eventually running into a bug where they’ve checked that their input iterable isn’t length 0 and can’t figure out why they’re getting Q(pk__in=[]), but that’s better than accidentally exposing your entire table.

Edit: Actually, Q(Q()) can still cause problems. It evaluates to True and bypasses the logic checking for empty Q() objects. Simple to fix, but probably belongs in a separate ticket.

Edit2: And my Q(Q()) fix is revealing problems with deconstructing Q objects of query expressions. That might end up being it’s own ticket as well…