Fixed #22936 -- Obsoleted Field.get_prep_lookup()/get_db_prep_lookup()

Thanks Tim Graham for completing the initial patch.
This commit is contained in:
Claude Paroz 2016-04-23 19:13:31 +02:00
parent 1206eea11e
commit 388bb5bd9a
19 changed files with 89 additions and 220 deletions

View File

@ -232,6 +232,9 @@ class BooleanFieldListFilter(FieldListFilter):
self.lookup_val = request.GET.get(self.lookup_kwarg)
self.lookup_val2 = request.GET.get(self.lookup_kwarg2)
super(BooleanFieldListFilter, self).__init__(field, request, params, model, model_admin, field_path)
if (self.used_parameters and self.lookup_kwarg in self.used_parameters and
self.used_parameters[self.lookup_kwarg] in ('1', '0')):
self.used_parameters[self.lookup_kwarg] = bool(int(self.used_parameters[self.lookup_kwarg]))
def expected_parameters(self):
return [self.lookup_kwarg, self.lookup_kwarg2]

View File

@ -128,7 +128,6 @@ def url_params_from_lookup_dict(lookups):
if isinstance(v, (tuple, list)):
v = ','.join(str(x) for x in v)
elif isinstance(v, bool):
# See django.db.fields.BooleanField.get_prep_lookup
v = ('0', '1')[v]
else:
v = six.text_type(v)

View File

@ -316,13 +316,6 @@ class GeometryField(GeoSelectFormatMixin, BaseSpatialField):
params = [connection.ops.Adapter(value)]
return params
def get_prep_lookup(self, lookup_type, value):
if lookup_type == 'contains':
# 'contains' name might conflict with the "normal" contains lookup,
# for which the value is not prepared, but left as-is.
return self.get_prep_value(value)
return super(GeometryField, self).get_prep_lookup(lookup_type, value)
def get_db_prep_save(self, value, connection):
"Prepares the value for saving in the database."
if not value:

View File

@ -31,13 +31,6 @@ class JSONField(Field):
return Json(value)
return value
def get_prep_lookup(self, lookup_type, value):
if lookup_type in ('has_key', 'has_keys', 'has_any_keys'):
return value
if isinstance(value, (dict, list)):
return Json(value)
return super(JSONField, self).get_prep_lookup(lookup_type, value)
def validate(self, value, model_instance):
super(JSONField, self).validate(value, model_instance)
try:

View File

@ -154,7 +154,7 @@ class RangeContainedBy(models.Lookup):
return sql % (lhs, rhs), params
def get_prep_lookup(self):
return RangeField().get_prep_lookup(self.lookup_name, self.rhs)
return RangeField().get_prep_value(self.rhs)
models.DateField.register_lookup(RangeContainedBy)

View File

@ -1,4 +1,5 @@
from django.db.models import Lookup, Transform
from django.utils.encoding import force_text
from .search import SearchVector, SearchVectorExact, SearchVectorField
@ -29,14 +30,18 @@ class Overlap(PostgresSimpleLookup):
class HasKey(PostgresSimpleLookup):
lookup_name = 'has_key'
operator = '?'
prepare_rhs = False
class HasKeys(PostgresSimpleLookup):
lookup_name = 'has_keys'
operator = '?&'
def get_prep_lookup(self):
return [force_text(item) for item in self.rhs]
class HasAnyKeys(PostgresSimpleLookup):
class HasAnyKeys(HasKeys):
lookup_name = 'has_any_keys'
operator = '?|'

View File

@ -213,7 +213,7 @@ class BaseExpression(object):
def _prepare(self, field):
"""
Hook used by Field.get_prep_lookup() to do custom preparation.
Hook used by Lookup.get_prep_lookup() to do custom preparation.
"""
return self

View File

