summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIan Foote <python@ian.feete.org>2016-11-06 10:37:07 +0000
committerTim Graham <timograham@gmail.com>2017-02-23 20:47:48 -0500
commit508b5debfb16843a8443ebac82c1fb91f15da687 (patch)
tree1281cdc5b664d118bf709686c49fc3ceae42dd4c
parent19b2dfd1bfe7fd716dd3d8bfa5f972070d83b42f (diff)
Refs #11964 -- Made Q objects deconstructible.
-rw-r--r--django/db/models/query_utils.py17
-rw-r--r--django/utils/tree.py7
-rw-r--r--tests/queries/test_q.py76
-rw-r--r--tests/utils_tests/test_tree.py16
4 files changed, 115 insertions, 1 deletions
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)