Fixed #14290 -- Made format localization faster by caching the format modules. Thanks, Teemu Kurppa and Anssi Kääriäinen for the report and initial patches.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@13898 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Jannis Leidel 2010-09-27 15:25:08 +00:00
parent 9c402f0ecc
commit 534792d055
8 changed files with 83 additions and 34 deletions

View File

@ -58,6 +58,11 @@ def force_unicode(s, encoding='utf-8', strings_only=False, errors='strict'):
If strings_only is True, don't convert (some) non-string-like objects. If strings_only is True, don't convert (some) non-string-like objects.
""" """
# Handle the common case first, saves 30-40% in performance when s
# is an instance of unicode. This function gets called often in that
# setting.
if isinstance(s, unicode):
return s
if strings_only and is_protected_type(s): if strings_only and is_protected_type(s):
return s return s
try: try:

View File

@ -7,34 +7,41 @@ from django.utils.importlib import import_module
from django.utils.encoding import smart_str from django.utils.encoding import smart_str
from django.utils import dateformat, numberformat, datetime_safe from django.utils import dateformat, numberformat, datetime_safe
# format_cache is a mapping from (format_type, lang) to the format string.
# By using the cache, it is possible to avoid running get_format_modules
# repeatedly.
_format_cache = {}
_format_modules_cache = {}
def iter_format_modules(lang):
"""
Does the heavy lifting of finding format modules.
"""
if check_for_language(lang) or settings.USE_L10N:
format_locations = ['django.conf.locale.%s']
if settings.FORMAT_MODULE_PATH:
format_locations.append(settings.FORMAT_MODULE_PATH + '.%s')
format_locations.reverse()
locale = to_locale(lang)
locales = set((locale, locale.split('_')[0]))
for location in format_locations:
for loc in locales:
try:
yield import_module('.formats', location % loc)
except ImportError:
pass
def get_format_modules(reverse=False): def get_format_modules(reverse=False):
""" """
Returns an iterator over the format modules found in the project and Django Returns an iterator over the format modules found
""" """
modules = [] lang = get_language()
if not check_for_language(get_language()) or not settings.USE_L10N: modules = _format_modules_cache.setdefault(lang, list(iter_format_modules(lang)))
return modules
locale = to_locale(get_language())
if settings.FORMAT_MODULE_PATH:
format_locations = [settings.FORMAT_MODULE_PATH + '.%s']
else:
format_locations = []
format_locations.append('django.conf.locale.%s')
for location in format_locations:
for l in (locale, locale.split('_')[0]):
try:
mod = import_module('.formats', location % l)
except ImportError:
pass
else:
# Don't return duplicates
if mod not in modules:
modules.append(mod)
if reverse: if reverse:
modules.reverse() modules.reverse()
return modules return modules
def get_format(format_type): def get_format(format_type, lang=None):
""" """
For a specific format type, returns the format for the current For a specific format type, returns the format for the current
language (locale), defaults to the format in the settings. language (locale), defaults to the format in the settings.
@ -42,11 +49,20 @@ def get_format(format_type):
""" """
format_type = smart_str(format_type) format_type = smart_str(format_type)
if settings.USE_L10N: if settings.USE_L10N:
for module in get_format_modules(): if lang is None:
try: lang = get_language()
return getattr(module, format_type) cache_key = (format_type, lang)
except AttributeError: try:
pass return _format_cache[cache_key] or getattr(settings, format_type)
except KeyError:
for module in get_format_modules():
try:
val = getattr(module, format_type)
_format_cache[cache_key] = val
return val
except AttributeError:
pass
_format_cache[cache_key] = None
return getattr(settings, format_type) return getattr(settings, format_type)
def date_format(value, format=None): def date_format(value, format=None):
@ -66,12 +82,16 @@ def number_format(value, decimal_pos=None):
""" """
Formats a numeric value using localization settings Formats a numeric value using localization settings
""" """
if settings.USE_L10N:
lang = get_language()
else:
lang = None
return numberformat.format( return numberformat.format(
value, value,
get_format('DECIMAL_SEPARATOR'), get_format('DECIMAL_SEPARATOR', lang),
decimal_pos, decimal_pos,
get_format('NUMBER_GROUPING'), get_format('NUMBER_GROUPING', lang),
get_format('THOUSAND_SEPARATOR'), get_format('THOUSAND_SEPARATOR', lang),
) )
def localize(value): def localize(value):
@ -97,7 +117,7 @@ def localize_input(value, default=None):
""" """
if isinstance(value, (decimal.Decimal, float, int)): if isinstance(value, (decimal.Decimal, float, int)):
return number_format(value) return number_format(value)
if isinstance(value, datetime.datetime): elif isinstance(value, datetime.datetime):
value = datetime_safe.new_datetime(value) value = datetime_safe.new_datetime(value)
format = smart_str(default or get_format('DATETIME_INPUT_FORMATS')[0]) format = smart_str(default or get_format('DATETIME_INPUT_FORMATS')[0])
return value.strftime(format) return value.strftime(format)