@ -741,8 +741,7 @@ class Field(RegisterLookupMixin):
"""Returns field's value prepared for interacting with the database
backend.
Used by the default implementations of ``get_db_prep_save``and
`get_db_prep_lookup```
Used by the default implementations of get_db_prep_save().
"""
if not prepared:
value = self.get_prep_value(value)
@ -755,36 +754,6 @@ class Field(RegisterLookupMixin):
return self.get_db_prep_value(value, connection=connection,
prepared=False)
def get_prep_lookup(self, lookup_type, value):
"""
Perform preliminary non-db specific lookup checks and conversions
"""
if hasattr(value, '_prepare'):
return value._prepare(self)
if lookup_type in {
'iexact', 'contains', 'icontains',
'startswith', 'istartswith', 'endswith', 'iendswith',
'isnull', 'search', 'regex', 'iregex',
}:
return value
elif lookup_type in ('exact', 'gt', 'gte', 'lt', 'lte'):
return self.get_prep_value(value)
elif lookup_type in ('range', 'in'):
return [self.get_prep_value(v) for v in value]
return self.get_prep_value(value)
def get_db_prep_lookup(self, lookup_type, value, connection,
prepared=False):
"""
Returns field's value prepared for database lookup.
"""
if not prepared:
value = self.get_prep_lookup(lookup_type, value)
prepared = True
return [value]
def has_default(self):
"""
Returns a boolean of whether this field has a default value.
@ -1049,20 +1018,11 @@ class BooleanField(Field):
params={'value': value},
)
def get_prep_lookup(self, lookup_type, value):
# Special-case handling for filters coming from a Web request (e.g. the
# admin interface). Only works for scalar values (not lists). If you're
# passing in a list, you might as well make things the right type when
# constructing the list.
if value in ('1', '0'):
value = bool(int(value))
return super(BooleanField, self).get_prep_lookup(lookup_type, value)
def get_prep_value(self, value):
value = super(BooleanField, self).get_prep_value(value)
if value is None:
return None
return bool(value)
return self.to_python(value)
def formfield(self, **kwargs):
# Unlike most fields, BooleanField figures out include_blank from
@ -1453,8 +1413,6 @@ class DateTimeField(DateField):
# contribute_to_class is inherited from DateField, it registers
# get_next_by_FOO and get_prev_by_FOO
# get_prep_lookup is inherited from DateField
def get_prep_value(self, value):
value = super(DateTimeField, self).get_prep_value(value)
value = self.to_python(value)
@ -2051,21 +2009,11 @@ class NullBooleanField(Field):
params={'value': value},
)
def get_prep_lookup(self, lookup_type, value):
# Special-case handling for filters coming from a Web request (e.g. the
# admin interface). Only works for scalar values (not lists). If you're
# passing in a list, you might as well make things the right type when
# constructing the list.
if value in ('1', '0'):
value = bool(int(value))
return super(NullBooleanField, self).get_prep_lookup(lookup_type,
value)
def get_prep_value(self, value):
value = super(NullBooleanField, self).get_prep_value(value)
if value is None:
return None
return bool(value)
return self.to_python(value)
def formfield(self, **kwargs):
defaults = {

View File

@ -271,11 +271,6 @@ class FileField(Field):
def get_internal_type(self):
return "FileField"
def get_prep_lookup(self, lookup_type, value):
if hasattr(value, 'name'):
value = value.name
return super(FileField, self).get_prep_lookup(lookup_type, value)
def get_prep_value(self, value):
"Returns field's value prepared for saving into a database."
value = super(FileField, self).get_prep_value(value)

View File

@ -44,15 +44,15 @@ class RelatedIn(In):
if not isinstance(self.lhs, MultiColSource) and self.rhs_is_direct_value():
# If we get here, we are dealing with single-column relations.
self.rhs = [get_normalized_value(val, self.lhs)[0] for val in self.rhs]
# We need to run the related field's get_prep_lookup(). Consider case
# We need to run the related field's get_prep_value(). Consider case
# ForeignKey to IntegerField given value 'abc'. The ForeignKey itself
# doesn't have validation for non-integers, so we must run validation
# using the target field.
if hasattr(self.lhs.output_field, 'get_path_info'):
# Run the target field's get_prep_lookup. We can safely assume there is
# Run the target field's get_prep_value. We can safely assume there is
# only one as we don't get to the direct value branch otherwise.
self.rhs = self.lhs.output_field.get_path_info()[-1].target_fields[-1].get_prep_lookup(
self.lookup_name, self.rhs)
target_field = self.lhs.output_field.get_path_info()[-1].target_fields[-1]
self.rhs = [target_field.get_prep_value(v) for v in self.rhs]
return super(RelatedIn, self).get_prep_lookup()
def as_sql(self, compiler, connection):
@ -88,15 +88,15 @@ class RelatedLookupMixin(object):
if not isinstance(self.lhs, MultiColSource) and self.rhs_is_direct_value():
# If we get here, we are dealing with single-column relations.
self.rhs = get_normalized_value(self.rhs, self.lhs)[0]
# We need to run the related field's get_prep_lookup(). Consider case
# We need to run the related field's get_prep_value(). Consider case
# ForeignKey to IntegerField given value 'abc'. The ForeignKey itself
# doesn't have validation for non-integers, so we must run validation
# using the target field.
if hasattr(self.lhs.output_field, 'get_path_info'):
# Get the target field. We can safely assume there is only one
# as we don't get to the direct value branch otherwise.
self.rhs = self.lhs.output_field.get_path_info()[-1].target_fields[-1].get_prep_lookup(
self.lookup_name, self.rhs)
target_field = self.lhs.output_field.get_path_info()[-1].target_fields[-1]
self.rhs = target_field.get_prep_value(self.rhs)
return super(RelatedLookupMixin, self).get_prep_lookup()

View File

@ -110,9 +110,6 @@ class ForeignObjectRel(object):
def one_to_one(self):
return self.field.one_to_one
def get_prep_lookup(self, lookup_name, value):
return self.field.get_prep_lookup(lookup_name, value)
def get_lookup(self, lookup_name):
return self.field.get_lookup(lookup_name)
@ -142,10 +139,6 @@ class ForeignObjectRel(object):
(x._get_pk_val(), smart_text(x)) for x in self.related_model._default_manager.all()
]
def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False):
# Defer to the actual field definition for db prep
return self.field.get_db_prep_lookup(lookup_type, value, connection=connection, prepared=prepared)
def is_hidden(self):
"Should the related object be hidden?"
return bool(self.related_name) and self.related_name[-1] == '+'

View File

@ -16,6 +16,7 @@ from django.utils.six.moves import range
class Lookup(object):
lookup_name = None
prepare_rhs = True
def __init__(self, lhs, rhs):
self.lhs, self.rhs = lhs, rhs
@ -56,12 +57,14 @@ class Lookup(object):
return sqls, sqls_params
def get_prep_lookup(self):
return self.lhs.output_field.get_prep_lookup(self.lookup_name, self.rhs)
if hasattr(self.rhs, '_prepare'):
return self.rhs._prepare(self.lhs.output_field)
if self.prepare_rhs and hasattr(self.lhs.output_field, 'get_prep_value'):
return self.lhs.output_field.get_prep_value(self.rhs)
return self.rhs
def get_db_prep_lookup(self, value, connection):
return (
'%s', self.lhs.output_field.get_db_prep_lookup(
self.lookup_name, value, connection, prepared=True))
return ('%s', [value])
def process_lhs(self, compiler, connection, lhs=None):
lhs = lhs or self.lhs
@ -199,6 +202,7 @@ Field.register_lookup(Exact)
class IExact(BuiltinLookup):
lookup_name = 'iexact'
prepare_rhs = False
def process_rhs(self, qn, connection):
rhs, params = super(IExact, self).process_rhs(qn, connection)
@ -254,6 +258,13 @@ IntegerField.register_lookup(IntegerLessThan)
class In(FieldGetDbPrepValueIterableMixin, BuiltinLookup):
lookup_name = 'in'
def get_prep_lookup(self):
if hasattr(self.rhs, '_prepare'):
return self.rhs._prepare(self.lhs.output_field)
if hasattr(self.lhs.output_field, 'get_prep_value'):
return [self.lhs.output_field.get_prep_value(v) for v in self.rhs]
return self.rhs
def process_rhs(self, compiler, connection):
db_rhs = getattr(self.rhs, '_db', None)
if db_rhs is not None and db_rhs != connection.alias:
@ -335,6 +346,7 @@ class PatternLookup(BuiltinLookup):
class Contains(PatternLookup):
lookup_name = 'contains'
prepare_rhs = False
def process_rhs(self, qn, connection):
rhs, params = super(Contains, self).process_rhs(qn, connection)
@ -346,11 +358,13 @@ Field.register_lookup(Contains)
class IContains(Contains):
lookup_name = 'icontains'
prepare_rhs = False
Field.register_lookup(IContains)
class StartsWith(PatternLookup):
lookup_name = 'startswith'
prepare_rhs = False
def process_rhs(self, qn, connection):
rhs, params = super(StartsWith, self).process_rhs(qn, connection)
@ -362,6 +376,7 @@ Field.register_lookup(StartsWith)
class IStartsWith(PatternLookup):
lookup_name = 'istartswith'
prepare_rhs = False
def process_rhs(self, qn, connection):
rhs, params = super(IStartsWith, self).process_rhs(qn, connection)
@ -373,6 +388,7 @@ Field.register_lookup(IStartsWith)
class EndsWith(PatternLookup):
lookup_name = 'endswith'
prepare_rhs = False
def process_rhs(self, qn, connection):
rhs, params = super(EndsWith, self).process_rhs(qn, connection)
@ -384,6 +400,7 @@ Field.register_lookup(EndsWith)
class IEndsWith(PatternLookup):
lookup_name = 'iendswith'
prepare_rhs = False
def process_rhs(self, qn, connection):
rhs, params = super(IEndsWith, self).process_rhs(qn, connection)
@ -396,6 +413,11 @@ Field.register_lookup(IEndsWith)
class Range(FieldGetDbPrepValueIterableMixin, BuiltinLookup):
lookup_name = 'range'
def get_prep_lookup(self):
if hasattr(self.rhs, '_prepare'):
return self.rhs._prepare(self.lhs.output_field)
return [self.lhs.output_field.get_prep_value(v) for v in self.rhs]
def get_rhs_op(self, connection, rhs):
return "BETWEEN %s AND %s" % (rhs[0], rhs[1])
@ -411,6 +433,7 @@ Field.register_lookup(Range)
class IsNull(BuiltinLookup):
lookup_name = 'isnull'
prepare_rhs = False
def as_sql(self, compiler, connection):
sql, params = compiler.compile(self.lhs)
@ -423,6 +446,7 @@ Field.register_lookup(IsNull)
class Search(BuiltinLookup):
lookup_name = 'search'
prepare_rhs = False
def as_sql(self, compiler, connection):
warnings.warn(
@ -438,6 +462,7 @@ Field.register_lookup(Search)
class Regex(BuiltinLookup):
lookup_name = 'regex'
prepare_rhs = False
def as_sql(self, compiler, connection):
if self.lookup_name in connection.operators:

View File

@ -577,67 +577,6 @@ the end. You should also update the model's attribute if you make any changes
to the value so that code holding references to the model will always see the
correct value.
.. _preparing-values-for-use-in-database-lookups:
Preparing values for use in database lookups
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
As with value conversions, preparing a value for database lookups is a
two phase process.
:meth:`.get_prep_lookup` performs the first phase of lookup preparation:
type conversion and data validation.
Prepares the ``value`` for passing to the database when used in a lookup (a
``WHERE`` constraint in SQL). The ``lookup_type`` parameter will be one of the
valid Django filter lookups: ``exact``, ``iexact``, ``contains``, ``icontains``,
``gt``, ``gte``, ``lt``, ``lte``, ``in``, ``startswith``, ``istartswith``,
``endswith``, ``iendswith``, ``range``, ``year``, ``month``, ``day``,
``isnull``, ``search``, ``regex``, and ``iregex``.
If you are using :doc:`custom lookups </howto/custom-lookups>`, the
``lookup_type`` can be any ``lookup_name`` used by the project's custom lookups.
Your method must be prepared to handle all of these ``lookup_type`` values and
should raise either a ``ValueError`` if the ``value`` is of the wrong sort (a
list when you were expecting an object, for example) or a ``TypeError`` if
your field does not support that type of lookup. For many fields, you can get
by with handling the lookup types that need special handling for your field
and pass the rest to the :meth:`~Field.get_db_prep_lookup` method of the parent
class.
If you needed to implement :meth:`.get_db_prep_save`, you will usually need to
implement :meth:`.get_prep_lookup`. If you don't, :meth:`.get_prep_value` will
be called by the default implementation, to manage ``exact``, ``gt``, ``gte``,
``lt``, ``lte``, ``in`` and ``range`` lookups.
You may also want to implement this method to limit the lookup types that could
be used with your custom field type.
Note that, for ``"range"`` and ``"in"`` lookups, ``get_prep_lookup`` will receive
a list of objects (presumably of the right type) and will need to convert them
to a list of things of the right type for passing to the database. Most of the
time, you can reuse ``get_prep_value()``, or at least factor out some common
pieces.
For example, the following code implements ``get_prep_lookup`` to limit the
accepted lookup types to ``exact`` and ``in``::
class HandField(models.Field):
# ...
def get_prep_lookup(self, lookup_type, value):
# We only handle 'exact' and 'in'. All others are errors.
if lookup_type == 'exact':
return self.get_prep_value(value)
elif lookup_type == 'in':
return [self.get_prep_value(v) for v in value]
else:
raise TypeError('Lookup type %r not supported.' % lookup_type)
For performing database-specific data conversions required by a lookup,
you can override :meth:`~Field.get_db_prep_lookup`.
.. _specifying-form-field-for-model-field:
Specifying the form field for a model field

View File

@ -1717,8 +1717,7 @@ Field API reference
``Field`` is an abstract class that represents a database table column.
Django uses fields to create the database table (:meth:`db_type`), to map
Python types to database (:meth:`get_prep_value`) and vice-versa
(:meth:`from_db_value`), and to apply :doc:`/ref/models/lookups`
(:meth:`get_prep_lookup`).
(:meth:`from_db_value`).
A field is thus a fundamental piece in different Django APIs, notably,
:class:`models <django.db.models.Model>` and :class:`querysets
@ -1847,26 +1846,6 @@ Field API reference
See :ref:`preprocessing-values-before-saving` for usage.
When a lookup is used on a field, the value may need to be "prepared".
Django exposes two methods for this:
.. method:: get_prep_lookup(lookup_type, value)
Prepares ``value`` to the database prior to be used in a lookup.
The ``lookup_type`` will be the registered name of the lookup. For
example: ``"exact"``, ``"iexact"``, or ``"contains"``.
See :ref:`preparing-values-for-use-in-database-lookups` for usage.
.. method:: get_db_prep_lookup(lookup_type, value, connection, prepared=False)
Similar to :meth:`get_db_prep_value`, but for performing a lookup.
As with :meth:`get_db_prep_value`, the specific connection that will
be used for the query is passed as ``connection``. In addition,
``prepared`` describes whether the value has already been prepared with
:meth:`get_prep_lookup`.
Fields often receive their values as a different type, either from
serialization or from forms.

View File

@ -677,6 +677,25 @@ You can check if your database has any of the removed hashers like this::
# Unsalted MD5 passwords might not have an 'md5$$' prefix:
User.objects.filter(password__length=32)
``Field.get_prep_lookup()`` and ``Field.get_db_prep_lookup()`` methods are removed
----------------------------------------------------------------------------------
If you have a custom field that implements either of these methods, register a
custom lookup for it. For example::
from django.db.models import Field
from django.db.models.lookups import Exact
class MyField(Field):
...
class MyFieldExact(Exact):
def get_prep_lookup(self):
# do_custom_stuff_for_myfield
....
MyField.register_lookup(MyFieldExact)
:mod:`django.contrib.gis`
-------------------------

View File

@ -1,29 +1,29 @@
from django.core.exceptions import ValidationError
from django.db import IntegrityError, connection, models, transaction
from django.db import IntegrityError, models, transaction
from django.test import SimpleTestCase, TestCase
from .models import BooleanModel, FksToBooleans, NullBooleanModel
class BooleanFieldTests(TestCase):
def _test_get_db_prep_lookup(self, f):
self.assertEqual(f.get_db_prep_lookup('exact', True, connection=connection), [True])
self.assertEqual(f.get_db_prep_lookup('exact', '1', connection=connection), [True])
self.assertEqual(f.get_db_prep_lookup('exact', 1, connection=connection), [True])
self.assertEqual(f.get_db_prep_lookup('exact', False, connection=connection), [False])
self.assertEqual(f.get_db_prep_lookup('exact', '0', connection=connection), [False])
self.assertEqual(f.get_db_prep_lookup('exact', 0, connection=connection), [False])
self.assertEqual(f.get_db_prep_lookup('exact', None, connection=connection), [None])
def _test_get_prep_value(self, f):
self.assertEqual(f.get_prep_value(True), True)
self.assertEqual(f.get_prep_value('1'), True)
self.assertEqual(f.get_prep_value(1), True)
self.assertEqual(f.get_prep_value(False), False)
self.assertEqual(f.get_prep_value('0'), False)
self.assertEqual(f.get_prep_value(0), False)
self.assertEqual(f.get_prep_value(None), None)
def _test_to_python(self, f):
self.assertIs(f.to_python(1), True)
self.assertIs(f.to_python(0), False)
def test_booleanfield_get_db_prep_lookup(self):
self._test_get_db_prep_lookup(models.BooleanField())
def test_booleanfield_get_prep_value(self):
self._test_get_prep_value(models.BooleanField())
def test_nullbooleanfield_get_db_prep_lookup(self):
self._test_get_db_prep_lookup(models.NullBooleanField())
def test_nullbooleanfield_get_prep_value(self):
self._test_get_prep_value(models.NullBooleanField())
def test_booleanfield_to_python(self):
self._test_to_python(models.BooleanField())

View File

@ -1,22 +0,0 @@
from django.db import connection, models
from django.test import SimpleTestCase
class CustomFieldTests(SimpleTestCase):
def test_get_prep_value_count(self):
"""
Field values are not prepared twice in get_db_prep_lookup() (#14786).
"""
class NoopField(models.TextField):
def __init__(self, *args, **kwargs):
self.prep_value_count = 0
super(NoopField, self).__init__(*args, **kwargs)
def get_prep_value(self, value):
self.prep_value_count += 1
return super(NoopField, self).get_prep_value(value)
field = NoopField()
field.get_db_prep_lookup('exact', 'TEST', connection=connection, prepared=False)
self.assertEqual(field.prep_value_count, 1)

View File

@ -2,7 +2,7 @@ from decimal import Decimal
from django.core import validators
from django.core.exceptions import ValidationError
from django.db import connection, models
from django.db import models
from django.test import TestCase
from .models import BigD, Foo
@ -27,9 +27,10 @@ class DecimalFieldTests(TestCase):
self.assertEqual(f._format(f.to_python('2.6')), '2.6')
self.assertEqual(f._format(None), None)
def test_get_db_prep_lookup(self):
def test_get_prep_value(self):
f = models.DecimalField(max_digits=5, decimal_places=1)
self.assertEqual(f.get_db_prep_lookup('exact', None, connection=connection), [None])
self.assertEqual(f.get_prep_value(None), None)
self.assertEqual(f.get_prep_value('2.4'), Decimal('2.4'))
def test_filter_with_strings(self):
"""

View File

@ -1229,9 +1229,8 @@ class Queries2Tests(TestCase):
)
def test_ticket12239(self):
# Float was being rounded to integer on gte queries on integer field. Tests
# show that gt, lt, gte, and lte work as desired. Note that the fix changes
# get_prep_lookup for gte and lt queries only.
# Custom lookups are registered to round float values correctly on gte
# and lt IntegerField queries.
self.assertQuerysetEqual(
Number.objects.filter(num__gt=11.9),
['<Number: 12>']