From 6de7f9ec60fbdc59797bc21803f16260bd203f04 Mon Sep 17 00:00:00 2001 From: Stefano Chiodino Date: Wed, 3 Oct 2018 00:17:23 +0100 Subject: [PATCH] Fixed #29598 -- Deprecated FloatRangeField in favor of DecimalRangeField. --- django/contrib/postgres/apps.py | 2 +- django/contrib/postgres/fields/ranges.py | 19 ++++- django/contrib/postgres/forms/ranges.py | 22 ++++-- docs/internals/deprecation.txt | 3 + docs/ref/checks.txt | 2 + docs/ref/contrib/postgres/fields.txt | 16 +++++ docs/ref/contrib/postgres/forms.txt | 15 ++++ docs/releases/2.2.txt | 4 ++ tests/postgres_tests/fields.py | 4 +- .../migrations/0002_create_test_models.py | 4 +- tests/postgres_tests/models.py | 4 +- tests/postgres_tests/test_introspection.py | 2 +- tests/postgres_tests/test_ranges.py | 71 +++++++++++-------- 13 files changed, 126 insertions(+), 42 deletions(-) diff --git a/django/contrib/postgres/apps.py b/django/contrib/postgres/apps.py index 1ab5074f19c..f1ec09806a2 100644 --- a/django/contrib/postgres/apps.py +++ b/django/contrib/postgres/apps.py @@ -19,7 +19,7 @@ class PostgresConfig(AppConfig): conn.introspection.data_types_reverse.update({ 3802: 'django.contrib.postgres.fields.JSONField', 3904: 'django.contrib.postgres.fields.IntegerRangeField', - 3906: 'django.contrib.postgres.fields.FloatRangeField', + 3906: 'django.contrib.postgres.fields.DecimalRangeField', 3910: 'django.contrib.postgres.fields.DateTimeRangeField', 3912: 'django.contrib.postgres.fields.DateRangeField', 3926: 'django.contrib.postgres.fields.BigIntegerRangeField', diff --git a/django/contrib/postgres/fields/ranges.py b/django/contrib/postgres/fields/ranges.py index 0bb914d383e..74ba4eb2301 100644 --- a/django/contrib/postgres/fields/ranges.py +++ b/django/contrib/postgres/fields/ranges.py @@ -10,7 +10,8 @@ from .utils import AttributeSetter __all__ = [ 'RangeField', 'IntegerRangeField', 'BigIntegerRangeField', - 'FloatRangeField', 'DateTimeRangeField', 'DateRangeField', + 'DecimalRangeField', 'DateTimeRangeField', 'DateRangeField', + 'FloatRangeField', ] @@ -100,7 +101,23 @@ class BigIntegerRangeField(RangeField): return 'int8range' +class DecimalRangeField(RangeField): + base_field = models.DecimalField + range_type = NumericRange + form_field = forms.DecimalRangeField + + def db_type(self, connection): + return 'numrange' + + class FloatRangeField(RangeField): + system_check_deprecated_details = { + 'msg': ( + 'FloatRangeField is deprecated and will be removed in Django 3.1.' + ), + 'hint': 'Use DecimalRangeField instead.', + 'id': 'fields.W902', + } base_field = models.FloatField range_type = NumericRange form_field = forms.FloatRangeField diff --git a/django/contrib/postgres/forms/ranges.py b/django/contrib/postgres/forms/ranges.py index 5f2b2434a3b..c36bec84793 100644 --- a/django/contrib/postgres/forms/ranges.py +++ b/django/contrib/postgres/forms/ranges.py @@ -1,13 +1,16 @@ +import warnings + from psycopg2.extras import DateRange, DateTimeTZRange, NumericRange from django import forms from django.core import exceptions from django.forms.widgets import MultiWidget +from django.utils.deprecation import RemovedInDjango31Warning from django.utils.translation import gettext_lazy as _ __all__ = [ - 'BaseRangeField', 'IntegerRangeField', 'FloatRangeField', - 'DateTimeRangeField', 'DateRangeField', 'RangeWidget', + 'BaseRangeField', 'IntegerRangeField', 'DecimalRangeField', + 'DateTimeRangeField', 'DateRangeField', 'FloatRangeField', 'RangeWidget', ] @@ -66,12 +69,23 @@ class IntegerRangeField(BaseRangeField): range_type = NumericRange -class FloatRangeField(BaseRangeField): +class DecimalRangeField(BaseRangeField): default_error_messages = {'invalid': _('Enter two numbers.')} - base_field = forms.FloatField + base_field = forms.DecimalField range_type = NumericRange +class FloatRangeField(DecimalRangeField): + base_field = forms.FloatField + + def __init__(self, **kwargs): + warnings.warn( + 'FloatRangeField is deprecated in favor of DecimalRangeField.', + RemovedInDjango31Warning, stacklevel=2, + ) + super().__init__(**kwargs) + + class DateTimeRangeField(BaseRangeField): default_error_messages = {'invalid': _('Enter two valid date/times.')} base_field = forms.DateTimeField diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 70433915270..1d03648ab1b 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -21,6 +21,9 @@ details on these changes. * A model's ``Meta.ordering`` will no longer affect ``GROUP BY`` queries. +* ``django.contrib.postgres.fields.FloatRangeField`` and + ``django.contrib.postgres.forms.FloatRangeField`` will be removed. + .. _deprecation-removed-in-3.0: 3.0 diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt index c8b13aa1004..dd74639d33d 100644 --- a/docs/ref/checks.txt +++ b/docs/ref/checks.txt @@ -168,6 +168,8 @@ Model fields check appeared in Django 1.10 and 1.11*. * **fields.E901**: ``CommaSeparatedIntegerField`` is removed except for support in historical migrations. +* **fields.W902**: ``FloatRangeField`` is deprecated and will be removed in + Django 3.1. File fields ~~~~~~~~~~~ diff --git a/docs/ref/contrib/postgres/fields.txt b/docs/ref/contrib/postgres/fields.txt index 0cac299f8d9..bcab67d1320 100644 --- a/docs/ref/contrib/postgres/fields.txt +++ b/docs/ref/contrib/postgres/fields.txt @@ -653,6 +653,18 @@ excluded; that is, ``[)``. returns a range in a canonical form that includes the lower bound and excludes the upper bound; that is ``[)``. +``DecimalRangeField`` +--------------------- + +.. class:: DecimalRangeField(**options) + + .. versionadded:: 2.2 + + Stores a range of floating point values. Based on a + :class:`~django.db.models.DecimalField`. Represented by a ``numrange`` in + the database and a :class:`~psycopg2:psycopg2.extras.NumericRange` in + Python. + ``FloatRangeField`` ------------------- @@ -662,6 +674,10 @@ excluded; that is, ``[)``. :class:`~django.db.models.FloatField`. Represented by a ``numrange`` in the database and a :class:`~psycopg2:psycopg2.extras.NumericRange` in Python. + .. deprecated:: 2.2 + + Use :class:`DecimalRangeField` instead. + ``DateTimeRangeField`` ---------------------- diff --git a/docs/ref/contrib/postgres/forms.txt b/docs/ref/contrib/postgres/forms.txt index 04adbc3a408..b5effb520ca 100644 --- a/docs/ref/contrib/postgres/forms.txt +++ b/docs/ref/contrib/postgres/forms.txt @@ -193,6 +193,17 @@ not greater than the upper bound. All of these fields use :class:`~django.contrib.postgres.fields.IntegerRangeField` and :class:`~django.contrib.postgres.fields.BigIntegerRangeField`. +``DecimalRangeField`` +~~~~~~~~~~~~~~~~~~~~~ + +.. class:: DecimalRangeField + + .. versionadded:: 2.2 + + Based on :class:`~django.forms.DecimalField` and translates its input into + :class:`~psycopg2:psycopg2.extras.NumericRange`. Default for + :class:`~django.contrib.postgres.fields.DecimalRangeField`. + ``FloatRangeField`` ~~~~~~~~~~~~~~~~~~~ @@ -202,6 +213,10 @@ not greater than the upper bound. All of these fields use :class:`~psycopg2:psycopg2.extras.NumericRange`. Default for :class:`~django.contrib.postgres.fields.FloatRangeField`. + .. deprecated:: 2.2 + + Use :class:`DecimalRangeField` instead. + ``DateTimeRangeField`` ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/releases/2.2.txt b/docs/releases/2.2.txt index 901868cebf0..7d85d30c4ac 100644 --- a/docs/releases/2.2.txt +++ b/docs/releases/2.2.txt @@ -320,3 +320,7 @@ Miscellaneous * The undocumented ``QuerySetPaginator`` alias of ``django.core.paginator.Paginator`` is deprecated. + +* The ``FloatRangeField`` model and form fields in ``django.contrib.postgres`` + are deprecated in favor of a new name, ``DecimalRangeField``, to match the + underlying ``numrange`` data type used in the database. diff --git a/tests/postgres_tests/fields.py b/tests/postgres_tests/fields.py index bcc6998a3c5..2275eb2ab2a 100644 --- a/tests/postgres_tests/fields.py +++ b/tests/postgres_tests/fields.py @@ -7,7 +7,7 @@ from django.db import models try: from django.contrib.postgres.fields import ( ArrayField, BigIntegerRangeField, CICharField, CIEmailField, - CITextField, DateRangeField, DateTimeRangeField, FloatRangeField, + CITextField, DateRangeField, DateTimeRangeField, DecimalRangeField, HStoreField, IntegerRangeField, JSONField, ) from django.contrib.postgres.search import SearchVectorField @@ -35,7 +35,7 @@ except ImportError: CITextField = models.Field DateRangeField = models.Field DateTimeRangeField = models.Field - FloatRangeField = models.Field + DecimalRangeField = models.Field HStoreField = models.Field IntegerRangeField = models.Field JSONField = DummyJSONField diff --git a/tests/postgres_tests/migrations/0002_create_test_models.py b/tests/postgres_tests/migrations/0002_create_test_models.py index 0e7ba938caa..5db8a713853 100644 --- a/tests/postgres_tests/migrations/0002_create_test_models.py +++ b/tests/postgres_tests/migrations/0002_create_test_models.py @@ -3,7 +3,7 @@ from django.db import migrations, models from ..fields import ( ArrayField, BigIntegerRangeField, CICharField, CIEmailField, CITextField, - DateRangeField, DateTimeRangeField, FloatRangeField, HStoreField, + DateRangeField, DateTimeRangeField, DecimalRangeField, HStoreField, IntegerRangeField, JSONField, SearchVectorField, ) from ..models import TagField @@ -209,7 +209,7 @@ class Migration(migrations.Migration): ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('ints', IntegerRangeField(null=True, blank=True)), ('bigints', BigIntegerRangeField(null=True, blank=True)), - ('floats', FloatRangeField(null=True, blank=True)), + ('decimals', DecimalRangeField(null=True, blank=True)), ('timestamps', DateTimeRangeField(null=True, blank=True)), ('dates', DateRangeField(null=True, blank=True)), ], diff --git a/tests/postgres_tests/models.py b/tests/postgres_tests/models.py index 841f246c6a3..cbe477e4026 100644 --- a/tests/postgres_tests/models.py +++ b/tests/postgres_tests/models.py @@ -3,7 +3,7 @@ from django.db import models from .fields import ( ArrayField, BigIntegerRangeField, CICharField, CIEmailField, CITextField, - DateRangeField, DateTimeRangeField, FloatRangeField, HStoreField, + DateRangeField, DateTimeRangeField, DecimalRangeField, HStoreField, IntegerRangeField, JSONField, SearchVectorField, ) @@ -129,7 +129,7 @@ class Line(PostgreSQLModel): class RangesModel(PostgreSQLModel): ints = IntegerRangeField(blank=True, null=True) bigints = BigIntegerRangeField(blank=True, null=True) - floats = FloatRangeField(blank=True, null=True) + decimals = DecimalRangeField(blank=True, null=True) timestamps = DateTimeRangeField(blank=True, null=True) dates = DateRangeField(blank=True, null=True) diff --git a/tests/postgres_tests/test_introspection.py b/tests/postgres_tests/test_introspection.py index 2ab2c96cead..8ae5b80da11 100644 --- a/tests/postgres_tests/test_introspection.py +++ b/tests/postgres_tests/test_introspection.py @@ -31,7 +31,7 @@ class InspectDBTests(PostgreSQLTestCase): [ 'ints = django.contrib.postgres.fields.IntegerRangeField(blank=True, null=True)', 'bigints = django.contrib.postgres.fields.BigIntegerRangeField(blank=True, null=True)', - 'floats = django.contrib.postgres.fields.FloatRangeField(blank=True, null=True)', + 'decimals = django.contrib.postgres.fields.DecimalRangeField(blank=True, null=True)', 'timestamps = django.contrib.postgres.fields.DateTimeRangeField(blank=True, null=True)', 'dates = django.contrib.postgres.fields.DateRangeField(blank=True, null=True)', ], diff --git a/tests/postgres_tests/test_ranges.py b/tests/postgres_tests/test_ranges.py index 6aa6c889dd7..c05ac196998 100644 --- a/tests/postgres_tests/test_ranges.py +++ b/tests/postgres_tests/test_ranges.py @@ -1,11 +1,13 @@ import datetime import json +from decimal import Decimal from django import forms from django.core import exceptions, serializers from django.db.models import DateField, DateTimeField, F, Func, Value -from django.test import override_settings +from django.test import ignore_warnings, override_settings from django.utils import timezone +from django.utils.deprecation import RemovedInDjango31Warning from . import PostgreSQLTestCase from .models import RangeLookupsModel, RangesModel @@ -27,7 +29,7 @@ class TestSaveLoad(PostgreSQLTestCase): instance = RangesModel( ints=NumericRange(0, 10), bigints=NumericRange(10, 20), - floats=NumericRange(20, 30), + decimals=NumericRange(20, 30), timestamps=DateTimeTZRange(now - datetime.timedelta(hours=1), now), dates=DateRange(now.date() - datetime.timedelta(days=1), now.date()), ) @@ -35,7 +37,7 @@ class TestSaveLoad(PostgreSQLTestCase): loaded = RangesModel.objects.get() self.assertEqual(instance.ints, loaded.ints) self.assertEqual(instance.bigints, loaded.bigints) - self.assertEqual(instance.floats, loaded.floats) + self.assertEqual(instance.decimals, loaded.decimals) self.assertEqual(instance.timestamps, loaded.timestamps) self.assertEqual(instance.dates, loaded.dates) @@ -54,18 +56,18 @@ class TestSaveLoad(PostgreSQLTestCase): def test_range_object_boundaries(self): r = NumericRange(0, 10, '[]') - instance = RangesModel(floats=r) + instance = RangesModel(decimals=r) instance.save() loaded = RangesModel.objects.get() - self.assertEqual(r, loaded.floats) - self.assertIn(10, loaded.floats) + self.assertEqual(r, loaded.decimals) + self.assertIn(10, loaded.decimals) def test_unbounded(self): r = NumericRange(None, None, '()') - instance = RangesModel(floats=r) + instance = RangesModel(decimals=r) instance.save() loaded = RangesModel.objects.get() - self.assertEqual(r, loaded.floats) + self.assertEqual(r, loaded.decimals) def test_empty(self): r = NumericRange(empty=True) @@ -331,13 +333,13 @@ class TestQueryingWithRanges(PostgreSQLTestCase): ) def test_f_ranges(self): - parent = RangesModel.objects.create(floats=NumericRange(0, 10)) + parent = RangesModel.objects.create(decimals=NumericRange(0, 10)) objs = [ RangeLookupsModel.objects.create(float=5, parent=parent), RangeLookupsModel.objects.create(float=99, parent=parent), ] self.assertSequenceEqual( - RangeLookupsModel.objects.filter(float__contained_by=F('parent__floats')), + RangeLookupsModel.objects.filter(float__contained_by=F('parent__decimals')), [objs[0]] ) @@ -356,7 +358,7 @@ class TestQueryingWithRanges(PostgreSQLTestCase): class TestSerialization(PostgreSQLTestCase): test_data = ( '[{"fields": {"ints": "{\\"upper\\": \\"10\\", \\"lower\\": \\"0\\", ' - '\\"bounds\\": \\"[)\\"}", "floats": "{\\"empty\\": true}", ' + '\\"bounds\\": \\"[)\\"}", "decimals": "{\\"empty\\": true}", ' '"bigints": null, "timestamps": "{\\"upper\\": \\"2014-02-02T12:12:12+00:00\\", ' '\\"lower\\": \\"2014-01-01T00:00:00+00:00\\", \\"bounds\\": \\"[)\\"}", ' '"dates": "{\\"upper\\": \\"2014-02-02\\", \\"lower\\": \\"2014-01-01\\", \\"bounds\\": \\"[)\\"}" }, ' @@ -370,7 +372,7 @@ class TestSerialization(PostgreSQLTestCase): def test_dumping(self): instance = RangesModel( - ints=NumericRange(0, 10), floats=NumericRange(empty=True), + ints=NumericRange(0, 10), decimals=NumericRange(empty=True), timestamps=DateTimeTZRange(self.lower_dt, self.upper_dt), dates=DateRange(self.lower_date, self.upper_date), ) @@ -386,7 +388,7 @@ class TestSerialization(PostgreSQLTestCase): def test_loading(self): instance = list(serializers.deserialize('json', self.test_data))[0].object self.assertEqual(instance.ints, NumericRange(0, 10)) - self.assertEqual(instance.floats, NumericRange(empty=True)) + self.assertEqual(instance.decimals, NumericRange(empty=True)) self.assertIsNone(instance.bigints) self.assertEqual(instance.dates, DateRange(self.lower_date, self.upper_date)) self.assertEqual(instance.timestamps, DateTimeTZRange(self.lower_dt, self.upper_dt)) @@ -435,11 +437,22 @@ class TestFormField(PostgreSQLTestCase): value = field.clean(['1', '2']) self.assertEqual(value, NumericRange(1, 2)) + @ignore_warnings(category=RemovedInDjango31Warning) def test_valid_floats(self): field = pg_forms.FloatRangeField() value = field.clean(['1.12345', '2.001']) self.assertEqual(value, NumericRange(1.12345, 2.001)) + def test_valid_decimal(self): + field = pg_forms.DecimalRangeField() + value = field.clean(['1.12345', '2.001']) + self.assertEqual(value, NumericRange(Decimal('1.12345'), Decimal('2.001'))) + + def test_float_range_field_deprecation(self): + msg = 'FloatRangeField is deprecated in favor of DecimalRangeField.' + with self.assertRaisesMessage(RemovedInDjango31Warning, msg): + pg_forms.FloatRangeField() + def test_valid_timestamps(self): field = pg_forms.DateTimeRangeField() value = field.clean(['01/01/2014 00:00:00', '02/02/2014 12:12:12']) @@ -544,44 +557,44 @@ class TestFormField(PostgreSQLTestCase): value = field.clean([1, '']) self.assertEqual(value, NumericRange(1, None)) - def test_float_lower_bound_higher(self): - field = pg_forms.FloatRangeField() + def test_decimal_lower_bound_higher(self): + field = pg_forms.DecimalRangeField() with self.assertRaises(exceptions.ValidationError) as cm: field.clean(['1.8', '1.6']) self.assertEqual(cm.exception.messages[0], 'The start of the range must not exceed the end of the range.') self.assertEqual(cm.exception.code, 'bound_ordering') - def test_float_open(self): - field = pg_forms.FloatRangeField() + def test_decimal_open(self): + field = pg_forms.DecimalRangeField() value = field.clean(['', '3.1415926']) - self.assertEqual(value, NumericRange(None, 3.1415926)) + self.assertEqual(value, NumericRange(None, Decimal('3.1415926'))) - def test_float_incorrect_data_type(self): - field = pg_forms.FloatRangeField() + def test_decimal_incorrect_data_type(self): + field = pg_forms.DecimalRangeField() with self.assertRaises(exceptions.ValidationError) as cm: field.clean('1.6') self.assertEqual(cm.exception.messages[0], 'Enter two numbers.') self.assertEqual(cm.exception.code, 'invalid') - def test_float_invalid_lower(self): - field = pg_forms.FloatRangeField() + def test_decimal_invalid_lower(self): + field = pg_forms.DecimalRangeField() with self.assertRaises(exceptions.ValidationError) as cm: field.clean(['a', '3.1415926']) self.assertEqual(cm.exception.messages[0], 'Enter a number.') - def test_float_invalid_upper(self): - field = pg_forms.FloatRangeField() + def test_decimal_invalid_upper(self): + field = pg_forms.DecimalRangeField() with self.assertRaises(exceptions.ValidationError) as cm: field.clean(['1.61803399', 'b']) self.assertEqual(cm.exception.messages[0], 'Enter a number.') - def test_float_required(self): - field = pg_forms.FloatRangeField(required=True) + def test_decimal_required(self): + field = pg_forms.DecimalRangeField(required=True) with self.assertRaises(exceptions.ValidationError) as cm: field.clean(['', '']) self.assertEqual(cm.exception.messages[0], 'This field is required.') value = field.clean(['1.61803399', '']) - self.assertEqual(value, NumericRange(1.61803399, None)) + self.assertEqual(value, NumericRange(Decimal('1.61803399'), None)) def test_date_lower_bound_higher(self): field = pg_forms.DateRangeField() @@ -680,9 +693,9 @@ class TestFormField(PostgreSQLTestCase): self.assertIsInstance(form_field, pg_forms.IntegerRangeField) def test_model_field_formfield_float(self): - model_field = pg_fields.FloatRangeField() + model_field = pg_fields.DecimalRangeField() form_field = model_field.formfield() - self.assertIsInstance(form_field, pg_forms.FloatRangeField) + self.assertIsInstance(form_field, pg_forms.DecimalRangeField) def test_model_field_formfield_date(self): model_field = pg_fields.DateRangeField()