View File

@ -1,4 +1,6 @@
from django.conf import settings from django.conf import settings
from django.utils.safestring import mark_safe
def format(number, decimal_sep, decimal_pos, grouping=0, thousand_sep=''): def format(number, decimal_sep, decimal_pos, grouping=0, thousand_sep=''):
""" """
@ -11,15 +13,20 @@ def format(number, decimal_sep, decimal_pos, grouping=0, thousand_sep=''):
* thousand_sep: Thousand separator symbol (for example ",") * thousand_sep: Thousand separator symbol (for example ",")
""" """
use_grouping = settings.USE_L10N and \
settings.USE_THOUSAND_SEPARATOR and grouping
# Make the common case fast:
if isinstance(number, int) and not use_grouping and not decimal_pos:
return mark_safe(unicode(number))
# sign # sign
if float(number) < 0: if float(number) < 0:
sign = '-' sign = '-'
else: else:
sign = '' sign = ''
# decimal part
str_number = unicode(number) str_number = unicode(number)
if str_number[0] == '-': if str_number[0] == '-':
str_number = str_number[1:] str_number = str_number[1:]
# decimal part
if '.' in str_number: if '.' in str_number:
int_part, dec_part = str_number.split('.') int_part, dec_part = str_number.split('.')
if decimal_pos: if decimal_pos:
@ -30,13 +37,12 @@ def format(number, decimal_sep, decimal_pos, grouping=0, thousand_sep=''):
dec_part = dec_part + ('0' * (decimal_pos - len(dec_part))) dec_part = dec_part + ('0' * (decimal_pos - len(dec_part)))
if dec_part: dec_part = decimal_sep + dec_part if dec_part: dec_part = decimal_sep + dec_part
# grouping # grouping
if settings.USE_L10N and settings.USE_THOUSAND_SEPARATOR and grouping: if use_grouping:
int_part_gd = '' int_part_gd = ''
for cnt, digit in enumerate(int_part[::-1]): for cnt, digit in enumerate(int_part[::-1]):
if cnt and not cnt % grouping: if cnt and not cnt % grouping:
int_part_gd += thousand_sep int_part_gd += thousand_sep
int_part_gd += digit int_part_gd += digit
int_part = int_part_gd[::-1] int_part = int_part_gd[::-1]
return sign + int_part + dec_part return sign + int_part + dec_part

View File

@ -8,10 +8,11 @@ import pickle
from django.conf import settings from django.conf import settings
from django.template import Template, Context from django.template import Template, Context
from django.test import TestCase from django.test import TestCase
from django.utils.formats import get_format, date_format, time_format, localize, localize_input from django.utils.formats import get_format, date_format, time_format, localize, localize_input, iter_format_modules
from django.utils.numberformat import format as nformat from django.utils.numberformat import format as nformat
from django.utils.safestring import mark_safe, SafeString, SafeUnicode from django.utils.safestring import mark_safe, SafeString, SafeUnicode
from django.utils.translation import ugettext, ugettext_lazy, activate, deactivate, gettext_lazy, to_locale from django.utils.translation import ugettext, ugettext_lazy, activate, deactivate, gettext_lazy, to_locale
from django.utils.importlib import import_module
from forms import I18nForm, SelectDateForm, SelectDateWidget, CompanyForm from forms import I18nForm, SelectDateForm, SelectDateWidget, CompanyForm
@ -423,6 +424,23 @@ class FormattingTests(TestCase):
finally: finally:
deactivate() deactivate()
def test_iter_format_modules(self):
"""
Tests the iter_format_modules function.
"""
activate('de-at')
old_format_module_path = settings.FORMAT_MODULE_PATH
try:
settings.USE_L10N = True
de_format_mod = import_module('django.conf.locale.de.formats')
self.assertEqual(list(iter_format_modules('de')), [de_format_mod])
settings.FORMAT_MODULE_PATH = 'regressiontests.i18n.other.locale'
test_de_format_mod = import_module('regressiontests.i18n.other.locale.de.formats')
self.assertEqual(list(iter_format_modules('de')), [test_de_format_mod, de_format_mod])
finally:
settings.FORMAT_MODULE_PATH = old_format_module_path
deactivate()
class MiscTests(TestCase): class MiscTests(TestCase):
def test_parse_spec_http_header(self): def test_parse_spec_http_header(self):