From 5bb7db2d6f939ba0626eb606102f478a4e613647 Mon Sep 17 00:00:00 2001 From: Keith Bussell Date: Fri, 3 Aug 2018 12:42:38 -0700 Subject: [PATCH 1/3] Always pass a valid rounding value Fixes #6015 Prevent an exception in `quantize()` when monkey-patching the decimal library as cdecimal in Python 2 environments by always passing a valid (not None) `rounding` value to `quantize()`. --- rest_framework/fields.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 3278cf51c..d5071a1aa 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1135,11 +1135,12 @@ class DecimalField(Field): return value context = decimal.getcontext().copy() + rounding = context.rounding if self.rounding is None else self.rounding if self.max_digits is not None: context.prec = self.max_digits return value.quantize( decimal.Decimal('.1') ** self.decimal_places, - rounding=self.rounding, + rounding=rounding, context=context ) From 6a42128ddf4a0a053ae95d4db65c67441db3e838 Mon Sep 17 00:00:00 2001 From: Keith Bussell Date: Mon, 6 Aug 2018 21:39:10 -0700 Subject: [PATCH 2/3] Add Python 2.7-specific comment --- rest_framework/fields.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index d5071a1aa..92caa6a38 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1135,6 +1135,7 @@ class DecimalField(Field): return value context = decimal.getcontext().copy() + # For Python 2.7 compatibility when using cdecimal rounding = context.rounding if self.rounding is None else self.rounding if self.max_digits is not None: context.prec = self.max_digits From 602eec8fa0a1e54be6cbd2c5945eb18f5bce5820 Mon Sep 17 00:00:00 2001 From: Keith Bussell Date: Mon, 6 Aug 2018 21:39:46 -0700 Subject: [PATCH 3/3] Add test for monkey-patched decimal in Python 2.7 For #6105 --- tests/test_fields.py | 16 ++++++++++++++++ tox.ini | 1 + 2 files changed, 17 insertions(+) diff --git a/tests/test_fields.py b/tests/test_fields.py index aa3391a72..27d947cd0 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -7,6 +7,7 @@ from decimal import ROUND_DOWN, ROUND_UP, Decimal import pytest import pytz +from _pytest.monkeypatch import MonkeyPatch from django.core.exceptions import ValidationError as DjangoValidationError from django.http import QueryDict from django.test import TestCase, override_settings @@ -17,6 +18,11 @@ import rest_framework from rest_framework import exceptions, serializers from rest_framework.fields import DjangoImageField, is_simple_callable +try: + import cdecimal +except ImportError: + cdecimal = False + try: import typings except ImportError: @@ -1128,6 +1134,16 @@ class TestQuantizedValueForDecimal(TestCase): expected_digit_tuple = (0, (1, 2, 0, 0), -2) assert value == expected_digit_tuple + @unittest.skipUnless(cdecimal, 'requires python 2.7') + def test_quantize_on_monkey_patched_cdecimal(self): + # Monkey-patch cdecimal to replace decimal in for DecimalField + monkeypatch = MonkeyPatch() + + with monkeypatch.context() as m: + m.setattr('rest_framework.fields.decimal', cdecimal) + f = rest_framework.fields.DecimalField(max_digits=4, decimal_places=2) + f.quantize(cdecimal.Decimal('1.234')) + class TestNoDecimalPlaces(FieldValues): valid_inputs = { diff --git a/tox.ini b/tox.ini index 11c37ccfc..0a4e34020 100644 --- a/tox.ini +++ b/tox.ini @@ -24,6 +24,7 @@ deps = django20: Django>=2.0,<2.1 django21: Django>=2.1b1,<2.2 djangomaster: https://github.com/django/django/archive/master.tar.gz + py27: m3-cdecimal -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt