Repository: encode/django-rest-framework

Issue:
Don't call `.format()` on lazy-translated strings during Field.__init__
This from IRC...

```
[17:04:05] <__zer01>     after adding the `min_value` kwarg to a serializer.DecimalField I get a `django.core.exceptions.AppRegistryNotReady: The translation infrastructure cannot be initialized before the apps registry is ready.` error when running tests, any ideas?
[17:07:57] <__zer01>     here's the end of the stack trace http://pastebin.com/B7ECdL6w
```

Which is:

```
File "/home/eugenio/spartan/rest/serializers.py", line 80, in MySerializer min_value=1.01)
File "/home/eugenio/Envs/spartan/lib/python3.4/site-packages/rest_framework/fields.py", line 906, in __init__
message = self.error_messages['min_value'].format(min_value=self.min_value)
File "/home/eugenio/Envs/spartan/lib/python3.4/site-packages/django/utils/functional.py", line 136, in __wrapper__ res = func(*self.__args, **self.__kw)
File "/home/eugenio/Envs/spartan/lib/python3.4/site-packages/django/utils/translation/__init__.py", line 84, in ugettext
return _trans.ugettext(message)
File "/home/eugenio/Envs/spartan/lib/python3.4/site-packages/django/utils/translation/trans_real.py", line 321, in gettext
return do_translate(message, 'gettext')
File "/home/eugenio/Envs/spartan/lib/python3.4/site-packages/django/utils/translation/trans_real.py", line 304, in do_translate
_default = _default or translation(settings.LANGUAGE_CODE)
File "/home/eugenio/Envs/spartan/lib/python3.4/site-packages/django/utils/translation/trans_real.py", line 206, in translation
_translations[language] = DjangoTranslation(language)
File "/home/eugenio/Envs/spartan/lib/python3.4/site-packages/django/utils/translation/trans_real.py", line 116, in __init__
self._add_installed_apps_translations()
File "/home/eugenio/Envs/spartan/lib/python3.4/site-packages/django/utils/translation/trans_real.py", line 164, in _add_installed_apps_translations
"The translation infrastructure cannot be initialized before the "
django.core.exceptions.AppRegistryNotReady: The translation infrastructure cannot be initialized before the apps registry is ready. Check that you don't make non-lazy gettext calls at import time.
```

And...

```
[17:08:41]   yuriheupa (~yuriheupa@177.16.212.34) joined the channel.
[17:09:26]   yuriheupa (~yuriheupa@177.16.212.34) left IRC. (Remote host closed the connection)
[17:11:54] <tomchristie>     Hrm
[17:13:14] <__zer01>     I'm running djangorestframework==3.2.3 and Django==1.8.4
[17:13:28] <tomchristie>     I think that may need raising as an issue
[17:14:13] <tomchristie>     The error is being raised because we're calling .format on a lazy-translated error message
[17:14:34] <tomchristie>     Due to it being passed a min_value which is included in the message
[17:15:00] <tomchristie>     Prob is, that causes the translation machinary to attempt to run, and produce the translated output
[17:15:12] <tomchristie>     But that can't happen yet because the app isn't setup and running
[17:15:23] <tomchristie>     Looks like Django internally handles this a little diff, eg
[17:15:34] <tomchristie>     This… https://github.com/django/django/blob/master/django/forms/fields.py#L258
[17:15:43] <tomchristie>     vs
[17:16:09] <tomchristie>     This: https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/fields.py#L820-L821
[17:16:23] <tomchristie>     Notive that we're calling .format when the field is instantiated
[17:16:31] <tomchristie>     But same is not happening in Django
[17:16:37] <tomchristie>     You may be able to resolve
[17:16:53] <tomchristie>     by ensuring that you're not importing the serializers prior to the app setup
[17:16:54]   marc_v92 (~marc_v92@unaffiliated/marc-v92/x-6202358) joined the channel.
[17:17:07] <tomchristie>     (Eg if you import them in your settings I guess that's when it blows up)
```



Discussion:
Really all that needs to be changed here is that these particular lines

``` python

message = self.error_messages['max_value'].format(max_value=self.max_value)
```

need to be removed?

Doing this and running the tests does not yield any test failures, but then again I am not sure where else `message` is used.

ping @tomchristie or @kevin-brown for the above. If this is the right approach and I have already made the changes and could push them out. Let me know when you get a chance :smile: 

@nryoung we may lack a test for that particular string. It looks like the omitting the format will result in the  max_value not being correctly filled whenever an error occurs.

> @nryoung we may lack a test for that particular string. It looks like the omitting the format will result in the max_value not being correctly filled whenever an error occurs.

