Perform a logical exclusive OR on a Django Q object

Question:

I would like to perform a logical exclusive OR (XOR) on django.db.models.Q objects, using operator module to limit the choices of a model field to a subset of foreignkey. I am doing this in Django 1.4.3 along with Python 2.7.2. I had something like this:

import operator

from django.conf import settings
from django.db import models
from django.db.models import Q
from django.contrib.auth.models import User, Group

def query_group_lkup(group_name):
    return Q(user__user__groups__name__exact=group_name)

class Book(models.Model):
    author = models.ForeignKey(
                 User,
                 verbose_name=_("Author"),
                 null=False,
                 default='',
                 related_name="%(app_label)s_%(class)s_author",
                 # This would have provide an exclusive OR on the selected group name for User
                 limit_choices_to=reduce(
                     operator.xor,
                     map(query_group_lkup, getattr(settings, 'AUTHORIZED_AUTHORS', ''))
                 )

AUTHORIZED_AUTHORS is a list of existing group names.

But this did not work, because Q objects do not support ^ operator (only | and & operators from the docs). The message from the stacktrace was (partly) the following:

File "/home/moi/.virtualenvs/venv/lib/python2.7/site-packages/django/db/models/loading.py", line 64, in _populate
    self.load_app(app_name, True)
  File "/home/moi/.virtualenvs/venv/lib/python2.7/site-packages/django/db/models/loading.py", line 88, in load_app
    models = import_module('.models', app_name)
  File "/home/moi/.virtualenvs/venv/lib/python2.7/site-packages/django/utils/importlib.py", line 35, in import_module
    __import__(name)
  File "/opt/dvpt/toto/apps/book/models.py", line 42, in <module>
    class Book(models.Model):
  File "/opt/dvpt/toto/apps/book/models.py", line 100, in Book
    map(query_group_lkup, getattr(settings, 'AUTHORIZED_AUTHORS', ''))
TypeError: unsupported operand type(s) for ^: 'Q' and 'Q'

Therefore, inspired by this answer I attempted to implement an XOR for my specific lookup. It is not really flexible as the lookup is hardcoded (I would need to use kwargs in the arguments of query_xor for example…). I ended up doing something like this:

from django.conf import settings
from django.db import models
from django.db.models import Q
from django.db.models.query import EmptyQuerySet
from django.contrib.auth.models import User, Group

def query_xor_group(names_group):
    """Get a XOR of the queries that match the group names in names_group."""

    if not len(names_group):
        return EmptyQuerySet()
    elif len(names_group) == 1:
        return Q(user__user__groups__name__exact=names_group[0])

    q_chain_or = Q(user__user__groups__name__exact=names_group[0])
    q_chain_and = Q(user__user__groups__name__exact=names_group[0])

    for name in names_group[1:]:
        query = Q(user__user__groups__name__exact=name)
        q_chain_or |= query
        q_chain_and &= query

    return q_chain_or & ~q_chain_and

class Book(models.Model):
    author = models.ForeignKey(
                 User,
                 verbose_name=_("author"),
                 null=False,
                 default='',
                 related_name="%(app_label)s_%(class)s_author",
                 # This provides an exclusive OR on the SELECT group name for User
                 limit_choices_to=query_xor_group(getattr(settings, 'AUTHORIZED_AUTHORS', ''))
                 )

It works as I want but I seems to me rather not pythonic (especially the query_xor_group method).
Would there be a better (more direct way) of doing this?

Basically, my question can be stripped of the limit_choices_to part and be summarized as:

How can I make a bitwise exclusive OR on a set of django.db.models.Q objects in a Djangonic way?

Answers:

You could add an __xor__() method to Q that uses and/or/not to do the XOR logic.

from django.db.models import Q

class QQ:
    def __xor__(self, other):    
        not_self = self.clone()
        not_other = other.clone()
        not_self.negate()
        not_other.negate()

        x = self & not_other
        y = not_self & other

        return x | y

Q.__bases__ += (QQ, )

After doing this I was able to Q(...) ^ Q(...) in a filter() call.

Foobar.objects.filter(Q(blah=1) ^ Q(bar=2)) 

Which means the original attempt no longer throws an unsupported operand exception.

limit_choices_to=reduce(
                     operator.xor,
                     map(query_group_lkup, getattr(settings, 'AUTHORIZED_AUTHORS', ''))
                 )

Tested in Django 1.6.1 on Python 2.7.5

Answered By: Daniel Rucci

Django 4.1 added support for XOR:

Q objects and querysets can now be combined using ^ as the exclusive or (XOR) operator. XOR is natively supported on MariaDB and MySQL. For databases that do not support XOR, the query will be converted to an equivalent using AND, OR, and NOT.

It means you can now write Foobar.objects.filter(Q(blah=1) ^ Q(bar=2)) without monkey patching.
It was worth waiting nine years, wasn’t it?

Answered By: Benoit Blanchon