Fixed #16211 -- Added comparison and negation ops to F() expressions

Work done by Walter Doekes and Trac alias knoeb. Reviewed by Simon
Charette.
This commit is contained in:
Anssi Kääriäinen 2012-09-30 17:51:06 +03:00
parent ddd7d1af20
commit 28abf5f0eb
7 changed files with 150 additions and 22 deletions

View File

@ -913,6 +913,9 @@ class BaseDatabaseOperations(object):
can vary between backends (e.g., Oracle with %% and &) and between can vary between backends (e.g., Oracle with %% and &) and between
subexpression types (e.g., date expressions) subexpression types (e.g., date expressions)
""" """
if connector == 'NOT':
assert len(sub_expressions) == 1
return 'NOT (%s)' % sub_expressions[0]
conn = ' %s ' % connector conn = ' %s ' % connector
return conn.join(sub_expressions) return conn.join(sub_expressions)

View File

@ -18,6 +18,17 @@ class ExpressionNode(tree.Node):
AND = '&' AND = '&'
OR = '|' OR = '|'
# Unary operator (needs special attention in combine_expression)
NOT = 'NOT'
# Comparison operators
EQ = '='
GE = '>='
GT = '>'
LE = '<='
LT = '<'
NE = '<>'
def __init__(self, children=None, connector=None, negated=False): def __init__(self, children=None, connector=None, negated=False):
if children is not None and len(children) > 1 and connector is None: if children is not None and len(children) > 1 and connector is None:
raise TypeError('You have to specify a connector.') raise TypeError('You have to specify a connector.')
@ -93,6 +104,32 @@ class ExpressionNode(tree.Node):
def __ror__(self, other): def __ror__(self, other):
return self._combine(other, self.OR, True) return self._combine(other, self.OR, True)
def __invert__(self):
obj = ExpressionNode([self], connector=self.NOT, negated=True)
return obj
def __eq__(self, other):
return self._combine(other, self.EQ, False)
def __ge__(self, other):
return self._combine(other, self.GE, False)
def __gt__(self, other):
return self._combine(other, self.GT, False)
def __le__(self, other):
return self._combine(other, self.LE, False)
def __lt__(self, other):
return self._combine(other, self.LT, False)
def __ne__(self, other):
return self._combine(other, self.NE, False)
def __bool__(self):
raise TypeError('Boolean operators should be avoided. Use bitwise operators.')
__nonzero__ = __bool__
def prepare_database_save(self, unused): def prepare_database_save(self, unused):
return self return self

View File

@ -88,7 +88,11 @@ class Node(object):
Otherwise, the whole tree is pushed down one level and a new root Otherwise, the whole tree is pushed down one level and a new root
connector is created, connecting the existing tree and the new node. connector is created, connecting the existing tree and the new node.
""" """
if node in self.children and conn_type == self.connector: # Using for loop with 'is' instead of 'if node in children' so node
# __eq__ method doesn't get called. The __eq__ method can be overriden
# by subtypes, for example the F-expression.
for child in self.children:
if node is child and conn_type == self.connector:
return return
if len(self.children) < 2: if len(self.children) < 2:
self.connector = conn_type self.connector = conn_type

View File

@ -176,6 +176,10 @@ Django 1.5 also includes several smaller improvements worth noting:
:setting:`DEBUG` is `True` are sent to the console (unless you redefine the :setting:`DEBUG` is `True` are sent to the console (unless you redefine the
logger in your :setting:`LOGGING` setting). logger in your :setting:`LOGGING` setting).
* :ref:`F() expressions <query-expressions>` now support comparison operations
and inversion, expanding the types of expressions that can be passed to the
database.
Backwards incompatible changes in 1.5 Backwards incompatible changes in 1.5
===================================== =====================================

View File