This does seem to be the case. It seems like we could use the `message` class attribute of Django's `MaxValueValidator` since it is a simple inheritance of this class: https://github.com/django/django/blob/master/django/core/validators.py#L318

I have been doing some research on this.

#### For Django versions >= 1.8 `message` is optional in the `__init__` method:
- https://github.com/django/django/blob/stable/1.8.x/django/core/validators.py#L275

but, `message` is also defaulted as a class attribute here:
- https://github.com/django/django/blob/stable/1.8.x/django/core/validators.py#L298

#### For Django versions < 1.8 `message` is not expected:
- https://github.com/django/django/blob/stable/1.7.x/django/core/validators.py#L242

thus, that is why it is popped off here:
- https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/compat.py#L125

but, `message` is also defaulted here:
- https://github.com/django/django/blob/stable/1.7.x/django/core/validators.py#L258

I believe, that removing the `message` assignments here and other similar ones:
- https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/fields.py#L823

and not passing `message` in to the instantiation of the validator, it should default to the Django class attribute:
- https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/fields.py#L824

I will make a PR for what I outline above. As always, let me know if I am wrong here.


Resolving patch:
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -11,13 +11,18 @@
 import django
 from django.apps import apps
 from django.conf import settings
-from django.core.exceptions import ImproperlyConfigured
+from django.core.exceptions import ImproperlyConfigured, ValidationError
+from django.core.validators import \
+    MaxLengthValidator as DjangoMaxLengthValidator
+from django.core.validators import MaxValueValidator as DjangoMaxValueValidator
+from django.core.validators import \
+    MinLengthValidator as DjangoMinLengthValidator
+from django.core.validators import MinValueValidator as DjangoMinValueValidator
 from django.db import connection, models, transaction
 from django.template import Context, RequestContext, Template
 from django.utils import six
 from django.views.generic import View
 
