8000 Improve performance of lazy validation message formatting (#6709) · coderanger/django-rest-framework@c2293e9 · GitHub
[go: up one dir, main page]

Skip to content

Commit c2293e9

Browse files
bluetechrpkilby
authored andcommitted
Improve performance of lazy validation message formatting (encode#6709)
1 parent 62ed1f8 commit c2293e9

File tree

4 files changed

+62
-64
lines changed

4 files changed

+62
-64
lines changed

rest_framework/compat.py

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import sys
66

77
from django.conf import settings
8-
from django.core import validators
98
from django.views.generic import View
109

1110
try:
@@ -238,34 +237,5 @@ def md_filter_add_syntax_highlight(md):
238237
INDENT_SEPARATORS = (',', ': ')
239238

240239

241-
class CustomValidatorMessage:
242-
"""
243-
We need to avoid evaluation of `lazy` translated `message` in `django.core.validators.BaseValidator.__init__`.
244-
https://github.com/django/django/blob/75ed5900321d170debef4ac452b8b3cf8a1c2384/django/core/validators.py#L297
245-
246-
Ref: https://github.com/encode/django-rest-framework/pull/5452
247-
"""
248-
249-
def __init__(self, *args, **kwargs):
250-
self.message = kwargs.pop('message', self.message)
251-
super().__init__(*args, **kwargs)
252-
253-
254-
class MinValueValidator(CustomValidatorMessage, validators.MinValueValidator):
255-
pass
256-
257-
258-
class MaxValueValidator(CustomValidatorMessage, validators.MaxValueValidator):
259-
pass
260-
261-
262-
class MinLengthValidator(CustomValidatorMessage, validators.MinLengthValidator):
263-
pass
264-
265-
266-
class MaxLengthValidator(CustomValidatorMessage, validators.MaxLengthValidator):
267-
pass
268-
269-
270240
# Version Constants.
271241
PY36 = sys.version_info >= (3, 6)

rest_framework/fields.py

Lines changed: 17 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
from django.core.exceptions import ObjectDoesNotExist
1313
from django.core.exceptions import ValidationError as DjangoValidationError
1414
from django.core.validators import (
15-
EmailValidator, RegexValidator, URLValidator, ip_address_validators
15+
EmailValidator, MaxLengthValidator, MaxValueValidator, MinLengthValidator,
16+
MinValueValidator, RegexValidator, URLValidator, ip_address_validators
1617
)
1718
from django.forms import FilePathField as DjangoFilePathField
1819
from django.forms import ImageField as DjangoImageField
@@ -23,20 +24,17 @@
2324
from django.utils.duration import duration_string
2425
from django.utils.encoding import is_protected_type, smart_text
2526
from django.utils.formats import localize_input, sanitize_separators
26-
from django.utils.functional import lazy
2727
from django.utils.ipv6 import clean_ipv6_address
2828
from django.utils.timezone import utc
2929
from django.utils.translation import gettext_lazy as _
3030
from pytz.exceptions import InvalidTimeError
3131

3232
from rest_framework import ISO_8601
33-
from rest_framework.compat import (
34-
MaxLengthValidator, MaxValueValidator, MinLengthValidator,
35-
MinValueValidator, ProhibitNullCharactersValidator
36-
)
33+
from rest_framework.compat import ProhibitNullCharactersValidator
3734
from rest_framework.exceptions import ErrorDetail, ValidationError
3835
from rest_framework.settings import api_settings
3936
from rest_framework.utils import html, humanize_datetime, json, representation
37+
from rest_framework.utils.formatting import lazy_format
4038

4139

4240
class empty:
@@ -749,12 +747,11 @@ def __init__(self, **kwargs):
749747
self.min_length = kwargs.pop('min_length', None)
750748
super().__init__(**kwargs)
751749
if self.max_length is not None:
752-
message = lazy(self.error_messages['max_length'].format, str)(max_length=self.max_length)
750+
message = lazy_format(self.error_messages['max_length'], max_length=self.max_length)
753751
self.validators.append(
754752
MaxLengthValidator(self.max_length, message=message))
755753
if self.min_length is not None:
756-
message = lazy(
757-
self.error_messages['min_length'].format, str)(min_length=self.min_length)
754+
message = lazy_format(self.error_messages['min_length'], min_length=self.min_length)
758755
self.validators.append(
759756
MinLengthValidator(self.min_length, message=message))
760757

@@ -915,13 +912,11 @@ def __init__(self, **kwargs):
915912
self.min_value = kwargs.pop('min_value', None)
916913
super().__init__(**kwargs)
917914
if self.max_value is not None:
918-
message = lazy(
919-
self.error_messages['max_value'].format, str)(max_value=self.max_value)
915+
message = lazy_format(self.error_messages['max_value'], max_value=self.max_value)
920916
self.validators.append(
921917
MaxValueValidator(self.max_value, message=message))
922918
if self.min_value is not None:
923-
message = lazy(
924-
self.error_messages['min_value'].format, str)(min_value=self.min_value)
919+
message = lazy_format(self.error_messages['min_value'], min_value=self.min_value)
925920
self.validators.append(
926921
MinValueValidator(self.min_value, message=message))
927922

@@ -953,15 +948,11 @@ def __init__(self, **kwargs):
953948
self.min_value = kwargs.pop('min_value', None)
954949
super().__init__(**kwargs)
955950
if self.max_value is not None:
956-
message = lazy(
957-
self.error_messages['max_value'].format,
958-
str)(max_value=self.max_value)
951+
message = lazy_format(self.error_messages['max_value'], max_value=self.max_value)
959952
self.validators.append(
960953
MaxValueValidator(self.max_value, message=message))
961954
if self.min_value is not None:
962-
message = lazy(
963-
self.error_messages['min_value'].format,
964-
str)(min_value=self.min_value)
955+
message = lazy_format(self.error_messages['min_value'], min_value=self.min_value)
965956
self.validators.append(
966957
MinValueValidator(self.min_value, message=message))
967958

@@ -1012,14 +1003,11 @@ def __init__(self, max_digits, decimal_places, coerce_to_string=None, max_value=
10121003
super().__init__(**kwargs)
10131004

10141005
if self.max_value is not None:
1015-
message = lazy(
1016-
self.error_messages['max_value'].format,
1017-
str)(max_value=self.max_value)
1006+
message = lazy_format(self.error_messages['max_value'], max_value=self.max_value)
10181007
self.validators.append(
10191008
MaxValueValidator(self.max_value, message=message))
10201009
if self.min_value is not None:
1021-
message = lazy(
1022-
self.error_messages['min_value'].format, str)(min_value=self.min_value)
1010+
message = lazy_format(self.error_messages['min_value'], min_value=self.min_value)
10231011
self.validators.append(
10241012
MinValueValidator(self.min_value, message=message))
10251013

@@ -1357,15 +1345,11 @@ def __init__(self, **kwargs):
13571345
self.min_value = kwargs.pop('min_value', None)
13581346
super().__init__(**kwargs)
13591347
if self.max_value is not None:
1360-
message = lazy(
1361-
self.error_messages['max_value'].format,
1362-
str)(max_value=self.max_value)
1348+
message = lazy_format(self.error_messages['max_value'], max_value=self.max_value)
13631349
self.validators.append(
13641350
MaxValueValidator(self.max_value, message=message))
13651351
if self.min_value is not None:
1366-
message = lazy(
1367-
self.error_messages['min_value'].format,
1368-
str)(min_value=self.min_value)
1352+
message = lazy_format(self.error_messages['min_value'], min_value=self.min_value)
13691353
self.validators.append(
13701354
MinValueValidator(self.min_value, message=message))
13711355

@@ -1610,10 +1594,10 @@ def __init__(self, *args, **kwargs):
16101594
super().__init__(*args, **kwargs)
16111595
self.child.bind(field_name='', parent=self)
16121596
if self.max_length is not None:
1613-
message = lazy(self.error_messages['max_length'].format, str)(max_length=self.max_length)
1597+
message = lazy_format(self.error_messages['max_length'], max_length=self.max_length)
16141598
self.validators.append(MaxLengthValidator(self.max_length, message=message))
16151599
if self.min_length is not None:
1616-
message = lazy(self.error_messages['min_length'].format, str)(min_length=self.min_length)
1600+
message = lazy_format(self.error_messages['min_length'], min_length=self.min_length)
16171601
self.validators.append(MinLengthValidator(self.min_length, message=message))
16181602

16191603
def get_value(self, dictionary):
@@ -1887,8 +1871,7 @@ def __init__(self, model_field, **kwargs):
18871871
max_length = kwargs.pop('max_length', None)
18881872
super().__init__(**kwargs)
18891873
if max_length is not None:
1890-
message = lazy(
1891-
self.error_messages['max_length'].format, str)(max_length=self.max_length)
1874+
message = lazy_format(self.error_messages['max_length'], max_length=self.max_length)
18921875
self.validators.append(
18931876
MaxLengthValidator(self.max_length, message=message))
18941877

rest_framework/utils/formatting.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,29 @@ def markup_description(description):
6565
description = escape(description).replace('\n', '<br />')
6666
description = '<p>' + description + '</p>'
6767
return mark_safe(description)
68+
69+
70+
class lazy_format:
71+
"""
72+
Delay formatting until it's actually needed.
73+
74+
Useful when the format string or one of the arguments is lazy.
75+
76+
Not using Django's lazy because it is too slow.
77+
"""
78+
__slots__ = ('format_string', 'args', 'kwargs', 'result')
79+
80+
def __init__(self, format_string, *args, **kwargs):
81+
self.result = None
82+
self.format_string = format_string
83+
self.args = args
84+
self.kwargs = kwargs
85+
86+
def __str__(self):
87+
if self.result is None:
88+
self.result = self.format_string.format(*self.args, **self.kwargs)
89+
self.format_string, self.args, self.kwargs = None, None, None
90+
return self.result
91+
92+
def __mod__(self, value):
93+
return str(self) % value

tests/test_utils.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from unittest import mock
2+
13
from django.conf.urls import url
24
from django.test import TestCase, override_settings
35

@@ -6,6 +8,7 @@
68
from rest_framework.serializers import ModelSerializer
79
from rest_framework.utils import json
810
from rest_framework.utils.breadcrumbs import get_breadcrumbs
11+
from rest_framework.utils.formatting import lazy_format
912
from rest_framework.utils.urls import remove_query_param, replace_query_param
1013
from rest_framework.views import APIView
1114
from rest_framework.viewsets import ModelViewSet
@@ -257,3 +260,19 @@ def test_invalid_unicode(self):
257260
removed_key = 'page'
258261

259262
assert key in remove_query_param(q, removed_key)
263+
264+
265+
class LazyFormatTests(TestCase):
266+
def test_it_formats_correctly(self):
267+
formatted = lazy_format('Does {} work? {answer}: %s', 'it', answer='Yes')
268+
assert str(formatted) == 'Does it work? Yes: %s'
269+
assert formatted % 'it does' == 'Does it work? Yes: it does'
270+
271+
def test_it_formats_lazily(self):
272+
message = mock.Mock(wraps='message')
273+
formatted = lazy_format(message)
274+
assert message.format.call_count == 0
275+
str(formatted)
276+
assert message.format.call_count == 1
277+
str(formatted)
278+
assert message.format.call_count == 1

0 commit comments

Comments
 (0)
0