diff --git a/django/db/models/query_utils.py b/django/db/models/query_utils.py index 7b1c46f15c..fe1d8f9f69 100644 --- a/django/db/models/query_utils.py +++ b/django/db/models/query_utils.py @@ -56,7 +56,9 @@ class Q(tree.Node): default = AND def __init__(self, *args, **kwargs): - super().__init__(children=list(args) + list(kwargs.items())) + connector = kwargs.pop('_connector', None) + negated = kwargs.pop('_negated', False) + super().__init__(children=list(args) + list(kwargs.items()), connector=connector, negated=negated) def _combine(self, other, conn): if not isinstance(other, Q): @@ -86,6 +88,19 @@ class Q(tree.Node): query.promote_joins(joins) return clause + def deconstruct(self): + path = '%s.%s' % (self.__class__.__module__, self.__class__.__name__) + args, kwargs = (), {} + if len(self.children) == 1 and not isinstance(self.children[0], Q): + child = self.children[0] + kwargs = {child[0]: child[1]} + else: + args = tuple(self.children) + kwargs = {'_connector': self.connector} + if self.negated: + kwargs['_negated'] = True + return path, args, kwargs + class DeferredAttribute: """ diff --git a/django/utils/tree.py b/django/utils/tree.py index 15f3c58205..74612736f1 100644 --- a/django/utils/tree.py +++ b/django/utils/tree.py @@ -63,6 +63,13 @@ class Node: """Return True if 'other' is a direct child of this instance.""" return other in self.children + def __eq__(self, other): + if self.__class__ != other.__class__: + return False + if (self.connector, self.negated) == (other.connector, other.negated): + return self.children == other.children + return False + def add(self, data, conn_type, squash=True): """ Combine this tree and the data represented by data using the diff --git a/tests/queries/test_q.py b/tests/queries/test_q.py new file mode 100644 index 0000000000..9b9fba6ba1 --- /dev/null +++ b/tests/queries/test_q.py @@ -0,0 +1,76 @@ +from django.db.models import F, Q +from django.test import SimpleTestCase + + +class QTests(SimpleTestCase): + def test_deconstruct(self): + q = Q(price__gt=F('discounted_price')) + path, args, kwargs = q.deconstruct() + self.assertEqual(path, 'django.db.models.query_utils.Q') + self.assertEqual(args, ()) + self.assertEqual(kwargs, {'price__gt': F('discounted_price')}) + + def test_deconstruct_negated(self): + q = ~Q(price__gt=F('discounted_price')) + path, args, kwargs = q.deconstruct() + self.assertEqual(path, 'django.db.models.query_utils.Q') + self.assertEqual(args, ()) + self.assertEqual(kwargs, { + 'price__gt': F('discounted_price'), + '_negated': True, + }) + + def test_deconstruct_or(self): + q1 = Q(price__gt=F('discounted_price')) + q2 = Q(price=F('discounted_price')) + q = q1 | q2 + path, args, kwargs = q.deconstruct() + self.assertEqual(path, 'django.db.models.query_utils.Q') + self.assertEqual(args, ( + ('price__gt', F('discounted_price')), + ('price', F('discounted_price')), + )) + self.assertEqual(kwargs, {'_connector': 'OR'}) + + def test_deconstruct_and(self): + q1 = Q(price__gt=F('discounted_price')) + q2 = Q(price=F('discounted_price')) + q = q1 & q2 + path, args, kwargs = q.deconstruct() + self.assertEqual(path, 'django.db.models.query_utils.Q') + self.assertEqual(args, ( + ('price__gt', F('discounted_price')), + ('price', F('discounted_price')), + )) + self.assertEqual(kwargs, {'_connector': 'AND'}) + + def test_deconstruct_nested(self): + q = Q(Q(price__gt=F('discounted_price'))) + path, args, kwargs = q.deconstruct() + self.assertEqual(path, 'django.db.models.query_utils.Q') + self.assertEqual(args, (Q(price__gt=F('discounted_price')),)) + self.assertEqual(kwargs, {'_connector': 'AND'}) + + def test_reconstruct(self): + q = Q(price__gt=F('discounted_price')) + path, args, kwargs = q.deconstruct() + self.assertEqual(Q(*args, **kwargs), q) + + def test_reconstruct_negated(self): + q = ~Q(price__gt=F('discounted_price')) + path, args, kwargs = q.deconstruct() + self.assertEqual(Q(*args, **kwargs), q) + + def test_reconstruct_or(self): + q1 = Q(price__gt=F('discounted_price')) + q2 = Q(price=F('discounted_price')) + q = q1 | q2 + path, args, kwargs = q.deconstruct() + self.assertEqual(Q(*args, **kwargs), q) + + def test_reconstruct_and(self): + q1 = Q(price__gt=F('discounted_price')) + q2 = Q(price=F('discounted_price')) + q = q1 & q2 + path, args, kwargs = q.deconstruct() + self.assertEqual(Q(*args, **kwargs), q) diff --git a/tests/utils_tests/test_tree.py b/tests/utils_tests/test_tree.py index 8ab73e2b92..98db5f6012 100644 --- a/tests/utils_tests/test_tree.py +++ b/tests/utils_tests/test_tree.py @@ -55,3 +55,19 @@ class NodeTests(unittest.TestCase): node5 = copy.deepcopy(self.node1) self.assertIs(self.node1.children, node4.children) self.assertIsNot(self.node1.children, node5.children) + + def test_eq_children(self): + node = Node(self.node1_children) + self.assertEqual(node, self.node1) + self.assertNotEqual(node, self.node2) + + def test_eq_connector(self): + new_node = Node(connector='NEW') + default_node = Node(connector='DEFAULT') + self.assertEqual(default_node, self.node2) + self.assertNotEqual(default_node, new_node) + + def test_eq_negated(self): + node = Node(negated=False) + negated = Node(negated=True) + self.assertNotEqual(negated, node)