-
 try:
     from django.urls import (
         NoReverseMatch, RegexURLPattern, RegexURLResolver, ResolverMatch, Resolver404, get_script_prefix, reverse, reverse_lazy, resolve
@@ -293,6 +298,28 @@ def pygments_css(style):
 except ImportError:
     DecimalValidator = None
 
+class CustomValidatorMessage(object):
+    """
+    We need to avoid evaluation of `lazy` translated `message` in `django.core.validators.BaseValidator.__init__`.
+    https://github.com/django/django/blob/75ed5900321d170debef4ac452b8b3cf8a1c2384/django/core/validators.py#L297
+    
+    Ref: https://github.com/encode/django-rest-framework/pull/5452
+    """
+    def __init__(self, *args, **kwargs):
+        self.message = kwargs.pop('message', self.message)
+        super(CustomValidatorMessage, self).__init__(*args, **kwargs)
+
+class MinValueValidator(CustomValidatorMessage, DjangoMinValueValidator):
+    pass
+
+class MaxValueValidator(CustomValidatorMessage, DjangoMaxValueValidator):
+    pass
+
+class MinLengthValidator(CustomValidatorMessage, DjangoMinLengthValidator):
+    pass
+
+class MaxLengthValidator(CustomValidatorMessage, DjangoMaxLengthValidator):
+    pass
 
 def set_rollback():
     if hasattr(transaction, 'set_rollback'):
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -13,8 +13,7 @@
 from django.core.exceptions import ValidationError as DjangoValidationError
 from django.core.exceptions import ObjectDoesNotExist
 from django.core.validators import (
-    EmailValidator, MaxLengthValidator, MaxValueValidator, MinLengthValidator,
-    MinValueValidator, RegexValidator, URLValidator, ip_address_validators
+    EmailValidator, RegexValidator, URLValidator, ip_address_validators
 )
 from django.forms import FilePathField as DjangoFilePathField
 from django.forms import ImageField as DjangoImageField
@@ -25,14 +24,16 @@
 from django.utils.duration import duration_string
 from django.utils.encoding import is_protected_type, smart_text
 from django.utils.formats import localize_input, sanitize_separators
+from django.utils.functional import lazy
 from django.utils.ipv6 import clean_ipv6_address
 from django.utils.timezone import utc
 from django.utils.translation import ugettext_lazy as _
 
 from rest_framework import ISO_8601
 from rest_framework.compat import (
-    InvalidTimeError, get_remote_field, unicode_repr, unicode_to_repr,
-    value_from_object
+    InvalidTimeError, MaxLengthValidator, MaxValueValidator,
+    MinLengthValidator, MinValueValidator, get_remote_field, unicode_repr,
+    unicode_to_repr, value_from_object
 )
 from rest_framework.exceptions import ErrorDetail, ValidationError
 from rest_framework.settings import api_settings
@@ -750,11 +751,17 @@ def __init__(self, **kwargs):
         self.min_length = kwargs.pop('min_length', None)
         super(CharField, self).__init__(**kwargs)
         if self.max_length is not None:
-            message = self.error_messages['max_length'].format(max_length=self.max_length)
-            self.validators.append(MaxLengthValidator(self.max_length, message=message))
+            message = lazy(
+                self.error_messages['max_length'].format,
+                six.text_type)(max_length=self.max_length)
+            self.validators.append(
+                MaxLengthValidator(self.max_length, message=message))
         if self.min_length is not None:
-            message = self.error_messages['min_length'].format(min_length=self.min_length)
-            self.validators.append(MinLengthValidator(self.min_length, message=message))
+            message = lazy(
+                self.error_messages['min_length'].format,
+                six.text_type)(min_length=self.min_length)
+            self.validators.append(
+                MinLengthValidator(self.min_length, message=message))
 
     def run_validation(self, data=empty):
         # Test for the empty string here so that it does not get validated,
@@ -909,11 +916,17 @@ def __init__(self, **kwargs):
         self.min_value = kwargs.pop('min_value', None)
         super(IntegerField, self).__init__(**kwargs)
         if self.max_value is not None:
-            message = self.error_messages['max_value'].format(max_value=self.max_value)
-            self.validators.append(MaxValueValidator(self.max_value, message=message))
+            message = lazy(
+                self.error_messages['max_value'].format,
+                six.text_type)(max_value=self.max_value)
+            self.validators.append(
+                MaxValueValidator(self.max_value, message=message))
         if self.min_value is not None:
-            message = self.error_messages['min_value'].format(min_value=self.min_value)
-            self.validators.append(MinValueValidator(self.min_value, message=message))
+            message = lazy(
+                self.error_messages['min_value'].format,
+                six.text_type)(min_value=self.min_value)
+            self.validators.append(
+                MinValueValidator(self.min_value, message=message))
 
     def to_internal_value(self, data):
         if isinstance(data, six.text_type) and len(data) > self.MAX_STRING_LENGTH:
@@ -943,11 +956,17 @@ def __init__(self, **kwargs):
         self.min_value = kwargs.pop('min_value', None)
         super(FloatField, self).__init__(**kwargs)
         if self.max_value is not None:
-            message = self.error_messages['max_value'].format(max_value=self.max_value)
-            self.validators.append(MaxValueValidator(self.max_value, message=message))
+            message = lazy(
+                self.error_messages['max_value'].format,
+                six.text_type)(max_value=self.max_value)
+            self.validators.append(
+                MaxValueValidator(self.max_value, message=message))
         if self.min_value is not None:
-            message = self.error_messages['min_value'].format(min_value=self.min_value)
-            self.validators.append(MinValueValidator(self.min_value, message=message))
+            message = lazy(
+                self.error_messages['min_value'].format,
+                six.text_type)(min_value=self.min_value)
+            self.validators.append(
+                MinValueValidator(self.min_value, message=message))
 
     def to_internal_value(self, data):
 
@@ -996,11 +1015,17 @@ def __init__(self, max_digits, decimal_places, coerce_to_string=None, max_value=
         super(DecimalField, self).__init__(**kwargs)
 
         if self.max_value is not None:
-            message = self.error_messages['max_value'].format(max_value=self.max_value)
-            self.validators.append(MaxValueValidator(self.max_value, message=message))
+            message = lazy(
+                self.error_messages['max_value'].format,
+                six.text_type)(max_value=self.max_value)
+            self.validators.append(
+                MaxValueValidator(self.max_value, message=message))
         if self.min_value is not None:
-            message = self.error_messages['min_value'].format(min_value=self.min_value)
-            self.validators.append(MinValueValidator(self.min_value, message=message))
+            message = lazy(
+                self.error_messages['min_value'].format,
+                six.text_type)(min_value=self.min_value)
+            self.validators.append(
+                MinValueValidator(self.min_value, message=message))
 
     def to_internal_value(self, data):
         """
@@ -1797,8 +1822,11 @@ def __init__(self, model_field, **kwargs):
         max_length = kwargs.pop('max_length', None)
         super(ModelField, self).__init__(**kwargs)
         if max_length is not None:
-            message = self.error_messages['max_length'].format(max_length=max_length)
-            self.validators.append(MaxLengthValidator(max_length, message=message))
+            message = lazy(
+                self.error_messages['max_length'].format,
+                six.text_type)(max_length=self.max_length)
+            self.validators.append(
+                MaxLengthValidator(self.max_length, message=message))
 
     def to_internal_value(self, data):
         rel = get_remote_field(self.model_field, default=None)


Test patch: