From 534792d05591eb2d19dc48ee89dcd2d9b5d461c0 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Mon, 27 Sep 2010 15:25:08 +0000 Subject: [PATCH] =?UTF-8?q?Fixed=20#14290=20--=20Made=20format=20localizat?= =?UTF-8?q?ion=20faster=20by=20caching=20the=20format=20modules.=20Thanks,?= =?UTF-8?q?=20Teemu=20Kurppa=20and=20Anssi=20K=C3=A4=C3=A4ri=C3=A4inen=20f?= =?UTF-8?q?or=20the=20report=20and=20initial=20patches.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit git-svn-id: http://code.djangoproject.com/svn/django/trunk@13898 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/utils/encoding.py | 5 ++ django/utils/formats.py | 80 ++++++++++++------- django/utils/numberformat.py | 12 ++- tests/regressiontests/i18n/other/__init__.py | 0 .../i18n/other/locale/__init__.py | 0 .../i18n/other/locale/de/__init__.py | 0 .../i18n/other/locale/de/formats.py | 0 tests/regressiontests/i18n/tests.py | 20 ++++- 8 files changed, 83 insertions(+), 34 deletions(-) create mode 100644 tests/regressiontests/i18n/other/__init__.py create mode 100644 tests/regressiontests/i18n/other/locale/__init__.py create mode 100644 tests/regressiontests/i18n/other/locale/de/__init__.py create mode 100644 tests/regressiontests/i18n/other/locale/de/formats.py diff --git a/django/utils/encoding.py b/django/utils/encoding.py index e2d7249903..9301419b5c 100644 --- a/django/utils/encoding.py +++ b/django/utils/encoding.py @@ -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. """ + # 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): return s try: diff --git a/django/utils/formats.py b/django/utils/formats.py index 372998f221..0ec79f1590 100644 --- a/django/utils/formats.py +++ b/django/utils/formats.py @@ -7,34 +7,41 @@ from django.utils.importlib import import_module from django.utils.encoding import smart_str 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): """ - Returns an iterator over the format modules found in the project and Django + Returns an iterator over the format modules found """ - modules = [] - if not check_for_language(get_language()) or not settings.USE_L10N: - 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) + lang = get_language() + modules = _format_modules_cache.setdefault(lang, list(iter_format_modules(lang))) if reverse: modules.reverse() 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 language (locale), defaults to the format in the settings. @@ -42,11 +49,20 @@ def get_format(format_type): """ format_type = smart_str(format_type) if settings.USE_L10N: - for module in get_format_modules(): - try: - return getattr(module, format_type) - except AttributeError: - pass + if lang is None: + lang = get_language() + cache_key = (format_type, lang) + try: + 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) def date_format(value, format=None): @@ -66,12 +82,16 @@ def number_format(value, decimal_pos=None): """ Formats a numeric value using localization settings """ + if settings.USE_L10N: + lang = get_language() + else: + lang = None return numberformat.format( value, - get_format('DECIMAL_SEPARATOR'), + get_format('DECIMAL_SEPARATOR', lang), decimal_pos, - get_format('NUMBER_GROUPING'), - get_format('THOUSAND_SEPARATOR'), + get_format('NUMBER_GROUPING', lang), + get_format('THOUSAND_SEPARATOR', lang), ) def localize(value): @@ -97,7 +117,7 @@ def localize_input(value, default=None): """ if isinstance(value, (decimal.Decimal, float, int)): return number_format(value) - if isinstance(value, datetime.datetime): + elif isinstance(value, datetime.datetime): value = datetime_safe.new_datetime(value) format = smart_str(default or get_format('DATETIME_INPUT_FORMATS')[0]) return value.strftime(format) diff --git a/django/utils/numberformat.py b/django/utils/numberformat.py index 129c27f4da..069f49851b 100644 --- a/django/utils/numberformat.py +++ b/django/utils/numberformat.py @@ -1,4 +1,6 @@ from django.conf import settings +from django.utils.safestring import mark_safe + 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 ",") """ + 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 if float(number) < 0: sign = '-' else: sign = '' - # decimal part str_number = unicode(number) if str_number[0] == '-': str_number = str_number[1:] + # decimal part if '.' in str_number: int_part, dec_part = str_number.split('.') 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))) if dec_part: dec_part = decimal_sep + dec_part # grouping - if settings.USE_L10N and settings.USE_THOUSAND_SEPARATOR and grouping: + if use_grouping: int_part_gd = '' for cnt, digit in enumerate(int_part[::-1]): if cnt and not cnt % grouping: int_part_gd += thousand_sep int_part_gd += digit int_part = int_part_gd[::-1] - return sign + int_part + dec_part diff --git a/tests/regressiontests/i18n/other/__init__.py b/tests/regressiontests/i18n/other/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/i18n/other/locale/__init__.py b/tests/regressiontests/i18n/other/locale/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/i18n/other/locale/de/__init__.py b/tests/regressiontests/i18n/other/locale/de/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/i18n/other/locale/de/formats.py b/tests/regressiontests/i18n/other/locale/de/formats.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/i18n/tests.py b/tests/regressiontests/i18n/tests.py index 7e2e9f8228..01f6aa0d3f 100644 --- a/tests/regressiontests/i18n/tests.py +++ b/tests/regressiontests/i18n/tests.py @@ -8,10 +8,11 @@ import pickle from django.conf import settings from django.template import Template, Context 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.safestring import mark_safe, SafeString, SafeUnicode 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 @@ -423,6 +424,23 @@ class FormattingTests(TestCase): finally: 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): def test_parse_spec_http_header(self):