Skip to content
This repository was archived by the owner on Apr 2, 2021. It is now read-only.

Commit 5cdd6fe

Browse files
bluetechfdintino
authored andcommitted
Improve performance of lazy validation message formatting (encode#6709)
Backport to 3.9.x branch
1 parent b1c4d8b commit 5cdd6fe

File tree

5 files changed

+69
-71
lines changed

5 files changed

+69
-71
lines changed

requirements/requirements-testing.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
pytest==4.3.0
33
pytest-django==3.4.8
44
pytest-cov==2.6.1
5+
mock==3.0.5; python_version < '3.0'

rest_framework/compat.py

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import sys
99

1010
from django.conf import settings
11-
from django.core import validators
1211
from django.utils import six
1312
from django.views.generic import View
1413

@@ -299,34 +298,5 @@ def md_filter_add_syntax_highlight(md):
299298
INDENT_SEPARATORS = (b',', b': ')
300299

301300

302-
class CustomValidatorMessage(object):
303-
"""
304-
We need to avoid evaluation of `lazy` translated `message` in `django.core.validators.BaseValidator.__init__`.
305-
https://github.com/django/django/blob/75ed5900321d170debef4ac452b8b3cf8a1c2384/django/core/validators.py#L297
306-
307-
Ref: https://github.com/encode/django-rest-framework/pull/5452
308-
"""
309-
310-
def __init__(self, *args, **kwargs):
311-
self.message = kwargs.pop('message', self.message)
312-
super(CustomValidatorMessage, self).__init__(*args, **kwargs)
313-
314-
315-
class MinValueValidator(CustomValidatorMessage, validators.MinValueValidator):
316-
pass
317-
318-
319-
class MaxValueValidator(CustomValidatorMessage, validators.MaxValueValidator):
320-
pass
321-
322-
323-
class MinLengthValidator(CustomValidatorMessage, validators.MinLengthValidator):
324-
pass
325-
326-
327-
class MaxLengthValidator(CustomValidatorMessage, validators.MaxLengthValidator):
328-
pass
329-
330-
331301
# Version Constants.
332302
PY36 = sys.version_info >= (3, 6)

rest_framework/fields.py

Lines changed: 17 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
from django.core.exceptions import ObjectDoesNotExist
1414
from django.core.exceptions import ValidationError as DjangoValidationError
1515
from django.core.validators import (
16-
EmailValidator, RegexValidator, URLValidator, ip_address_validators
16+
EmailValidator, MaxLengthValidator, MaxValueValidator, MinLengthValidator,
17+
MinValueValidator, RegexValidator, URLValidator, ip_address_validators
1718
)
1819
from django.forms import FilePathField as DjangoFilePathField
1920
from django.forms import ImageField as DjangoImageField
@@ -24,21 +25,19 @@
2425
from django.utils.duration import duration_string
2526
from django.utils.encoding import is_protected_type, smart_text
2627
from django.utils.formats import localize_input, sanitize_separators
27-
from django.utils.functional import lazy
2828
from django.utils.ipv6 import clean_ipv6_address
2929
from django.utils.timezone import utc
3030
from django.utils.translation import ugettext_lazy as _
3131
from pytz.exceptions import InvalidTimeError
3232

3333
from rest_framework import ISO_8601
3434
from rest_framework.compat import (
35-
Mapping, MaxLengthValidator, MaxValueValidator, MinLengthValidator,
36-
MinValueValidator, ProhibitNullCharactersValidator, unicode_repr,
37-
unicode_to_repr
35+
Mapping, ProhibitNullCharactersValidator, unicode_repr, unicode_to_repr
3836
)
3937
from rest_framework.exceptions import ErrorDetail, ValidationError
4038
from rest_framework.settings import api_settings
4139
from rest_framework.utils import html, humanize_datetime, json, representation
40+
from rest_framework.utils.formatting import lazy_format
4241

4342

4443
class empty:
@@ -766,15 +765,11 @@ def __init__(self, **kwargs):
766765
self.min_length = kwargs.pop('min_length', None)
767766
super(CharField, self).__init__(**kwargs)
768767
if self.max_length is not None:
769-
message = lazy(
770-
self.error_messages['max_length'].format,
771-
six.text_type)(max_length=self.max_length)
768+
message = lazy_format(self.error_messages['max_length'], max_length=self.max_length)
772769
self.validators.append(
773770
MaxLengthValidator(self.max_length, message=message))
774771
if self.min_length is not None:
775-
message = lazy(
776-
self.error_messages['min_length'].format,
777-
six.text_type)(min_length=self.min_length)
772+
message = lazy_format(self.error_messages['min_length'], min_length=self.min_length)
778773
self.validators.append(
779774
MinLengthValidator(self.min_length, message=message))
780775

@@ -935,15 +930,11 @@ def __init__(self, **kwargs):
935930
self.min_value = kwargs.pop('min_value', None)
936931
super(IntegerField, self).__init__(**kwargs)
937932
if self.max_value is not None:
938-
message = lazy(
939-
self.error_messages['max_value'].format,
940-
six.text_type)(max_value=self.max_value)
933+
message = lazy_format(self.error_messages['max_value'], max_value=self.max_value)
941934
self.validators.append(
942935
MaxValueValidator(self.max_value, message=message))
943936
if self.min_value is not None:
944-
message = lazy(
945-
self.error_messages['min_value'].format,
946-
six.text_type)(min_value=self.min_value)
937+
message = lazy_format(self.error_messages['min_value'], min_value=self.min_value)
947938
self.validators.append(
948939
MinValueValidator(self.min_value, message=message))
949940

@@ -975,15 +966,11 @@ def __init__(self, **kwargs):
975966
self.min_value = kwargs.pop('min_value', None)
976967
super(FloatField, self).__init__(**kwargs)
977968
if self.max_value is not None:
978-
message = lazy(
979-
self.error_messages['max_value'].format,
980-
six.text_type)(max_value=self.max_value)
969+
message = lazy_format(self.error_messages['max_value'], max_value=self.max_value)
981970
self.validators.append(
982971
MaxValueValidator(self.max_value, message=message))
983972
if self.min_value is not None:
984-
message = lazy(
985-
self.error_messages['min_value'].format,
986-
six.text_type)(min_value=self.min_value)
973+
message = lazy_format(self.error_messages['min_value'], min_value=self.min_value)
987974
self.validators.append(
988975
MinValueValidator(self.min_value, message=message))
989976

@@ -1034,15 +1021,11 @@ def __init__(self, max_digits, decimal_places, coerce_to_string=None, max_value=
10341021
super(DecimalField, self).__init__(**kwargs)
10351022

10361023
if self.max_value is not None:
1037-
message = lazy(
1038-
self.error_messages['max_value'].format,
1039-
six.text_type)(max_value=self.max_value)
1024+
message = lazy_format(self.error_messages['max_value'], max_value=self.max_value)
10401025
self.validators.append(
10411026
MaxValueValidator(self.max_value, message=message))
10421027
if self.min_value is not None:
1043-
message = lazy(
1044-
self.error_messages['min_value'].format,
1045-
six.text_type)(min_value=self.min_value)
1028+
message = lazy_format(self.error_messages['min_value'], min_value=self.min_value)
10461029
self.validators.append(
10471030
MinValueValidator(self.min_value, message=message))
10481031

@@ -1380,15 +1363,11 @@ def __init__(self, **kwargs):
13801363
self.min_value = kwargs.pop('min_value', None)
13811364
super(DurationField, self).__init__(**kwargs)
13821365
if self.max_value is not None:
1383-
message = lazy(
1384-
self.error_messages['max_value'].format,
1385-
six.text_type)(max_value=self.max_value)
1366+
message = lazy_format(self.error_messages['max_value'], max_value=self.max_value)
13861367
self.validators.append(
13871368
MaxValueValidator(self.max_value, message=message))
13881369
if self.min_value is not None:
1389-
message = lazy(
1390-
self.error_messages['min_value'].format,
1391-
six.text_type)(min_value=self.min_value)
1370+
message = lazy_format(self.error_messages['min_value'], min_value=self.min_value)
13921371
self.validators.append(
13931372
MinValueValidator(self.min_value, message=message))
13941373

@@ -1633,10 +1612,10 @@ def __init__(self, *args, **kwargs):
16331612
super(ListField, self).__init__(*args, **kwargs)
16341613
self.child.bind(field_name='', parent=self)
16351614
if self.max_length is not None:
1636-
message = self.error_messages['max_length'].format(max_length=self.max_length)
1615+
message = lazy_format(self.error_messages['max_length'], max_length=self.max_length)
16371616
self.validators.append(MaxLengthValidator(self.max_length, message=message))
16381617
if self.min_length is not None:
1639-
message = self.error_messages['min_length'].format(min_length=self.min_length)
1618+
message = lazy_format(self.error_messages['min_length'], min_length=self.min_length)
16401619
self.validators.append(MinLengthValidator(self.min_length, message=message))
16411620

16421621
def get_value(self, dictionary):
@@ -1907,9 +1886,7 @@ def __init__(self, model_field, **kwargs):
19071886
max_length = kwargs.pop('max_length', None)
19081887
super(ModelField, self).__init__(**kwargs)
19091888
if max_length is not None:
1910-
message = lazy(
1911-
self.error_messages['max_length'].format,
1912-
six.text_type)(max_length=self.max_length)
1889+
message = lazy_format(self.error_messages['max_length'], max_length=self.max_length)
19131890
self.validators.append(
19141891
MaxLengthValidator(self.max_length, message=message))
19151892

rest_framework/utils/formatting.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55

66
import re
77

8-
from django.utils.encoding import force_text
8+
from django.utils import six
9+
from django.utils.encoding import force_text, python_2_unicode_compatible
910
from django.utils.html import escape
1011
from django.utils.safestring import mark_safe
1112

@@ -67,3 +68,30 @@ def markup_description(description):
6768
description = escape(description).replace('\n', '<br />')
6869
description = '<p>' + description + '</p>'
6970
return mark_safe(description)
71+
72+
73+
@python_2_unicode_compatible
74+
class lazy_format(object):
75+
"""
76+
Delay formatting until it's actually needed.
77+
78+
Useful when the format string or one of the arguments is lazy.
79+
80+
Not using Django's lazy because it is too slow.
81+
"""
82+
__slots__ = ('format_string', 'args', 'kwargs', 'result')
83+
84+
def __init__(self, format_string, *args, **kwargs):
85+
self.result = None
86+
self.format_string = format_string
87+
self.args = args
88+
self.kwargs = kwargs
89+
90+
def __str__(self):
91+
if self.result is None:
92+
self.result = self.format_string.format(*self.args, **self.kwargs)
93+
self.format_string, self.args, self.kwargs = None, None, None
94+
return self.result
95+
96+
def __mod__(self, value):
97+
return six.text_type(self) % value

tests/test_utils.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# -*- coding: utf-8 -*-
22
from __future__ import unicode_literals
33

4+
try:
5+
from unittest import mock
6+
except ImportError:
7+
import mock
8+
49
from django.conf.urls import url
510
from django.test import TestCase, override_settings
611

@@ -9,6 +14,7 @@
914
from rest_framework.serializers import ModelSerializer
1015
from rest_framework.utils import json
1116
from rest_framework.utils.breadcrumbs import get_breadcrumbs
17+
from rest_framework.utils.formatting import lazy_format
1218
from rest_framework.utils.urls import remove_query_param, replace_query_param
1319
from rest_framework.views import APIView
1420
from rest_framework.viewsets import ModelViewSet
@@ -260,3 +266,19 @@ def test_invalid_unicode(self):
260266
removed_key = 'page'
261267

262268
assert key in remove_query_param(q, removed_key)
269+
270+
271+
class LazyFormatTests(TestCase):
272+
def test_it_formats_correctly(self):
273+
formatted = lazy_format('Does {} work? {answer}: %s', 'it', answer='Yes')
274+
assert str(formatted) == 'Does it work? Yes: %s'
275+
assert formatted % 'it does' == 'Does it work? Yes: it does'
276+
277+
def test_it_formats_lazily(self):
278+
message = mock.Mock(wraps='message')
279+
formatted = lazy_format(message)
280+
assert message.format.call_count == 0
281+
str(formatted)
282+
assert message.format.call_count == 1
283+
str(formatted)
284+
assert message.format.call_count == 1

0 commit comments

Comments
 (0)