@ -640,6 +640,15 @@ that were modified more than 3 days after they were published::
>>> from datetime import timedelta >>> from datetime import timedelta
>>> Entry.objects.filter(mod_date__gt=F('pub_date') + timedelta(days=3)) >>> Entry.objects.filter(mod_date__gt=F('pub_date') + timedelta(days=3))
.. versionadded:: 1.5
Comparisons and negation operators for ``F()`` expressions
Django also supports the comparison operators ``==``, ``!=``, ``<=``, ``<``,
``>``, ``>=`` and the bitwise negation operator ``~`` (boolean ``not`` operator
will raise ``TypeError``)::
>>> Entry.objects.filter(is_heavily_quoted=~(F('n_pingbacks') < 100))
The pk lookup shortcut The pk lookup shortcut
---------------------- ----------------------

View File

@ -27,6 +27,8 @@ class Company(models.Model):
Employee, Employee,
related_name='company_point_of_contact_set', related_name='company_point_of_contact_set',
null=True) null=True)
is_large = models.BooleanField(
blank=True)
def __str__(self): def __str__(self):
return self.name return self.name

View File

@ -11,22 +11,22 @@ from .models import Company, Employee
class ExpressionsTests(TestCase): class ExpressionsTests(TestCase):
def test_filter(self): def test_filter(self):
Company.objects.create( Company.objects.create(
name="Example Inc.", num_employees=2300, num_chairs=5, name="Example Inc.", num_employees=2300, num_chairs=5, is_large=False,
ceo=Employee.objects.create(firstname="Joe", lastname="Smith") ceo=Employee.objects.create(firstname="Joe", lastname="Smith")
) )
Company.objects.create( Company.objects.create(
name="Foobar Ltd.", num_employees=3, num_chairs=4, name="Foobar Ltd.", num_employees=3, num_chairs=4, is_large=False,
ceo=Employee.objects.create(firstname="Frank", lastname="Meyer") ceo=Employee.objects.create(firstname="Frank", lastname="Meyer")
) )
Company.objects.create( Company.objects.create(
name="Test GmbH", num_employees=32, num_chairs=1, name="Test GmbH", num_employees=32, num_chairs=1, is_large=False,
ceo=Employee.objects.create(firstname="Max", lastname="Mustermann") ceo=Employee.objects.create(firstname="Max", lastname="Mustermann")
) )
company_query = Company.objects.values( company_query = Company.objects.values(
"name", "num_employees", "num_chairs" "name", "num_employees", "num_chairs", "is_large"
).order_by( ).order_by(
"name", "num_employees", "num_chairs" "name", "num_employees", "num_chairs", "is_large"
) )
# We can filter for companies where the number of employees is greater # We can filter for companies where the number of employees is greater
@ -37,11 +37,13 @@ class ExpressionsTests(TestCase):
"num_chairs": 5, "num_chairs": 5,
"name": "Example Inc.", "name": "Example Inc.",
"num_employees": 2300, "num_employees": 2300,
"is_large": False
}, },
{ {
"num_chairs": 1, "num_chairs": 1,
"name": "Test GmbH", "name": "Test GmbH",
"num_employees": 32 "num_employees": 32,
"is_large": False
}, },
], ],
lambda o: o lambda o: o
@ -55,17 +57,20 @@ class ExpressionsTests(TestCase):
{ {
"num_chairs": 2300, "num_chairs": 2300,
"name": "Example Inc.", "name": "Example Inc.",
"num_employees": 2300 "num_employees": 2300,
"is_large": False
}, },
{ {
"num_chairs": 3, "num_chairs": 3,
"name": "Foobar Ltd.", "name": "Foobar Ltd.",
"num_employees": 3 "num_employees": 3,
"is_large": False
}, },
{ {
"num_chairs": 32, "num_chairs": 32,
"name": "Test GmbH", "name": "Test GmbH",
"num_employees": 32 "num_employees": 32,
"is_large": False
} }
], ],
lambda o: o lambda o: o
@ -79,17 +84,20 @@ class ExpressionsTests(TestCase):
{ {
'num_chairs': 2302, 'num_chairs': 2302,
'name': 'Example Inc.', 'name': 'Example Inc.',
'num_employees': 2300 'num_employees': 2300,
'is_large': False
}, },
{ {
'num_chairs': 5, 'num_chairs': 5,
'name': 'Foobar Ltd.', 'name': 'Foobar Ltd.',
'num_employees': 3 'num_employees': 3,
'is_large': False
}, },
{ {
'num_chairs': 34, 'num_chairs': 34,
'name': 'Test GmbH', 'name': 'Test GmbH',
'num_employees': 32 'num_employees': 32,
'is_large': False
} }
], ],
lambda o: o, lambda o: o,
@ -104,17 +112,20 @@ class ExpressionsTests(TestCase):
{ {
'num_chairs': 6900, 'num_chairs': 6900,
'name': 'Example Inc.', 'name': 'Example Inc.',
'num_employees': 2300 'num_employees': 2300,
'is_large': False
}, },
{ {
'num_chairs': 9, 'num_chairs': 9,
'name': 'Foobar Ltd.', 'name': 'Foobar Ltd.',
'num_employees': 3 'num_employees': 3,
'is_large': False
}, },
{ {
'num_chairs': 96, 'num_chairs': 96,
'name': 'Test GmbH', 'name': 'Test GmbH',
'num_employees': 32 'num_employees': 32,
'is_large': False
} }
], ],
lambda o: o, lambda o: o,
@ -129,21 +140,80 @@ class ExpressionsTests(TestCase):
{ {
'num_chairs': 5294600, 'num_chairs': 5294600,
'name': 'Example Inc.', 'name': 'Example Inc.',
'num_employees': 2300 'num_employees': 2300,
'is_large': False
}, },
{ {
'num_chairs': 15, 'num_chairs': 15,
'name': 'Foobar Ltd.', 'name': 'Foobar Ltd.',
'num_employees': 3 'num_employees': 3,
'is_large': False
}, },
{ {
'num_chairs': 1088, 'num_chairs': 1088,
'name': 'Test GmbH', 'name': 'Test GmbH',
'num_employees': 32 'num_employees': 32,
'is_large': False
} }
], ],
lambda o: o, lambda o: o,
) )
# The comparison operators and the bitwise unary not can be used
# to assign to boolean fields
for expression in (
# Check boundaries
~(F('num_employees') < 33),
~(F('num_employees') <= 32),
(F('num_employees') > 2299),
(F('num_employees') >= 2300),
(F('num_employees') == 2300),
((F('num_employees') + 1 != 4) & (32 != F('num_employees'))),
# Inverted argument order works too
(2299 < F('num_employees')),
(2300 <= F('num_employees'))
):
# Test update by F-expression
company_query.update(
is_large=expression
)
# Compare results
self.assertQuerysetEqual(
company_query, [
{
'num_chairs': 5294600,
'name': 'Example Inc.',
'num_employees': 2300,
'is_large': True
},
{
'num_chairs': 15,
'name': 'Foobar Ltd.',
'num_employees': 3,
'is_large': False
},
{
'num_chairs': 1088,
'name': 'Test GmbH',
'num_employees': 32,
'is_large': False
}
],
lambda o: o,
)
# Reset values
company_query.update(
is_large=False
)
# The python boolean operators should be avoided as they yield
# unexpected results
test_gmbh = Company.objects.get(name="Test GmbH")
with self.assertRaises(TypeError):
test_gmbh.is_large = not F('is_large')
with self.assertRaises(TypeError):
test_gmbh.is_large = F('is_large') and F('is_large')
with self.assertRaises(TypeError):
test_gmbh.is_large = F('is_large') or F('is_large')
# The relation of a foreign key can become copied over to an other # The relation of a foreign key can become copied over to an other
# foreign key. # foreign key.
@ -202,9 +272,8 @@ class ExpressionsTests(TestCase):
test_gmbh.point_of_contact = None test_gmbh.point_of_contact = None
test_gmbh.save() test_gmbh.save()
self.assertTrue(test_gmbh.point_of_contact is None) self.assertTrue(test_gmbh.point_of_contact is None)
def test(): with self.assertRaises(ValueError):
test_gmbh.point_of_contact = F("ceo") test_gmbh.point_of_contact = F("ceo")
self.assertRaises(ValueError, test)
test_gmbh.point_of_contact = test_gmbh.ceo test_gmbh.point_of_contact = test_gmbh.ceo
test_gmbh.save() test_gmbh.save()