From 7cedb84c7fd29ccb765aa9e408fcdd23bce2af06 Mon Sep 17 00:00:00 2001 From: Jakob Schnitzer Date: Thu, 28 Sep 2017 19:17:42 +0200 Subject: [PATCH 001/139] Revert "Also check for `sys._MEIPASS` before assuming PyInstallerness (#526)" This reverts commit 0b34807ab3d9681bb653ac535e0fbc6f0ac3ffbb. --- babel/localedata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/babel/localedata.py b/babel/localedata.py index 4b6d3b68a..ae2154781 100644 --- a/babel/localedata.py +++ b/babel/localedata.py @@ -22,7 +22,7 @@ def get_base_dir(): - if getattr(sys, 'frozen', False) and getattr(sys, '_MEIPASS', None): + if getattr(sys, 'frozen', False): # we are running in a |PyInstaller| bundle basedir = sys._MEIPASS else: From 29da2d2d47b43dd71e5b696cb6b9cca90b4446c2 Mon Sep 17 00:00:00 2001 From: Jakob Schnitzer Date: Thu, 28 Sep 2017 19:18:32 +0200 Subject: [PATCH 002/139] Revert "Merge pull request #505 from wodo/pi_support" This reverts commit 653e6d4f654f0f3cbce57054654e09c519b9e412, reversing changes made to 5f3b00854bbc6fa6a03e04ab572a609eebcc1f72. --- babel/core.py | 2 +- babel/localedata.py | 12 +----------- tests/test_localedata.py | 12 ------------ 3 files changed, 2 insertions(+), 24 deletions(-) diff --git a/babel/core.py b/babel/core.py index df03b24ad..5140f49d7 100644 --- a/babel/core.py +++ b/babel/core.py @@ -68,7 +68,7 @@ def get_global(key): """ global _global_data if _global_data is None: - dirname = localedata.get_base_dir() + dirname = os.path.join(os.path.dirname(__file__)) filename = os.path.join(dirname, 'global.dat') if not os.path.isfile(filename): _raise_no_data_error() diff --git a/babel/localedata.py b/babel/localedata.py index ae2154781..0c94e49ea 100644 --- a/babel/localedata.py +++ b/babel/localedata.py @@ -16,23 +16,13 @@ import threading from collections import MutableMapping from itertools import chain -import sys from babel._compat import pickle, string_types -def get_base_dir(): - if getattr(sys, 'frozen', False): - # we are running in a |PyInstaller| bundle - basedir = sys._MEIPASS - else: - # we are running in a normal Python environment - basedir = os.path.dirname(__file__) - return basedir - _cache = {} _cache_lock = threading.RLock() -_dirname = os.path.join(get_base_dir(), 'locale-data') +_dirname = os.path.join(os.path.dirname(__file__), 'locale-data') def normalize_locale(name): diff --git a/tests/test_localedata.py b/tests/test_localedata.py index 3599b2157..6954d3886 100644 --- a/tests/test_localedata.py +++ b/tests/test_localedata.py @@ -14,7 +14,6 @@ import unittest import random from operator import methodcaller -import sys from babel import localedata, numbers @@ -94,17 +93,6 @@ def test_mixedcased_locale(): methodcaller(random.choice(['lower', 'upper']))(c) for c in l]) assert localedata.exists(locale_id) - -def test_pi_support_frozen(monkeypatch): - monkeypatch.setattr(sys, '_MEIPASS', 'testdir', raising=False) - monkeypatch.setattr(sys, 'frozen', True, raising=False) - assert localedata.get_base_dir() == 'testdir' - - -def test_pi_support_not_frozen(): - assert not getattr(sys, 'frozen', False) - assert localedata.get_base_dir().endswith('babel') - def test_locale_argument_acceptance(): # Testing None input. normalized_locale = localedata.normalize_locale(None) From a53fd344c988d8ad0ec7eeabcaf010c6ccb9c9e9 Mon Sep 17 00:00:00 2001 From: Kevin Deldycke Date: Mon, 16 Oct 2017 10:45:15 +0200 Subject: [PATCH 003/139] Keep tox dependencies in sync with travis config. Refs 1da04fd0af20162fe4e2b503a5fcae8f25267010 and #520. --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 812561da7..537465823 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,7 @@ envlist = py26, py27, pypy, py33, py34, py26-cdecimal, py27-cdecimal deps = pytest cdecimal: m3-cdecimal + freezegun whitelist_externals = make commands = make clean-cldr test From cf03578b117f92f0af23ae07a65c3e2eada5c30c Mon Sep 17 00:00:00 2001 From: Kevin Deldycke Date: Fri, 7 Apr 2017 16:09:14 +0200 Subject: [PATCH 004/139] Refactor decimal handling code. --- babel/numbers.py | 172 +++++++++++++++++++++++++++++------------- tests/test_numbers.py | 146 +++++++++++++++++++++++++++++++++-- 2 files changed, 259 insertions(+), 59 deletions(-) diff --git a/babel/numbers.py b/babel/numbers.py index 8728699fb..036513217 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -21,11 +21,18 @@ import re from datetime import date as date_, datetime as datetime_ from itertools import chain +import warnings +from itertools import chain from babel.core import default_locale, Locale, get_global from babel._compat import decimal, string_types from babel.localedata import locale_identifiers +try: + long +except NameError: + long = int + LC_NUMERIC = default_locale('LC_NUMERIC') @@ -304,14 +311,25 @@ def format_number(number, locale=LC_NUMERIC): >>> format_number(1099, locale='de_DE') u'1.099' + .. deprecated:: 2.6.0 + + Use babel.numbers.format_decimal() instead. :param number: the number to format :param locale: the `Locale` object or locale identifier + + """ - # Do we really need this one? + warnings.warn('Use babel.numbers.format_decimal() instead.', DeprecationWarning) return format_decimal(number, locale=locale) +def get_decimal_quantum(precision): + """Return minimal quantum of a number, as defined by precision.""" + assert isinstance(precision, (int, long, decimal.Decimal)) + return decimal.Decimal(10) ** (-precision) + + def format_decimal(number, format=None, locale=LC_NUMERIC): u"""Return the given decimal number formatted for a specific locale. @@ -412,14 +430,11 @@ def format_currency(number, currency, format=None, locale=LC_NUMERIC, try: pattern = locale.currency_formats[format_type] except KeyError: - raise UnknownCurrencyFormatError("%r is not a known currency format" - " type" % format_type) - if currency_digits: - precision = get_currency_precision(currency) - frac = (precision, precision) - else: - frac = None - return pattern.apply(number, locale, currency=currency, force_frac=frac) + raise UnknownCurrencyFormatError( + "%r is not a known currency format type" % format_type) + + return pattern.apply( + number, locale, currency=currency, currency_digits=currency_digits) def format_percent(number, format=None, locale=LC_NUMERIC): @@ -456,7 +471,7 @@ def format_scientific(number, format=None, locale=LC_NUMERIC): The format pattern can also be specified explicitly: - >>> format_scientific(1234567, u'##0E00', locale='en_US') + >>> format_scientific(1234567, u'##0.##E00', locale='en_US') u'1.23E06' :param number: the number to format @@ -615,7 +630,6 @@ def parse_precision(p): int_prec = parse_precision(integer) frac_prec = parse_precision(fraction) if exp: - frac_prec = parse_precision(integer + fraction) exp_plus = exp.startswith('+') exp = exp.lstrip('+') exp_prec = parse_precision(exp) @@ -633,6 +647,7 @@ class NumberPattern(object): def __init__(self, pattern, prefix, suffix, grouping, int_prec, frac_prec, exp_prec, exp_plus): + # Metadata of the decomposed parsed pattern. self.pattern = pattern self.prefix = prefix self.suffix = suffix @@ -641,68 +656,108 @@ def __init__(self, pattern, prefix, suffix, grouping, self.frac_prec = frac_prec self.exp_prec = exp_prec self.exp_plus = exp_plus - if '%' in ''.join(self.prefix + self.suffix): - self.scale = 2 - elif u'‰' in ''.join(self.prefix + self.suffix): - self.scale = 3 - else: - self.scale = 0 + self.scale = self.compute_scale() def __repr__(self): return '<%s %r>' % (type(self).__name__, self.pattern) - def apply(self, value, locale, currency=None, force_frac=None): - frac_prec = force_frac or self.frac_prec + def compute_scale(self): + """Return the scaling factor to apply to the number before rendering. + + Auto-set to a factor of 2 or 3 if presence of a ``%`` or ``‰`` sign is + detected in the prefix or suffix of the pattern. Default is to not mess + with the scale at all and keep it to 0. + """ + scale = 0 + if '%' in ''.join(self.prefix + self.suffix): + scale = 2 + elif u'‰' in ''.join(self.prefix + self.suffix): + scale = 3 + return scale + + def scientific_notation_elements(self, value, locale): + """ Returns normalized scientific notation components of a value. + """ + # Normalize value to only have one lead digit. + exp = value.adjusted() + value = value * get_decimal_quantum(exp) + assert value.adjusted() == 0 + + # Shift exponent and value by the minimum number of leading digits + # imposed by the rendering pattern. And always make that number + # greater or equal to 1. + lead_shift = max([1, min(self.int_prec)]) - 1 + exp = exp - lead_shift + value = value * get_decimal_quantum(-lead_shift) + + # Get exponent sign symbol. + exp_sign = '' + if exp < 0: + exp_sign = get_minus_sign_symbol(locale) + elif self.exp_plus: + exp_sign = get_plus_sign_symbol(locale) + + # Normalize exponent value now that we have the sign. + exp = abs(exp) + + return value, exp, exp_sign + + def apply(self, value, locale, currency=None, currency_digits=True): + """Renders into a string a number following the defined pattern. + """ if not isinstance(value, decimal.Decimal): value = decimal.Decimal(str(value)) + value = value.scaleb(self.scale) + + # Separate the absolute value from its sign. is_negative = int(value.is_signed()) - if self.exp_prec: # Scientific notation - exp = value.adjusted() - value = abs(value) - # Minimum number of integer digits - if self.int_prec[0] == self.int_prec[1]: - exp -= self.int_prec[0] - 1 - # Exponent grouping - elif self.int_prec[1]: - exp = int(exp / self.int_prec[1]) * self.int_prec[1] - if exp < 0: - value = value * 10**(-exp) - else: - value = value / 10**exp - exp_sign = '' - if exp < 0: - exp_sign = get_minus_sign_symbol(locale) - elif self.exp_plus: - exp_sign = get_plus_sign_symbol(locale) - exp = abs(exp) - number = u'%s%s%s%s' % \ - (self._format_significant(value, frac_prec[0], frac_prec[1]), - get_exponential_symbol(locale), exp_sign, - self._format_int(str(exp), self.exp_prec[0], - self.exp_prec[1], locale)) - elif '@' in self.pattern: # Is it a siginificant digits pattern? - text = self._format_significant(abs(value), + value = abs(value).normalize() + + # Prepare scientific notation metadata. + if self.exp_prec: + value, exp, exp_sign = self.scientific_notation_elements(value, locale) + + # Adjust the precision of the fractionnal part and force it to the + # currency's if neccessary. + frac_prec = self.frac_prec + if currency and currency_digits: + frac_prec = (get_currency_precision(currency), ) * 2 + + # Render scientific notation. + if self.exp_prec: + number = ''.join([ + self._quantize_value(value, locale, frac_prec), + get_exponential_symbol(locale), + exp_sign, + self._format_int( + str(exp), self.exp_prec[0], self.exp_prec[1], locale)]) + + # Is it a siginificant digits pattern? + elif '@' in self.pattern: + text = self._format_significant(value, self.int_prec[0], self.int_prec[1]) a, sep, b = text.partition(".") number = self._format_int(a, 0, 1000, locale) if sep: number += get_decimal_symbol(locale) + b - else: # A normal number pattern - precision = decimal.Decimal('1.' + '1' * frac_prec[1]) - rounded = value.quantize(precision) - a, sep, b = str(abs(rounded)).partition(".") - number = (self._format_int(a, self.int_prec[0], - self.int_prec[1], locale) + - self._format_frac(b or '0', locale, force_frac)) - retval = u'%s%s%s' % (self.prefix[is_negative], number, - self.suffix[is_negative]) + + # A normal number pattern. + else: + number = self._quantize_value(value, locale, frac_prec) + + retval = ''.join([ + self.prefix[is_negative], + number, + self.suffix[is_negative]]) + if u'¤' in retval: retval = retval.replace(u'¤¤¤', get_currency_name(currency, value, locale)) retval = retval.replace(u'¤¤', currency.upper()) retval = retval.replace(u'¤', get_currency_symbol(currency, locale)) + return retval # @@ -757,6 +812,15 @@ def _format_int(self, value, min, max, locale): gsize = self.grouping[1] return value + ret + def _quantize_value(self, value, locale, frac_prec): + quantum = get_decimal_quantum(frac_prec[1]) + rounded = value.quantize(quantum) + a, sep, b = str(rounded).partition(".") + number = (self._format_int(a, self.int_prec[0], + self.int_prec[1], locale) + + self._format_frac(b or '0', locale, frac_prec)) + return number + def _format_frac(self, value, locale, force_frac=None): min, max = force_frac or self.frac_prec if len(value) < min: diff --git a/tests/test_numbers.py b/tests/test_numbers.py index 5bcd1717d..5c8da3422 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -124,7 +124,7 @@ def test_scientific_notation(self): self.assertEqual(fmt, '1.2E3') # Exponent grouping fmt = numbers.format_scientific(12345, '##0.####E0', locale='en_US') - self.assertEqual(fmt, '12.345E3') + self.assertEqual(fmt, '1.2345E4') # Minimum number of int digits fmt = numbers.format_scientific(12345, '00.###E0', locale='en_US') self.assertEqual(fmt, '12.345E3') @@ -283,11 +283,45 @@ def test_format_decimal(): assert numbers.format_decimal(1.2345, locale='sv_SE') == u'1,234' assert numbers.format_decimal(1.2345, locale='de') == u'1,234' assert numbers.format_decimal(12345.5, locale='en_US') == u'12,345.5' + assert numbers.format_decimal(0001.2345000, locale='en_US') == u'1.234' + assert numbers.format_decimal(-0001.2346000, locale='en_US') == u'-1.235' + assert numbers.format_decimal(0000000.5, locale='en_US') == u'0.5' + assert numbers.format_decimal(000, locale='en_US') == u'0' + + +@pytest.mark.parametrize('input_value, expected_value', [ + ('10000', '10,000'), + ('1', '1'), + ('1.0', '1'), + ('1.1', '1.1'), + ('1.11', '1.11'), + ('1.110', '1.11'), + ('1.001', '1.001'), + ('1.00100', '1.001'), + ('01.00100', '1.001'), + ('101.00100', '101.001'), + ('00000', '0'), + ('0', '0'), + ('0.0', '0'), + ('0.1', '0.1'), + ('0.11', '0.11'), + ('0.110', '0.11'), + ('0.001', '0.001'), + ('0.00100', '0.001'), + ('00.00100', '0.001'), + ('000.00100', '0.001'), +]) +def test_format_decimal_precision(input_value, expected_value): + # Test precision conservation. + assert numbers.format_decimal( + decimal.Decimal(input_value), locale='en_US') == expected_value def test_format_currency(): assert (numbers.format_currency(1099.98, 'USD', locale='en_US') == u'$1,099.98') + assert (numbers.format_currency(0, 'USD', locale='en_US') + == u'$0.00') assert (numbers.format_currency(1099.98, 'USD', locale='es_CO') == u'US$\xa01.099,98') assert (numbers.format_currency(1099.98, 'EUR', locale='de_DE') @@ -306,10 +340,16 @@ def test_format_currency_format_type(): assert (numbers.format_currency(1099.98, 'USD', locale='en_US', format_type="standard") == u'$1,099.98') + assert (numbers.format_currency(0, 'USD', locale='en_US', + format_type="standard") + == u'$0.00') assert (numbers.format_currency(1099.98, 'USD', locale='en_US', format_type="accounting") == u'$1,099.98') + assert (numbers.format_currency(0, 'USD', locale='en_US', + format_type="accounting") + == u'$0.00') with pytest.raises(numbers.UnknownCurrencyFormatError) as excinfo: numbers.format_currency(1099.98, 'USD', locale='en_US', @@ -328,8 +368,37 @@ def test_format_currency_format_type(): == u'1.099,98') +@pytest.mark.parametrize('input_value, expected_value', [ + ('10000', '$10,000.00'), + ('1', '$1.00'), + ('1.0', '$1.00'), + ('1.1', '$1.10'), + ('1.11', '$1.11'), + ('1.110', '$1.11'), + ('1.001', '$1.00'), + ('1.00100', '$1.00'), + ('01.00100', '$1.00'), + ('101.00100', '$101.00'), + ('00000', '$0.00'), + ('0', '$0.00'), + ('0.0', '$0.00'), + ('0.1', '$0.10'), + ('0.11', '$0.11'), + ('0.110', '$0.11'), + ('0.001', '$0.00'), + ('0.00100', '$0.00'), + ('00.00100', '$0.00'), + ('000.00100', '$0.00'), +]) +def test_format_currency_precision(input_value, expected_value): + # Test precision conservation. + assert numbers.format_currency( + decimal.Decimal(input_value), 'USD', locale='en_US') == expected_value + + def test_format_percent(): assert numbers.format_percent(0.34, locale='en_US') == u'34%' + assert numbers.format_percent(0, locale='en_US') == u'0%' assert numbers.format_percent(0.34, u'##0%', locale='en_US') == u'34%' assert numbers.format_percent(34, u'##0', locale='en_US') == u'34' assert numbers.format_percent(25.1234, locale='en_US') == u'2,512%' @@ -339,14 +408,81 @@ def test_format_percent(): == u'25,123\u2030') -def test_scientific_exponent_displayed_as_integer(): - assert numbers.format_scientific(100000, locale='en_US') == u'1E5' +@pytest.mark.parametrize('input_value, expected_value', [ + ('100', '10,000%'), + ('0.01', '1%'), + ('0.010', '1%'), + ('0.011', '1%'), + ('0.0111', '1%'), + ('0.01110', '1%'), + ('0.01001', '1%'), + ('0.0100100', '1%'), + ('0.010100100', '1%'), + ('0.000000', '0%'), + ('0', '0%'), + ('0.00', '0%'), + ('0.01', '1%'), + ('0.011', '1%'), + ('0.0110', '1%'), + ('0.0001', '0%'), + ('0.000100', '0%'), + ('0.0000100', '0%'), + ('0.00000100', '0%'), +]) +def test_format_percent_precision(input_value, expected_value): + # Test precision conservation. + assert numbers.format_percent( + decimal.Decimal(input_value), locale='en_US') == expected_value def test_format_scientific(): assert numbers.format_scientific(10000, locale='en_US') == u'1E4' - assert (numbers.format_scientific(1234567, u'##0E00', locale='en_US') - == u'1.23E06') + assert numbers.format_scientific(4234567, u'#.#E0', locale='en_US') == u'4.2E6' + assert numbers.format_scientific(4234567, u'0E0000', locale='en_US') == u'4E0006' + assert numbers.format_scientific(4234567, u'##0E00', locale='en_US') == u'4E06' + assert numbers.format_scientific(4234567, u'##00E00', locale='en_US') == u'42E05' + assert numbers.format_scientific(4234567, u'0,000E00', locale='en_US') == u'4,235E03' + assert numbers.format_scientific(4234567, u'##0.#####E00', locale='en_US') == u'4.23457E06' + assert numbers.format_scientific(4234567, u'##0.##E00', locale='en_US') == u'4.23E06' + assert numbers.format_scientific(42, u'00000.000000E0000', locale='en_US') == u'42000.000000E-0003' + + +def test_default_scientific_format(): + """ Check the scientific format method auto-correct the rendering pattern + in case of a missing fractional part. + """ + assert numbers.format_scientific(12345, locale='en_US') == u'1E4' + assert numbers.format_scientific(12345.678, locale='en_US') == u'1E4' + assert numbers.format_scientific(12345, u'#E0', locale='en_US') == u'1E4' + assert numbers.format_scientific(12345.678, u'#E0', locale='en_US') == u'1E4' + + +@pytest.mark.parametrize('input_value, expected_value', [ + ('10000', '1E4'), + ('1', '1E0'), + ('1.0', '1E0'), + ('1.1', '1E0'), + ('1.11', '1E0'), + ('1.110', '1E0'), + ('1.001', '1E0'), + ('1.00100', '1E0'), + ('01.00100', '1E0'), + ('101.00100', '1E2'), + ('00000', '0E0'), + ('0', '0E0'), + ('0.0', '0E0'), + ('0.1', '1E-1'), + ('0.11', '1E-1'), + ('0.110', '1E-1'), + ('0.001', '1E-3'), + ('0.00100', '1E-3'), + ('00.00100', '1E-3'), + ('000.00100', '1E-3'), +]) +def test_format_scientific_precision(input_value, expected_value): + # Test precision conservation. + assert numbers.format_scientific( + decimal.Decimal(input_value), locale='en_US') == expected_value def test_parse_number(): From 812c9f0bbf19eb669ecd9195a4a02ba68253a857 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sat, 21 Oct 2017 09:47:07 -0700 Subject: [PATCH 005/139] Include license file in the generated wheel package The wheel package format supports including the license file. This is done using the [metadata] section in the setup.cfg file. For additional information on this feature, see: https://wheel.readthedocs.io/en/stable/index.html#including-the-license-in-the-generated-wheel-file --- setup.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.cfg b/setup.cfg index c2d8f87e9..d5f180a24 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,3 +7,6 @@ doctest_optionflags = ELLIPSIS NORMALIZE_WHITESPACE ALLOW_UNICODE IGNORE_EXCEPTI [bdist_wheel] universal = 1 + +[metadata] +license_file = LICENSE From a97b426476638c815bdfe11196aa2e9940444cb4 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sat, 21 Oct 2017 09:49:07 -0700 Subject: [PATCH 006/139] Test and document all supported Python versions Add Python 3.6 to Travis CI and tox. Add PyPy to tox. Add trove classifiers for all supported versions. Helps package users know if the library is compatible. --- .travis.yml | 2 ++ setup.py | 3 +++ tox.ini | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b7eff7d94..f9469c99a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,6 +35,8 @@ matrix: python: 3.5 env: - PYTHON_TEST_FLAGS=-bb + - os: linux + python: 3.6 install: - bash .ci/deps.${TRAVIS_OS_NAME}.sh diff --git a/setup.py b/setup.py index 146f38dc4..684c38718 100755 --- a/setup.py +++ b/setup.py @@ -44,12 +44,15 @@ def run(self): 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', + 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Libraries :: Python Modules', ], diff --git a/tox.ini b/tox.ini index 537465823..b71dec7c4 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26, py27, pypy, py33, py34, py26-cdecimal, py27-cdecimal +envlist = py26, py27, pypy, py33, py34, py35, py36, pypy3, py26-cdecimal, py27-cdecimal [testenv] deps = From 1539c8a89119f37890f46586d705d5a90cc98ae6 Mon Sep 17 00:00:00 2001 From: Kevin Deldycke Date: Fri, 7 Apr 2017 16:09:14 +0200 Subject: [PATCH 007/139] Allow bypass of decimal quantization. --- babel/numbers.py | 102 ++++++++++++++++++++++++++++++----- tests/test_numbers.py | 122 +++++++++++++++++++++++++++--------------- 2 files changed, 168 insertions(+), 56 deletions(-) diff --git a/babel/numbers.py b/babel/numbers.py index 036513217..1f77bcd23 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -22,7 +22,6 @@ from datetime import date as date_, datetime as datetime_ from itertools import chain import warnings -from itertools import chain from babel.core import default_locale, Locale, get_global from babel._compat import decimal, string_types @@ -324,13 +323,27 @@ def format_number(number, locale=LC_NUMERIC): return format_decimal(number, locale=locale) +def get_decimal_precision(number): + """Return maximum precision of a decimal instance's fractional part. + + Precision is extracted from the fractional part only. + """ + # Copied from: https://github.com/mahmoud/boltons/pull/59 + assert isinstance(number, decimal.Decimal) + decimal_tuple = number.normalize().as_tuple() + if decimal_tuple.exponent >= 0: + return 0 + return abs(decimal_tuple.exponent) + + def get_decimal_quantum(precision): """Return minimal quantum of a number, as defined by precision.""" assert isinstance(precision, (int, long, decimal.Decimal)) return decimal.Decimal(10) ** (-precision) -def format_decimal(number, format=None, locale=LC_NUMERIC): +def format_decimal( + number, format=None, locale=LC_NUMERIC, decimal_quantization=True): u"""Return the given decimal number formatted for a specific locale. >>> format_decimal(1.2345, locale='en_US') @@ -350,23 +363,36 @@ def format_decimal(number, format=None, locale=LC_NUMERIC): >>> format_decimal(12345.5, locale='en_US') u'12,345.5' + By default the locale is allowed to truncate and round a high-precision + number by forcing its format pattern onto the decimal part. You can bypass + this behavior with the `decimal_quantization` parameter: + + >>> format_decimal(1.2346, locale='en_US') + u'1.235' + >>> format_decimal(1.2346, locale='en_US', decimal_quantization=False) + u'1.2346' + :param number: the number to format :param format: :param locale: the `Locale` object or locale identifier + :param decimal_quantization: Truncate and round high-precision numbers to + the format pattern. Defaults to `True`. """ locale = Locale.parse(locale) if not format: format = locale.decimal_formats.get(format) pattern = parse_pattern(format) - return pattern.apply(number, locale) + return pattern.apply( + number, locale, decimal_quantization=decimal_quantization) class UnknownCurrencyFormatError(KeyError): """Exception raised when an unknown currency format is requested.""" -def format_currency(number, currency, format=None, locale=LC_NUMERIC, - currency_digits=True, format_type='standard'): +def format_currency( + number, currency, format=None, locale=LC_NUMERIC, currency_digits=True, + format_type='standard', decimal_quantization=True): u"""Return formatted currency value. >>> format_currency(1099.98, 'USD', locale='en_US') @@ -416,12 +442,23 @@ def format_currency(number, currency, format=None, locale=LC_NUMERIC, ... UnknownCurrencyFormatError: "'unknown' is not a known currency format type" + By default the locale is allowed to truncate and round a high-precision + number by forcing its format pattern onto the decimal part. You can bypass + this behavior with the `decimal_quantization` parameter: + + >>> format_currency(1099.9876, 'USD', locale='en_US') + u'$1,099.99' + >>> format_currency(1099.9876, 'USD', locale='en_US', decimal_quantization=False) + u'$1,099.9876' + :param number: the number to format :param currency: the currency code :param format: the format string to use :param locale: the `Locale` object or locale identifier - :param currency_digits: use the currency's number of decimal digits + :param currency_digits: use the currency's natural number of decimal digits :param format_type: the currency format type to use + :param decimal_quantization: Truncate and round high-precision numbers to + the format pattern. Defaults to `True`. """ locale = Locale.parse(locale) if format: @@ -434,10 +471,12 @@ def format_currency(number, currency, format=None, locale=LC_NUMERIC, "%r is not a known currency format type" % format_type) return pattern.apply( - number, locale, currency=currency, currency_digits=currency_digits) + number, locale, currency=currency, currency_digits=currency_digits, + decimal_quantization=decimal_quantization) -def format_percent(number, format=None, locale=LC_NUMERIC): +def format_percent( + number, format=None, locale=LC_NUMERIC, decimal_quantization=True): """Return formatted percent value for a specific locale. >>> format_percent(0.34, locale='en_US') @@ -452,18 +491,31 @@ def format_percent(number, format=None, locale=LC_NUMERIC): >>> format_percent(25.1234, u'#,##0\u2030', locale='en_US') u'25,123\u2030' + By default the locale is allowed to truncate and round a high-precision + number by forcing its format pattern onto the decimal part. You can bypass + this behavior with the `decimal_quantization` parameter: + + >>> format_percent(23.9876, locale='en_US') + u'2,399%' + >>> format_percent(23.9876, locale='en_US', decimal_quantization=False) + u'2,398.76%' + :param number: the percent number to format :param format: :param locale: the `Locale` object or locale identifier + :param decimal_quantization: Truncate and round high-precision numbers to + the format pattern. Defaults to `True`. """ locale = Locale.parse(locale) if not format: format = locale.percent_formats.get(format) pattern = parse_pattern(format) - return pattern.apply(number, locale) + return pattern.apply( + number, locale, decimal_quantization=decimal_quantization) -def format_scientific(number, format=None, locale=LC_NUMERIC): +def format_scientific( + number, format=None, locale=LC_NUMERIC, decimal_quantization=True): """Return value formatted in scientific notation for a specific locale. >>> format_scientific(10000, locale='en_US') @@ -474,15 +526,27 @@ def format_scientific(number, format=None, locale=LC_NUMERIC): >>> format_scientific(1234567, u'##0.##E00', locale='en_US') u'1.23E06' + By default the locale is allowed to truncate and round a high-precision + number by forcing its format pattern onto the decimal part. You can bypass + this behavior with the `decimal_quantization` parameter: + + >>> format_scientific(1234.9876, u'#.##E0', locale='en_US') + u'1.23E3' + >>> format_scientific(1234.9876, u'#.##E0', locale='en_US', decimal_quantization=False) + u'1.2349876E3' + :param number: the number to format :param format: :param locale: the `Locale` object or locale identifier + :param decimal_quantization: Truncate and round high-precision numbers to + the format pattern. Defaults to `True`. """ locale = Locale.parse(locale) if not format: format = locale.scientific_formats.get(format) pattern = parse_pattern(format) - return pattern.apply(number, locale) + return pattern.apply( + number, locale, decimal_quantization=decimal_quantization) class NumberFormatError(ValueError): @@ -702,8 +766,13 @@ def scientific_notation_elements(self, value, locale): return value, exp, exp_sign - def apply(self, value, locale, currency=None, currency_digits=True): + def apply( + self, value, locale, currency=None, currency_digits=True, + decimal_quantization=True): """Renders into a string a number following the defined pattern. + + Forced decimal quantization is active by default so we'll produce a + number string that is strictly following CLDR pattern definitions. """ if not isinstance(value, decimal.Decimal): value = decimal.Decimal(str(value)) @@ -724,6 +793,15 @@ def apply(self, value, locale, currency=None, currency_digits=True): if currency and currency_digits: frac_prec = (get_currency_precision(currency), ) * 2 + # Bump decimal precision to the natural precision of the number if it + # exceeds the one we're about to use. This adaptative precision is only + # triggered if the decimal quantization is disabled or if a scientific + # notation pattern has a missing mandatory fractional part (as in the + # default '#E0' pattern). This special case has been extensively + # discussed at https://github.com/python-babel/babel/pull/494#issuecomment-307649969 . + if not decimal_quantization or (self.exp_prec and frac_prec == (0, 0)): + frac_prec = (frac_prec[0], max([frac_prec[1], get_decimal_precision(value)])) + # Render scientific notation. if self.exp_prec: number = ''.join([ diff --git a/tests/test_numbers.py b/tests/test_numbers.py index 5c8da3422..48a260b40 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -16,10 +16,10 @@ from datetime import date -from babel import numbers +from babel import Locale, localedata, numbers from babel.numbers import ( - list_currencies, validate_currency, UnknownCurrencyError, is_currency, normalize_currency, get_currency_precision) -from babel.core import Locale + list_currencies, validate_currency, UnknownCurrencyError, is_currency, normalize_currency, + get_currency_precision, get_decimal_precision) from babel.localedata import locale_identifiers from babel._compat import decimal @@ -271,6 +271,12 @@ def test_get_group_symbol(): assert numbers.get_group_symbol('en_US') == u',' +def test_decimal_precision(): + assert get_decimal_precision(decimal.Decimal('0.110')) == 2 + assert get_decimal_precision(decimal.Decimal('1.0')) == 0 + assert get_decimal_precision(decimal.Decimal('10000')) == 0 + + def test_format_number(): assert numbers.format_number(1099, locale='en_US') == u'1,099' assert numbers.format_number(1099, locale='de_DE') == u'1.099' @@ -314,7 +320,14 @@ def test_format_decimal(): def test_format_decimal_precision(input_value, expected_value): # Test precision conservation. assert numbers.format_decimal( - decimal.Decimal(input_value), locale='en_US') == expected_value + decimal.Decimal(input_value), locale='en_US', decimal_quantization=False) == expected_value + + +def test_format_decimal_quantization(): + # Test all locales. + for locale_code in localedata.locale_identifiers(): + assert numbers.format_decimal( + '0.9999999999', locale=locale_code, decimal_quantization=False).endswith('9999999999') is True def test_format_currency(): @@ -375,25 +388,32 @@ def test_format_currency_format_type(): ('1.1', '$1.10'), ('1.11', '$1.11'), ('1.110', '$1.11'), - ('1.001', '$1.00'), - ('1.00100', '$1.00'), - ('01.00100', '$1.00'), - ('101.00100', '$101.00'), + ('1.001', '$1.001'), + ('1.00100', '$1.001'), + ('01.00100', '$1.001'), + ('101.00100', '$101.001'), ('00000', '$0.00'), ('0', '$0.00'), ('0.0', '$0.00'), ('0.1', '$0.10'), ('0.11', '$0.11'), ('0.110', '$0.11'), - ('0.001', '$0.00'), - ('0.00100', '$0.00'), - ('00.00100', '$0.00'), - ('000.00100', '$0.00'), + ('0.001', '$0.001'), + ('0.00100', '$0.001'), + ('00.00100', '$0.001'), + ('000.00100', '$0.001'), ]) def test_format_currency_precision(input_value, expected_value): # Test precision conservation. assert numbers.format_currency( - decimal.Decimal(input_value), 'USD', locale='en_US') == expected_value + decimal.Decimal(input_value), 'USD', locale='en_US', decimal_quantization=False) == expected_value + + +def test_format_currency_quantization(): + # Test all locales. + for locale_code in localedata.locale_identifiers(): + assert numbers.format_currency( + '0.9999999999', 'USD', locale=locale_code, decimal_quantization=False).find('9999999999') > -1 def test_format_percent(): @@ -412,36 +432,43 @@ def test_format_percent(): ('100', '10,000%'), ('0.01', '1%'), ('0.010', '1%'), - ('0.011', '1%'), - ('0.0111', '1%'), - ('0.01110', '1%'), - ('0.01001', '1%'), - ('0.0100100', '1%'), - ('0.010100100', '1%'), + ('0.011', '1.1%'), + ('0.0111', '1.11%'), + ('0.01110', '1.11%'), + ('0.01001', '1.001%'), + ('0.0100100', '1.001%'), + ('0.010100100', '1.01001%'), ('0.000000', '0%'), ('0', '0%'), ('0.00', '0%'), ('0.01', '1%'), - ('0.011', '1%'), - ('0.0110', '1%'), - ('0.0001', '0%'), - ('0.000100', '0%'), - ('0.0000100', '0%'), - ('0.00000100', '0%'), + ('0.011', '1.1%'), + ('0.0110', '1.1%'), + ('0.0001', '0.01%'), + ('0.000100', '0.01%'), + ('0.0000100', '0.001%'), + ('0.00000100', '0.0001%'), ]) def test_format_percent_precision(input_value, expected_value): # Test precision conservation. assert numbers.format_percent( - decimal.Decimal(input_value), locale='en_US') == expected_value + decimal.Decimal(input_value), locale='en_US', decimal_quantization=False) == expected_value + + +def test_format_percent_quantization(): + # Test all locales. + for locale_code in localedata.locale_identifiers(): + assert numbers.format_percent( + '0.9999999999', locale=locale_code, decimal_quantization=False).find('99999999') > -1 def test_format_scientific(): assert numbers.format_scientific(10000, locale='en_US') == u'1E4' assert numbers.format_scientific(4234567, u'#.#E0', locale='en_US') == u'4.2E6' - assert numbers.format_scientific(4234567, u'0E0000', locale='en_US') == u'4E0006' - assert numbers.format_scientific(4234567, u'##0E00', locale='en_US') == u'4E06' - assert numbers.format_scientific(4234567, u'##00E00', locale='en_US') == u'42E05' - assert numbers.format_scientific(4234567, u'0,000E00', locale='en_US') == u'4,235E03' + assert numbers.format_scientific(4234567, u'0E0000', locale='en_US') == u'4.234567E0006' + assert numbers.format_scientific(4234567, u'##0E00', locale='en_US') == u'4.234567E06' + assert numbers.format_scientific(4234567, u'##00E00', locale='en_US') == u'42.34567E05' + assert numbers.format_scientific(4234567, u'0,000E00', locale='en_US') == u'4,234.567E03' assert numbers.format_scientific(4234567, u'##0.#####E00', locale='en_US') == u'4.23457E06' assert numbers.format_scientific(4234567, u'##0.##E00', locale='en_US') == u'4.23E06' assert numbers.format_scientific(42, u'00000.000000E0000', locale='en_US') == u'42000.000000E-0003' @@ -451,29 +478,29 @@ def test_default_scientific_format(): """ Check the scientific format method auto-correct the rendering pattern in case of a missing fractional part. """ - assert numbers.format_scientific(12345, locale='en_US') == u'1E4' - assert numbers.format_scientific(12345.678, locale='en_US') == u'1E4' - assert numbers.format_scientific(12345, u'#E0', locale='en_US') == u'1E4' - assert numbers.format_scientific(12345.678, u'#E0', locale='en_US') == u'1E4' + assert numbers.format_scientific(12345, locale='en_US') == u'1.2345E4' + assert numbers.format_scientific(12345.678, locale='en_US') == u'1.2345678E4' + assert numbers.format_scientific(12345, u'#E0', locale='en_US') == u'1.2345E4' + assert numbers.format_scientific(12345.678, u'#E0', locale='en_US') == u'1.2345678E4' @pytest.mark.parametrize('input_value, expected_value', [ ('10000', '1E4'), ('1', '1E0'), ('1.0', '1E0'), - ('1.1', '1E0'), - ('1.11', '1E0'), - ('1.110', '1E0'), - ('1.001', '1E0'), - ('1.00100', '1E0'), - ('01.00100', '1E0'), - ('101.00100', '1E2'), + ('1.1', '1.1E0'), + ('1.11', '1.11E0'), + ('1.110', '1.11E0'), + ('1.001', '1.001E0'), + ('1.00100', '1.001E0'), + ('01.00100', '1.001E0'), + ('101.00100', '1.01001E2'), ('00000', '0E0'), ('0', '0E0'), ('0.0', '0E0'), ('0.1', '1E-1'), - ('0.11', '1E-1'), - ('0.110', '1E-1'), + ('0.11', '1.1E-1'), + ('0.110', '1.1E-1'), ('0.001', '1E-3'), ('0.00100', '1E-3'), ('00.00100', '1E-3'), @@ -482,7 +509,14 @@ def test_default_scientific_format(): def test_format_scientific_precision(input_value, expected_value): # Test precision conservation. assert numbers.format_scientific( - decimal.Decimal(input_value), locale='en_US') == expected_value + decimal.Decimal(input_value), locale='en_US', decimal_quantization=False) == expected_value + + +def test_format_scientific_quantization(): + # Test all locales. + for locale_code in localedata.locale_identifiers(): + assert numbers.format_scientific( + '0.9999999999', locale=locale_code, decimal_quantization=False).find('999999999') > -1 def test_parse_number(): From 75b8f7899f0080e99a7793c4b97de07be06af518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 16 Sep 2017 00:07:04 +0300 Subject: [PATCH 008/139] Python 3.6 invalid escape sequence deprecation fixes https://docs.python.org/3/whatsnew/3.6.html#deprecated-python-behavior --- babel/core.py | 2 +- babel/dates.py | 4 ++-- babel/localtime/_unix.py | 6 +++--- babel/messages/checkers.py | 2 +- babel/messages/frontend.py | 2 +- scripts/make-release.py | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/babel/core.py b/babel/core.py index df03b24ad..7d38d43ec 100644 --- a/babel/core.py +++ b/babel/core.py @@ -1118,7 +1118,7 @@ def parse_locale(identifier, sep='_'): def get_locale_identifier(tup, sep='_'): """The reverse of :func:`parse_locale`. It creates a locale identifier out of a ``(language, territory, script, variant)`` tuple. Items can be set to - ``None`` and trailing ``None``\s can also be left out of the tuple. + ``None`` and trailing ``None``\\s can also be left out of the tuple. >>> get_locale_identifier(('de', 'DE', None, '1999')) 'de_DE_1999' diff --git a/babel/dates.py b/babel/dates.py index d1fafe2a7..3a075eb47 100644 --- a/babel/dates.py +++ b/babel/dates.py @@ -1155,7 +1155,7 @@ def parse_date(string, locale=LC_TIME): # FIXME: this currently only supports numbers, but should also support month # names, both in the requested locale, and english - numbers = re.findall('(\d+)', string) + numbers = re.findall(r'(\d+)', string) year = numbers[indexes['Y']] if len(year) == 2: year = 2000 + int(year) @@ -1198,7 +1198,7 @@ def parse_time(string, locale=LC_TIME): # and seconds should be optional, maybe minutes too # oh, and time-zones, of course - numbers = re.findall('(\d+)', string) + numbers = re.findall(r'(\d+)', string) hour = int(numbers[indexes['H']]) minute = int(numbers[indexes['M']]) second = int(numbers[indexes['S']]) diff --git a/babel/localtime/_unix.py b/babel/localtime/_unix.py index 8a8b4e967..561de0d6a 100644 --- a/babel/localtime/_unix.py +++ b/babel/localtime/_unix.py @@ -100,9 +100,9 @@ def _get_localzone(_root='/'): # OpenSUSE has a TIMEZONE setting in /etc/sysconfig/clock and # Gentoo has a TIMEZONE setting in /etc/conf.d/clock # We look through these files for a timezone: - zone_re = re.compile('\s*ZONE\s*=\s*\"') - timezone_re = re.compile('\s*TIMEZONE\s*=\s*\"') - end_re = re.compile('\"') + zone_re = re.compile(r'\s*ZONE\s*=\s*"') + timezone_re = re.compile(r'\s*TIMEZONE\s*=\s*"') + end_re = re.compile(r'"') for filename in ('etc/sysconfig/clock', 'etc/conf.d/clock'): tzpath = os.path.join(_root, filename) diff --git a/babel/messages/checkers.py b/babel/messages/checkers.py index 24ecdcfed..c4669c53b 100644 --- a/babel/messages/checkers.py +++ b/babel/messages/checkers.py @@ -61,7 +61,7 @@ def python_format(catalog, message): def _validate_format(format, alternative): """Test format string `alternative` against `format`. `format` can be the - msgid of a message and `alternative` one of the `msgstr`\s. The two + msgid of a message and `alternative` one of the `msgstr`\\s. The two arguments are not interchangeable as `alternative` may contain less placeholders if `format` uses named placeholders. diff --git a/babel/messages/frontend.py b/babel/messages/frontend.py index fb171e36a..388c14b7a 100644 --- a/babel/messages/frontend.py +++ b/babel/messages/frontend.py @@ -391,7 +391,7 @@ def finalize_options(self): if self.input_paths: if isinstance(self.input_paths, string_types): - self.input_paths = re.split(',\s*', self.input_paths) + self.input_paths = re.split(r',\s*', self.input_paths) elif self.distribution is not None: self.input_paths = dict.fromkeys([ k.split('.', 1)[0] diff --git a/scripts/make-release.py b/scripts/make-release.py index dc9bb31e6..245608a73 100755 --- a/scripts/make-release.py +++ b/scripts/make-release.py @@ -23,7 +23,7 @@ def parse_changelog(): with open('CHANGES') as f: lineiter = iter(f) for line in lineiter: - match = re.search('^Version\s+(.*)', line.strip()) + match = re.search(r'^Version\s+(.*)', line.strip()) if match is None: continue version = match.group(1).strip() From f2626a8793575c291ed8d9edbb0b99778a964dfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 16 Sep 2017 00:08:07 +0300 Subject: [PATCH 009/139] Simplify Linux distro specific explicit timezone setting search --- babel/localtime/_unix.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/babel/localtime/_unix.py b/babel/localtime/_unix.py index 561de0d6a..8f25fe7b8 100644 --- a/babel/localtime/_unix.py +++ b/babel/localtime/_unix.py @@ -100,9 +100,7 @@ def _get_localzone(_root='/'): # OpenSUSE has a TIMEZONE setting in /etc/sysconfig/clock and # Gentoo has a TIMEZONE setting in /etc/conf.d/clock # We look through these files for a timezone: - zone_re = re.compile(r'\s*ZONE\s*=\s*"') - timezone_re = re.compile(r'\s*TIMEZONE\s*=\s*"') - end_re = re.compile(r'"') + timezone_re = re.compile(r'\s*(TIME)?ZONE\s*=\s*"(?P.+)"') for filename in ('etc/sysconfig/clock', 'etc/conf.d/clock'): tzpath = os.path.join(_root, filename) @@ -110,17 +108,10 @@ def _get_localzone(_root='/'): continue with open(tzpath, 'rt') as tzfile: for line in tzfile: - # Look for the ZONE= setting. - match = zone_re.match(line) - if match is None: - # No ZONE= setting. Look for the TIMEZONE= setting. - match = timezone_re.match(line) + match = timezone_re.match(line) if match is not None: - # Some setting existed - line = line[match.end():] - etctz = line[:end_re.search(line).start()] - # We found a timezone + etctz = match.group("etctz") return pytz.timezone(etctz.replace(' ', '_')) # No explicit setting existed. Use localtime From 66f51babed216adbc4f11aaa715ca355cf88a867 Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 28 Dec 2017 00:17:29 +0200 Subject: [PATCH 010/139] Drop EOL Python 2.6 and 3.3 --- .travis.yml | 8 -------- docs/dev.rst | 3 +-- setup.py | 3 +-- tox.ini | 2 +- 4 files changed, 3 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index f9469c99a..b819e3e5e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,12 +11,6 @@ cache: matrix: include: - - os: linux - python: 2.6 - - os: linux - python: 2.6 - env: - - CDECIMAL=m3-cdecimal - os: linux python: 2.7 - os: linux @@ -27,8 +21,6 @@ matrix: python: pypy - os: linux python: pypy3 - - os: linux - python: 3.3 - os: linux python: 3.4 - os: linux diff --git a/docs/dev.rst b/docs/dev.rst index afc8b12d6..0ff033ff8 100644 --- a/docs/dev.rst +++ b/docs/dev.rst @@ -30,9 +30,8 @@ Python Versions At the moment the following Python versions should be supported: -* Python 2.6 * Python 2.7 -* Python 3.3 and up +* Python 3.4 and up * PyPy tracking 2.7 and 3.2 and up While PyPy does not currently support 3.3, it does support traditional diff --git a/setup.py b/setup.py index 684c38718..8e268e16b 100755 --- a/setup.py +++ b/setup.py @@ -45,10 +45,8 @@ def run(self): 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', @@ -56,6 +54,7 @@ def run(self): 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Libraries :: Python Modules', ], + python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', packages=['babel', 'babel.messages', 'babel.localtime'], include_package_data=True, install_requires=[ diff --git a/tox.ini b/tox.ini index b71dec7c4..ab733469a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26, py27, pypy, py33, py34, py35, py36, pypy3, py26-cdecimal, py27-cdecimal +envlist = py27, pypy, py33, py34, py35, py36, pypy3, py27-cdecimal [testenv] deps = From 44f509c6ab67a1a7154d0ffbb8e626e80c3d32be Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 28 Dec 2017 00:25:54 +0200 Subject: [PATCH 011/139] Replace function call with set literal --- babel/languages.py | 2 +- babel/messages/catalog.py | 4 ++-- babel/messages/checkers.py | 6 +++--- babel/plural.py | 2 +- tests/messages/test_frontend.py | 30 +++++++++--------------------- tests/test_languages.py | 4 +++- tests/test_numbers.py | 2 +- tests/test_support.py | 2 +- 8 files changed, 21 insertions(+), 31 deletions(-) diff --git a/babel/languages.py b/babel/languages.py index 40f5d9828..0513d002d 100644 --- a/babel/languages.py +++ b/babel/languages.py @@ -26,7 +26,7 @@ def get_official_languages(territory, regional=False, de_facto=False): """ territory = str(territory).upper() - allowed_stati = set(("official",)) + allowed_stati = {"official"} if regional: allowed_stati.add("official_regional") if de_facto: diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py index fd40058ad..93d0440a8 100644 --- a/babel/messages/catalog.py +++ b/babel/messages/catalog.py @@ -531,7 +531,7 @@ def __iter__(self): buf.append('%s: %s' % (name, value)) flags = set() if self.fuzzy: - flags |= set(['fuzzy']) + flags |= {'fuzzy'} yield Message(u'', '\n'.join(buf), flags=flags) for key in self._messages: yield self._messages[key] @@ -762,7 +762,7 @@ def _merge(message, oldkey, newkey): message.string = message.string[0] message.flags |= oldmsg.flags if fuzzy: - message.flags |= set([u'fuzzy']) + message.flags |= {u'fuzzy'} self[message.id] = message for message in template: diff --git a/babel/messages/checkers.py b/babel/messages/checkers.py index 24ecdcfed..97e6e197b 100644 --- a/babel/messages/checkers.py +++ b/babel/messages/checkers.py @@ -17,9 +17,9 @@ #: list of format chars that are compatible to each other _string_format_compatibilities = [ - set(['i', 'd', 'u']), - set(['x', 'X']), - set(['f', 'F', 'g', 'G']) + {'i', 'd', 'u'}, + {'x', 'X'}, + {'f', 'F', 'g', 'G'} ] diff --git a/babel/plural.py b/babel/plural.py index a23f8b53f..06f349ea9 100644 --- a/babel/plural.py +++ b/babel/plural.py @@ -241,7 +241,7 @@ def to_gettext(rule): """ rule = PluralRule.parse(rule) - used_tags = rule.tags | set([_fallback_tag]) + used_tags = rule.tags | {_fallback_tag} _compile = _GettextCompiler().compile _get_index = [tag for tag in _plural_tags if tag in used_tags].index diff --git a/tests/messages/test_frontend.py b/tests/messages/test_frontend.py index 20904a350..3ac0ac314 100644 --- a/tests/messages/test_frontend.py +++ b/tests/messages/test_frontend.py @@ -1343,26 +1343,14 @@ def test_extract_keyword_args_384(split, arg_name): "extract -F babel-django.cfg --add-comments Translators: -o django232.pot %s ." % kwarg_text ) assert isinstance(cmdinst, extract_messages) - assert set(cmdinst.keywords.keys()) == set(( - '_', - 'dgettext', - 'dngettext', - 'gettext', - 'gettext_lazy', - 'gettext_noop', - 'N_', - 'ngettext', - 'ngettext_lazy', - 'npgettext', - 'npgettext_lazy', - 'pgettext', - 'pgettext_lazy', - 'ugettext', - 'ugettext_lazy', - 'ugettext_noop', - 'ungettext', - 'ungettext_lazy', - )) + assert set(cmdinst.keywords.keys()) == {'_', 'dgettext', 'dngettext', + 'gettext', 'gettext_lazy', + 'gettext_noop', 'N_', 'ngettext', + 'ngettext_lazy', 'npgettext', + 'npgettext_lazy', 'pgettext', + 'pgettext_lazy', 'ugettext', + 'ugettext_lazy', 'ugettext_noop', + 'ungettext', 'ungettext_lazy'} @pytest.mark.parametrize("kwarg,expected", [ @@ -1384,7 +1372,7 @@ def test_extract_distutils_keyword_arg_388(kwarg, expected): assert set(cmdinst.keywords.keys()) == set(expected) # Test the comma-separated comment argument while we're at it: - assert set(cmdinst.add_comments) == set(("Bar", "Foo")) + assert set(cmdinst.add_comments) == {"Bar", "Foo"} def test_update_catalog_boolean_args(): diff --git a/tests/test_languages.py b/tests/test_languages.py index 594149fa7..23af26d53 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -11,4 +11,6 @@ def test_official_languages(): def test_get_language_info(): - assert set(get_territory_language_info("HU").keys()) == set(("hu", "en", "de", "ro", "hr", "sk", "sl")) + assert set(get_territory_language_info("HU").keys()) == {"hu", "en", "de", + "ro", "hr", "sk", + "sl"} diff --git a/tests/test_numbers.py b/tests/test_numbers.py index 48a260b40..6cc6e438e 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -177,7 +177,7 @@ def test_list_currencies(): list_currencies('yo!') assert excinfo.value.args[0] == "expected only letters, got 'yo!'" - assert list_currencies(locale='pa_Arab') == set(['PKR', 'INR', 'EUR']) + assert list_currencies(locale='pa_Arab') == {'PKR', 'INR', 'EUR'} assert list_currencies(locale='kok') == set([]) assert len(list_currencies()) == 296 diff --git a/tests/test_support.py b/tests/test_support.py index f2a99b7ba..6f9fb3246 100644 --- a/tests/test_support.py +++ b/tests/test_support.py @@ -358,4 +358,4 @@ def test_catalog_merge_files(): t2._catalog["bar"] = "quux" t1.merge(t2) assert t1.files == ["pro.mo"] - assert set(t1._catalog.keys()) == set(('', 'foo', 'bar')) + assert set(t1._catalog.keys()) == {'', 'foo', 'bar'} From 2e6f6b9bd48f93a86dec5b317d4c2eb89a3a6301 Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 28 Dec 2017 11:06:59 +0200 Subject: [PATCH 012/139] Remove redundant parentheses --- babel/dates.py | 4 ++-- babel/messages/catalog.py | 4 ++-- babel/messages/frontend.py | 2 +- tests/messages/test_catalog.py | 2 +- tests/messages/test_extract.py | 4 ++-- tests/messages/test_frontend.py | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/babel/dates.py b/babel/dates.py index d1fafe2a7..377dfebb0 100644 --- a/babel/dates.py +++ b/babel/dates.py @@ -521,7 +521,7 @@ def get_timezone_location(dt_or_tzinfo=None, locale=LC_TIME, return_city=False): territory = 'ZZ' # invalid/unknown territory_name = locale.territories[territory] if not return_city and territory and len(get_global('territory_zones').get(territory, [])) == 1: - return region_format % (territory_name) + return region_format % territory_name # Otherwise, include the city in the output fallback_format = locale.zone_formats['fallback'] @@ -1303,7 +1303,7 @@ def extract(self, char): elif char == 'H': return self.value.hour elif char == 'h': - return (self.value.hour % 12 or 12) + return self.value.hour % 12 or 12 elif char == 'm': return self.value.minute elif char == 'a': diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py index 93d0440a8..b293e3c73 100644 --- a/babel/messages/catalog.py +++ b/babel/messages/catalog.py @@ -120,8 +120,8 @@ def __cmp__(self, other): """Compare Messages, taking into account plural ids""" def values_to_compare(obj): if isinstance(obj, Message) and obj.pluralizable: - return (obj.id[0], obj.context or '') - return (obj.id, obj.context or '') + return obj.id[0], obj.context or '' + return obj.id, obj.context or '' return cmp(values_to_compare(self), values_to_compare(other)) def __gt__(self, other): diff --git a/babel/messages/frontend.py b/babel/messages/frontend.py index fb171e36a..bee745a7e 100644 --- a/babel/messages/frontend.py +++ b/babel/messages/frontend.py @@ -978,7 +978,7 @@ def parse_mapping(fileobj, filename=None): method = extractors[method] method_map[idx] = (pattern, method) - return (method_map, options_map) + return method_map, options_map def parse_keywords(strings=[]): diff --git a/tests/messages/test_catalog.py b/tests/messages/test_catalog.py index 2d2880c38..07ed724b9 100644 --- a/tests/messages/test_catalog.py +++ b/tests/messages/test_catalog.py @@ -101,7 +101,7 @@ def test_update_message_changed_to_plural(self): def test_update_message_changed_to_simple(self): cat = catalog.Catalog() - cat.add((u'foo' u'foos'), (u'Voh', u'Vöhs')) + cat.add(u'foo' u'foos', (u'Voh', u'Vöhs')) tmpl = catalog.Catalog() tmpl.add(u'foo') cat.update(tmpl) diff --git a/tests/messages/test_extract.py b/tests/messages/test_extract.py index 873439de4..17b090fc1 100644 --- a/tests/messages/test_extract.py +++ b/tests/messages/test_extract.py @@ -178,7 +178,7 @@ def test_triple_quoted_strings(self): messages = list(extract.extract_python(buf, extract.DEFAULT_KEYWORDS.keys(), [], {})) - self.assertEqual([(1, '_', (u'pylons'), []), + self.assertEqual([(1, '_', u'pylons', []), (2, 'ngettext', (u'elvis', u'elvises', None), []), (3, 'ngettext', (u'elvis', u'elvises', None), [])], messages) @@ -350,7 +350,7 @@ def test_different_signatures(self): self.assertEqual((None, u'hello', u'there'), messages[2][2]) self.assertEqual((None, None), messages[3][2]) self.assertEqual(None, messages[4][2]) - self.assertEqual(('foo'), messages[5][2]) + self.assertEqual('foo', messages[5][2]) def test_utf8_message(self): buf = BytesIO(u""" diff --git a/tests/messages/test_frontend.py b/tests/messages/test_frontend.py index 3ac0ac314..10325fe61 100644 --- a/tests/messages/test_frontend.py +++ b/tests/messages/test_frontend.py @@ -1140,7 +1140,7 @@ def test_compile_catalog(self): assert not os.path.isfile(mo_file), 'Expected no file at %r' % mo_file self.assertEqual("""\ catalog %s is marked as fuzzy, skipping -""" % (po_file), sys.stderr.getvalue()) +""" % po_file, sys.stderr.getvalue()) def test_compile_fuzzy_catalog(self): po_file = self._po_file('de_DE') From 0c404b58b25f8e2e115d51fddd5d5716969af219 Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 28 Dec 2017 11:12:01 +0200 Subject: [PATCH 013/139] Remove unused imports --- babel/numbers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/babel/numbers.py b/babel/numbers.py index 1f77bcd23..73ff816c6 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -20,16 +20,16 @@ # - http://www.unicode.org/reports/tr35/ (Appendix G.6) import re from datetime import date as date_, datetime as datetime_ -from itertools import chain import warnings from babel.core import default_locale, Locale, get_global from babel._compat import decimal, string_types -from babel.localedata import locale_identifiers try: + # Python 2 long except NameError: + # Python 3 long = int From 05974ff14feda12a0d292a560313961796e69113 Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 28 Dec 2017 11:13:50 +0200 Subject: [PATCH 014/139] Replace comparison with None with equality operator --- tests/test_localedata.py | 4 ++-- tests/test_numbers.py | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_localedata.py b/tests/test_localedata.py index 3599b2157..5d6c8008a 100644 --- a/tests/test_localedata.py +++ b/tests/test_localedata.py @@ -108,12 +108,12 @@ def test_pi_support_not_frozen(): def test_locale_argument_acceptance(): # Testing None input. normalized_locale = localedata.normalize_locale(None) - assert normalized_locale == None + assert normalized_locale is None locale_exist = localedata.exists(None) assert locale_exist == False # # Testing list input. normalized_locale = localedata.normalize_locale(['en_us', None]) - assert normalized_locale == None + assert normalized_locale is None locale_exist = localedata.exists(['en_us', None]) assert locale_exist == False diff --git a/tests/test_numbers.py b/tests/test_numbers.py index 6cc6e438e..2795b0f47 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -206,13 +206,13 @@ def test_is_currency(): def test_normalize_currency(): assert normalize_currency('EUR') == 'EUR' assert normalize_currency('eUr') == 'EUR' - assert normalize_currency('FUU') == None - assert normalize_currency('') == None - assert normalize_currency(None) == None - assert normalize_currency(' EUR ') == None - assert normalize_currency(' ') == None - assert normalize_currency([]) == None - assert normalize_currency(set()) == None + assert normalize_currency('FUU') is None + assert normalize_currency('') is None + assert normalize_currency(None) is None + assert normalize_currency(' EUR ') is None + assert normalize_currency(' ') is None + assert normalize_currency([]) is None + assert normalize_currency(set()) is None def test_get_currency_name(): From 6126d3bb3be6d51f8676638f9475e0048c44a494 Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 28 Dec 2017 11:39:20 +0200 Subject: [PATCH 015/139] Drop EOL Python 3.3 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index ab733469a..33d1ab890 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27, pypy, py33, py34, py35, py36, pypy3, py27-cdecimal +envlist = py27, pypy, py34, py35, py36, pypy3, py27-cdecimal [testenv] deps = From edfb518a589aa6d7bc4243dc2430a5b0f200ca8c Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 15 Jan 2018 12:43:52 +0200 Subject: [PATCH 016/139] Become 2.5.2 --- CHANGES | 8 ++++++++ babel/__init__.py | 2 +- docs/conf.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 0efd2044c..e0f47e87d 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,14 @@ Babel Changelog =============== +Version 2.5.2 +------------- + +Bugfixes +~~~~~~~~ + +* Revert the unnecessary PyInstaller fixes from 2.5.0 and 2.5.1 (#533) (@yagebu) + Version 2.5.1 ------------- diff --git a/babel/__init__.py b/babel/__init__.py index ba972b5a4..400dbdf3f 100644 --- a/babel/__init__.py +++ b/babel/__init__.py @@ -21,4 +21,4 @@ negotiate_locale, parse_locale, get_locale_identifier -__version__ = '2.5.1' +__version__ = '2.5.2' diff --git a/docs/conf.py b/docs/conf.py index a0220168a..f90fe6865 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,7 +53,7 @@ # The short X.Y version. version = '2.5' # The full version, including alpha/beta/rc tags. -release = '2.5.1' +release = '2.5.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From 9ba950c5ed92496a161cf481b694dd13b534f21d Mon Sep 17 00:00:00 2001 From: StevenJ Date: Mon, 25 Sep 2017 16:03:14 -0700 Subject: [PATCH 017/139] pofile.py: Added new exception called PoFileError and thrown if flagged This new exception is thrown when the po parser finds an invalid pofile. This helps handle invalid po files that are parsed. Invalid po files may cause other possible errors such as a UnicodeEncodeError. Closes https://github.com/python-babel/babel/issues/531 --- babel/messages/pofile.py | 20 +++++++++-- tests/messages/test_pofile.py | 66 ++++++++++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/babel/messages/pofile.py b/babel/messages/pofile.py index 696ec3e97..beb6f3570 100644 --- a/babel/messages/pofile.py +++ b/babel/messages/pofile.py @@ -19,6 +19,7 @@ from babel._compat import text_type + def unescape(string): r"""Reverse `escape` the given string. @@ -73,6 +74,15 @@ def denormalize(string): return unescape(string) +class PoFileError(Exception): + """Exception thrown by PoParser when an invalid po file is encountered.""" + def __init__(self, message, catalog, line, lineno): + super(PoFileError, self).__init__('{message} on {lineno}'.format(message=message, lineno=lineno)) + self.catalog = catalog + self.line = line + self.lineno = lineno + + class _NormalizedString(object): def __init__(self, *args): @@ -104,11 +114,12 @@ class PoFileParser(object): 'msgid_plural', ] - def __init__(self, catalog, ignore_obsolete=False): + def __init__(self, catalog, ignore_obsolete=False, abort_invalid=False): self.catalog = catalog self.ignore_obsolete = ignore_obsolete self.counter = 0 self.offset = 0 + self.abort_invalid = abort_invalid self._reset_message_state() def _reset_message_state(self): @@ -276,11 +287,13 @@ def parse(self, fileobj): self._add_message() def _invalid_pofile(self, line, lineno, msg): + if self.abort_invalid: + raise PoFileError(msg, self.catalog, line, lineno) print("WARNING:", msg) print("WARNING: Problem on line {0}: {1}".format(lineno + 1, line)) -def read_po(fileobj, locale=None, domain=None, ignore_obsolete=False, charset=None): +def read_po(fileobj, locale=None, domain=None, ignore_obsolete=False, charset=None, abort_invalid=False): """Read messages from a ``gettext`` PO (portable object) file from the given file-like object and return a `Catalog`. @@ -325,9 +338,10 @@ def read_po(fileobj, locale=None, domain=None, ignore_obsolete=False, charset=No :param domain: the message domain :param ignore_obsolete: whether to ignore obsolete messages in the input :param charset: the character set of the catalog. + :param abort_invalid: abort read if po file is invalid """ catalog = Catalog(locale=locale, domain=domain, charset=charset) - parser = PoFileParser(catalog, ignore_obsolete) + parser = PoFileParser(catalog, ignore_obsolete, abort_invalid=abort_invalid) parser.parse(fileobj) return catalog diff --git a/tests/messages/test_pofile.py b/tests/messages/test_pofile.py index f6cd66dba..002954f8a 100644 --- a/tests/messages/test_pofile.py +++ b/tests/messages/test_pofile.py @@ -13,6 +13,7 @@ from datetime import datetime import unittest +import sys from babel.core import Locale from babel.messages.catalog import Catalog, Message @@ -20,7 +21,6 @@ from babel.util import FixedOffsetTimezone from babel._compat import StringIO, BytesIO - class ReadPoTestCase(unittest.TestCase): def test_preserve_locale(self): @@ -429,6 +429,70 @@ def test_missing_plural_in_the_middle(self): self.assertEqual("", message.string[1]) self.assertEqual("Vohs [text]", message.string[2]) + def test_abort_invalid_po_file(self): + invalid_po = ''' + msgctxt "" + "{\"checksum\": 2148532640, \"cxt\": \"collector_thankyou\", \"id\": " + "270005359}" + msgid "" + "Thank you very much for your time.\n" + "If you have any questions regarding this survey, please contact Fulano " + "at nadie@blah.com" + msgstr "Merci de prendre le temps de remplir le sondage. + Pour toute question, veuillez communiquer avec Fulano à nadie@blah.com + " + ''' + invalid_po_2 = ''' + msgctxt "" + "{\"checksum\": 2148532640, \"cxt\": \"collector_thankyou\", \"id\": " + "270005359}" + msgid "" + "Thank you very much for your time.\n" + "If you have any questions regarding this survey, please contact Fulano " + "at fulano@blah.com." + msgstr "Merci de prendre le temps de remplir le sondage. + Pour toute question, veuillez communiquer avec Fulano a fulano@blah.com + " + ''' + # Catalog not created, throws Unicode Error + buf = StringIO(invalid_po) + output = None + + # This should only be thrown under py27 + if sys.version_info.major == 2: + with self.assertRaises(UnicodeEncodeError): + output = pofile.read_po(buf, locale='fr', abort_invalid=False) + assert not output + else: + output = pofile.read_po(buf, locale='fr', abort_invalid=False) + assert isinstance(output, Catalog) + + # Catalog not created, throws PoFileError + buf = StringIO(invalid_po_2) + output = None + with self.assertRaises(pofile.PoFileError) as e: + output = pofile.read_po(buf, locale='fr', abort_invalid=True) + assert not output + + # Catalog is created with warning, no abort + buf = StringIO(invalid_po_2) + output = pofile.read_po(buf, locale='fr', abort_invalid=False) + assert isinstance(output, Catalog) + + # Catalog not created, aborted with PoFileError + buf = StringIO(invalid_po_2) + output = None + with self.assertRaises(pofile.PoFileError) as e: + output = pofile.read_po(buf, locale='fr', abort_invalid=True) + assert not output + + def test_invalid_pofile_with_abort_flag(self): + parser = pofile.PoFileParser(None, abort_invalid=True) + lineno = 10 + line = 'Algo esta mal' + msg = 'invalid file' + with self.assertRaises(pofile.PoFileError) as e: + parser._invalid_pofile(line, lineno, msg) class WritePoTestCase(unittest.TestCase): From 7dca6fad8e46cd6c491d1ff1f84f3c297f3f5ddd Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 21 Nov 2016 16:38:14 +0200 Subject: [PATCH 018/139] numbers: allow number patterns with no dynamic parts whatsoever --- babel/numbers.py | 2 +- tests/test_numbers.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/babel/numbers.py b/babel/numbers.py index 73ff816c6..cbd147475 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -611,7 +611,7 @@ def parse_decimal(string, locale=LC_NUMERIC): NUMBER_TOKEN = r'[0-9@#.,E+]' PREFIX_PATTERN = r"(?P(?:'[^']*'|%s)*)" % PREFIX_END -NUMBER_PATTERN = r"(?P%s+)" % NUMBER_TOKEN +NUMBER_PATTERN = r"(?P%s*)" % NUMBER_TOKEN SUFFIX_PATTERN = r"(?P.*)" number_re = re.compile(r"%s%s%s" % (PREFIX_PATTERN, NUMBER_PATTERN, diff --git a/tests/test_numbers.py b/tests/test_numbers.py index 2795b0f47..d9ca991ec 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -589,3 +589,8 @@ def test_numberpattern_repr(): format = u'¤#,##0.00;(¤#,##0.00)' np = numbers.parse_pattern(format) assert repr(format) in repr(np) + + +def test_parse_static_pattern(): + assert numbers.parse_pattern('Kun') # in the So locale in CLDR 30 + # TODO: static patterns might not be correctly `apply()`ed at present \ No newline at end of file From 63741104da581fbed53ad854d00835cf5f87a8d6 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 21 Nov 2016 16:38:54 +0200 Subject: [PATCH 019/139] cldr: use CLDR 30.0.2 (and fix test changes) --- babel/units.py | 2 +- scripts/download_import_cldr.py | 6 +++--- scripts/import_cldr.py | 12 ++++++++---- tests/test_date_intervals.py | 2 +- tests/test_lists.py | 2 +- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/babel/units.py b/babel/units.py index 1ea5b17cc..4260b3d56 100644 --- a/babel/units.py +++ b/babel/units.py @@ -201,7 +201,7 @@ def format_compound_unit( '150 kilometer per timme' >>> format_compound_unit(150, "kilowatt", denominator_unit="year", locale="fi") - '150 kilowattia vuodessa' + '150 kilowattia / vuosi' >>> format_compound_unit(32.5, "ton", 15, denominator_unit="hour", locale="en") '32.5 tons per 15 hours' diff --git a/scripts/download_import_cldr.py b/scripts/download_import_cldr.py index 4cc6a4b9b..9914a113e 100755 --- a/scripts/download_import_cldr.py +++ b/scripts/download_import_cldr.py @@ -13,9 +13,9 @@ from urllib import urlretrieve -URL = 'http://unicode.org/Public/cldr/29/core.zip' -FILENAME = 'core-29.zip' -FILESUM = '44d117e6e591a8f9655602ff0abdee105df3cabe' +URL = 'http://unicode.org/Public/cldr/30.0.2/core.zip' +FILENAME = 'core-30.0.2.zip' +FILESUM = '7d21d5f34a2b94f78f737d9279bcaae85ccc5332' BLKSIZE = 131072 diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py index 7b9e7734b..4f49491ca 100755 --- a/scripts/import_cldr.py +++ b/scripts/import_cldr.py @@ -708,14 +708,18 @@ def parse_number_symbols(data, tree): def parse_decimal_formats(data, tree): decimal_formats = data.setdefault('decimal_formats', {}) for elem in tree.findall('.//decimalFormats/decimalFormatLength'): - type = elem.attrib.get('type') - if _should_skip_elem(elem, type, decimal_formats): + length_type = elem.attrib.get('type') + if _should_skip_elem(elem, length_type, decimal_formats): continue if elem.findall('./alias'): # TODO map the alias to its target continue - pattern = text_type(elem.findtext('./decimalFormat/pattern')) - decimal_formats[type] = numbers.parse_pattern(pattern) + for pattern_el in elem.findall('./decimalFormat/pattern'): + pattern_type = pattern_el.attrib.get('type') + pattern = numbers.parse_pattern(text_type(pattern_el.text)) + if not pattern_type: + # Regular decimal format. + decimal_formats[length_type] = pattern def parse_scientific_formats(data, tree): diff --git a/tests/test_date_intervals.py b/tests/test_date_intervals.py index e5a797a94..2e5f0d6e2 100644 --- a/tests/test_date_intervals.py +++ b/tests/test_date_intervals.py @@ -13,7 +13,7 @@ def test_format_interval_same_instant_1(): - assert dates.format_interval(TEST_DT, TEST_DT, "yMMMd", fuzzy=False, locale="fi") == "8. tammikuuta 2016" + assert dates.format_interval(TEST_DT, TEST_DT, "yMMMd", fuzzy=False, locale="fi") == "8. tammik. 2016" def test_format_interval_same_instant_2(): diff --git a/tests/test_lists.py b/tests/test_lists.py index f5021ea50..bd297ec38 100644 --- a/tests/test_lists.py +++ b/tests/test_lists.py @@ -9,6 +9,6 @@ def test_format_list(): (['string1', 'string2'], 'en', u'string1 and string2'), (['string1', 'string2', 'string3'], 'en', u'string1, string2, and string3'), (['string1', 'string2', 'string3'], 'zh', u'string1、string2和string3'), - (['string1', 'string2', 'string3', 'string4'], 'ne', u'string1 र string2, string3 र string4'), + (['string1', 'string2', 'string3', 'string4'], 'ne', u'string1,string2, string3 र string4'), ]: assert lists.format_list(list, locale=locale) == expected From 77849ec967f9ff6f8470753abc2976c94b22aa2e Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 21 Nov 2016 16:39:53 +0200 Subject: [PATCH 020/139] cldr: Parse compact decimal formats that appear in CLDR 30 --- scripts/import_cldr.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py index 4f49491ca..94fe85ea2 100755 --- a/scripts/import_cldr.py +++ b/scripts/import_cldr.py @@ -717,7 +717,19 @@ def parse_decimal_formats(data, tree): for pattern_el in elem.findall('./decimalFormat/pattern'): pattern_type = pattern_el.attrib.get('type') pattern = numbers.parse_pattern(text_type(pattern_el.text)) - if not pattern_type: + if pattern_type: + # This is a compact decimal format, see: + # http://www.unicode.org/reports/tr35/tr35-45/tr35-numbers.html#Compact_Number_Formats + + # These are mapped into a `compact_decimal_formats` dictionary + # with the format {length: {count: {multiplier: pattern}}}. + + # TODO: Add support for formatting them. + compact_decimal_formats = data.setdefault('compact_decimal_formats', {}) + length_map = compact_decimal_formats.setdefault(length_type, {}) + length_count_map = length_map.setdefault(pattern_el.attrib['count'], {}) + length_count_map[pattern_type] = pattern + else: # Regular decimal format. decimal_formats[length_type] = pattern From a43d46486746b270f44e5412fc227d5f803bdf9e Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 18 Aug 2017 13:14:46 +0300 Subject: [PATCH 021/139] cldr: use CLDR 31.0.1 (and fix test changes); see below! CLDR 31.0 separated the GMT and UTC time zones, so formatting of that time zone has changed. Most notably, "UTC" replaces "+0000" when formatting the timezone. This could break code that expects +0000. A subset (the parts supported by Babel) of the migration guide from http://cldr.unicode.org/index/downloads/cldr-31: * The locales in the language-territory population tables have been changed to be the canonical format, dropping the script where it is the default. So "ku_Latn" changes to "ku". * Plural rules: The Portuguese plural rules have changed so that all (and only) integers and decimal fractions < 2 are singular. * Timezones: The GMT timezone has been split from the UTC timezone. * Timezones: New timezone bcp47 codes have been added. * Languages "hr" and "sr" are no longer a short distance apart, for political reasons. * The primary names for CZ changed from "Czech Republic" to "Czechia", with the longer name now the alternate. --- babel/core.py | 2 +- scripts/download_import_cldr.py | 8 ++++---- tests/test_core.py | 3 ++- tests/test_dates.py | 14 +++++++------- tests/test_languages.py | 7 ++++--- tests/test_numbers.py | 4 ++-- 6 files changed, 20 insertions(+), 18 deletions(-) diff --git a/babel/core.py b/babel/core.py index c8b180214..5a9091fae 100644 --- a/babel/core.py +++ b/babel/core.py @@ -39,7 +39,7 @@ def get_global(key): information independent of individual locales. >>> get_global('zone_aliases')['UTC'] - u'Etc/GMT' + u'Etc/UTC' >>> get_global('zone_territories')['Europe/Berlin'] u'DE' diff --git a/scripts/download_import_cldr.py b/scripts/download_import_cldr.py index 9914a113e..61b277326 100755 --- a/scripts/download_import_cldr.py +++ b/scripts/download_import_cldr.py @@ -13,9 +13,9 @@ from urllib import urlretrieve -URL = 'http://unicode.org/Public/cldr/30.0.2/core.zip' -FILENAME = 'core-30.0.2.zip' -FILESUM = '7d21d5f34a2b94f78f737d9279bcaae85ccc5332' +URL = 'http://www.unicode.org/Public/cldr/31.0.1/core.zip' +FILENAME = 'core-31.0.1.zip' +FILESUM = '01ade6c2d1f358e63c2ab6e2861d4caa7114ff45' BLKSIZE = 131072 @@ -82,7 +82,7 @@ def main(): os.remove(zip_path) urlretrieve(URL, zip_path, reporthook) changed = True - print + print() common_path = os.path.join(cldr_path, 'common') if changed or not os.path.isdir(common_path): diff --git a/tests/test_core.py b/tests/test_core.py index e3d8faffa..4f985ca74 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -58,7 +58,8 @@ def test_ignore_invalid_locales_in_lc_ctype(os_environ): def test_get_global(): - assert core.get_global('zone_aliases')['UTC'] == 'Etc/GMT' + assert core.get_global('zone_aliases')['GMT'] == 'Etc/GMT' + assert core.get_global('zone_aliases')['UTC'] == 'Etc/UTC' assert core.get_global('zone_territories')['Europe/Berlin'] == 'DE' diff --git a/tests/test_dates.py b/tests/test_dates.py index f74cd396d..9a01d985e 100644 --- a/tests/test_dates.py +++ b/tests/test_dates.py @@ -292,7 +292,7 @@ def test_with_float(self): d = datetime(2012, 4, 1, 15, 30, 29, tzinfo=timezone('UTC')) epoch = float(calendar.timegm(d.timetuple())) formatted_string = dates.format_datetime(epoch, format='long', locale='en_US') - self.assertEqual(u'April 1, 2012 at 3:30:29 PM +0000', formatted_string) + self.assertEqual(u'April 1, 2012 at 3:30:29 PM UTC', formatted_string) def test_timezone_formats(self): dt = datetime(2016, 1, 13, 7, 8, 35) @@ -358,9 +358,9 @@ def test_timezone_formats(self): formatted_string = dates.format_datetime(dt, 'OOOO', locale='en') self.assertEqual(u'GMT+00:00', formatted_string) formatted_string = dates.format_datetime(dt, 'VV', locale='en') - self.assertEqual(u'Etc/GMT', formatted_string) + self.assertEqual(u'Etc/UTC', formatted_string) formatted_string = dates.format_datetime(dt, 'VVV', locale='en') - self.assertEqual(u'GMT', formatted_string) + self.assertEqual(u'UTC', formatted_string) formatted_string = dates.format_datetime(dt, 'X', locale='en') self.assertEqual(u'Z', formatted_string) formatted_string = dates.format_datetime(dt, 'XX', locale='en') @@ -430,7 +430,7 @@ def test_with_float(self): d = datetime(2012, 4, 1, 15, 30, 29, tzinfo=timezone('UTC')) epoch = float(calendar.timegm(d.timetuple())) formatted_time = dates.format_time(epoch, format='long', locale='en_US') - self.assertEqual(u'3:30:29 PM +0000', formatted_time) + self.assertEqual(u'3:30:29 PM UTC', formatted_time) def test_with_date_fields_in_pattern(self): self.assertRaises(AttributeError, dates.format_time, date(2007, 4, 1), @@ -511,7 +511,7 @@ def test_can_format_time_with_non_pytz_timezone(self): utc = self._utc() t = datetime(2007, 4, 1, 15, 30, tzinfo=utc) formatted_time = dates.format_time(t, 'long', tzinfo=utc, locale='en') - self.assertEqual('3:30:00 PM +0000', formatted_time) + self.assertEqual('3:30:00 PM UTC', formatted_time) def test_get_period_names(): @@ -633,8 +633,8 @@ def test_get_timezone_name(): assert (dates.get_timezone_name('Europe/Berlin', locale='en_US') == "Central European Time") - assert (dates.get_timezone_name(1400000000, locale='en_US', width='short') == "Unknown Region (GMT) Time") - assert (dates.get_timezone_name(time(16, 20), locale='en_US', width='short') == "+0000") + assert (dates.get_timezone_name(1400000000, locale='en_US', width='short') == "Unknown Region (UTC) Time") + assert (dates.get_timezone_name(time(16, 20), locale='en_US', width='short') == "UTC") def test_format_date(): diff --git a/tests/test_languages.py b/tests/test_languages.py index 23af26d53..32f0d67d5 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -11,6 +11,7 @@ def test_official_languages(): def test_get_language_info(): - assert set(get_territory_language_info("HU").keys()) == {"hu", "en", "de", - "ro", "hr", "sk", - "sl"} + assert ( + set(get_territory_language_info("HU")) == + {"hu", "fr", "en", "de", "ro", "hr", "sk", "sl"} + ) diff --git a/tests/test_numbers.py b/tests/test_numbers.py index d9ca991ec..83990058c 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -180,7 +180,7 @@ def test_list_currencies(): assert list_currencies(locale='pa_Arab') == {'PKR', 'INR', 'EUR'} assert list_currencies(locale='kok') == set([]) - assert len(list_currencies()) == 296 + assert len(list_currencies()) == 297 def test_validate_currency(): @@ -593,4 +593,4 @@ def test_numberpattern_repr(): def test_parse_static_pattern(): assert numbers.parse_pattern('Kun') # in the So locale in CLDR 30 - # TODO: static patterns might not be correctly `apply()`ed at present \ No newline at end of file + # TODO: static patterns might not be correctly `apply()`ed at present From 7bfa3a51a20ddb9dd78e9587095a7986bfc716fb Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 19 Jan 2018 13:31:17 +0200 Subject: [PATCH 022/139] Update py.test * Add py.test's .cache to ignore * setup.cfg: Use [tool:pytest] instead of deprecated [pytest] --- .ci/appveyor.yml | 2 +- .gitignore | 3 ++- .travis.yml | 2 +- setup.cfg | 2 +- tox.ini | 3 ++- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.ci/appveyor.yml b/.ci/appveyor.yml index 2dfbdd884..816b938f6 100644 --- a/.ci/appveyor.yml +++ b/.ci/appveyor.yml @@ -27,7 +27,7 @@ install: - "python --version" - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" # Build data files - - "pip install --upgrade pytest==2.8.5 pytest-cov==2.2.0 codecov freezegun==0.3.9" + - "pip install --upgrade pytest==3.3.2 pytest-cov==2.5.1 codecov freezegun==0.3.9" - "pip install --editable ." - "python setup.py import_cldr" diff --git a/.gitignore b/.gitignore index d8f8bc164..e15875ba0 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ build dist .DS_Store .tox +.cache test-env **/__pycache__ babel/global.dat @@ -18,4 +19,4 @@ babel/global.dat.json tests/messages/data/project/i18n/long_messages.pot tests/messages/data/project/i18n/temp* tests/messages/data/project/i18n/en_US -/venv* +/venv* \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index b819e3e5e..63dcd3ec0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,7 +33,7 @@ matrix: install: - bash .ci/deps.${TRAVIS_OS_NAME}.sh - pip install --upgrade pip - - pip install --upgrade pytest==2.8.5 pytest-cov==2.2.0 $CDECIMAL freezegun==0.3.9 + - pip install --upgrade $CDECIMAL pytest==3.3.2 pytest-cov==2.5.1 freezegun==0.3.9 - pip install --editable . script: diff --git a/setup.cfg b/setup.cfg index d5f180a24..4e877a36f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ [aliases] release = sdist bdist_wheel -[pytest] +[tool:pytest] norecursedirs = venv* .* _* scripts {args} doctest_optionflags = ELLIPSIS NORMALIZE_WHITESPACE ALLOW_UNICODE IGNORE_EXCEPTION_DETAIL diff --git a/tox.ini b/tox.ini index 33d1ab890..925ea7fe0 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,8 @@ envlist = py27, pypy, py34, py35, py36, pypy3, py27-cdecimal [testenv] deps = - pytest + pytest==3.3.2 + pytest-cov==2.5.1 cdecimal: m3-cdecimal freezegun whitelist_externals = make From ff7e3d2e74ff2c271294ae850f5fa139ce981488 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 19 Jan 2018 13:27:44 +0200 Subject: [PATCH 023/139] CLDR: Use CLDR 32.0.1 --- babel/dates.py | 2 +- scripts/download_import_cldr.py | 6 +++--- tests/test_numbers.py | 3 +-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/babel/dates.py b/babel/dates.py index 1cc796222..a30d6d8e2 100644 --- a/babel/dates.py +++ b/babel/dates.py @@ -982,7 +982,7 @@ def format_interval(start, end, skeleton=None, tzinfo=None, fuzzy=True, locale=L u'15.\u201317.1.2016' >>> format_interval(time(12, 12), time(16, 16), "Hm", locale="en_GB") - '12:12 \u2013 16:16' + '12:12\u201316:16' >>> format_interval(time(5, 12), time(16, 16), "hm", locale="en_US") '5:12 AM \u2013 4:16 PM' diff --git a/scripts/download_import_cldr.py b/scripts/download_import_cldr.py index 61b277326..1d3c17bf6 100755 --- a/scripts/download_import_cldr.py +++ b/scripts/download_import_cldr.py @@ -13,9 +13,9 @@ from urllib import urlretrieve -URL = 'http://www.unicode.org/Public/cldr/31.0.1/core.zip' -FILENAME = 'core-31.0.1.zip' -FILESUM = '01ade6c2d1f358e63c2ab6e2861d4caa7114ff45' +URL = 'http://unicode.org/Public/cldr/32.0.1/core.zip' +FILENAME = 'core-32.0.1.zip' +FILESUM = '571e33f482543f02ebd06c9b43d127a87c637c8c' BLKSIZE = 131072 diff --git a/tests/test_numbers.py b/tests/test_numbers.py index 83990058c..3ad9c663f 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -178,9 +178,8 @@ def test_list_currencies(): assert excinfo.value.args[0] == "expected only letters, got 'yo!'" assert list_currencies(locale='pa_Arab') == {'PKR', 'INR', 'EUR'} - assert list_currencies(locale='kok') == set([]) - assert len(list_currencies()) == 297 + assert len(list_currencies()) == 299 def test_validate_currency(): From bd2ac889827f55fc6d48b3e63fa38e3c090f8892 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 19 Jan 2018 13:39:07 +0200 Subject: [PATCH 024/139] Lists: add support for various list styles other than the default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This was inspired by the CLDR 32 release notes: > New “disjunctive” list style (eg “a, b, or c”) --- babel/core.py | 6 ++--- babel/lists.py | 55 ++++++++++++++++++++++++++++++++++++------ scripts/import_cldr.py | 10 +++++--- tests/test_lists.py | 7 ++++++ 4 files changed, 63 insertions(+), 15 deletions(-) diff --git a/babel/core.py b/babel/core.py index 5a9091fae..5a1a9125d 100644 --- a/babel/core.py +++ b/babel/core.py @@ -857,11 +857,11 @@ def list_patterns(self): .. note:: The format of the value returned may change between Babel versions. - >>> Locale('en').list_patterns['start'] + >>> Locale('en').list_patterns['standard']['start'] u'{0}, {1}' - >>> Locale('en').list_patterns['end'] + >>> Locale('en').list_patterns['standard']['end'] u'{0}, and {1}' - >>> Locale('en_GB').list_patterns['end'] + >>> Locale('en_GB').list_patterns['standard']['end'] u'{0} and {1}' """ return self._data['list_patterns'] diff --git a/babel/lists.py b/babel/lists.py index 82e5590c1..329437975 100644 --- a/babel/lists.py +++ b/babel/lists.py @@ -11,7 +11,7 @@ * ``LC_ALL``, and * ``LANG`` - :copyright: (c) 2015 by the Babel Team. + :copyright: (c) 2015, 2018 by the Babel Team. :license: BSD, see LICENSE for more details. """ @@ -20,16 +20,46 @@ DEFAULT_LOCALE = default_locale() -def format_list(lst, locale=DEFAULT_LOCALE): +def format_list(lst, style='standard', locale=DEFAULT_LOCALE): """ Format the items in `lst` as a list. - >>> format_list(['apples', 'oranges', 'pears'], 'en') + >>> format_list(['apples', 'oranges', 'pears'], locale='en') u'apples, oranges, and pears' - >>> format_list(['apples', 'oranges', 'pears'], 'zh') + >>> format_list(['apples', 'oranges', 'pears'], locale='zh') u'apples\u3001oranges\u548cpears' + >>> format_list(['omena', 'peruna', 'aplari'], style='or', locale='fi') + u'omena, peruna tai aplari' + + These styles are defined, but not all are necessarily available in all locales. + The following text is verbatim from the Unicode TR35-49 spec [1]. + + * standard: + A typical 'and' list for arbitrary placeholders. + eg. "January, February, and March" + * standard-short: + A short version of a 'and' list, suitable for use with short or abbreviated placeholder values. + eg. "Jan., Feb., and Mar." + * or: + A typical 'or' list for arbitrary placeholders. + eg. "January, February, or March" + * or-short: + A short version of an 'or' list. + eg. "Jan., Feb., or Mar." + * unit: + A list suitable for wide units. + eg. "3 feet, 7 inches" + * unit-short: + A list suitable for short units + eg. "3 ft, 7 in" + * unit-narrow: + A list suitable for narrow units, where space on the screen is very limited. + eg. "3′ 7″" + + [1]: https://www.unicode.org/reports/tr35/tr35-49/tr35-general.html#ListPatterns :param lst: a sequence of items to format in to a list + :param style: the style to format the list with. See above for description. :param locale: the locale """ locale = Locale.parse(locale) @@ -37,12 +67,21 @@ def format_list(lst, locale=DEFAULT_LOCALE): return '' if len(lst) == 1: return lst[0] + + if style not in locale.list_patterns: + raise ValueError('Locale %s does not support list formatting style %r (supported are %s)' % ( + locale, + style, + list(sorted(locale.list_patterns)), + )) + patterns = locale.list_patterns[style] + if len(lst) == 2: - return locale.list_patterns['2'].format(*lst) + return patterns['2'].format(*lst) - result = locale.list_patterns['start'].format(lst[0], lst[1]) + result = patterns['start'].format(lst[0], lst[1]) for elem in lst[2:-1]: - result = locale.list_patterns['middle'].format(result, elem) - result = locale.list_patterns['end'].format(result, lst[-1]) + result = patterns['middle'].format(result, elem) + result = patterns['end'].format(result, lst[-1]) return result diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py index 94fe85ea2..d50e14667 100755 --- a/scripts/import_cldr.py +++ b/scripts/import_cldr.py @@ -397,7 +397,7 @@ def _process_local_datas(sup, srcdir, destdir, force=False, dump_json=False): data["day_period_rules"] = day_period_rules[locale_id] parse_locale_display_names(data, tree) - + parse_list_patterns(data, tree) parse_dates(data, tree, sup, regions, territory) for calendar in tree.findall('.//calendars/calendar'): @@ -478,12 +478,14 @@ def parse_locale_display_names(data, tree): scripts = data.setdefault('scripts', {}) for elem in tree.findall('.//scripts/script'): _import_type_text(scripts, elem) + + +def parse_list_patterns(data, tree): list_patterns = data.setdefault('list_patterns', {}) for listType in tree.findall('.//listPatterns/listPattern'): - if 'type' in listType.attrib: - continue + by_type = list_patterns.setdefault(listType.attrib.get('type', 'standard'), {}) for listPattern in listType.findall('listPatternPart'): - list_patterns[listPattern.attrib['type']] = _text(listPattern) + by_type[listPattern.attrib['type']] = _text(listPattern) def parse_dates(data, tree, sup, regions, territory): diff --git a/tests/test_lists.py b/tests/test_lists.py index bd297ec38..e843a6358 100644 --- a/tests/test_lists.py +++ b/tests/test_lists.py @@ -1,4 +1,6 @@ # coding=utf-8 +import pytest + from babel import lists @@ -12,3 +14,8 @@ def test_format_list(): (['string1', 'string2', 'string3', 'string4'], 'ne', u'string1,string2, string3 र string4'), ]: assert lists.format_list(list, locale=locale) == expected + + +def test_format_list_error(): + with pytest.raises(ValueError): + lists.format_list(['a', 'b', 'c'], style='orange', locale='en') From b7e1895c2c3fd90d21c658506c3f009e510989eb Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 25 Jan 2018 18:17:06 +0200 Subject: [PATCH 025/139] Test that CLDR 32 properly formats Russian start-of-year dates. Fixes #485 --- tests/test_dates.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_dates.py b/tests/test_dates.py index 9a01d985e..6df4631f5 100644 --- a/tests/test_dates.py +++ b/tests/test_dates.py @@ -783,3 +783,10 @@ def test_no_inherit_metazone_formatting(): assert dates.format_time(t, format='long', locale='en_GB') == "07:00:00 Pacific Standard Time" assert dates.get_timezone_name(t, width='short', locale='en_US') == "PST" assert dates.get_timezone_name(t, width='short', locale='en_GB') == "Pacific Standard Time" + + +def test_russian_week_numbering(): + # See https://github.com/python-babel/babel/issues/485 + v = date(2017, 1, 1) + assert dates.format_date(v, format='YYYY-ww',locale='ru_RU') == '2016-52' # This would have returned 2017-01 prior to CLDR 32 + assert dates.format_date(v, format='YYYY-ww',locale='de_DE') == '2016-52' \ No newline at end of file From 57832c6e3d3b6513d66a3825de9bf537bb07be17 Mon Sep 17 00:00:00 2001 From: xmo-odoo Date: Fri, 26 Jan 2018 11:14:57 +0100 Subject: [PATCH 026/139] Add explicit signatures to some date autofunctions Because default_locale is eager, it generates misleading documentation when built (including on the site) as the doc will show the LC_TIME for the machine which built the doc. An explicit autofunction signature fixes that and provides a more correct view of the signature. An alternative fix would be for the LC_TIME value in the module to be a custom object with a repr of LC_TIME or something along those lines. Fixes #542 --- docs/api/dates.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/api/dates.rst b/docs/api/dates.rst index 0fa9f1f32..f3e59b63b 100644 --- a/docs/api/dates.rst +++ b/docs/api/dates.rst @@ -9,17 +9,17 @@ Python `datetime`, `date` and `time` objects and work with timezones. Date and Time Formatting ------------------------ -.. autofunction:: format_datetime +.. autofunction:: format_datetime(datetime=None, format='medium', tzinfo=None, locale=default_locale('LC_TIME')) -.. autofunction:: format_date +.. autofunction:: format_date(date=None, format='medium', locale=default_locale('LC_TIME')) -.. autofunction:: format_time +.. autofunction:: format_time(time=None, format='medium', tzinfo=None, locale=default_locale('LC_TIME')) -.. autofunction:: format_timedelta +.. autofunction:: format_timedelta(delta, granularity='second', threshold=.85, add_direction=False, format='long', locale=default_locale('LC_TIME')) -.. autofunction:: format_skeleton +.. autofunction:: format_skeleton(skeleton, datetime=None, tzinfo=None, fuzzy=True, locale=default_locale('LC_TIME')) -.. autofunction:: format_interval +.. autofunction:: format_interval(start, end, skeleton=None, tzinfo=None, fuzzy=True, locale=default_locale('LC_TIME')) Timezone Functionality ---------------------- From c60e26ab85561bc90709008c10606f5e0b2ac1a1 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 2 Feb 2018 17:29:52 +0200 Subject: [PATCH 027/139] messages: allow processing files that are in locales unknown to Babel Fixes #553, #555 --- babel/messages/catalog.py | 54 +++++++++++++++++++++++++++-------- tests/messages/test_pofile.py | 23 +++++++++++++++ 2 files changed, 65 insertions(+), 12 deletions(-) diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py index b293e3c73..cd01ec4e4 100644 --- a/babel/messages/catalog.py +++ b/babel/messages/catalog.py @@ -19,11 +19,11 @@ from copy import copy from babel import __version__ as VERSION -from babel.core import Locale +from babel.core import Locale, UnknownLocaleError from babel.dates import format_datetime from babel.messages.plurals import get_plural from babel.util import odict, distinct, LOCALTZ, FixedOffsetTimezone -from babel._compat import string_types, number_types, PY2, cmp +from babel._compat import string_types, number_types, PY2, cmp, text_type __all__ = ['Message', 'Catalog', 'TranslationError'] @@ -267,8 +267,6 @@ def __init__(self, locale=None, domain=None, header_comment=DEFAULT_HEADER, :param fuzzy: the fuzzy bit on the catalog header """ self.domain = domain - if locale: - locale = Locale.parse(locale) self.locale = locale self._header_comment = header_comment self._messages = odict() @@ -301,6 +299,36 @@ def __init__(self, locale=None, domain=None, header_comment=DEFAULT_HEADER, self._num_plurals = None self._plural_expr = None + def _set_locale(self, locale): + if locale is None: + self._locale_identifier = None + self._locale = None + return + + if isinstance(locale, Locale): + self._locale_identifier = text_type(locale) + self._locale = locale + return + + if isinstance(locale, string_types): + self._locale_identifier = text_type(locale) + try: + self._locale = Locale.parse(locale) + except UnknownLocaleError: + self._locale = None + return + + raise TypeError('`locale` must be a Locale, a locale identifier string, or None; got %r' % locale) + + def _get_locale(self): + return self._locale + + def _get_locale_identifier(self): + return self._locale_identifier + + locale = property(_get_locale, _set_locale) + locale_identifier = property(_get_locale_identifier) + def _get_header_comment(self): comment = self._header_comment year = datetime.now(LOCALTZ).strftime('%Y') @@ -310,9 +338,9 @@ def _get_header_comment(self): .replace('VERSION', self.version) \ .replace('YEAR', year) \ .replace('ORGANIZATION', self.copyright_holder) - if self.locale: - comment = comment.replace('Translations template', '%s translations' - % self.locale.english_name) + locale_name = (self.locale.english_name if self.locale else self.locale_identifier) + if locale_name: + comment = comment.replace('Translations template', '%s translations' % locale_name) return comment def _set_header_comment(self, string): @@ -366,12 +394,12 @@ def _get_mime_headers(self): else: headers.append(('PO-Revision-Date', self.revision_date)) headers.append(('Last-Translator', self.last_translator)) - if self.locale is not None: - headers.append(('Language', str(self.locale))) - if (self.locale is not None) and ('LANGUAGE' in self.language_team): + if self.locale_identifier: + headers.append(('Language', str(self.locale_identifier))) + if self.locale_identifier and ('LANGUAGE' in self.language_team): headers.append(('Language-Team', self.language_team.replace('LANGUAGE', - str(self.locale)))) + str(self.locale_identifier)))) else: headers.append(('Language-Team', self.language_team)) if self.locale is not None: @@ -396,7 +424,7 @@ def _set_mime_headers(self, headers): self.last_translator = value elif name == 'language': value = value.replace('-', '_') - self.locale = Locale.parse(value) + self._set_locale(value) elif name == 'language-team': self.language_team = value elif name == 'content-type': @@ -490,6 +518,8 @@ def plural_expr(self): '(n != 1)' >>> Catalog(locale='ga').plural_expr '(n==1 ? 0 : n==2 ? 1 : n>=3 && n<=6 ? 2 : n>=7 && n<=10 ? 3 : 4)' + >>> Catalog(locale='ding').plural_expr # unknown locale + '(n != 1)' :type: `string_types`""" if self._plural_expr is None: diff --git a/tests/messages/test_pofile.py b/tests/messages/test_pofile.py index 002954f8a..b2c6f6702 100644 --- a/tests/messages/test_pofile.py +++ b/tests/messages/test_pofile.py @@ -494,6 +494,7 @@ def test_invalid_pofile_with_abort_flag(self): with self.assertRaises(pofile.PoFileError) as e: parser._invalid_pofile(line, lineno, msg) + class WritePoTestCase(unittest.TestCase): def test_join_locations(self): @@ -864,3 +865,25 @@ def test_denormalize_on_msgstr_without_empty_first_line(self): self.assertEqual(expected_denormalized, pofile.denormalize(msgstr)) self.assertEqual(expected_denormalized, pofile.denormalize('""\n' + msgstr)) + + +def test_unknown_language_roundtrip(): + buf = StringIO(r''' +msgid "" +msgstr "" +"Language: sr_SP\n"''') + catalog = pofile.read_po(buf) + assert catalog.locale_identifier == 'sr_SP' + assert not catalog.locale + buf = BytesIO() + pofile.write_po(buf, catalog) + assert 'sr_SP' in buf.getvalue().decode() + + +def test_unknown_language_write(): + catalog = Catalog(locale='sr_SP') + assert catalog.locale_identifier == 'sr_SP' + assert not catalog.locale + buf = BytesIO() + pofile.write_po(buf, catalog) + assert 'sr_SP' in buf.getvalue().decode() From 6170eff2b566ccf4a17139cf6195df4d1d02629c Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 2 Feb 2018 17:50:04 +0200 Subject: [PATCH 028/139] import_cldr: choice patterns in currencies are deprecated, so remove todo --- scripts/import_cldr.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py index d50e14667..ab676a901 100755 --- a/scripts/import_cldr.py +++ b/scripts/import_cldr.py @@ -770,7 +770,6 @@ def parse_currency_names(data, tree): name.attrib['count']] = text_type(name.text) else: currency_names[code] = text_type(name.text) - # TODO: support choice patterns for currency symbol selection symbol = elem.find('symbol') if symbol is not None and 'draft' not in symbol.attrib and 'choice' not in symbol.attrib: currency_symbols[code] = text_type(symbol.text) From b611f0c6175e85e9caea6dab8add35d1a2968dec Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 2 Feb 2018 17:52:47 +0200 Subject: [PATCH 029/139] import_cldr: avoid importing alt=narrow currency symbols Fixes #397 Closes #483, #556 --- scripts/import_cldr.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py index ab676a901..b81fa6091 100755 --- a/scripts/import_cldr.py +++ b/scripts/import_cldr.py @@ -770,8 +770,11 @@ def parse_currency_names(data, tree): name.attrib['count']] = text_type(name.text) else: currency_names[code] = text_type(name.text) - symbol = elem.find('symbol') - if symbol is not None and 'draft' not in symbol.attrib and 'choice' not in symbol.attrib: + for symbol in elem.findall('symbol'): + if 'draft' in symbol.attrib or 'choice' in symbol.attrib: # Skip drafts and choice-patterns + continue + if symbol.attrib.get('alt'): # Skip alternate forms + continue currency_symbols[code] = text_type(symbol.text) From 47fa155ace10d32fd69867043486e87f50ad97eb Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 2 Feb 2018 18:45:19 +0200 Subject: [PATCH 030/139] Add script to generate the AUTHORS file and regenerate it --- AUTHORS | 114 +++++++++++++++++++++++++++++------- scripts/generate_authors.py | 40 +++++++++++++ 2 files changed, 134 insertions(+), 20 deletions(-) create mode 100644 scripts/generate_authors.py diff --git a/AUTHORS b/AUTHORS index b9208fe59..58a9bee28 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,25 +1,99 @@ -Babel is written and maintained by the Babel team and various contributors: - -Maintainer and Current Project Lead: -- Armin Ronacher - -Contributors: +Babel is written and maintained by the Babel team and various contributors: -- Christopher Lenz -- Alex Morega -- Felix Schwarz -- Pedro Algarvio -- Jeroen Ruigrok van der Werven -- Philip Jenvey -- Tobias Bieniek -- Jonas Borgström -- Daniel Neuhäuser -- Nick Retallack -- Thomas Waldmann -- Lennart Regebro -- Isaac Jurado -- Craig Loftus +- Christopher Lenz +- Armin Ronacher +- Aarni Koskela +- Alex Morega +- Lasse Schuirmann +- Felix Schwarz +- Pedro Algarvio +- Jeroen Ruigrok van der Werven +- Philip Jenvey +- benselme +- Isaac Jurado +- Tobias Bieniek +- Erick Wilder +- Michael Birtwell +- Jonas Borgström +- Kevin Deldycke +- Ville Skyttä +- Hugo +- Heungsub Lee +- Jakob Schnitzer +- Sachin Paliwal +- Alex Willmer +- Daniel Neuhäuser +- Jennifer Wang +- Lukas Balaga +- sudheesh001 +- Jon Dufresne +- Xavier Fernandez +- KO. Mattsson +- Sébastien Diemer +- alexbodn@gmail.com +- saurabhiiit +- srisankethu +- Erik Romijn +- Lukas B +- Ryan J Ollos +- Arturas Moskvinas +- Leonardo Pistone +- Jun Omae +- Hyunjun Kim +- xmo-odoo +- StevenJ +- Jungmo Ku +- Simeon Visser +- Narendra Vardi +- Stefane Fermigier +- Narayan Acharya +- François Magimel +- Wolfgang Doll +- Roy Williams +- Marc-André Dufresne +- Abhishek Tiwari +- David Baumgold +- Alex Kuzmenko +- Georg Schölly +- ldwoolley +- Rodrigo Ramírez Norambuena +- Jakub Wilk +- Roman Rader +- Max Shenfield +- Nicolas Grilly +- Kenny Root +- Adam Chainz +- Sébastien Fievet +- Anthony Sottile +- Yuriy Shatrov +- iamshubh22 +- Sven Anderson +- Eoin Nugent +- Roman Imankulov +- David Stanek +- Roy Wellington Ⅳ +- Florian Schulze +- Todd M. Guerra +- Joseph Breihan +- Craig Loftus +- The Gitter Badger +- Régis Behmo +- Julen Ruiz Aizpuru +- astaric +- Felix Yan +- Philip_Tzou +- Jesús Espino +- Jeremy Weinstein +- James Page +- masklinn +- Sjoerd Langkemper +- Matt Iversen +- Alexander A. Dyshev +- Dirkjan Ochtman +- Nick Retallack +- Thomas Waldmann +- xen Babel was previously developed under the Copyright of Edgewall Software. The following copyright notice holds true for releases before 2013: "Copyright (c) diff --git a/scripts/generate_authors.py b/scripts/generate_authors.py new file mode 100644 index 000000000..409f24e36 --- /dev/null +++ b/scripts/generate_authors.py @@ -0,0 +1,40 @@ +from collections import Counter +from subprocess import check_output + +import os + +root_path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..')) + + +def get_sorted_authors_list(): + authors = check_output(['git', 'log', '--format=%aN'], cwd=root_path).decode('UTF-8') + counts = Counter(authors.splitlines()) + return [author for (author, count) in counts.most_common()] + + +def get_authors_file_content(): + author_list = '\n'.join('- %s' % a for a in get_sorted_authors_list()) + + return ''' +Babel is written and maintained by the Babel team and various contributors: + +{author_list} + +Babel was previously developed under the Copyright of Edgewall Software. The +following copyright notice holds true for releases before 2013: "Copyright (c) +2007 - 2011 by Edgewall Software" + +In addition to the regular contributions Babel includes a fork of Lennart +Regebro's tzlocal that originally was licensed under the CC0 license. The +original copyright of that project is "Copyright 2013 by Lennart Regebro". +'''.format(author_list=author_list) + + +def write_authors_file(): + content = get_authors_file_content() + with open(os.path.join(root_path, 'AUTHORS'), 'w', encoding='UTF-8') as fp: + fp.write(content) + + +if __name__ == '__main__': + write_authors_file() From 6af596705230ae92f9d007002b0fcc9b24db2944 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 2 Feb 2018 18:51:30 +0200 Subject: [PATCH 031/139] Update copyright header years, and include the Babel team --- LICENSE | 2 +- babel/__init__.py | 2 +- babel/core.py | 2 +- babel/dates.py | 2 +- babel/localedata.py | 2 +- babel/localtime/__init__.py | 2 +- babel/localtime/_unix.py | 1 + babel/messages/__init__.py | 2 +- babel/messages/catalog.py | 2 +- babel/messages/checkers.py | 2 +- babel/messages/extract.py | 2 +- babel/messages/frontend.py | 2 +- babel/messages/jslexer.py | 2 +- babel/messages/mofile.py | 2 +- babel/messages/plurals.py | 2 +- babel/messages/pofile.py | 2 +- babel/numbers.py | 2 +- babel/plural.py | 2 +- babel/support.py | 2 +- babel/util.py | 2 +- docs/conf.py | 2 +- scripts/dump_data.py | 2 +- scripts/dump_global.py | 2 +- scripts/import_cldr.py | 2 +- tests/messages/test_catalog.py | 2 +- tests/messages/test_checkers.py | 2 +- tests/messages/test_extract.py | 2 +- tests/messages/test_frontend.py | 2 +- tests/messages/test_mofile.py | 2 +- tests/messages/test_plurals.py | 2 +- tests/messages/test_pofile.py | 2 +- tests/test_core.py | 2 +- tests/test_dates.py | 4 ++-- tests/test_localedata.py | 2 +- tests/test_numbers.py | 2 +- tests/test_plural.py | 2 +- tests/test_support.py | 2 +- tests/test_util.py | 2 +- 38 files changed, 39 insertions(+), 38 deletions(-) diff --git a/LICENSE b/LICENSE index 1f1f55b60..b517a5294 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (C) 2013 by the Babel Team, see AUTHORS for more information. +Copyright (c) 2013-2018 by the Babel Team, see AUTHORS for more information. All rights reserved. diff --git a/babel/__init__.py b/babel/__init__.py index 5a388226b..c4e4d20ac 100644 --- a/babel/__init__.py +++ b/babel/__init__.py @@ -13,7 +13,7 @@ access to various locale display names, localized number and date formatting, etc. - :copyright: (c) 2013 by the Babel Team. + :copyright: (c) 2013-2018 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/core.py b/babel/core.py index 5a1a9125d..d028c07da 100644 --- a/babel/core.py +++ b/babel/core.py @@ -5,7 +5,7 @@ Core locale representation and locale data access. - :copyright: (c) 2013 by the Babel Team. + :copyright: (c) 2013-2018 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/dates.py b/babel/dates.py index a30d6d8e2..cf58a485d 100644 --- a/babel/dates.py +++ b/babel/dates.py @@ -12,7 +12,7 @@ * ``LC_ALL``, and * ``LANG`` - :copyright: (c) 2013 by the Babel Team. + :copyright: (c) 2013-2018 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/localedata.py b/babel/localedata.py index 0c94e49ea..a638e58af 100644 --- a/babel/localedata.py +++ b/babel/localedata.py @@ -8,7 +8,7 @@ :note: The `Locale` class, which uses this module under the hood, provides a more convenient interface for accessing the locale data. - :copyright: (c) 2013 by the Babel Team. + :copyright: (c) 2013-2018 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/localtime/__init__.py b/babel/localtime/__init__.py index 883ff1661..bb4341d60 100644 --- a/babel/localtime/__init__.py +++ b/babel/localtime/__init__.py @@ -6,7 +6,7 @@ Babel specific fork of tzlocal to determine the local timezone of the system. - :copyright: (c) 2013 by the Babel Team. + :copyright: (c) 2013-2018 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/localtime/_unix.py b/babel/localtime/_unix.py index 8f25fe7b8..c2194694c 100644 --- a/babel/localtime/_unix.py +++ b/babel/localtime/_unix.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from __future__ import with_statement import os import re diff --git a/babel/messages/__init__.py b/babel/messages/__init__.py index 1b63bae2e..7dd6f8b6d 100644 --- a/babel/messages/__init__.py +++ b/babel/messages/__init__.py @@ -5,7 +5,7 @@ Support for ``gettext`` message catalogs. - :copyright: (c) 2013 by the Babel Team. + :copyright: (c) 2013-2018 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py index b293e3c73..5f05427bb 100644 --- a/babel/messages/catalog.py +++ b/babel/messages/catalog.py @@ -5,7 +5,7 @@ Data structures for message catalogs. - :copyright: (c) 2013 by the Babel Team. + :copyright: (c) 2013-2018 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/messages/checkers.py b/babel/messages/checkers.py index a1e6e8d60..d04ad70ec 100644 --- a/babel/messages/checkers.py +++ b/babel/messages/checkers.py @@ -7,7 +7,7 @@ :since: version 0.9 - :copyright: (c) 2013 by the Babel Team. + :copyright: (c) 2013-2018 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/messages/extract.py b/babel/messages/extract.py index 351a29070..009bea2b4 100644 --- a/babel/messages/extract.py +++ b/babel/messages/extract.py @@ -13,7 +13,7 @@ The main entry points into the extraction functionality are the functions `extract_from_dir` and `extract_from_file`. - :copyright: (c) 2013 by the Babel Team. + :copyright: (c) 2013-2018 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/messages/frontend.py b/babel/messages/frontend.py index b573a97ca..d848f2dfc 100644 --- a/babel/messages/frontend.py +++ b/babel/messages/frontend.py @@ -5,7 +5,7 @@ Frontends for the message extraction functionality. - :copyright: (c) 2013 by the Babel Team. + :copyright: (c) 2013-2018 by the Babel Team. :license: BSD, see LICENSE for more details. """ from __future__ import print_function diff --git a/babel/messages/jslexer.py b/babel/messages/jslexer.py index 30d6e5405..04d02761d 100644 --- a/babel/messages/jslexer.py +++ b/babel/messages/jslexer.py @@ -6,7 +6,7 @@ A simple JavaScript 1.5 lexer which is used for the JavaScript extractor. - :copyright: (c) 2013 by the Babel Team. + :copyright: (c) 2013-2018 by the Babel Team. :license: BSD, see LICENSE for more details. """ from collections import namedtuple diff --git a/babel/messages/mofile.py b/babel/messages/mofile.py index 79042e003..7bddd1845 100644 --- a/babel/messages/mofile.py +++ b/babel/messages/mofile.py @@ -5,7 +5,7 @@ Writing of files in the ``gettext`` MO (machine object) format. - :copyright: (c) 2013 by the Babel Team. + :copyright: (c) 2013-2018 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/messages/plurals.py b/babel/messages/plurals.py index 92cefa79a..067ac9826 100644 --- a/babel/messages/plurals.py +++ b/babel/messages/plurals.py @@ -5,7 +5,7 @@ Plural form definitions. - :copyright: (c) 2013 by the Babel Team. + :copyright: (c) 2013-2018 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/messages/pofile.py b/babel/messages/pofile.py index beb6f3570..ea8d7d7e2 100644 --- a/babel/messages/pofile.py +++ b/babel/messages/pofile.py @@ -6,7 +6,7 @@ Reading and writing of files in the ``gettext`` PO (portable object) format. - :copyright: (c) 2013 by the Babel Team. + :copyright: (c) 2013-2018 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/numbers.py b/babel/numbers.py index cbd147475..cb851e4ca 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -12,7 +12,7 @@ * ``LC_ALL``, and * ``LANG`` - :copyright: (c) 2013 by the Babel Team. + :copyright: (c) 2013-2018 by the Babel Team. :license: BSD, see LICENSE for more details. """ # TODO: diff --git a/babel/plural.py b/babel/plural.py index 06f349ea9..38093fe6e 100644 --- a/babel/plural.py +++ b/babel/plural.py @@ -5,7 +5,7 @@ CLDR Plural support. See UTS #35. - :copyright: (c) 2013 by the Babel Team. + :copyright: (c) 2013-2018 by the Babel Team. :license: BSD, see LICENSE for more details. """ import re diff --git a/babel/support.py b/babel/support.py index 24bc9aaa1..932fcf2bb 100644 --- a/babel/support.py +++ b/babel/support.py @@ -8,7 +8,7 @@ .. note: the code in this module is not used by Babel itself - :copyright: (c) 2013 by the Babel Team. + :copyright: (c) 2013-2018 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/util.py b/babel/util.py index af8c762ec..0150827de 100644 --- a/babel/util.py +++ b/babel/util.py @@ -5,7 +5,7 @@ Various utility classes and functions. - :copyright: (c) 2013 by the Babel Team. + :copyright: (c) 2013-2018 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/docs/conf.py b/docs/conf.py index 432387e7c..5aebe94d9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -44,7 +44,7 @@ # General information about the project. project = u'Babel' -copyright = u'2017, The Babel Team' +copyright = u'2018, The Babel Team' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/scripts/dump_data.py b/scripts/dump_data.py index 0bb3e9117..5e1286053 100755 --- a/scripts/dump_data.py +++ b/scripts/dump_data.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software +# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # # This software is licensed as described in the file COPYING, which diff --git a/scripts/dump_global.py b/scripts/dump_global.py index 2970bc2ba..ad3d35da9 100755 --- a/scripts/dump_global.py +++ b/scripts/dump_global.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software +# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # # This software is licensed as described in the file COPYING, which diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py index b81fa6091..89fc1a7f6 100755 --- a/scripts/import_cldr.py +++ b/scripts/import_cldr.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software +# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # # This software is licensed as described in the file COPYING, which diff --git a/tests/messages/test_catalog.py b/tests/messages/test_catalog.py index 07ed724b9..f9fa72443 100644 --- a/tests/messages/test_catalog.py +++ b/tests/messages/test_catalog.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software +# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # # This software is licensed as described in the file COPYING, which diff --git a/tests/messages/test_checkers.py b/tests/messages/test_checkers.py index e9c34bcb7..6af81c64f 100644 --- a/tests/messages/test_checkers.py +++ b/tests/messages/test_checkers.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2008-2011 Edgewall Software +# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # # This software is licensed as described in the file COPYING, which diff --git a/tests/messages/test_extract.py b/tests/messages/test_extract.py index 17b090fc1..13ce8735a 100644 --- a/tests/messages/test_extract.py +++ b/tests/messages/test_extract.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software +# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # # This software is licensed as described in the file COPYING, which diff --git a/tests/messages/test_frontend.py b/tests/messages/test_frontend.py index 10325fe61..0f12f1e77 100644 --- a/tests/messages/test_frontend.py +++ b/tests/messages/test_frontend.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software +# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # # This software is licensed as described in the file COPYING, which diff --git a/tests/messages/test_mofile.py b/tests/messages/test_mofile.py index 5fedc600a..c590facc8 100644 --- a/tests/messages/test_mofile.py +++ b/tests/messages/test_mofile.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software +# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # # This software is licensed as described in the file COPYING, which diff --git a/tests/messages/test_plurals.py b/tests/messages/test_plurals.py index 2e7553c16..3792a1b28 100644 --- a/tests/messages/test_plurals.py +++ b/tests/messages/test_plurals.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2008-2011 Edgewall Software +# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # # This software is licensed as described in the file COPYING, which diff --git a/tests/messages/test_pofile.py b/tests/messages/test_pofile.py index 002954f8a..1a04b4e6f 100644 --- a/tests/messages/test_pofile.py +++ b/tests/messages/test_pofile.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software +# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # # This software is licensed as described in the file COPYING, which diff --git a/tests/test_core.py b/tests/test_core.py index 4f985ca74..7fa9e0036 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software +# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # # This software is licensed as described in the file COPYING, which diff --git a/tests/test_dates.py b/tests/test_dates.py index 6df4631f5..5a46c1b8a 100644 --- a/tests/test_dates.py +++ b/tests/test_dates.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software +# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # # This software is licensed as described in the file COPYING, which @@ -789,4 +789,4 @@ def test_russian_week_numbering(): # See https://github.com/python-babel/babel/issues/485 v = date(2017, 1, 1) assert dates.format_date(v, format='YYYY-ww',locale='ru_RU') == '2016-52' # This would have returned 2017-01 prior to CLDR 32 - assert dates.format_date(v, format='YYYY-ww',locale='de_DE') == '2016-52' \ No newline at end of file + assert dates.format_date(v, format='YYYY-ww',locale='de_DE') == '2016-52' diff --git a/tests/test_localedata.py b/tests/test_localedata.py index f678d5ba7..be5694a7d 100644 --- a/tests/test_localedata.py +++ b/tests/test_localedata.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software +# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # # This software is licensed as described in the file COPYING, which diff --git a/tests/test_numbers.py b/tests/test_numbers.py index 3ad9c663f..1f4a0fe00 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software +# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # # This software is licensed as described in the file COPYING, which diff --git a/tests/test_plural.py b/tests/test_plural.py index be7414994..852a7c755 100644 --- a/tests/test_plural.py +++ b/tests/test_plural.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2008-2011 Edgewall Software +# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # # This software is licensed as described in the file COPYING, which diff --git a/tests/test_support.py b/tests/test_support.py index 6f9fb3246..7e63ba2e9 100644 --- a/tests/test_support.py +++ b/tests/test_support.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software +# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # # This software is licensed as described in the file COPYING, which diff --git a/tests/test_util.py b/tests/test_util.py index 4c769bea3..61d4f4fa7 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software +# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # # This software is licensed as described in the file COPYING, which From 5e50652b2c73eac42f9e6c99b7337d82b8b581cb Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 2 Feb 2018 18:52:10 +0200 Subject: [PATCH 032/139] Rephrase copyright headers to link to the LICENSE file instead of COPYING Fixes #398 --- scripts/dump_data.py | 2 +- scripts/dump_global.py | 2 +- scripts/import_cldr.py | 2 +- tests/messages/test_catalog.py | 2 +- tests/messages/test_checkers.py | 2 +- tests/messages/test_extract.py | 2 +- tests/messages/test_frontend.py | 2 +- tests/messages/test_mofile.py | 2 +- tests/messages/test_plurals.py | 2 +- tests/messages/test_pofile.py | 2 +- tests/test_core.py | 2 +- tests/test_dates.py | 2 +- tests/test_localedata.py | 2 +- tests/test_numbers.py | 2 +- tests/test_plural.py | 2 +- tests/test_support.py | 2 +- tests/test_util.py | 2 +- 17 files changed, 17 insertions(+), 17 deletions(-) diff --git a/scripts/dump_data.py b/scripts/dump_data.py index 5e1286053..3bf35f2be 100755 --- a/scripts/dump_data.py +++ b/scripts/dump_data.py @@ -4,7 +4,7 @@ # Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # -# This software is licensed as described in the file COPYING, which +# This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # diff --git a/scripts/dump_global.py b/scripts/dump_global.py index ad3d35da9..641f3c018 100755 --- a/scripts/dump_global.py +++ b/scripts/dump_global.py @@ -4,7 +4,7 @@ # Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # -# This software is licensed as described in the file COPYING, which +# This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py index 89fc1a7f6..cd0ec37c1 100755 --- a/scripts/import_cldr.py +++ b/scripts/import_cldr.py @@ -4,7 +4,7 @@ # Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # -# This software is licensed as described in the file COPYING, which +# This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # diff --git a/tests/messages/test_catalog.py b/tests/messages/test_catalog.py index f9fa72443..1b23832a6 100644 --- a/tests/messages/test_catalog.py +++ b/tests/messages/test_catalog.py @@ -3,7 +3,7 @@ # Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # -# This software is licensed as described in the file COPYING, which +# This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # diff --git a/tests/messages/test_checkers.py b/tests/messages/test_checkers.py index 6af81c64f..be4afb341 100644 --- a/tests/messages/test_checkers.py +++ b/tests/messages/test_checkers.py @@ -3,7 +3,7 @@ # Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # -# This software is licensed as described in the file COPYING, which +# This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # diff --git a/tests/messages/test_extract.py b/tests/messages/test_extract.py index 13ce8735a..fa50a4c9e 100644 --- a/tests/messages/test_extract.py +++ b/tests/messages/test_extract.py @@ -3,7 +3,7 @@ # Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # -# This software is licensed as described in the file COPYING, which +# This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # diff --git a/tests/messages/test_frontend.py b/tests/messages/test_frontend.py index 0f12f1e77..d2e9e35cf 100644 --- a/tests/messages/test_frontend.py +++ b/tests/messages/test_frontend.py @@ -3,7 +3,7 @@ # Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # -# This software is licensed as described in the file COPYING, which +# This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # diff --git a/tests/messages/test_mofile.py b/tests/messages/test_mofile.py index c590facc8..7038b1d83 100644 --- a/tests/messages/test_mofile.py +++ b/tests/messages/test_mofile.py @@ -3,7 +3,7 @@ # Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # -# This software is licensed as described in the file COPYING, which +# This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # diff --git a/tests/messages/test_plurals.py b/tests/messages/test_plurals.py index 3792a1b28..2ba15e911 100644 --- a/tests/messages/test_plurals.py +++ b/tests/messages/test_plurals.py @@ -3,7 +3,7 @@ # Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # -# This software is licensed as described in the file COPYING, which +# This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # diff --git a/tests/messages/test_pofile.py b/tests/messages/test_pofile.py index 1a04b4e6f..091e0a6a6 100644 --- a/tests/messages/test_pofile.py +++ b/tests/messages/test_pofile.py @@ -3,7 +3,7 @@ # Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # -# This software is licensed as described in the file COPYING, which +# This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # diff --git a/tests/test_core.py b/tests/test_core.py index 7fa9e0036..f22ab309b 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -3,7 +3,7 @@ # Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # -# This software is licensed as described in the file COPYING, which +# This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # diff --git a/tests/test_dates.py b/tests/test_dates.py index 5a46c1b8a..1cb60f169 100644 --- a/tests/test_dates.py +++ b/tests/test_dates.py @@ -3,7 +3,7 @@ # Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # -# This software is licensed as described in the file COPYING, which +# This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # diff --git a/tests/test_localedata.py b/tests/test_localedata.py index be5694a7d..37c1304e9 100644 --- a/tests/test_localedata.py +++ b/tests/test_localedata.py @@ -3,7 +3,7 @@ # Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # -# This software is licensed as described in the file COPYING, which +# This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # diff --git a/tests/test_numbers.py b/tests/test_numbers.py index 1f4a0fe00..cdbe0663c 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -3,7 +3,7 @@ # Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # -# This software is licensed as described in the file COPYING, which +# This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # diff --git a/tests/test_plural.py b/tests/test_plural.py index 852a7c755..546e12a14 100644 --- a/tests/test_plural.py +++ b/tests/test_plural.py @@ -3,7 +3,7 @@ # Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # -# This software is licensed as described in the file COPYING, which +# This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # diff --git a/tests/test_support.py b/tests/test_support.py index 7e63ba2e9..64b60d85f 100644 --- a/tests/test_support.py +++ b/tests/test_support.py @@ -3,7 +3,7 @@ # Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # -# This software is licensed as described in the file COPYING, which +# This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # diff --git a/tests/test_util.py b/tests/test_util.py index 61d4f4fa7..a3607d5fc 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -3,7 +3,7 @@ # Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team # All rights reserved. # -# This software is licensed as described in the file COPYING, which +# This software is licensed as described in the file LICENSE, which # you should have received as part of this distribution. The terms # are also available at http://babel.edgewall.org/wiki/License. # From 41e8169c63ac6b6ec673c74a9e1f7ebaa8c6be87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Thu, 8 Mar 2018 00:16:47 +0100 Subject: [PATCH 033/139] Skip empty message when writing mo file Instead of using the msgid, this allows to have gettext fallback to retrieve the translation. Otherwise gettext will anyway return msgid. --- babel/messages/mofile.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/babel/messages/mofile.py b/babel/messages/mofile.py index 79042e003..8aa704f96 100644 --- a/babel/messages/mofile.py +++ b/babel/messages/mofile.py @@ -153,8 +153,8 @@ def write_mo(fileobj, catalog, use_fuzzy=False): in the output """ messages = list(catalog) - if not use_fuzzy: - messages[1:] = [m for m in messages[1:] if not m.fuzzy] + messages[1:] = [m for m in messages[1:] + if m.string and (use_fuzzy or not m.fuzzy)] messages.sort() ids = strs = b'' @@ -178,10 +178,7 @@ def write_mo(fileobj, catalog, use_fuzzy=False): ]) else: msgid = message.id.encode(catalog.charset) - if not message.string: - msgstr = message.id.encode(catalog.charset) - else: - msgstr = message.string.encode(catalog.charset) + msgstr = message.string.encode(catalog.charset) if message.context: msgid = b'\x04'.join([message.context.encode(catalog.charset), msgid]) From cbc8cf8308d0958ddbde7a01c430c58a40c00308 Mon Sep 17 00:00:00 2001 From: PTrottier <31802216+PTrottier@users.noreply.github.com> Date: Fri, 27 Apr 2018 10:42:56 -0400 Subject: [PATCH 034/139] Month and Day must be valid tokens ````>>> dt = datetime(2007, 04, 01, 15, 30, tzinfo=UTC) File "", line 1 dt = datetime(2007, 04, 01, 15, 30, tzinfo=UTC) ^ SyntaxError: invalid token```` See: https://docs.python.org/3.5/library/datetime.html#datetime.datetime.month --- docs/dates.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dates.rst b/docs/dates.rst index bb3398abb..0ddd7bc95 100644 --- a/docs/dates.rst +++ b/docs/dates.rst @@ -286,7 +286,7 @@ directly interface with it from within Babel: >>> from datetime import time >>> from babel.dates import get_timezone, UTC - >>> dt = datetime(2007, 04, 01, 15, 30, tzinfo=UTC) + >>> dt = datetime(2007, 4, 1, 15, 30, tzinfo=UTC) >>> eastern = get_timezone('US/Eastern') >>> format_datetime(dt, 'H:mm Z', tzinfo=eastern, locale='en_US') u'11:30 -0400' From 1377559c830d6c3166f1e1bdced9953e0f4045fa Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 7 May 2018 13:01:25 +0300 Subject: [PATCH 035/139] Restore force_frac to NumberPattern.apply(), as deprecated This retains backwards compatibility (at least as far as the function's signature is concerned) with users that are using NumberPattern.apply() directly. See: https://github.com/python-babel/babel/issues/550 See: https://github.com/python-babel/babel/issues/569 --- babel/numbers.py | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/babel/numbers.py b/babel/numbers.py index cbd147475..a3a93a66f 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -767,12 +767,38 @@ def scientific_notation_elements(self, value, locale): return value, exp, exp_sign def apply( - self, value, locale, currency=None, currency_digits=True, - decimal_quantization=True): + self, + value, + locale, + currency=None, + currency_digits=True, + decimal_quantization=True, + force_frac=None, + ): """Renders into a string a number following the defined pattern. Forced decimal quantization is active by default so we'll produce a number string that is strictly following CLDR pattern definitions. + + :param value: The value to format. If this is not a Decimal object, + it will be cast to one. + :type value: decimal.Decimal|float|int + :param locale: The locale to use for formatting. + :type locale: str|babel.core.Locale + :param currency: Which currency, if any, to format as. + :type currency: str|None + :param currency_digits: Whether or not to use the currency's precision. + If false, the pattern's precision is used. + :type currency_digits: bool + :param decimal_quantization: Whether decimal numbers should be forcibly + quantized to produce a formatted output + strictly matching the CLDR definition for + the locale. + :type decimal_quantization: bool + :param force_frac: DEPRECATED - a forced override for `self.frac_prec` + for a single formatting invocation. + :return: Formatted decimal string. + :rtype: str """ if not isinstance(value, decimal.Decimal): value = decimal.Decimal(str(value)) @@ -789,9 +815,14 @@ def apply( # Adjust the precision of the fractionnal part and force it to the # currency's if neccessary. - frac_prec = self.frac_prec - if currency and currency_digits: + if force_frac: + # TODO (3.x?): Remove this parameter + warnings.warn('The force_frac parameter to NumberPattern.apply() is deprecated.', DeprecationWarning) + frac_prec = force_frac + elif currency and currency_digits: frac_prec = (get_currency_precision(currency), ) * 2 + else: + frac_prec = self.frac_prec # Bump decimal precision to the natural precision of the number if it # exceeds the one we're about to use. This adaptative precision is only From 77dc9d4024b78c2339f7cf3bff1a2e8be8e2d0f7 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 28 May 2018 12:12:23 +0300 Subject: [PATCH 036/139] import_cldr: ignore formatting rules for non-Latin numbering systems Previously the script could have inadvertently merged formatting rules between numbering systems due to the XML selectors used. This makes sure only Latin rules are used for the time being. When support for other numbering systems is properly added (see #470), these checks can be changed. --- scripts/import_cldr.py | 157 ++++++++++++++++++++++++++--------------- 1 file changed, 101 insertions(+), 56 deletions(-) diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py index cd0ec37c1..60aa6c280 100755 --- a/scripts/import_cldr.py +++ b/scripts/import_cldr.py @@ -389,6 +389,8 @@ def _process_local_datas(sup, srcdir, destdir, force=False, dump_json=False): territory != '001' and territory or None ])) + data['locale_id'] = locale_id + if locale_id in plural_rules: data['plural_form'] = plural_rules[locale_id] if locale_id in ordinal_rules: @@ -430,6 +432,31 @@ def _process_local_datas(sup, srcdir, destdir, force=False, dump_json=False): write_datafile(data_filename, data, dump_json=dump_json) +def _should_skip_number_elem(data, elem): + """ + Figure out whether the numbering-containing element `elem` is in a currently + non-supported (i.e. currently non-Latin) numbering system. + + If it is, a warning is raised. + + :param data: The root data element, for formatting the warning. + :param elem: Element with `numberSystem` key + :return: Boolean + """ + number_system = elem.get('numberSystem', 'latn') + + if number_system != 'latn': + log('%s: Unsupported number system "%s" in <%s numberSystem="%s">' % ( + data['locale_id'], + number_system, + elem.tag, + number_system, + )) + return True + + return False + + def _should_skip_elem(elem, type=None, dest=None): """ Check whether the given element should be skipped. @@ -701,59 +728,73 @@ def parse_calendar_datetime_skeletons(data, calendar): def parse_number_symbols(data, tree): number_symbols = data.setdefault('number_symbols', {}) - for elem in tree.findall('.//numbers/symbols/*'): - if _should_skip_elem(elem): + for symbol_elem in tree.findall('.//numbers/symbols'): + if _should_skip_number_elem(data, symbol_elem): # TODO: Support other number systems continue - number_symbols[elem.tag] = text_type(elem.text) + + for elem in symbol_elem.findall('./*'): + if _should_skip_elem(elem): + continue + number_symbols[elem.tag] = text_type(elem.text) def parse_decimal_formats(data, tree): decimal_formats = data.setdefault('decimal_formats', {}) - for elem in tree.findall('.//decimalFormats/decimalFormatLength'): - length_type = elem.attrib.get('type') - if _should_skip_elem(elem, length_type, decimal_formats): + for df_elem in tree.findall('.//decimalFormats'): + if _should_skip_number_elem(data, df_elem): # TODO: Support other number systems continue - if elem.findall('./alias'): - # TODO map the alias to its target - continue - for pattern_el in elem.findall('./decimalFormat/pattern'): - pattern_type = pattern_el.attrib.get('type') - pattern = numbers.parse_pattern(text_type(pattern_el.text)) - if pattern_type: - # This is a compact decimal format, see: - # http://www.unicode.org/reports/tr35/tr35-45/tr35-numbers.html#Compact_Number_Formats - - # These are mapped into a `compact_decimal_formats` dictionary - # with the format {length: {count: {multiplier: pattern}}}. - - # TODO: Add support for formatting them. - compact_decimal_formats = data.setdefault('compact_decimal_formats', {}) - length_map = compact_decimal_formats.setdefault(length_type, {}) - length_count_map = length_map.setdefault(pattern_el.attrib['count'], {}) - length_count_map[pattern_type] = pattern - else: - # Regular decimal format. - decimal_formats[length_type] = pattern + for elem in df_elem.findall('./decimalFormatLength'): + length_type = elem.attrib.get('type') + if _should_skip_elem(elem, length_type, decimal_formats): + continue + if elem.findall('./alias'): + # TODO map the alias to its target + continue + for pattern_el in elem.findall('./decimalFormat/pattern'): + pattern_type = pattern_el.attrib.get('type') + pattern = numbers.parse_pattern(text_type(pattern_el.text)) + if pattern_type: + # This is a compact decimal format, see: + # http://www.unicode.org/reports/tr35/tr35-45/tr35-numbers.html#Compact_Number_Formats + + # These are mapped into a `compact_decimal_formats` dictionary + # with the format {length: {count: {multiplier: pattern}}}. + + # TODO: Add support for formatting them. + compact_decimal_formats = data.setdefault('compact_decimal_formats', {}) + length_map = compact_decimal_formats.setdefault(length_type, {}) + length_count_map = length_map.setdefault(pattern_el.attrib['count'], {}) + length_count_map[pattern_type] = pattern + else: + # Regular decimal format. + decimal_formats[length_type] = pattern def parse_scientific_formats(data, tree): scientific_formats = data.setdefault('scientific_formats', {}) - for elem in tree.findall('.//scientificFormats/scientificFormatLength'): - type = elem.attrib.get('type') - if _should_skip_elem(elem, type, scientific_formats): + for sf_elem in tree.findall('.//scientificFormats'): + if _should_skip_number_elem(data, sf_elem): # TODO: Support other number systems continue - pattern = text_type(elem.findtext('scientificFormat/pattern')) - scientific_formats[type] = numbers.parse_pattern(pattern) + for elem in sf_elem.findall('./scientificFormatLength'): + type = elem.attrib.get('type') + if _should_skip_elem(elem, type, scientific_formats): + continue + pattern = text_type(elem.findtext('scientificFormat/pattern')) + scientific_formats[type] = numbers.parse_pattern(pattern) def parse_percent_formats(data, tree): percent_formats = data.setdefault('percent_formats', {}) - for elem in tree.findall('.//percentFormats/percentFormatLength'): - type = elem.attrib.get('type') - if _should_skip_elem(elem, type, percent_formats): + + for pf_elem in tree.findall('.//percentFormats'): + if _should_skip_number_elem(data, pf_elem): # TODO: Support other number systems continue - pattern = text_type(elem.findtext('percentFormat/pattern')) - percent_formats[type] = numbers.parse_pattern(pattern) + for elem in pf_elem.findall('.//percentFormatLength'): + type = elem.attrib.get('type') + if _should_skip_elem(elem, type, percent_formats): + continue + pattern = text_type(elem.findtext('percentFormat/pattern')) + percent_formats[type] = numbers.parse_pattern(pattern) def parse_currency_names(data, tree): @@ -837,25 +878,29 @@ def parse_interval_formats(data, tree): def parse_currency_formats(data, tree): currency_formats = data.setdefault('currency_formats', {}) - for length_elem in tree.findall('.//currencyFormats/currencyFormatLength'): - curr_length_type = length_elem.attrib.get('type') - for elem in length_elem.findall('currencyFormat'): - type = elem.attrib.get('type') - if curr_length_type: - # Handle ``, etc. - # TODO(3.x): use nested dicts instead of colon-separated madness - type = '%s:%s' % (type, curr_length_type) - if _should_skip_elem(elem, type, currency_formats): - continue - for child in elem.getiterator(): - if child.tag == 'alias': - currency_formats[type] = Alias( - _translate_alias(['currency_formats', elem.attrib['type']], - child.attrib['path']) - ) - elif child.tag == 'pattern': - pattern = text_type(child.text) - currency_formats[type] = numbers.parse_pattern(pattern) + for currency_format in tree.findall('.//currencyFormats'): + if _should_skip_number_elem(data, currency_format): # TODO: Support other number systems + continue + + for length_elem in currency_format.findall('./currencyFormatLength'): + curr_length_type = length_elem.attrib.get('type') + for elem in length_elem.findall('currencyFormat'): + type = elem.attrib.get('type') + if curr_length_type: + # Handle ``, etc. + # TODO(3.x): use nested dicts instead of colon-separated madness + type = '%s:%s' % (type, curr_length_type) + if _should_skip_elem(elem, type, currency_formats): + continue + for child in elem.getiterator(): + if child.tag == 'alias': + currency_formats[type] = Alias( + _translate_alias(['currency_formats', elem.attrib['type']], + child.attrib['path']) + ) + elif child.tag == 'pattern': + pattern = text_type(child.text) + currency_formats[type] = numbers.parse_pattern(pattern) def parse_day_period_rules(tree): From 28797063ac12b6373afe3e9a797a9fd97f675e12 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 28 May 2018 12:33:44 +0300 Subject: [PATCH 037/139] test_smoke: use format_decimal instead of format_number --- tests/test_smoke.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_smoke.py b/tests/test_smoke.py index 3993f608c..23d82aa13 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -34,4 +34,4 @@ def test_smoke_numbers(locale): 10, # Plain old integer 0, # Zero ): - assert numbers.format_number(number, locale=locale) + assert numbers.format_decimal(number, locale=locale) From 3471306e9cd08214d12e6e4af81d8b9dbcd70874 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 28 May 2018 12:34:31 +0300 Subject: [PATCH 038/139] frontend, test_frontend: don't use deprecated U read mode on Py3 --- babel/messages/frontend.py | 7 +++++-- tests/messages/test_frontend.py | 36 ++++++++++++++++----------------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/babel/messages/frontend.py b/babel/messages/frontend.py index d848f2dfc..31fbaa46b 100644 --- a/babel/messages/frontend.py +++ b/babel/messages/frontend.py @@ -22,7 +22,7 @@ from babel import __version__ as VERSION from babel import Locale, localedata -from babel._compat import StringIO, string_types, text_type +from babel._compat import StringIO, string_types, text_type, PY2 from babel.core import UnknownLocaleError from babel.messages.catalog import Catalog from babel.messages.extract import DEFAULT_KEYWORDS, DEFAULT_MAPPING, check_and_call_extract_file, extract_from_dir @@ -39,6 +39,9 @@ from configparser import RawConfigParser +po_file_read_mode = ('rU' if PY2 else 'r') + + def listify_value(arg, split=None): """ Make a list out of an argument. @@ -485,7 +488,7 @@ def _get_mappings(self): mappings = [] if self.mapping_file: - with open(self.mapping_file, 'U') as fileobj: + with open(self.mapping_file, po_file_read_mode) as fileobj: method_map, options_map = parse_mapping(fileobj) for path in self.input_paths: mappings.append((path, method_map, options_map)) diff --git a/tests/messages/test_frontend.py b/tests/messages/test_frontend.py index d2e9e35cf..3e3cc8bd5 100644 --- a/tests/messages/test_frontend.py +++ b/tests/messages/test_frontend.py @@ -28,7 +28,7 @@ from babel import __version__ as VERSION from babel.dates import format_datetime from babel.messages import frontend, Catalog -from babel.messages.frontend import CommandLineInterface, extract_messages, update_catalog +from babel.messages.frontend import CommandLineInterface, extract_messages, update_catalog, po_file_read_mode from babel.util import LOCALTZ from babel.messages.pofile import read_po, write_po from babel._compat import StringIO @@ -124,7 +124,7 @@ def test_input_paths_is_treated_as_list(self): self.cmd.finalize_options() self.cmd.run() - with open(self._pot_file(), 'U') as f: + with open(self._pot_file(), po_file_read_mode) as f: catalog = read_po(f) msg = catalog.get('bar') self.assertEqual(1, len(msg.locations)) @@ -204,7 +204,7 @@ def test_extraction_with_default_mapping(self): 'year': time.strftime('%Y'), 'date': format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')} - with open(self._pot_file(), 'U') as f: + with open(self._pot_file(), po_file_read_mode) as f: actual_content = f.read() self.assertEqual(expected_content, actual_content) @@ -257,7 +257,7 @@ def test_extraction_with_mapping_file(self): 'year': time.strftime('%Y'), 'date': format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')} - with open(self._pot_file(), 'U') as f: + with open(self._pot_file(), po_file_read_mode) as f: actual_content = f.read() self.assertEqual(expected_content, actual_content) @@ -315,7 +315,7 @@ def test_extraction_with_mapping_dict(self): 'year': time.strftime('%Y'), 'date': format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')} - with open(self._pot_file(), 'U') as f: + with open(self._pot_file(), po_file_read_mode) as f: actual_content = f.read() self.assertEqual(expected_content, actual_content) @@ -346,7 +346,7 @@ def test_extraction_add_location_file(self): msgstr[1] "" """ - with open(self._pot_file(), 'U') as f: + with open(self._pot_file(), po_file_read_mode) as f: actual_content = f.read() self.assertEqual(expected_content, actual_content) @@ -440,7 +440,7 @@ def test_with_output_dir(self): """ % {'version': VERSION, 'date': format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')} - with open(po_file, 'U') as f: + with open(po_file, po_file_read_mode) as f: actual_content = f.read() self.assertEqual(expected_content, actual_content) @@ -492,7 +492,7 @@ def test_keeps_catalog_non_fuzzy(self): """ % {'version': VERSION, 'date': format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')} - with open(po_file, 'U') as f: + with open(po_file, po_file_read_mode) as f: actual_content = f.read() self.assertEqual(expected_content, actual_content) @@ -546,7 +546,7 @@ def test_correct_init_more_than_2_plurals(self): """ % {'version': VERSION, 'date': format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')} - with open(po_file, 'U') as f: + with open(po_file, po_file_read_mode) as f: actual_content = f.read() self.assertEqual(expected_content, actual_content) @@ -597,7 +597,7 @@ def test_correct_init_singular_plural_forms(self): """ % {'version': VERSION, 'date': format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='ja_JP')} - with open(po_file, 'U') as f: + with open(po_file, po_file_read_mode) as f: actual_content = f.read() self.assertEqual(expected_content, actual_content) @@ -658,7 +658,7 @@ def test_supports_no_wrap(self): 'date': format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en_US'), 'long_message': long_message} - with open(po_file, 'U') as f: + with open(po_file, po_file_read_mode) as f: actual_content = f.read() self.assertEqual(expected_content, actual_content) @@ -718,7 +718,7 @@ def test_supports_width(self): 'date': format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en_US'), 'long_message': long_message} - with open(po_file, 'U') as f: + with open(po_file, po_file_read_mode) as f: actual_content = f.read() self.assertEqual(expected_content, actual_content) @@ -879,7 +879,7 @@ def test_extract_with_default_mapping(self): 'year': time.strftime('%Y'), 'date': format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')} - with open(pot_file, 'U') as f: + with open(pot_file, po_file_read_mode) as f: actual_content = f.read() self.assertEqual(expected_content, actual_content) @@ -930,7 +930,7 @@ def test_extract_with_mapping_file(self): 'year': time.strftime('%Y'), 'date': format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')} - with open(pot_file, 'U') as f: + with open(pot_file, po_file_read_mode) as f: actual_content = f.read() self.assertEqual(expected_content, actual_content) @@ -979,7 +979,7 @@ def test_extract_with_exact_file(self): 'year': time.strftime('%Y'), 'date': format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')} - with open(pot_file, 'U') as f: + with open(pot_file, po_file_read_mode) as f: actual_content = f.read() self.assertEqual(expected_content, actual_content) @@ -1027,7 +1027,7 @@ def test_init_with_output_dir(self): """ % {'version': VERSION, 'date': format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')} - with open(po_file, 'U') as f: + with open(po_file, po_file_read_mode) as f: actual_content = f.read() self.assertEqual(expected_content, actual_content) @@ -1077,7 +1077,7 @@ def test_init_singular_plural_forms(self): """ % {'version': VERSION, 'date': format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')} - with open(po_file, 'U') as f: + with open(po_file, po_file_read_mode) as f: actual_content = f.read() self.assertEqual(expected_content, actual_content) @@ -1127,7 +1127,7 @@ def test_init_more_than_2_plural_forms(self): """ % {'version': VERSION, 'date': format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')} - with open(po_file, 'U') as f: + with open(po_file, po_file_read_mode) as f: actual_content = f.read() self.assertEqual(expected_content, actual_content) From 4a9c634c48e90c01653a96b290fd40eccae34eb7 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 28 May 2018 12:37:17 +0300 Subject: [PATCH 039/139] test_support: Don't use deprecated inspect methods --- tests/test_support.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/test_support.py b/tests/test_support.py index 64b60d85f..314c18825 100644 --- a/tests/test_support.py +++ b/tests/test_support.py @@ -24,6 +24,8 @@ from babel.messages.mofile import write_mo from babel._compat import BytesIO, PY2 +get_arg_spec = (inspect.getargspec if PY2 else inspect.getfullargspec) + @pytest.mark.usefixtures("os_environ") class TranslationsTestCase(unittest.TestCase): @@ -206,9 +208,10 @@ def test_method_signature_compatibility(self): for name in self.method_names(): translations_method = getattr(self.translations, name) null_method = getattr(self.null_translations, name) - signature = inspect.getargspec - self.assertEqual(signature(translations_method), - signature(null_method)) + self.assertEqual( + get_arg_spec(translations_method), + get_arg_spec(null_method), + ) def test_same_return_values(self): data = { @@ -219,8 +222,8 @@ def test_same_return_values(self): for name in self.method_names(): method = getattr(self.translations, name) null_method = getattr(self.null_translations, name) - signature = inspect.getargspec(method) - parameter_names = [name for name in signature[0] if name != 'self'] + signature = get_arg_spec(method) + parameter_names = [name for name in signature.args if name != 'self'] values = [data[name] for name in parameter_names] self.assertEqual(method(*values), null_method(*values)) From 0c6b3c87749dad880fbfdb0ddfbac4997ef04055 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 28 May 2018 12:39:11 +0300 Subject: [PATCH 040/139] frontend: use ConfigParser.read_file() on Python 3 --- babel/messages/frontend.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/babel/messages/frontend.py b/babel/messages/frontend.py index 31fbaa46b..16cb90054 100644 --- a/babel/messages/frontend.py +++ b/babel/messages/frontend.py @@ -966,7 +966,12 @@ def parse_mapping(fileobj, filename=None): parser = RawConfigParser() parser._sections = odict(parser._sections) # We need ordered sections - parser.readfp(fileobj, filename) + + if PY2: + parser.readfp(fileobj, filename) + else: + parser.read_file(fileobj, filename) + for section in parser.sections(): if section == 'extractors': extractors = dict(parser.items(section)) From 34a6ccce1d93843d53efb5985ff5bbb7ea063e31 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 28 May 2018 12:57:45 +0300 Subject: [PATCH 041/139] _compat: add force_text a la Django --- babel/_compat.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/babel/_compat.py b/babel/_compat.py index aea338938..1131f44af 100644 --- a/babel/_compat.py +++ b/babel/_compat.py @@ -8,11 +8,12 @@ if not PY2: text_type = str + binary_type = bytes string_types = (str,) integer_types = (int, ) - unichr = chr text_to_native = lambda s, enc: s + unichr = chr iterkeys = lambda d: iter(d.keys()) itervalues = lambda d: iter(d.values()) @@ -31,6 +32,7 @@ else: text_type = unicode + binary_type = str string_types = (str, unicode) integer_types = (int, long) @@ -57,6 +59,14 @@ number_types = integer_types + (float,) +def force_text(s, encoding='utf-8', errors='strict'): + if isinstance(s, text_type): + return s + if isinstance(s, binary_type): + return s.decode(encoding, errors) + return text_type(s) + + # # Since Python 3.3, a fast decimal implementation is already included in the # standard library. Otherwise use cdecimal when available From 9f2be4127a301c0e3145a730bbc4d08afe78614d Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 28 May 2018 12:58:09 +0300 Subject: [PATCH 042/139] catalog: fix bytes-to-strings comparisons when parsing headers on Py3 --- babel/messages/catalog.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py index f85bfb156..6d61a3826 100644 --- a/babel/messages/catalog.py +++ b/babel/messages/catalog.py @@ -23,7 +23,7 @@ from babel.dates import format_datetime from babel.messages.plurals import get_plural from babel.util import odict, distinct, LOCALTZ, FixedOffsetTimezone -from babel._compat import string_types, number_types, PY2, cmp, text_type +from babel._compat import string_types, number_types, PY2, cmp, text_type, force_text __all__ = ['Message', 'Catalog', 'TranslationError'] @@ -413,7 +413,8 @@ def _get_mime_headers(self): def _set_mime_headers(self, headers): for name, value in headers: - name = name.lower() + name = force_text(name.lower(), encoding=self.charset) + value = force_text(value, encoding=self.charset) if name == 'project-id-version': parts = value.split(' ') self.project = u' '.join(parts[:-1]) From f48219816f92d00d06f6a103cfbc84b7c284b867 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 28 May 2018 13:01:28 +0300 Subject: [PATCH 043/139] tox: passenv PYTHON_TEST_FLAGS so one can run `env PYTHON_TEST_FLAGS=-bb tox` --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 925ea7fe0..b46fed704 100644 --- a/tox.ini +++ b/tox.ini @@ -6,9 +6,10 @@ deps = pytest==3.3.2 pytest-cov==2.5.1 cdecimal: m3-cdecimal - freezegun + freezegun==0.3.9 whitelist_externals = make commands = make clean-cldr test +passenv = PYTHON_TEST_FLAGS [pep8] ignore = E501,E731,W503 From 08864f28460f16035b2ffdfcb9704af0c3faa164 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 28 May 2018 16:22:03 +0300 Subject: [PATCH 044/139] Download CLDR 33.0; fix test changes --- scripts/download_import_cldr.py | 10 +++++----- tests/test_dates.py | 16 ++++++++-------- tests/test_numbers.py | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/scripts/download_import_cldr.py b/scripts/download_import_cldr.py index 1d3c17bf6..b6e6f4301 100755 --- a/scripts/download_import_cldr.py +++ b/scripts/download_import_cldr.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import contextlib import os @@ -13,9 +13,9 @@ from urllib import urlretrieve -URL = 'http://unicode.org/Public/cldr/32.0.1/core.zip' -FILENAME = 'core-32.0.1.zip' -FILESUM = '571e33f482543f02ebd06c9b43d127a87c637c8c' +URL = 'https://unicode.org/Public/cldr/33/core.zip' +FILENAME = 'core-33.0.zip' +FILESUM = 'fa3490082c086d21257153609642f54fcf788fcfda4966fe67f3f6daca0d58b9' BLKSIZE = 131072 @@ -53,7 +53,7 @@ def is_good_file(filename): if not os.path.isfile(filename): log('Local copy \'%s\' not found', filename) return False - h = hashlib.sha1() + h = hashlib.sha256() with open(filename, 'rb') as f: while 1: blk = f.read(BLKSIZE) diff --git a/tests/test_dates.py b/tests/test_dates.py index 1cb60f169..b8c293ba6 100644 --- a/tests/test_dates.py +++ b/tests/test_dates.py @@ -134,16 +134,16 @@ def test_local_day_of_week(self): self.assertEqual('7', fmt['e']) # monday is first day of week fmt = dates.DateTimeFormat(d, locale='en_US') self.assertEqual('01', fmt['ee']) # sunday is first day of week - fmt = dates.DateTimeFormat(d, locale='bn_BD') - self.assertEqual('03', fmt['ee']) # friday is first day of week + fmt = dates.DateTimeFormat(d, locale='ar_BH') + self.assertEqual('02', fmt['ee']) # saturday is first day of week d = date(2007, 4, 2) # a monday fmt = dates.DateTimeFormat(d, locale='de_DE') self.assertEqual('1', fmt['e']) # monday is first day of week fmt = dates.DateTimeFormat(d, locale='en_US') self.assertEqual('02', fmt['ee']) # sunday is first day of week - fmt = dates.DateTimeFormat(d, locale='bn_BD') - self.assertEqual('04', fmt['ee']) # friday is first day of week + fmt = dates.DateTimeFormat(d, locale='ar_BH') + self.assertEqual('03', fmt['ee']) # saturday is first day of week def test_local_day_of_week_standalone(self): d = date(2007, 4, 1) # a sunday @@ -151,16 +151,16 @@ def test_local_day_of_week_standalone(self): self.assertEqual('7', fmt['c']) # monday is first day of week fmt = dates.DateTimeFormat(d, locale='en_US') self.assertEqual('1', fmt['c']) # sunday is first day of week - fmt = dates.DateTimeFormat(d, locale='bn_BD') - self.assertEqual('3', fmt['c']) # friday is first day of week + fmt = dates.DateTimeFormat(d, locale='ar_BH') + self.assertEqual('2', fmt['c']) # saturday is first day of week d = date(2007, 4, 2) # a monday fmt = dates.DateTimeFormat(d, locale='de_DE') self.assertEqual('1', fmt['c']) # monday is first day of week fmt = dates.DateTimeFormat(d, locale='en_US') self.assertEqual('2', fmt['c']) # sunday is first day of week - fmt = dates.DateTimeFormat(d, locale='bn_BD') - self.assertEqual('4', fmt['c']) # friday is first day of week + fmt = dates.DateTimeFormat(d, locale='ar_BH') + self.assertEqual('3', fmt['c']) # saturday is first day of week def test_pattern_day_of_week(self): dt = datetime(2016, 2, 6) diff --git a/tests/test_numbers.py b/tests/test_numbers.py index cdbe0663c..493c1a7fe 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -179,7 +179,7 @@ def test_list_currencies(): assert list_currencies(locale='pa_Arab') == {'PKR', 'INR', 'EUR'} - assert len(list_currencies()) == 299 + assert len(list_currencies()) == 300 def test_validate_currency(): From 5f7b868725fcea817967820d35a4e0ebb02b3756 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 28 May 2018 16:32:54 +0300 Subject: [PATCH 045/139] Ignore .*cache, to ignore .pytest_cache --- .gitignore | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index e15875ba0..50e5838f6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,22 +1,22 @@ -*~ -*.swp -.idea -*.so -docs/_build +**/__pycache__ +*.egg +*.egg-info *.pyc *.pyo -*.egg-info -*.egg -build -dist +*.so +*.swp +*~ +.*cache .DS_Store +.idea .tox -.cache -test-env -**/__pycache__ +/venv* babel/global.dat babel/global.dat.json -tests/messages/data/project/i18n/long_messages.pot -tests/messages/data/project/i18n/temp* +build +dist +docs/_build +test-env tests/messages/data/project/i18n/en_US -/venv* \ No newline at end of file +tests/messages/data/project/i18n/long_messages.pot +tests/messages/data/project/i18n/temp* \ No newline at end of file From a5ecaa321817d3705cbda1476f6e9f06daa1e847 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 28 May 2018 17:08:31 +0300 Subject: [PATCH 046/139] Become 2.6.0 --- AUTHORS | 3 ++- CHANGES | 49 ++++++++++++++++++++++++++++++++++++++++++----- babel/__init__.py | 2 +- docs/conf.py | 4 ++-- 4 files changed, 49 insertions(+), 9 deletions(-) diff --git a/AUTHORS b/AUTHORS index 58a9bee28..69e110ecc 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,9 +1,9 @@ Babel is written and maintained by the Babel team and various contributors: +- Aarni Koskela - Christopher Lenz - Armin Ronacher -- Aarni Koskela - Alex Morega - Lasse Schuirmann - Felix Schwarz @@ -41,6 +41,7 @@ Babel is written and maintained by the Babel team and various contributors: - Leonardo Pistone - Jun Omae - Hyunjun Kim +- PTrottier - xmo-odoo - StevenJ - Jungmo Ku diff --git a/CHANGES b/CHANGES index eff75e070..6d9b0e5fa 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,45 @@ Babel Changelog =============== +Version 2.6.0 +------------- + +Possibly incompatible changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These may be backward incompatible in some cases, as some more-or-less internal APIs have changed. +Please feel free to file issues if you bump into anything strange and we'll try to help! + +* Numbers: Refactor decimal handling code and allow bypass of decimal quantization. (@kdeldycke) (PR #538) +* Messages: allow processing files that are in locales unknown to Babel (@akx) (PR #557) +* General: Drop support for EOL Python 2.6 and 3.3 (@hugovk) (PR #546) + +Other changes +~~~~~~~~~~~~~ + +* CLDR: Use CLDR 33 (@akx) (PR #581) +* Lists: Add support for various list styles other than the default (@akx) (#552) +* Messages: Add new PoFileError exception (@Bedrock02) (PR #532) +* Times: Simplify Linux distro specific explicit timezone setting search (@scop) (PR #528) + +Bugfixes +~~~~~~~~ + +* CLDR: avoid importing alt=narrow currency symbols (@akx) (PR #558) +* CLDR: ignore non-Latin numbering systems (@akx) (PR #579) +* Docs: Fix improper example for date formatting (@PTrottier) (PR #574) +* Tooling: Fix some deprecation warnings (@akx) (PR #580) + +Tooling & docs +~~~~~~~~~~~~~~ + +* Add explicit signatures to some date autofunctions (@xmo-odoo) (PR #554) +* Include license file in the generated wheel package (@jdufresne) (PR #539) +* Python 3.6 invalid escape sequence deprecation fixes (@scop) (PR #528) +* Test and document all supported Python versions (@jdufresne) (PR #540) +* Update copyright header years and authors file (@akx) (PR #559) + + Version 2.5.3 ------------- @@ -210,7 +249,7 @@ Version 2.1 - Parse and honour the locale inheritance exceptions (https://github.com/python-babel/babel/issues/97) -- Fix Locale.parse using ``global.dat`` incompatible types +- Fix Locale.parse using ``global.dat`` incompatible types (https://github.com/python-babel/babel/issues/174) - Fix display of negative offsets in ``FixedOffsetTimezone`` (https://github.com/python-babel/babel/issues/214) @@ -218,7 +257,7 @@ Version 2.1 build, should improve compilation time for large projects - Add support for "narrow" format for ``format_timedelta`` - Add universal wheel support -- Support 'Language' header field in .PO files +- Support 'Language' header field in .PO files (fixes https://github.com/python-babel/babel/issues/76) - Test suite enhancements (coverage, broken tests fixed, etc) - Documentation updated @@ -313,7 +352,7 @@ Version 1.0 string does not contain any string formattings (:trac:`150`). - Fix Serbian plural forms (:trac:`213`). - Small speed improvement in format_date() (:trac:`216`). -- Fix so frontend.CommandLineInterface.run does not accumulate logging +- Fix so frontend.CommandLineInterface.run does not accumulate logging handlers (:trac:`227`, reported with initial patch by dfraser) - Fix exception if environment contains an invalid locale setting (:trac:`200`) @@ -390,11 +429,11 @@ Version 0.9.6 string does not contain any string formattings (:trac:`150`). - Fix Serbian plural forms (:trac:`213`). - Small speed improvement in format_date() (:trac:`216`). -- Fix number formatting for locales where CLDR specifies alt or draft +- Fix number formatting for locales where CLDR specifies alt or draft items (:trac:`217`) - Fix bad check in format_time (:trac:`257`, reported with patch and tests by jomae) -- Fix so frontend.CommandLineInterface.run does not accumulate logging +- Fix so frontend.CommandLineInterface.run does not accumulate logging handlers (:trac:`227`, reported with initial patch by dfraser) - Fix exception if environment contains an invalid locale setting (:trac:`200`) diff --git a/babel/__init__.py b/babel/__init__.py index c4e4d20ac..de44ce6c1 100644 --- a/babel/__init__.py +++ b/babel/__init__.py @@ -21,4 +21,4 @@ negotiate_locale, parse_locale, get_locale_identifier -__version__ = '2.5.3' +__version__ = '2.6.0' diff --git a/docs/conf.py b/docs/conf.py index 5aebe94d9..47196622a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -51,9 +51,9 @@ # built documents. # # The short X.Y version. -version = '2.5' +version = '2.6' # The full version, including alpha/beta/rc tags. -release = '2.5.3' +release = '2.6.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From 772b8402a77dab1ee683e0f715eb9db52fc66936 Mon Sep 17 00:00:00 2001 From: Luke Plant Date: Mon, 4 Jun 2018 20:53:28 +0300 Subject: [PATCH 047/139] numbers: implement currency formatting with long display names. Fixes #578 --- babel/numbers.py | 62 ++++++++++++++++++++++++++++++++++++++++++ scripts/import_cldr.py | 9 ++++++ tests/test_numbers.py | 38 ++++++++++++++++++++++++++ 3 files changed, 109 insertions(+) diff --git a/babel/numbers.py b/babel/numbers.py index 564d7ce19..9df19e931 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -442,6 +442,17 @@ def format_currency( ... UnknownCurrencyFormatError: "'unknown' is not a known currency format type" + You can also pass format_type='name' to use long display names. The order of + the number and currency name, along with the correct localized plural form + of the currency name, is chosen according to locale: + + >>> format_currency(1, 'USD', locale='en_US', format_type='name') + u'1.00 US dollar' + >>> format_currency(1099.98, 'USD', locale='en_US', format_type='name') + u'1,099.98 US dollars' + >>> format_currency(1099.98, 'USD', locale='ee', format_type='name') + u'us ga dollar 1,099.98' + By default the locale is allowed to truncate and round a high-precision number by forcing its format pattern onto the decimal part. You can bypass this behavior with the `decimal_quantization` parameter: @@ -459,7 +470,12 @@ def format_currency( :param format_type: the currency format type to use :param decimal_quantization: Truncate and round high-precision numbers to the format pattern. Defaults to `True`. + """ + if format_type == 'name': + return _format_currency_long_name(number, currency, format=format, + locale=locale, currency_digits=currency_digits, + decimal_quantization=decimal_quantization) locale = Locale.parse(locale) if format: pattern = parse_pattern(format) @@ -475,6 +491,52 @@ def format_currency( decimal_quantization=decimal_quantization) +def _format_currency_long_name( + number, currency, format=None, locale=LC_NUMERIC, currency_digits=True, + format_type='standard', decimal_quantization=True): + # Algorithm described here: + # https://www.unicode.org/reports/tr35/tr35-numbers.html#Currencies + locale = Locale.parse(locale) + # Step 1. + # There are no examples of items with explicit count (0 or 1) in current + # locale data. So there is no point implementing that. + # Step 2. + if isinstance(number, string_types): + plural_category = locale.plural_form(float(number)) + else: + plural_category = locale.plural_form(number) + + # Step 3. + try: + unit_pattern = locale._data['currency_unit_patterns'][plural_category] + except LookupError: + unit_pattern = locale._data['currency_unit_patterns']['other'] + + # Step 4. + try: + display_name = locale._data['currency_names_plural'][currency][plural_category] + except LookupError: + try: + display_name = locale._data['currency_names_plural'][currency]['other'] + except LookupError: + try: + display_name = locale._data['currency_names'][currency] + except LookupError: + display_name = currency + + # Step 5. + if not format: + format = locale.decimal_formats.get(format) + + pattern = parse_pattern(format) + + number_part = pattern.apply( + number, locale, currency=currency, currency_digits=currency_digits, + decimal_quantization=decimal_quantization) + + return unit_pattern.format(number_part, display_name) + + def format_percent( number, format=None, locale=LC_NUMERIC, decimal_quantization=True): """Return formatted percent value for a specific locale. diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py index 60aa6c280..40887f0d7 100755 --- a/scripts/import_cldr.py +++ b/scripts/import_cldr.py @@ -423,6 +423,7 @@ def _process_local_datas(sup, srcdir, destdir, force=False, dump_json=False): parse_percent_formats(data, tree) parse_currency_formats(data, tree) + parse_currency_unit_patterns(data, tree) parse_currency_names(data, tree) parse_unit_patterns(data, tree) parse_date_fields(data, tree) @@ -903,6 +904,14 @@ def parse_currency_formats(data, tree): currency_formats[type] = numbers.parse_pattern(pattern) +def parse_currency_unit_patterns(data, tree): + currency_unit_patterns = data.setdefault('currency_unit_patterns', {}) + for unit_pattern_elem in tree.findall('.//currencyFormats/unitPattern'): + count = unit_pattern_elem.attrib['count'] + pattern = text_type(unit_pattern_elem.text) + currency_unit_patterns[count] = pattern + + def parse_day_period_rules(tree): """ Parse dayPeriodRule data into a dict. diff --git a/tests/test_numbers.py b/tests/test_numbers.py index 493c1a7fe..f0239cb3a 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -415,6 +415,44 @@ def test_format_currency_quantization(): '0.9999999999', 'USD', locale=locale_code, decimal_quantization=False).find('9999999999') > -1 +def test_format_currency_long_display_name(): + assert (numbers.format_currency(1099.98, 'USD', locale='en_US', format_type='name') + == u'1,099.98 US dollars') + assert (numbers.format_currency(1.00, 'USD', locale='en_US', format_type='name') + == u'1.00 US dollar') + assert (numbers.format_currency(1.00, 'EUR', locale='en_US', format_type='name') + == u'1.00 euro') + assert (numbers.format_currency(2, 'EUR', locale='en_US', format_type='name') + == u'2.00 euros') + # This tests that '{1} {0}' unitPatterns are found: + assert (numbers.format_currency(1, 'USD', locale='sw', format_type='name') + == u'dola ya Marekani 1.00') + # This tests unicode chars: + assert (numbers.format_currency(1099.98, 'USD', locale='es_GT', format_type='name') + == u'dólares estadounidenses 1,099.98') + # Test for completely unknown currency, should fallback to currency code + assert (numbers.format_currency(1099.98, 'XAB', locale='en_US', format_type='name') + == u'1,099.98 XAB') + + +def test_format_currency_long_display_name_all(): + for locale_code in localedata.locale_identifiers(): + assert numbers.format_currency( + 1, 'USD', locale=locale_code, format_type='name').find('1') > -1 + assert numbers.format_currency( + '1', 'USD', locale=locale_code, format_type='name').find('1') > -1 + + +def test_format_currency_long_display_name_custom_format(): + assert (numbers.format_currency(1099.98, 'USD', locale='en_US', + format_type='name', format='##0') + == '1099.98 US dollars') + assert (numbers.format_currency(1099.98, 'USD', locale='en_US', + format_type='name', format='##0', + currency_digits=False) + == '1100 US dollars') + + def test_format_percent(): assert numbers.format_percent(0.34, locale='en_US') == u'34%' assert numbers.format_percent(0, locale='en_US') == u'0%' From 4b5097f6064dda281ce7a1c5ef74e047cfdc558c Mon Sep 17 00:00:00 2001 From: Luke Plant Date: Thu, 7 Jun 2018 14:37:44 +0300 Subject: [PATCH 048/139] Skip non-latn number formats for currency unit patterns --- scripts/import_cldr.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py index 40887f0d7..f1dd391be 100755 --- a/scripts/import_cldr.py +++ b/scripts/import_cldr.py @@ -906,10 +906,13 @@ def parse_currency_formats(data, tree): def parse_currency_unit_patterns(data, tree): currency_unit_patterns = data.setdefault('currency_unit_patterns', {}) - for unit_pattern_elem in tree.findall('.//currencyFormats/unitPattern'): - count = unit_pattern_elem.attrib['count'] - pattern = text_type(unit_pattern_elem.text) - currency_unit_patterns[count] = pattern + for currency_formats_elem in tree.findall('.//currencyFormats'): + if _should_skip_number_elem(data, currency_formats_elem): # TODO: Support other number systems + continue + for unit_pattern_elem in currency_formats_elem.findall('./unitPattern'): + count = unit_pattern_elem.attrib['count'] + pattern = text_type(unit_pattern_elem.text) + currency_unit_patterns[count] = pattern def parse_day_period_rules(tree): From 4c7e9b645cfec72672f836e04d8587ca66ba98c3 Mon Sep 17 00:00:00 2001 From: Luke Plant Date: Thu, 7 Jun 2018 15:21:19 +0300 Subject: [PATCH 049/139] Simplify format_currency code by pulling out/using helpers. In detail: 1. Use the already existing get_currency_name function which does the plural form logic already. 2. Create a similar `get_currency_unit_pattern` function. This function could be useful if someone wanted to write something very similar to the _format_currency_long_name functionality but with some different customizations, so it is now publicly documented. 3. Simplify the _format_currency_long_name function - it is now much flatter. 4. Add more tests to ensure certain cases are really covered. (e.g. different unit patterns depending on the count) An upshot of the changes is that we have reduced (and made more consistent) the number of places where we need to peek into `Locale._data` - get_currency_name and get_currency_unit_pattern are the only places that babel.numbers does this. --- babel/numbers.py | 52 ++++++++++++++++++++++++++++++------------- docs/api/numbers.rst | 2 ++ tests/test_numbers.py | 21 ++++++++++++++++- 3 files changed, 58 insertions(+), 17 deletions(-) diff --git a/babel/numbers.py b/babel/numbers.py index 9df19e931..cc825c866 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -157,6 +157,36 @@ def get_currency_precision(currency): return precisions.get(currency, precisions['DEFAULT'])[0] +def get_currency_unit_pattern(currency, count=None, locale=LC_NUMERIC): + """ + Return the unit pattern used for long display of a currency value + for a given locale. + This is a string containing ``{0}`` where the numeric part + should be substituted and ``{1}`` where the currency long display + name should be substituted. + + >>> get_currency_unit_pattern('USD', locale='en_US', count=10) + u'{0} {1}' + + .. versionadded:: 2.7.0 + + :param currency: the currency code. + :param count: the optional count. If provided the unit + pattern for that number will be returned. + :param locale: the `Locale` object or locale identifier. + """ + loc = Locale.parse(locale) + if count is not None: + plural_form = loc.plural_form(count) + try: + return loc._data['currency_unit_patterns'][plural_form] + except LookupError: + # Fall back to 'other' + pass + + return loc._data['currency_unit_patterns']['other'] + + def get_territory_currencies(territory, start_date=None, end_date=None, tender=True, non_tender=False, include_details=False): @@ -501,28 +531,18 @@ def _format_currency_long_name( # There are no examples of items with explicit count (0 or 1) in current # locale data. So there is no point implementing that. # Step 2. + + # Correct number to numeric type, important for looking up plural rules: if isinstance(number, string_types): - plural_category = locale.plural_form(float(number)) + number_n = float(number) else: - plural_category = locale.plural_form(number) + number_n = number # Step 3. - try: - unit_pattern = locale._data['currency_unit_patterns'][plural_category] - except LookupError: - unit_pattern = locale._data['currency_unit_patterns']['other'] + unit_pattern = get_currency_unit_pattern(currency, count=number_n, locale=locale) # Step 4. - try: - display_name = locale._data['currency_names_plural'][currency][plural_category] - except LookupError: - try: - display_name = locale._data['currency_names_plural'][currency]['other'] - except LookupError: - try: - display_name = locale._data['currency_names'][currency] - except LookupError: - display_name = currency + display_name = get_currency_name(currency, count=number_n, locale=locale) # Step 5. if not format: diff --git a/docs/api/numbers.rst b/docs/api/numbers.rst index 1b21425ee..758cebaf3 100644 --- a/docs/api/numbers.rst +++ b/docs/api/numbers.rst @@ -38,6 +38,8 @@ Data Access .. autofunction:: get_currency_symbol +.. autofunction:: get_currency_unit_pattern + .. autofunction:: get_decimal_symbol .. autofunction:: get_plus_sign_symbol diff --git a/tests/test_numbers.py b/tests/test_numbers.py index f0239cb3a..32f4280e7 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -19,7 +19,7 @@ from babel import Locale, localedata, numbers from babel.numbers import ( list_currencies, validate_currency, UnknownCurrencyError, is_currency, normalize_currency, - get_currency_precision, get_decimal_precision) + get_currency_precision, get_decimal_precision, get_currency_unit_pattern) from babel.localedata import locale_identifiers from babel._compat import decimal @@ -228,6 +228,17 @@ def test_get_currency_precision(): assert get_currency_precision('JPY') == 0 +def test_get_currency_unit_pattern(): + assert get_currency_unit_pattern('USD', locale='en_US') == '{0} {1}' + assert get_currency_unit_pattern('USD', locale='es_GT') == '{1} {0}' + + # 'ro' locale various pattern according to count + assert get_currency_unit_pattern('USD', locale='ro', count=1) == '{0} {1}' + assert get_currency_unit_pattern('USD', locale='ro', count=2) == '{0} {1}' + assert get_currency_unit_pattern('USD', locale='ro', count=100) == '{0} de {1}' + assert get_currency_unit_pattern('USD', locale='ro') == '{0} de {1}' + + def test_get_territory_currencies(): assert numbers.get_territory_currencies('AT', date(1995, 1, 1)) == ['ATS'] assert numbers.get_territory_currencies('AT', date(2011, 1, 1)) == ['EUR'] @@ -434,6 +445,14 @@ def test_format_currency_long_display_name(): assert (numbers.format_currency(1099.98, 'XAB', locale='en_US', format_type='name') == u'1,099.98 XAB') + # Test for finding different unit patterns depending on count + assert (numbers.format_currency(1, 'USD', locale='ro', format_type='name') + == u'1,00 dolar american') + assert (numbers.format_currency(2, 'USD', locale='ro', format_type='name') + == u'2,00 dolari americani') + assert (numbers.format_currency(100, 'USD', locale='ro', format_type='name') + == u'100,00 de dolari americani') + def test_format_currency_long_display_name_all(): for locale_code in localedata.locale_identifiers(): From 93e162a3194a8ac7e94ebbd965f705ba31e58318 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sat, 9 Jun 2018 19:30:51 -0700 Subject: [PATCH 050/139] Update all pypi.python.org URLs to pypi.org For details on the new PyPI, see the blog post: https://pythoninsider.blogspot.ca/2018/04/new-pypi-launched-legacy-pypi-shutting.html --- docs/_templates/sidebar-links.html | 2 +- docs/numbers.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/_templates/sidebar-links.html b/docs/_templates/sidebar-links.html index a55b2dd96..99c347420 100644 --- a/docs/_templates/sidebar-links.html +++ b/docs/_templates/sidebar-links.html @@ -9,7 +9,7 @@

Other Formats

Useful Links

diff --git a/docs/numbers.rst b/docs/numbers.rst index 1443b7cf5..b2a952281 100644 --- a/docs/numbers.rst +++ b/docs/numbers.rst @@ -135,7 +135,7 @@ behaves as desired. .. _Decimal: https://docs.python.org/3/library/decimal.html#decimal-objects .. _Context: https://docs.python.org/3/library/decimal.html#context-objects .. _`UTS #35 section 3.3`: http://www.unicode.org/reports/tr35/tr35-numbers.html#Formatting -.. _cdecimal: https://pypi.python.org/pypi/cdecimal +.. _cdecimal: https://pypi.org/project/cdecimal/ Parsing Numbers From 50610283e6d5d43b660c453f20f96f7703f883f7 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sat, 9 Jun 2018 19:44:44 -0700 Subject: [PATCH 051/139] Use https URLs throughout project where available Updated docs and code comments. --- .ci/appveyor.yml | 2 +- .ci/run_with_env.cmd | 4 ++-- CONTRIBUTING.md | 2 +- babel/dates.py | 10 +++++----- babel/languages.py | 2 +- babel/numbers.py | 2 +- babel/plural.py | 4 ++-- babel/support.py | 2 +- babel/units.py | 8 ++++---- docs/_templates/sidebar-links.html | 4 ++-- docs/conf.py | 2 +- docs/dates.rst | 2 +- docs/installation.rst | 2 +- docs/intro.rst | 2 +- docs/locale.rst | 4 ++-- docs/messages.rst | 6 +++--- docs/numbers.rst | 4 ++-- scripts/import_cldr.py | 4 ++-- 18 files changed, 33 insertions(+), 33 deletions(-) diff --git a/.ci/appveyor.yml b/.ci/appveyor.yml index 816b938f6..f357713f4 100644 --- a/.ci/appveyor.yml +++ b/.ci/appveyor.yml @@ -4,7 +4,7 @@ environment: global: # SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the # /E:ON and /V:ON options are not enabled in the batch script intepreter - # See: http://stackoverflow.com/a/13751649/163740 + # See: https://stackoverflow.com/a/13751649 CMD_IN_ENV: "cmd /E:ON /V:ON /C .\\.ci\\run_with_env.cmd" matrix: diff --git a/.ci/run_with_env.cmd b/.ci/run_with_env.cmd index 3a472bc83..0f5b8e097 100644 --- a/.ci/run_with_env.cmd +++ b/.ci/run_with_env.cmd @@ -13,10 +13,10 @@ :: :: More details at: :: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows -:: http://stackoverflow.com/a/13751649/163740 +:: https://stackoverflow.com/a/13751649 :: :: Author: Olivier Grisel -:: License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ +:: License: CC0 1.0 Universal: https://creativecommons.org/publicdomain/zero/1.0/ @ECHO OFF SET COMMAND_TO_RUN=%* diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c1b7e6959..079ef06b2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,5 +51,5 @@ shall be amended so the history is not cluttered by "fixup commits". ## Writing Good Commits Please see -http://api.coala.io/en/latest/Developers/Writing_Good_Commits.html +https://api.coala.io/en/latest/Developers/Writing_Good_Commits.html for guidelines on how to write good commits and proper commit messages. diff --git a/babel/dates.py b/babel/dates.py index cf58a485d..4bae3eaa0 100644 --- a/babel/dates.py +++ b/babel/dates.py @@ -34,7 +34,7 @@ # be inherited, the inheritance of this value can be explicitly disabled by # use of the 'no inheritance marker' as the value, which is 3 simultaneous [sic] # empty set characters ( U+2205 )." -# - http://www.unicode.org/reports/tr35/tr35-dates.html#Metazone_Names +# - https://www.unicode.org/reports/tr35/tr35-dates.html#Metazone_Names NO_INHERITANCE_MARKER = u'\u2205\u2205\u2205' @@ -592,7 +592,7 @@ def get_timezone_name(dt_or_tzinfo=None, width='long', uncommon=False, format. For more information see `LDML Appendix J: Time Zone Display Names - `_ + `_ .. versionadded:: 0.9 @@ -1018,7 +1018,7 @@ def format_interval(start, end, skeleton=None, tzinfo=None, fuzzy=True, locale=L locale = Locale.parse(locale) # NB: The quote comments below are from the algorithm description in - # http://www.unicode.org/reports/tr35/tr35-dates.html#intervalFormats + # https://www.unicode.org/reports/tr35/tr35-dates.html#intervalFormats # > Look for the intervalFormatItem element that matches the "skeleton", # > starting in the current locale and then following the locale fallback @@ -1527,7 +1527,7 @@ def get_week_number(self, day_of_period, day_of_week=None): } #: The pattern characters declared in the Date Field Symbol Table -#: (http://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table) +#: (https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table) #: in order of decreasing magnitude. PATTERN_CHAR_ORDER = "GyYuUQqMLlwWdDFgEecabBChHKkjJmsSAzZOvVXx" @@ -1675,7 +1675,7 @@ def split_interval_pattern(pattern): Split an interval-describing datetime pattern into multiple pieces. > The pattern is then designed to be broken up into two pieces by determining the first repeating field. - - http://www.unicode.org/reports/tr35/tr35-dates.html#intervalFormats + - https://www.unicode.org/reports/tr35/tr35-dates.html#intervalFormats >>> split_interval_pattern(u'E d.M. \u2013 E d.M.') [u'E d.M. \u2013 ', 'E d.M.'] diff --git a/babel/languages.py b/babel/languages.py index 0513d002d..097436705 100644 --- a/babel/languages.py +++ b/babel/languages.py @@ -60,7 +60,7 @@ def get_territory_language_info(territory): .. note:: Note that the format of the dict returned may change between Babel versions. - See http://www.unicode.org/cldr/charts/latest/supplemental/territory_language_information.html + See https://www.unicode.org/cldr/charts/latest/supplemental/territory_language_information.html :param territory: Territory code :type territory: str diff --git a/babel/numbers.py b/babel/numbers.py index cc825c866..518d4945e 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -17,7 +17,7 @@ """ # TODO: # Padding and rounding increments in pattern: -# - http://www.unicode.org/reports/tr35/ (Appendix G.6) +# - https://www.unicode.org/reports/tr35/ (Appendix G.6) import re from datetime import date as date_, datetime as datetime_ import warnings diff --git a/babel/plural.py b/babel/plural.py index 38093fe6e..eb1eef268 100644 --- a/babel/plural.py +++ b/babel/plural.py @@ -33,7 +33,7 @@ def extract_operands(source): t visible fractional digits in n, without trailing zeros. ====== =============================================================== - .. _`CLDR rules`: http://www.unicode.org/reports/tr35/tr35-33/tr35-numbers.html#Operands + .. _`CLDR rules`: https://www.unicode.org/reports/tr35/tr35-33/tr35-numbers.html#Operands :param source: A real number :type source: int|float|decimal.Decimal @@ -91,7 +91,7 @@ class PluralRule(object): exclusive; for a given numeric value, only one rule should apply (i.e. the condition should only be true for one of the plural rule elements. - .. _`CLDR rules`: http://www.unicode.org/reports/tr35/tr35-33/tr35-numbers.html#Language_Plural_Rules + .. _`CLDR rules`: https://www.unicode.org/reports/tr35/tr35-33/tr35-numbers.html#Language_Plural_Rules """ __slots__ = ('abstract', '_func') diff --git a/babel/support.py b/babel/support.py index 932fcf2bb..87cb05967 100644 --- a/babel/support.py +++ b/babel/support.py @@ -343,7 +343,7 @@ def udngettext(self, domain, singular, plural, num): dungettext = udngettext # Most of the downwards code, until it get's included in stdlib, from: - # http://bugs.python.org/file10036/gettext-pgettext.patch + # https://bugs.python.org/file10036/gettext-pgettext.patch # # The encoding of a msgctxt and a msgid in a .mo file is # msgctxt + "\x04" + msgid (gettext version >= 0.15) diff --git a/babel/units.py b/babel/units.py index 4260b3d56..be851c230 100644 --- a/babel/units.py +++ b/babel/units.py @@ -26,7 +26,7 @@ def get_unit_name(measurement_unit, length='long', locale=LC_NUMERIC): :param measurement_unit: the code of a measurement unit. Known units can be found in the CLDR Unit Validity XML file: - http://unicode.org/repos/cldr/tags/latest/common/validity/unit.xml + https://unicode.org/repos/cldr/tags/latest/common/validity/unit.xml :param length: "short", "long" or "narrow" :param locale: the `Locale` object or locale identifier @@ -44,7 +44,7 @@ def _find_unit_pattern(unit_id, locale=LC_NUMERIC): Expand an unit into a qualified form. Known units can be found in the CLDR Unit Validity XML file: - http://unicode.org/repos/cldr/tags/latest/common/validity/unit.xml + https://unicode.org/repos/cldr/tags/latest/common/validity/unit.xml >>> _find_unit_pattern("radian", locale="en") 'angle-radian' @@ -105,7 +105,7 @@ def format_unit(value, measurement_unit, length='long', format=None, locale=LC_N :param value: the value to format. If this is a string, no number formatting will be attempted. :param measurement_unit: the code of a measurement unit. Known units can be found in the CLDR Unit Validity XML file: - http://unicode.org/repos/cldr/tags/latest/common/validity/unit.xml + https://unicode.org/repos/cldr/tags/latest/common/validity/unit.xml :param length: "short", "long" or "narrow" :param format: An optional format, as accepted by `format_decimal`. :param locale: the `Locale` object or locale identifier @@ -219,7 +219,7 @@ def format_compound_unit( >>> format_compound_unit(format_currency(35, "JPY", locale="de"), denominator_unit="liter", locale="de") '35\\xa0\\xa5 pro Liter' - See http://www.unicode.org/reports/tr35/tr35-general.html#perUnitPatterns + See https://www.unicode.org/reports/tr35/tr35-general.html#perUnitPatterns :param numerator_value: The numerator value. This may be a string, in which case it is considered preformatted and the unit is ignored. diff --git a/docs/_templates/sidebar-links.html b/docs/_templates/sidebar-links.html index 99c347420..71d11b850 100644 --- a/docs/_templates/sidebar-links.html +++ b/docs/_templates/sidebar-links.html @@ -10,6 +10,6 @@

Useful Links

diff --git a/docs/conf.py b/docs/conf.py index 47196622a..63c2d7749 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -253,7 +253,7 @@ #texinfo_show_urls = 'footnote' intersphinx_mapping = { - 'http://docs.python.org/2': None, + 'https://docs.python.org/2/': None, } extlinks = { diff --git a/docs/dates.rst b/docs/dates.rst index 0ddd7bc95..44201877e 100644 --- a/docs/dates.rst +++ b/docs/dates.rst @@ -124,7 +124,7 @@ the `Locale Data Markup Language specification`_. The following table is just a relatively brief overview. .. _`Locale Data Markup Language specification`: - http://unicode.org/reports/tr35/#Date_Format_Patterns + https://unicode.org/reports/tr35/#Date_Format_Patterns Date Fields ----------- diff --git a/docs/installation.rst b/docs/installation.rst index 0aea3abfe..0a7804e86 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -80,7 +80,7 @@ use a git checkout. Get the git checkout in a new virtualenv and run in development mode:: - $ git clone http://github.com/python-babel/babel.git + $ git clone https://github.com/python-babel/babel Initialized empty Git repository in ~/dev/babel/.git/ $ cd babel $ virtualenv venv diff --git a/docs/intro.rst b/docs/intro.rst index db36f32d3..7733f70b1 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -43,7 +43,7 @@ of locale data, such as the localized names of countries, languages, or time-zones, which are frequently needed in web-based applications. For these requirements, Babel includes data extracted from the `Common -Locale Data Repository (CLDR) `_, and provides a +Locale Data Repository (CLDR) `_, and provides a number of convenient methods for accessing and using this data. See :ref:`locale-data`, :ref:`date-and-time`, and :ref:`numbers` for more information on this aspect of Babel. diff --git a/docs/locale.rst b/docs/locale.rst index cf4f6d5c5..425fb776c 100644 --- a/docs/locale.rst +++ b/docs/locale.rst @@ -15,8 +15,8 @@ and you'd like to display the names of those countries in the language the user prefers. Instead of translating all those country names yourself in your application, you can make use of the translations provided by the locale data included with Babel, which is based on the `Common Locale Data Repository -(CLDR) `_ developed and maintained by the `Unicode -Consortium `_. +(CLDR) `_ developed and maintained by the `Unicode +Consortium `_. The ``Locale`` Class diff --git a/docs/messages.rst b/docs/messages.rst index f1f695c24..9c66a42eb 100644 --- a/docs/messages.rst +++ b/docs/messages.rst @@ -32,8 +32,8 @@ used in an application. They are commonly stored in PO (Portable Object) and MO (Machine Object) files, the formats of which are defined by the GNU `gettext`_ tools and the GNU `translation project`_. - .. _`gettext`: http://www.gnu.org/software/gettext/ - .. _`translation project`: http://sourceforge.net/projects/translation + .. _`gettext`: https://www.gnu.org/software/gettext/ + .. _`translation project`: https://sourceforge.net/projects/translation/ The general procedure for building message catalogs looks something like this: @@ -81,7 +81,7 @@ extracted from source files can not only depend on the file extension, but needs to be controllable in a precise manner. .. _`Jinja2`: http://jinja.pocoo.org/ -.. _`Genshi`: http://genshi.edgewall.org/ +.. _`Genshi`: https://genshi.edgewall.org/ Babel accepts a configuration file to specify this mapping of files to extraction methods, which is described below. diff --git a/docs/numbers.rst b/docs/numbers.rst index b2a952281..df834eaf8 100644 --- a/docs/numbers.rst +++ b/docs/numbers.rst @@ -47,7 +47,7 @@ The syntax for custom number format patterns is described in detail in the the specification. The following table is just a relatively brief overview. .. _`Locale Data Markup Language specification`: - http://unicode.org/reports/tr35/#Number_Format_Patterns + https://unicode.org/reports/tr35/#Number_Format_Patterns +----------+-----------------------------------------------------------------+ | Symbol | Description | @@ -134,7 +134,7 @@ behaves as desired. .. _Decimal: https://docs.python.org/3/library/decimal.html#decimal-objects .. _Context: https://docs.python.org/3/library/decimal.html#context-objects -.. _`UTS #35 section 3.3`: http://www.unicode.org/reports/tr35/tr35-numbers.html#Formatting +.. _`UTS #35 section 3.3`: https://www.unicode.org/reports/tr35/tr35-numbers.html#Formatting .. _cdecimal: https://pypi.org/project/cdecimal/ diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py index f1dd391be..a3e8f69ea 100755 --- a/scripts/import_cldr.py +++ b/scripts/import_cldr.py @@ -756,7 +756,7 @@ def parse_decimal_formats(data, tree): pattern = numbers.parse_pattern(text_type(pattern_el.text)) if pattern_type: # This is a compact decimal format, see: - # http://www.unicode.org/reports/tr35/tr35-45/tr35-numbers.html#Compact_Number_Formats + # https://www.unicode.org/reports/tr35/tr35-45/tr35-numbers.html#Compact_Number_Formats # These are mapped into a `compact_decimal_formats` dictionary # with the format {length: {count: {multiplier: pattern}}}. @@ -861,7 +861,7 @@ def parse_date_fields(data, tree): def parse_interval_formats(data, tree): - # http://www.unicode.org/reports/tr35/tr35-dates.html#intervalFormats + # https://www.unicode.org/reports/tr35/tr35-dates.html#intervalFormats interval_formats = data.setdefault("interval_formats", {}) for elem in tree.findall("dateTimeFormats/intervalFormats/*"): if 'draft' in elem.attrib: From f7ac6530a70ad3b58a85add5ace83131777beb83 Mon Sep 17 00:00:00 2001 From: Charly C Date: Sat, 16 Jun 2018 11:09:13 +0200 Subject: [PATCH 052/139] add a `strict` mode to `parse_decimal()` Fixes https://github.com/python-babel/babel/issues/589 --- babel/numbers.py | 25 ++++++++++++++++++++++--- tests/test_numbers.py | 19 +++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/babel/numbers.py b/babel/numbers.py index 518d4945e..509150cb9 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -661,7 +661,7 @@ def parse_number(string, locale=LC_NUMERIC): raise NumberFormatError('%r is not a valid number' % string) -def parse_decimal(string, locale=LC_NUMERIC): +def parse_decimal(string, locale=LC_NUMERIC, strict=False): """Parse localized decimal string into a decimal. >>> parse_decimal('1,099.98', locale='en_US') @@ -676,17 +676,36 @@ def parse_decimal(string, locale=LC_NUMERIC): ... NumberFormatError: '2,109,998' is not a valid decimal number + If `strict` is set to `True` and the given string contains a number + formatted in an irregular way, an exception is raised: + + >>> parse_decimal('30.00', locale='de', strict=True) + Traceback (most recent call last): + ... + NumberFormatError: '30.00' is not a properly formatted decimal number + :param string: the string to parse :param locale: the `Locale` object or locale identifier + :param strict: controls whether numbers formatted in a weird way are + accepted or rejected :raise NumberFormatError: if the string can not be converted to a decimal number """ locale = Locale.parse(locale) + group_symbol = get_group_symbol(locale) + decimal_symbol = get_decimal_symbol(locale) try: - return decimal.Decimal(string.replace(get_group_symbol(locale), '') - .replace(get_decimal_symbol(locale), '.')) + parsed = decimal.Decimal(string.replace(group_symbol, '') + .replace(decimal_symbol, '.')) except decimal.InvalidOperation: raise NumberFormatError('%r is not a valid decimal number' % string) + if strict and group_symbol in string: + proper = format_decimal(parsed, locale=locale, decimal_quantization=False) + if string != proper and string.rstrip('0') != (proper + decimal_symbol): + raise NumberFormatError( + "%r is not a properly formatted decimal number" % string + ) + return parsed PREFIX_END = r'[^0-9@#.,]' diff --git a/tests/test_numbers.py b/tests/test_numbers.py index 32f4280e7..50c53dec4 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -165,6 +165,25 @@ def test_can_parse_decimals(self): self.assertRaises(numbers.NumberFormatError, lambda: numbers.parse_decimal('2,109,998', locale='de')) + def test_parse_decimal_strict_mode(self): + # Numbers with a misplaced grouping symbol should be rejected + with self.assertRaises(numbers.NumberFormatError): + numbers.parse_decimal('11.11', locale='de', strict=True) + # Partially grouped numbers should be rejected + with self.assertRaises(numbers.NumberFormatError): + numbers.parse_decimal('2000,000', locale='en_US', strict=True) + # Numbers with duplicate grouping symbols should be rejected + with self.assertRaises(numbers.NumberFormatError): + numbers.parse_decimal('0,,000', locale='en_US', strict=True) + # Properly formatted numbers should be accepted + assert str(numbers.parse_decimal('1.001', locale='de', strict=True)) == '1001' + # Trailing zeroes should be accepted + assert str(numbers.parse_decimal('3.00', locale='en_US', strict=True)) == '3.00' + # Numbers without any grouping symbol should be accepted + assert str(numbers.parse_decimal('2000.1', locale='en_US', strict=True)) == '2000.1' + # High precision numbers should be accepted + assert str(numbers.parse_decimal('5,000001', locale='fr', strict=True)) == '5.000001' + def test_list_currencies(): assert isinstance(list_currencies(), set) From 6bf8745f6a913af71357583e8e97116fcfd444ca Mon Sep 17 00:00:00 2001 From: Changaco Date: Sun, 17 Jun 2018 09:17:02 +0200 Subject: [PATCH 053/139] include more information in error message --- babel/numbers.py | 19 +++++++++++++++---- tests/test_numbers.py | 3 +++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/babel/numbers.py b/babel/numbers.py index 509150cb9..b0cfe65e7 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -682,7 +682,7 @@ def parse_decimal(string, locale=LC_NUMERIC, strict=False): >>> parse_decimal('30.00', locale='de', strict=True) Traceback (most recent call last): ... - NumberFormatError: '30.00' is not a properly formatted decimal number + NumberFormatError: '30.00' is not a properly formatted decimal number. Did you mean '3.000'? Or maybe '30,00'? :param string: the string to parse :param locale: the `Locale` object or locale identifier @@ -702,9 +702,20 @@ def parse_decimal(string, locale=LC_NUMERIC, strict=False): if strict and group_symbol in string: proper = format_decimal(parsed, locale=locale, decimal_quantization=False) if string != proper and string.rstrip('0') != (proper + decimal_symbol): - raise NumberFormatError( - "%r is not a properly formatted decimal number" % string - ) + try: + parsed_alt = decimal.Decimal(string.replace(decimal_symbol, '') + .replace(group_symbol, '.')) + except decimal.InvalidOperation: + raise NumberFormatError( + "%r is not a properly formatted decimal number. Did you mean %r?" % + (string, proper) + ) + else: + proper_alt = format_decimal(parsed_alt, locale=locale, decimal_quantization=False) + raise NumberFormatError( + "%r is not a properly formatted decimal number. Did you mean %r? Or maybe %r?" % + (string, proper, proper_alt) + ) return parsed diff --git a/tests/test_numbers.py b/tests/test_numbers.py index 50c53dec4..b9dcb7278 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -169,6 +169,9 @@ def test_parse_decimal_strict_mode(self): # Numbers with a misplaced grouping symbol should be rejected with self.assertRaises(numbers.NumberFormatError): numbers.parse_decimal('11.11', locale='de', strict=True) + # Numbers with two misplaced grouping symbols should be rejected + with self.assertRaises(numbers.NumberFormatError): + numbers.parse_decimal('80.00.00', locale='de', strict=True) # Partially grouped numbers should be rejected with self.assertRaises(numbers.NumberFormatError): numbers.parse_decimal('2000,000', locale='en_US', strict=True) From b5f6f362440faf862ea887a4157252874c59d910 Mon Sep 17 00:00:00 2001 From: Changaco Date: Sun, 17 Jun 2018 09:55:02 +0200 Subject: [PATCH 054/139] add a `suggestions` attribute to `NumberFormatError` --- babel/numbers.py | 13 +++++++++---- docs/api/numbers.rst | 1 + tests/test_numbers.py | 12 ++++++++---- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/babel/numbers.py b/babel/numbers.py index b0cfe65e7..d3d223836 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -634,6 +634,11 @@ def format_scientific( class NumberFormatError(ValueError): """Exception raised when a string cannot be parsed into a number.""" + def __init__(self, message, suggestions=None): + super(NumberFormatError, self).__init__(message) + #: a list of properly formatted numbers derived from the invalid input + self.suggestions = suggestions + def parse_number(string, locale=LC_NUMERIC): """Parse localized number string into an integer. @@ -706,16 +711,16 @@ def parse_decimal(string, locale=LC_NUMERIC, strict=False): parsed_alt = decimal.Decimal(string.replace(decimal_symbol, '') .replace(group_symbol, '.')) except decimal.InvalidOperation: - raise NumberFormatError( + raise NumberFormatError(( "%r is not a properly formatted decimal number. Did you mean %r?" % (string, proper) - ) + ), suggestions=[proper]) else: proper_alt = format_decimal(parsed_alt, locale=locale, decimal_quantization=False) - raise NumberFormatError( + raise NumberFormatError(( "%r is not a properly formatted decimal number. Did you mean %r? Or maybe %r?" % (string, proper, proper_alt) - ) + ), suggestions=[proper, proper_alt]) return parsed diff --git a/docs/api/numbers.rst b/docs/api/numbers.rst index 758cebaf3..f9b0833a2 100644 --- a/docs/api/numbers.rst +++ b/docs/api/numbers.rst @@ -30,6 +30,7 @@ Exceptions ---------- .. autoexception:: NumberFormatError + :members: Data Access ----------- diff --git a/tests/test_numbers.py b/tests/test_numbers.py index b9dcb7278..9c3896ce5 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -167,17 +167,21 @@ def test_can_parse_decimals(self): def test_parse_decimal_strict_mode(self): # Numbers with a misplaced grouping symbol should be rejected - with self.assertRaises(numbers.NumberFormatError): + with self.assertRaises(numbers.NumberFormatError) as info: numbers.parse_decimal('11.11', locale='de', strict=True) + assert info.exception.suggestions == ['1.111', '11,11'] # Numbers with two misplaced grouping symbols should be rejected - with self.assertRaises(numbers.NumberFormatError): + with self.assertRaises(numbers.NumberFormatError) as info: numbers.parse_decimal('80.00.00', locale='de', strict=True) + assert info.exception.suggestions == ['800.000'] # Partially grouped numbers should be rejected - with self.assertRaises(numbers.NumberFormatError): + with self.assertRaises(numbers.NumberFormatError) as info: numbers.parse_decimal('2000,000', locale='en_US', strict=True) + assert info.exception.suggestions == ['2,000,000', '2,000'] # Numbers with duplicate grouping symbols should be rejected - with self.assertRaises(numbers.NumberFormatError): + with self.assertRaises(numbers.NumberFormatError) as info: numbers.parse_decimal('0,,000', locale='en_US', strict=True) + assert info.exception.suggestions == ['0'] # Properly formatted numbers should be accepted assert str(numbers.parse_decimal('1.001', locale='de', strict=True)) == '1001' # Trailing zeroes should be accepted From 83f959ebf23266b5ae7432589c2f4983f0d817b4 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 18 Jun 2018 14:43:32 -0600 Subject: [PATCH 055/139] Bumped version requirement on pytz Here you use the utc attribute on the _pytz object: https://github.com/python-babel/babel/blob/23ca4bf2b187748e5e8372c6dae541fbcc5fbe5d/babel/util.py#L304 Which was added in this commit https://github.com/stub42/pytz/commit/ae82fbc71b374a5a5b32bf3bbb38ebb97af6e715 Added the corrected version requirement for it's appearance --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8e268e16b..27c7541bb 100755 --- a/setup.py +++ b/setup.py @@ -61,7 +61,7 @@ def run(self): # This version identifier is currently necessary as # pytz otherwise does not install on pip 1.4 or # higher. - 'pytz>=0a', + 'pytz>=2015.7', ], cmdclass={'import_cldr': import_cldr}, From 359ecffca479dfe032d0f7210d5cd8160599c816 Mon Sep 17 00:00:00 2001 From: Bryn Truscott Date: Thu, 19 Jul 2018 15:50:16 +0100 Subject: [PATCH 056/139] Small fixes to avoid stack traces due to badly formatted .po file --- babel/messages/pofile.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/babel/messages/pofile.py b/babel/messages/pofile.py index ea8d7d7e2..fe376318a 100644 --- a/babel/messages/pofile.py +++ b/babel/messages/pofile.py @@ -183,9 +183,12 @@ def _process_message_line(self, lineno, line, obsolete=False): def _process_keyword_line(self, lineno, line, obsolete=False): for keyword in self._keywords: - if line.startswith(keyword) and line[len(keyword)] in [' ', '[']: - arg = line[len(keyword):] - break + try: + if line.startswith(keyword) and line[len(keyword)] in [' ', '[']: + arg = line[len(keyword):] + break + except IndexError: + self._invalid_pofile(line, lineno, "Keyword must be followed by a string") else: self._invalid_pofile(line, lineno, "Start of line didn't match any expected keyword.") return @@ -290,7 +293,7 @@ def _invalid_pofile(self, line, lineno, msg): if self.abort_invalid: raise PoFileError(msg, self.catalog, line, lineno) print("WARNING:", msg) - print("WARNING: Problem on line {0}: {1}".format(lineno + 1, line)) + print(u"WARNING: Problem on line {0}: {1}".format(lineno + 1, line)) def read_po(fileobj, locale=None, domain=None, ignore_obsolete=False, charset=None, abort_invalid=False): From 3f1dffbb2dffc88957384fc4af378003299c68db Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 20 Jul 2018 13:36:22 +0300 Subject: [PATCH 057/139] Fix pofile test failure --- tests/messages/test_pofile.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/tests/messages/test_pofile.py b/tests/messages/test_pofile.py index 5313329aa..9872e9d54 100644 --- a/tests/messages/test_pofile.py +++ b/tests/messages/test_pofile.py @@ -456,16 +456,8 @@ def test_abort_invalid_po_file(self): ''' # Catalog not created, throws Unicode Error buf = StringIO(invalid_po) - output = None - - # This should only be thrown under py27 - if sys.version_info.major == 2: - with self.assertRaises(UnicodeEncodeError): - output = pofile.read_po(buf, locale='fr', abort_invalid=False) - assert not output - else: - output = pofile.read_po(buf, locale='fr', abort_invalid=False) - assert isinstance(output, Catalog) + output = pofile.read_po(buf, locale='fr', abort_invalid=False) + assert isinstance(output, Catalog) # Catalog not created, throws PoFileError buf = StringIO(invalid_po_2) From 0843782e72376fc44896bc93d50555d278184ddc Mon Sep 17 00:00:00 2001 From: Serban Constantin Date: Fri, 3 Aug 2018 16:09:09 +0300 Subject: [PATCH 058/139] don't repeat suggestions in parse_decimal strict Don't repeat suggestions for `0.00` in languages which use commas as delimiters combined with strict mode. --- babel/numbers.py | 19 +++++++++++++++---- tests/test_numbers.py | 4 ++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/babel/numbers.py b/babel/numbers.py index d3d223836..4306e9360 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -689,6 +689,11 @@ def parse_decimal(string, locale=LC_NUMERIC, strict=False): ... NumberFormatError: '30.00' is not a properly formatted decimal number. Did you mean '3.000'? Or maybe '30,00'? + >>> parse_decimal('0.00', locale='de', strict=True) + Traceback (most recent call last): + ... + NumberFormatError: '0.00' is not a properly formatted decimal number. Did you mean '0'? + :param string: the string to parse :param locale: the `Locale` object or locale identifier :param strict: controls whether numbers formatted in a weird way are @@ -717,10 +722,16 @@ def parse_decimal(string, locale=LC_NUMERIC, strict=False): ), suggestions=[proper]) else: proper_alt = format_decimal(parsed_alt, locale=locale, decimal_quantization=False) - raise NumberFormatError(( - "%r is not a properly formatted decimal number. Did you mean %r? Or maybe %r?" % - (string, proper, proper_alt) - ), suggestions=[proper, proper_alt]) + if proper_alt == proper: + raise NumberFormatError(( + "%r is not a properly formatted decimal number. Did you mean %r?" % + (string, proper) + ), suggestions=[proper]) + else: + raise NumberFormatError(( + "%r is not a properly formatted decimal number. Did you mean %r? Or maybe %r?" % + (string, proper, proper_alt) + ), suggestions=[proper, proper_alt]) return parsed diff --git a/tests/test_numbers.py b/tests/test_numbers.py index 9c3896ce5..d0f24bd54 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -182,6 +182,10 @@ def test_parse_decimal_strict_mode(self): with self.assertRaises(numbers.NumberFormatError) as info: numbers.parse_decimal('0,,000', locale='en_US', strict=True) assert info.exception.suggestions == ['0'] + # Return only suggestion for 0 on strict + with self.assertRaises(numbers.NumberFormatError) as info: + numbers.parse_decimal('0.00', locale='de', strict=True) + assert info.exception.suggestions == ['0'] # Properly formatted numbers should be accepted assert str(numbers.parse_decimal('1.001', locale='de', strict=True)) == '1001' # Trailing zeroes should be accepted From 41140232602ce150756e417d22f98cac419789d6 Mon Sep 17 00:00:00 2001 From: Brian Cappello Date: Tue, 14 Aug 2018 19:21:16 -0600 Subject: [PATCH 059/139] add support to util.pathmatch for matching the start of a string --- babel/util.py | 21 ++++++++++++++++++++- tests/test_util.py | 5 +++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/babel/util.py b/babel/util.py index 0150827de..fb93f16d5 100644 --- a/babel/util.py +++ b/babel/util.py @@ -151,6 +151,16 @@ def pathmatch(pattern, filename): >>> pathmatch('**.py', 'templates/index.html') False + >>> pathmatch('./foo/**.py', 'foo/bar/baz.py') + True + >>> pathmatch('./foo/**.py', 'bar/baz.py') + False + + >>> pathmatch('^foo/**.py', 'foo/bar/baz.py') + True + >>> pathmatch('^foo/**.py', 'bar/baz.py') + False + >>> pathmatch('**/templates/*.html', 'templates/index.html') True >>> pathmatch('**/templates/*.html', 'templates/foo/bar.html') @@ -167,7 +177,16 @@ def pathmatch(pattern, filename): '**/': '(?:.+/)*?', '**': '(?:.+/)*?[^/]+', } - buf = [] + + if pattern.startswith('^'): + buf = ['^'] + pattern = pattern[1:] + elif pattern.startswith('./'): + buf = ['^'] + pattern = pattern[2:] + else: + buf = [] + for idx, part in enumerate(re.split('([?*]+/?)', pattern)): if idx % 2: buf.append(symbols[part]) diff --git a/tests/test_util.py b/tests/test_util.py index a3607d5fc..ef591102e 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -31,6 +31,11 @@ def test_pathmatch(): assert not util.pathmatch('**.py', 'templates/index.html') assert util.pathmatch('**/templates/*.html', 'templates/index.html') assert not util.pathmatch('**/templates/*.html', 'templates/foo/bar.html') + assert util.pathmatch('^foo/**.py', 'foo/bar/baz/blah.py') + assert not util.pathmatch('^foo/**.py', 'blah/foo/bar/baz.py') + assert util.pathmatch('./foo/**.py', 'foo/bar/baz/blah.py') + assert util.pathmatch('./blah.py', 'blah.py') + assert not util.pathmatch('./foo/**.py', 'blah/foo/bar/baz.py') def test_odict_pop(): From 85f6587c8556592e969b23a92f7432d24d464532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Sun, 19 Aug 2018 18:03:02 +0200 Subject: [PATCH 060/139] Test empty translation uses fallback --- tests/messages/test_mofile.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/messages/test_mofile.py b/tests/messages/test_mofile.py index 5fedc600a..66c41d9a3 100644 --- a/tests/messages/test_mofile.py +++ b/tests/messages/test_mofile.py @@ -71,3 +71,26 @@ def test_more_plural_forms(self): catalog2.add(('Fuzz', 'Fuzzes'), ('', '', '')) buf = BytesIO() mofile.write_mo(buf, catalog2) + + def test_empty_translation_with_fallback(self): + catalog1 = Catalog(locale='fr_FR') + catalog1.add(u'', '''\ +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n''') + catalog1.add(u'Fuzz', '') + buf1 = BytesIO() + mofile.write_mo(buf1, catalog1) + buf1.seek(0) + catalog2 = Catalog(locale='fr') + catalog2.add(u'', '''\ +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n''') + catalog2.add(u'Fuzz', 'Flou') + buf2 = BytesIO() + mofile.write_mo(buf2, catalog2) + buf2.seek(0) + + translations = Translations(fp=buf1) + translations.add_fallback(Translations(fp=buf2)) + + self.assertEqual(u'Flou', translations.ugettext('Fuzz')) From 65d6bf39f2a2b4bbbbc02cd1e00c44ce9d7c1c5b Mon Sep 17 00:00:00 2001 From: Julien Palard Date: Tue, 30 Oct 2018 23:50:35 +0100 Subject: [PATCH 061/139] Using ABCs from collections instead of collections.abc is deprecated. And it will stop working in Python 3.8 according to the warning. --- babel/_compat.py | 3 ++- babel/localedata.py | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/babel/_compat.py b/babel/_compat.py index 1131f44af..849f749e3 100644 --- a/babel/_compat.py +++ b/babel/_compat.py @@ -29,6 +29,7 @@ cmp = lambda a, b: (a > b) - (a < b) array_tobytes = array.array.tobytes + from collections import abc else: text_type = unicode @@ -54,7 +55,7 @@ cmp = cmp array_tobytes = array.array.tostring - + import collections as abc number_types = integer_types + (float,) diff --git a/babel/localedata.py b/babel/localedata.py index a638e58af..2a8c4423d 100644 --- a/babel/localedata.py +++ b/babel/localedata.py @@ -14,10 +14,9 @@ import os import threading -from collections import MutableMapping from itertools import chain -from babel._compat import pickle, string_types +from babel._compat import pickle, string_types, abc _cache = {} @@ -187,7 +186,7 @@ def resolve(self, data): return data -class LocaleDataDict(MutableMapping): +class LocaleDataDict(abc.MutableMapping): """Dictionary wrapper that automatically resolves aliases to the actual values. """ From 8a8a337c153147a6bef5d3d69b854fa8fb9501e6 Mon Sep 17 00:00:00 2001 From: NotAFile Date: Sat, 15 Dec 2018 17:38:17 +0100 Subject: [PATCH 062/139] switch print statement in docs to print function --- docs/messages.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/messages.rst b/docs/messages.rst index 9c66a42eb..3ac035607 100644 --- a/docs/messages.rst +++ b/docs/messages.rst @@ -17,13 +17,13 @@ application as subject to localization, by wrapping them in functions such as .. code-block:: python - print _("Hello") + print(_("Hello")) instead of just: .. code-block:: python - print "Hello" + print("Hello") to make the string "Hello" localizable. From c4f790500fd946f6d00bbc90c32086f881b23ba6 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Thu, 1 Nov 2018 19:05:56 -0700 Subject: [PATCH 063/139] Remove all references to deprecated easy_install easy_install is deprecated and its use is discouraged by PyPA: https://setuptools.readthedocs.io/en/latest/easy_install.html > Warning: Easy Install is deprecated. Do not use it. Instead use pip. Follow upstream advice and only recommended supported tools. --- docs/installation.rst | 53 ++++--------------------------------------- 1 file changed, 5 insertions(+), 48 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 0a7804e86..ce778b04c 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -5,8 +5,7 @@ Installation Babel is distributed as a standard Python package fully set up with all the dependencies it needs. It primarily depends on the excellent `pytz`_ -library for timezone handling. To install it you can use ``easy_install`` -or ``pip``. +library for timezone handling. To install it you can use ``pip``. .. _pytz: http://pytz.sourceforge.net/ @@ -17,17 +16,12 @@ virtualenv Virtualenv is probably what you want to use during development, and if you have shell access to your production machines, you'll probably want to use -it there, too. +it there, too. Use ``pip`` to install it:: -If you are on Mac OS X or Linux, chances are that one of the following two -commands will work for you:: + $ sudo pip install virtualenv - $ sudo easy_install virtualenv - -If you are on Windows and don't have the `easy_install` command, you must -install it first. Check the :ref:`windows-easy-install` section for more -information about how to do that. Once you have it installed, run the same -commands as above, but without the `sudo` prefix. +If you're on Windows, run it in a command-prompt window with administrator +privileges, and leave out ``sudo``. Once you have virtualenv installed, just fire up a shell and create your own environment. I usually create a project folder and a `venv` @@ -66,8 +60,6 @@ with root privileges:: $ sudo pip install Babel -If `pip` is not available on your system you can use `easy_install`. - (On Windows systems, run it in a command-prompt window with administrator privileges, and leave out `sudo`.) @@ -101,38 +93,3 @@ This will pull also in the dependencies and activate the git head as the current version inside the virtualenv. Then all you have to do is run ``git pull origin`` to update to the latest version. If the CLDR data changes you will have to re-run ``python setup.py import_cldr``. - -.. _windows-easy-install: - -`pip` and `distribute` on Windows ------------------------------------ - -On Windows, installation of `easy_install` is a little bit trickier, but -still quite easy. The easiest way to do it is to download the -`distribute_setup.py`_ file and run it. The easiest way to run the file -is to open your downloads folder and double-click on the file. - -Next, add the `easy_install` command and other Python scripts to the -command search path, by adding your Python installation's Scripts folder -to the `PATH` environment variable. To do that, right-click on the -"Computer" icon on the Desktop or in the Start menu, and choose "Properties". -Then click on "Advanced System settings" (in Windows XP, click on the -"Advanced" tab instead). Then click on the "Environment variables" button. -Finally, double-click on the "Path" variable in the "System variables" section, -and add the path of your Python interpreter's Scripts folder. Be sure to -delimit it from existing values with a semicolon. Assuming you are using -Python 2.7 on the default path, add the following value:: - - - ;C:\Python27\Scripts - -And you are done! To check that it worked, open the Command Prompt and execute -``easy_install``. If you have User Account Control enabled on Windows Vista or -Windows 7, it should prompt you for administrator privileges. - -Now that you have ``easy_install``, you can use it to install ``pip``:: - - > easy_install pip - - -.. _distribute_setup.py: http://python-distribute.org/distribute_setup.py From 5dbc37af339aff21439d007f5ed11e5a2862609e Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sat, 8 Dec 2018 04:50:45 -0800 Subject: [PATCH 064/139] Add testing and document support for Python 3.7 --- .ci/appveyor.yml | 2 +- .travis.yml | 7 ++++++- setup.py | 1 + tox.ini | 4 ++-- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.ci/appveyor.yml b/.ci/appveyor.yml index f357713f4..d4135e080 100644 --- a/.ci/appveyor.yml +++ b/.ci/appveyor.yml @@ -27,7 +27,7 @@ install: - "python --version" - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" # Build data files - - "pip install --upgrade pytest==3.3.2 pytest-cov==2.5.1 codecov freezegun==0.3.9" + - "pip install --upgrade pytest==3.3.2 pytest-cov==2.5.1 codecov freezegun==0.3.11" - "pip install --editable ." - "python setup.py import_cldr" diff --git a/.travis.yml b/.travis.yml index 63dcd3ec0..c9c0a9b7b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ +dist: xenial language: python # Use travis docker infrastructure for greater speed @@ -18,8 +19,10 @@ matrix: env: - CDECIMAL=m3-cdecimal - os: linux + dist: trusty python: pypy - os: linux + dist: trusty python: pypy3 - os: linux python: 3.4 @@ -29,11 +32,13 @@ matrix: - PYTHON_TEST_FLAGS=-bb - os: linux python: 3.6 + - os: linux + python: 3.7 install: - bash .ci/deps.${TRAVIS_OS_NAME}.sh - pip install --upgrade pip - - pip install --upgrade $CDECIMAL pytest==3.3.2 pytest-cov==2.5.1 freezegun==0.3.9 + - pip install --upgrade $CDECIMAL pytest==3.3.2 pytest-cov==2.5.1 freezegun==0.3.11 - pip install --editable . script: diff --git a/setup.py b/setup.py index 27c7541bb..0032a3a05 100755 --- a/setup.py +++ b/setup.py @@ -50,6 +50,7 @@ def run(self): 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Libraries :: Python Modules', diff --git a/tox.ini b/tox.ini index b46fed704..87a53f0f9 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,12 @@ [tox] -envlist = py27, pypy, py34, py35, py36, pypy3, py27-cdecimal +envlist = py27, pypy, py34, py35, py36, py37, pypy3, py27-cdecimal [testenv] deps = pytest==3.3.2 pytest-cov==2.5.1 cdecimal: m3-cdecimal - freezegun==0.3.9 + freezegun==0.3.11 whitelist_externals = make commands = make clean-cldr test passenv = PYTHON_TEST_FLAGS From b7dda2a0de98344426359087a3d3c239b74de548 Mon Sep 17 00:00:00 2001 From: mondeja Date: Sat, 13 Oct 2018 14:49:30 +0200 Subject: [PATCH 065/139] Avoid KeyError trying to get data on WindowsXP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I've found this issue trying to compile documentation with Sphinx on WindowsXP with `make html` command. Some data dictionaries are void on my machine, so using lazy 'get' on those can prevent next `KeyError`: ``` C:\Path\to\my\project\doc>make html {'MapID': '8,9', 'Index': 165, 'Std': 'Hora estándar árabe', 'Display': '(GMT+04 :00) Abu Dhabi, Muscat', 'TZI': b'\x10\xff\xff\xff\x00\x00\x00\x00\xc4\xff\xff\x ff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x 00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'Dlt': 'Hora de verano árab e'} {'MapID': '-1,71', 'Index': 158, 'Std': 'Hora estándar árabe D', 'Display': '(GM T+03:00) Baghdad', 'TZI': b'L\xff\xff\xff\x00\x00\x00\x00\xc4\xff\xff\xff\x00\x0 0\n\x00\x00\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\ x01\x00\x03\x00\x00\x00\x00\x00\x00\x00', 'Dlt': 'Hora de verano árabe'} {'MapID': '12,13', 'Index': 195, 'Std': 'Hora estándar de Asia C.', 'Display': ' (GMT+06:00) Astana, Dhaka', 'TZI': b'\x98\xfe\xff\xff\x00\x00\x00\x00\xc4\xff\xf f\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0 0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'Dlt': 'Hora de verano d e Asia C.'} {} Traceback (most recent call last): File "c:\python34\lib\runpy.py", line 170, in _run_module_as_main "__main__", mod_spec) File "c:\python34\lib\runpy.py", line 85, in _run_code exec(code, run_globals) File "C:\Python34\Scripts\sphinx-build.exe\__main__.py", line 5, in File "c:\python34\lib\site-packages\sphinx\cmd\build.py", line 25, in from sphinx.application import Sphinx File "c:\python34\lib\site-packages\sphinx\application.py", line 29, in from sphinx.config import Config, check_unicode File "c:\python34\lib\site-packages\sphinx\config.py", line 28, in from sphinx.util.i18n import format_date File "c:\python34\lib\site-packages\sphinx\util\i18n.py", line 20, in import babel.dates File "c:\python34\lib\site-packages\babel\dates.py", line 29, in from babel.util import UTC, LOCALTZ File "c:\python34\lib\site-packages\babel\util.py", line 19, in from babel import localtime File "c:\python34\lib\site-packages\babel\localtime\__init__.py", line 74, in LOCALTZ = get_localzone() File "c:\python34\lib\site-packages\babel\localtime\__init__.py", line 70, in get_localzone return _get_localzone() File "c:\python34\lib\site-packages\babel\localtime\_win32.py", line 97, in _g et_localzone return pytz.timezone(get_localzone_name()) File "c:\python34\lib\site-packages\babel\localtime\_win32.py", line 70, in ge t_localzone_name if data['Std'] == tzwin: KeyError: 'Std' ``` --- babel/localtime/_win32.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/babel/localtime/_win32.py b/babel/localtime/_win32.py index 3752dffac..65cc0885d 100644 --- a/babel/localtime/_win32.py +++ b/babel/localtime/_win32.py @@ -66,7 +66,7 @@ def get_localzone_name(): sub = winreg.OpenKey(tzkey, subkey) data = valuestodict(sub) sub.Close() - if data['Std'] == tzwin: + if data.get('Std', None) == tzwin: tzkeyname = subkey break From 09528e5e7096ef047b87893ff55f31adffb5d598 Mon Sep 17 00:00:00 2001 From: Alberto Mardegan Date: Thu, 24 Jan 2019 22:32:25 +0300 Subject: [PATCH 066/139] Download CLDR 34.0 --- babel/numbers.py | 2 +- babel/units.py | 2 +- scripts/download_import_cldr.py | 6 +++--- tests/test_numbers.py | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/babel/numbers.py b/babel/numbers.py index 4306e9360..e5650dd52 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -448,7 +448,7 @@ def format_currency( >>> format_currency(1099.98, 'JPY', locale='en_US') u'\\xa51,100' >>> format_currency(1099.98, 'COP', u'#,##0.00', locale='es_ES') - u'1.100' + u'1.099,98' However, the number of decimal digits can be overriden from the currency information, by setting the last parameter to ``False``: diff --git a/babel/units.py b/babel/units.py index be851c230..e58bf81c2 100644 --- a/babel/units.py +++ b/babel/units.py @@ -82,7 +82,7 @@ def format_unit(value, measurement_unit, length='long', format=None, locale=LC_N >>> from babel._compat import decimal >>> format_unit(decimal.Decimal("-42.774"), 'temperature-celsius', 'short', format='#.0', locale='fr') - u'-42,8 \\xb0C' + u'-42,8\\u202f\\xb0C' The locale's usual pluralization rules are respected. diff --git a/scripts/download_import_cldr.py b/scripts/download_import_cldr.py index b6e6f4301..f0d9b10bc 100755 --- a/scripts/download_import_cldr.py +++ b/scripts/download_import_cldr.py @@ -13,9 +13,9 @@ from urllib import urlretrieve -URL = 'https://unicode.org/Public/cldr/33/core.zip' -FILENAME = 'core-33.0.zip' -FILESUM = 'fa3490082c086d21257153609642f54fcf788fcfda4966fe67f3f6daca0d58b9' +URL = 'https://unicode.org/Public/cldr/34/core.zip' +FILENAME = 'cldr-core-34.0.zip' +FILESUM = '1d79f6ce294ff350a3ee59d7dd69451fd24776f7105fd6a55c255ffb813c03ba' BLKSIZE = 131072 diff --git a/tests/test_numbers.py b/tests/test_numbers.py index d0f24bd54..29bdc2447 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -209,7 +209,7 @@ def test_list_currencies(): assert list_currencies(locale='pa_Arab') == {'PKR', 'INR', 'EUR'} - assert len(list_currencies()) == 300 + assert len(list_currencies()) == 303 def test_validate_currency(): @@ -412,7 +412,7 @@ def test_format_currency_format_type(): assert (numbers.format_currency(1099.98, 'JPY', locale='en_US') == u'\xa51,100') assert (numbers.format_currency(1099.98, 'COP', u'#,##0.00', locale='es_ES') - == u'1.100') + == u'1.099,98') assert (numbers.format_currency(1099.98, 'JPY', locale='en_US', currency_digits=False) == u'\xa51,099.98') From cece931c2fef3a612e5c7da9baccf4eed41b3647 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sun, 30 Dec 2018 08:28:40 -0800 Subject: [PATCH 067/139] Remove unused imports throughout tests --- tests/messages/test_pofile.py | 1 - tests/test_localedata.py | 2 +- tests/test_numbers.py | 3 +-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/messages/test_pofile.py b/tests/messages/test_pofile.py index 9872e9d54..a6973d7ec 100644 --- a/tests/messages/test_pofile.py +++ b/tests/messages/test_pofile.py @@ -13,7 +13,6 @@ from datetime import datetime import unittest -import sys from babel.core import Locale from babel.messages.catalog import Catalog, Message diff --git a/tests/test_localedata.py b/tests/test_localedata.py index 37c1304e9..dceb984be 100644 --- a/tests/test_localedata.py +++ b/tests/test_localedata.py @@ -15,7 +15,7 @@ import random from operator import methodcaller -from babel import localedata, numbers +from babel import localedata class MergeResolveTestCase(unittest.TestCase): diff --git a/tests/test_numbers.py b/tests/test_numbers.py index 29bdc2447..99aa96b7e 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -16,11 +16,10 @@ from datetime import date -from babel import Locale, localedata, numbers +from babel import localedata, numbers from babel.numbers import ( list_currencies, validate_currency, UnknownCurrencyError, is_currency, normalize_currency, get_currency_precision, get_decimal_precision, get_currency_unit_pattern) -from babel.localedata import locale_identifiers from babel._compat import decimal From e4d834d4c1bffe05e71ee2ec21449b6b0b115e99 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Wed, 10 Oct 2018 20:34:09 -0700 Subject: [PATCH 068/139] Remove unnecessary compat shim 'binary_type' The bytes type is available on all supported Pythons. On Python 2, it is an alias of str, same as binary_type. By removing the shim, makes the code more forward compatible. --- babel/_compat.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/babel/_compat.py b/babel/_compat.py index 849f749e3..11b4d7a6b 100644 --- a/babel/_compat.py +++ b/babel/_compat.py @@ -8,7 +8,6 @@ if not PY2: text_type = str - binary_type = bytes string_types = (str,) integer_types = (int, ) @@ -33,7 +32,6 @@ else: text_type = unicode - binary_type = str string_types = (str, unicode) integer_types = (int, long) @@ -63,7 +61,7 @@ def force_text(s, encoding='utf-8', errors='strict'): if isinstance(s, text_type): return s - if isinstance(s, binary_type): + if isinstance(s, bytes): return s.decode(encoding, errors) return text_type(s) From 60669579a7e3189112ce4ecb983928040af79e73 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sun, 30 Dec 2018 10:54:34 -0500 Subject: [PATCH 069/139] Replace odict with Python's collection.OrderedDict The odict class duplicates collection.OrderedDict from Python's standard lib. Simplify the code by using builtin Python features. https://docs.python.org/3/library/collections.html#collections.OrderedDict --- CHANGES | 13 +++++++ babel/messages/catalog.py | 9 ++--- babel/messages/frontend.py | 5 +-- babel/util.py | 74 ++------------------------------------ tests/test_util.py | 14 -------- 5 files changed, 24 insertions(+), 91 deletions(-) diff --git a/CHANGES b/CHANGES index 6d9b0e5fa..622b4c573 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,19 @@ Babel Changelog =============== +UNRELEASED +---------- + +Possibly incompatible changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These may be backward incompatible in some cases, as some more-or-less internal +APIs have changed. Please feel free to file issues if you bump into anything +strange and we'll try to help! + +* General: Internal uses of ``babel.util.odict`` have been replaced with + ``collections.OrderedDict`` from The Python standard library. + Version 2.6.0 ------------- diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py index 6d61a3826..649d0e2fe 100644 --- a/babel/messages/catalog.py +++ b/babel/messages/catalog.py @@ -13,6 +13,7 @@ import time from cgi import parse_header +from collections import OrderedDict from datetime import datetime, time as time_ from difflib import get_close_matches from email import message_from_string @@ -22,7 +23,7 @@ from babel.core import Locale, UnknownLocaleError from babel.dates import format_datetime from babel.messages.plurals import get_plural -from babel.util import odict, distinct, LOCALTZ, FixedOffsetTimezone +from babel.util import distinct, LOCALTZ, FixedOffsetTimezone from babel._compat import string_types, number_types, PY2, cmp, text_type, force_text __all__ = ['Message', 'Catalog', 'TranslationError'] @@ -269,7 +270,7 @@ def __init__(self, locale=None, domain=None, header_comment=DEFAULT_HEADER, self.domain = domain self.locale = locale self._header_comment = header_comment - self._messages = odict() + self._messages = OrderedDict() self.project = project or 'PROJECT' self.version = version or 'VERSION' @@ -295,7 +296,7 @@ def __init__(self, locale=None, domain=None, header_comment=DEFAULT_HEADER, self.revision_date = revision_date self.fuzzy = fuzzy - self.obsolete = odict() # Dictionary of obsolete messages + self.obsolete = OrderedDict() # Dictionary of obsolete messages self._num_plurals = None self._plural_expr = None @@ -754,7 +755,7 @@ def update(self, template, no_fuzzy_matching=False, update_header_comment=False) """ messages = self._messages remaining = messages.copy() - self._messages = odict() + self._messages = OrderedDict() # Prepare for fuzzy matching fuzzy_candidates = [] diff --git a/babel/messages/frontend.py b/babel/messages/frontend.py index 16cb90054..9213cca41 100644 --- a/babel/messages/frontend.py +++ b/babel/messages/frontend.py @@ -17,6 +17,7 @@ import shutil import sys import tempfile +from collections import OrderedDict from datetime import datetime from locale import getpreferredencoding @@ -28,7 +29,7 @@ from babel.messages.extract import DEFAULT_KEYWORDS, DEFAULT_MAPPING, check_and_call_extract_file, extract_from_dir from babel.messages.mofile import write_mo from babel.messages.pofile import read_po, write_po -from babel.util import LOCALTZ, odict +from babel.util import LOCALTZ from distutils import log as distutils_log from distutils.cmd import Command as _Command from distutils.errors import DistutilsOptionError, DistutilsSetupError @@ -965,7 +966,7 @@ def parse_mapping(fileobj, filename=None): options_map = {} parser = RawConfigParser() - parser._sections = odict(parser._sections) # We need ordered sections + parser._sections = OrderedDict(parser._sections) # We need ordered sections if PY2: parser.readfp(fileobj, filename) diff --git a/babel/util.py b/babel/util.py index fb93f16d5..c443b6902 100644 --- a/babel/util.py +++ b/babel/util.py @@ -10,6 +10,7 @@ """ import codecs +import collections from datetime import timedelta, tzinfo import os import re @@ -220,77 +221,8 @@ def wraptext(text, width=70, initial_indent='', subsequent_indent=''): return wrapper.wrap(text) -class odict(dict): - """Ordered dict implementation. - - :see: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/107747 - """ - - def __init__(self, data=None): - dict.__init__(self, data or {}) - self._keys = list(dict.keys(self)) - - def __delitem__(self, key): - dict.__delitem__(self, key) - self._keys.remove(key) - - def __setitem__(self, key, item): - new_key = key not in self - dict.__setitem__(self, key, item) - if new_key: - self._keys.append(key) - - def __iter__(self): - return iter(self._keys) - iterkeys = __iter__ - - def clear(self): - dict.clear(self) - self._keys = [] - - def copy(self): - d = odict() - d.update(self) - return d - - def items(self): - return zip(self._keys, self.values()) - - def iteritems(self): - return izip(self._keys, self.itervalues()) - - def keys(self): - return self._keys[:] - - def pop(self, key, default=missing): - try: - value = dict.pop(self, key) - self._keys.remove(key) - return value - except KeyError as e: - if default == missing: - raise e - else: - return default - - def popitem(self, key): - self._keys.remove(key) - return dict.popitem(key) - - def setdefault(self, key, failobj=None): - dict.setdefault(self, key, failobj) - if key not in self._keys: - self._keys.append(key) - - def update(self, dict): - for (key, val) in dict.items(): - self[key] = val - - def values(self): - return map(self.get, self._keys) - - def itervalues(self): - return imap(self.get, self._keys) +# TODO (Babel 3.x): Remove this re-export +odict = collections.OrderedDict class FixedOffsetTimezone(tzinfo): diff --git a/tests/test_util.py b/tests/test_util.py index ef591102e..0fc59e5c4 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -38,20 +38,6 @@ def test_pathmatch(): assert not util.pathmatch('./foo/**.py', 'blah/foo/bar/baz.py') -def test_odict_pop(): - odict = util.odict() - odict[0] = 1 - value = odict.pop(0) - assert 1 == value - assert [] == list(odict.items()) - assert odict.pop(2, None) is None - try: - odict.pop(2) - assert False - except KeyError: - assert True - - class FixedOffsetTimezoneTestCase(unittest.TestCase): def test_zone_negative_offset(self): From 60af039d9b282b46e37f6f8556005c339f5c24a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Fri, 1 Mar 2019 09:43:00 +0100 Subject: [PATCH 070/139] frontend: Add omit-header to update_catalog It is the same option as in extract_message but for when updating the catalog. --- babel/messages/frontend.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/babel/messages/frontend.py b/babel/messages/frontend.py index 9213cca41..f673f900c 100644 --- a/babel/messages/frontend.py +++ b/babel/messages/frontend.py @@ -647,6 +647,8 @@ class update_catalog(Command): ('output-file=', 'o', "name of the output file (default " "'//LC_MESSAGES/.po')"), + ('omit-header', None, + "do not include msgid "" entry in header"), ('locale=', 'l', 'locale of the catalog to compile'), ('width=', 'w', @@ -661,15 +663,19 @@ class update_catalog(Command): ('update-header-comment', None, 'update target header comment'), ('previous', None, - 'keep previous msgids of translated messages') + 'keep previous msgids of translated messages'), + ] + boolean_options = [ + 'omit-header', 'no-wrap', 'ignore-obsolete', 'no-fuzzy-matching', + 'previous', 'update-header-comment', ] - boolean_options = ['no-wrap', 'ignore-obsolete', 'no-fuzzy-matching', 'previous', 'update-header-comment'] def initialize_options(self): self.domain = 'messages' self.input_file = None self.output_dir = None self.output_file = None + self.omit_header = False self.locale = None self.width = None self.no_wrap = False @@ -740,6 +746,7 @@ def run(self): try: with open(tmpname, 'wb') as tmpfile: write_po(tmpfile, catalog, + omit_header=self.omit_header, ignore_obsolete=self.ignore_obsolete, include_previous=self.previous, width=self.width) except: From ea5bc4988bf7c3be84d296eb874aa11ed86c907d Mon Sep 17 00:00:00 2001 From: BT-sschmid <39914536+BT-sschmid@users.noreply.github.com> Date: Fri, 1 Mar 2019 12:00:44 +0100 Subject: [PATCH 071/139] fixes #619 wrong weeknumber for 31.12.2018 (#621) The weeknumber was calculated to 53, but by definition the value must compute to 1. the fix will compute the weeknumber by using date.isocalendar if locale.first_week_day == 0. Also the computation of the year format 'YYYY' is replaced by isocalendar. --- babel/dates.py | 16 +++++++++++++--- tests/test_dates.py | 9 +++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/babel/dates.py b/babel/dates.py index 4bae3eaa0..ec86991a0 100644 --- a/babel/dates.py +++ b/babel/dates.py @@ -1319,9 +1319,7 @@ def format_era(self, char, num): def format_year(self, char, num): value = self.value.year if char.isupper(): - week = self.get_week_number(self.get_day_of_year()) - if week == 0: - value -= 1 + value = self.value.isocalendar()[0] year = self.format(value, num) if num == 2: year = year[-2:] @@ -1505,8 +1503,20 @@ def get_week_number(self, day_of_period, day_of_week=None): if first_day < 0: first_day += 7 week_number = (day_of_period + first_day - 1) // 7 + if 7 - first_day >= self.locale.min_week_days: week_number += 1 + + if self.locale.first_week_day == 0: + # Correct the weeknumber in case of iso-calendar usage (first_week_day=0). + # If the weeknumber exceeds the maximum number of weeks for the given year + # we must count from zero.For example the above calculation gives week 53 + # for 2018-12-31. By iso-calender definition 2018 has a max of 52 + # weeks, thus the weeknumber must be 53-52=1. + max_weeks = date(year=self.value.year, day=28, month=12).isocalendar()[1] + if week_number > max_weeks: + week_number -= max_weeks + return week_number diff --git a/tests/test_dates.py b/tests/test_dates.py index b8c293ba6..d77c0ea42 100644 --- a/tests/test_dates.py +++ b/tests/test_dates.py @@ -79,6 +79,15 @@ def test_week_of_year_last_us_extra_week(self): fmt = dates.DateTimeFormat(d, locale='en_US') self.assertEqual('53', fmt['w']) + def test_week_of_year_de_first_us_last_with_year(self): + d = date(2018,12,31) + fmt = dates.DateTimeFormat(d, locale='de_DE') + self.assertEqual('1', fmt['w']) + self.assertEqual('2019', fmt['YYYY']) + fmt = dates.DateTimeFormat(d, locale='en_US') + self.assertEqual('53', fmt['w']) + self.assertEqual('2018',fmt['yyyy']) + def test_week_of_month_first(self): d = date(2006, 1, 8) fmt = dates.DateTimeFormat(d, locale='de_DE') From 69cda1d2082444ed99836d92135f175d20555463 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 1 Mar 2019 13:38:31 +0200 Subject: [PATCH 072/139] Skip alt=... for week data (minDays, firstDay, weekendStart, weekendEnd) Fixes #624 --- scripts/import_cldr.py | 8 ++++++++ tests/test_dates.py | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py index a3e8f69ea..ed2e19c04 100755 --- a/scripts/import_cldr.py +++ b/scripts/import_cldr.py @@ -520,18 +520,26 @@ def parse_dates(data, tree, sup, regions, territory): week_data = data.setdefault('week_data', {}) supelem = sup.find('.//weekData') for elem in supelem.findall('minDays'): + if _should_skip_elem(elem): + continue territories = elem.attrib['territories'].split() if territory in territories or any([r in territories for r in regions]): week_data['min_days'] = int(elem.attrib['count']) for elem in supelem.findall('firstDay'): + if _should_skip_elem(elem): + continue territories = elem.attrib['territories'].split() if territory in territories or any([r in territories for r in regions]): week_data['first_day'] = weekdays[elem.attrib['day']] for elem in supelem.findall('weekendStart'): + if _should_skip_elem(elem): + continue territories = elem.attrib['territories'].split() if territory in territories or any([r in territories for r in regions]): week_data['weekend_start'] = weekdays[elem.attrib['day']] for elem in supelem.findall('weekendEnd'): + if _should_skip_elem(elem): + continue territories = elem.attrib['territories'].split() if territory in territories or any([r in territories for r in regions]): week_data['weekend_end'] = weekdays[elem.attrib['day']] diff --git a/tests/test_dates.py b/tests/test_dates.py index d77c0ea42..e8592091d 100644 --- a/tests/test_dates.py +++ b/tests/test_dates.py @@ -799,3 +799,9 @@ def test_russian_week_numbering(): v = date(2017, 1, 1) assert dates.format_date(v, format='YYYY-ww',locale='ru_RU') == '2016-52' # This would have returned 2017-01 prior to CLDR 32 assert dates.format_date(v, format='YYYY-ww',locale='de_DE') == '2016-52' + + +def test_en_gb_first_weekday(): + assert Locale.parse('en').first_week_day == 0 # Monday in general + assert Locale.parse('en_US').first_week_day == 6 # Sunday in the US + assert Locale.parse('en_GB').first_week_day == 0 # Monday in the UK \ No newline at end of file From b33c95530b0d5e717fd36f5b775137c83c3bcf67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Thu, 14 Mar 2019 19:54:42 +0100 Subject: [PATCH 073/139] Fix conftest.py compatibility with pytest 4.3 While pytest in tox.ini is explicitly set to be 3.3.2, in Fedora 31 we are about to update to a newer version of pytest. In order to be able to test babel, we need pytest 4.3 support. This adds support for pytest 4.3 without breaking support for 3.3.2. --- tests/conftest.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index be93b2be7..5b14b1ca7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,6 +10,9 @@ def os_environ(monkeypatch): def pytest_generate_tests(metafunc): - if hasattr(metafunc.function, "all_locales"): - from babel.localedata import locale_identifiers - metafunc.parametrize("locale", list(locale_identifiers())) + if hasattr(metafunc.function, "pytestmark"): + for mark in metafunc.function.pytestmark: + if mark.name == "all_locales": + from babel.localedata import locale_identifiers + metafunc.parametrize("locale", list(locale_identifiers())) + break From 897fa81beb6236ff4f4e6402c49e257459dcacb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Thu, 14 Mar 2019 20:24:23 +0100 Subject: [PATCH 074/139] Tests: Update pytest and pytest-cov --- .ci/appveyor.yml | 2 +- .travis.yml | 2 +- tox.ini | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.ci/appveyor.yml b/.ci/appveyor.yml index d4135e080..eb9ba8656 100644 --- a/.ci/appveyor.yml +++ b/.ci/appveyor.yml @@ -27,7 +27,7 @@ install: - "python --version" - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" # Build data files - - "pip install --upgrade pytest==3.3.2 pytest-cov==2.5.1 codecov freezegun==0.3.11" + - "pip install --upgrade pytest==4.3.1 pytest-cov==2.6.1 codecov freezegun==0.3.11" - "pip install --editable ." - "python setup.py import_cldr" diff --git a/.travis.yml b/.travis.yml index c9c0a9b7b..9fad4fc30 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,7 +38,7 @@ matrix: install: - bash .ci/deps.${TRAVIS_OS_NAME}.sh - pip install --upgrade pip - - pip install --upgrade $CDECIMAL pytest==3.3.2 pytest-cov==2.5.1 freezegun==0.3.11 + - pip install --upgrade $CDECIMAL pytest==4.3.1 pytest-cov==2.6.1 freezegun==0.3.11 - pip install --editable . script: diff --git a/tox.ini b/tox.ini index 87a53f0f9..b3f8041f4 100644 --- a/tox.ini +++ b/tox.ini @@ -3,8 +3,8 @@ envlist = py27, pypy, py34, py35, py36, py37, pypy3, py27-cdecimal [testenv] deps = - pytest==3.3.2 - pytest-cov==2.5.1 + pytest==4.3.1 + pytest-cov==2.6.1 cdecimal: m3-cdecimal freezegun==0.3.11 whitelist_externals = make From 8c59f09dcde2a22c925613b3ea44be80ccfeafce Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 27 May 2019 10:22:24 +0300 Subject: [PATCH 075/139] Travis: test on Python 3.8-dev --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 9fad4fc30..174913952 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,6 +34,8 @@ matrix: python: 3.6 - os: linux python: 3.7 + - os: linux + python: 3.8-dev install: - bash .ci/deps.${TRAVIS_OS_NAME}.sh From cfe74846d606493d40f85115170da59ee38d78ce Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 27 May 2019 10:29:26 +0300 Subject: [PATCH 076/139] Use CLDR 35.1 --- scripts/download_import_cldr.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/download_import_cldr.py b/scripts/download_import_cldr.py index f0d9b10bc..f118c6900 100755 --- a/scripts/download_import_cldr.py +++ b/scripts/download_import_cldr.py @@ -13,9 +13,9 @@ from urllib import urlretrieve -URL = 'https://unicode.org/Public/cldr/34/core.zip' -FILENAME = 'cldr-core-34.0.zip' -FILESUM = '1d79f6ce294ff350a3ee59d7dd69451fd24776f7105fd6a55c255ffb813c03ba' +URL = 'https://unicode.org/Public/cldr/35.1/core.zip' +FILENAME = 'cldr-core-35.1.zip' +FILESUM = 'e2ede8cb8f9c29157e281ee9e696ce540a72c598841bed595a406b710eea87b0' BLKSIZE = 131072 From bd2c44b71cf8d22235df4f187c814b3e0e490683 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 27 May 2019 10:50:41 +0300 Subject: [PATCH 077/139] Cache locale_identifiers() Fixes #620 --- babel/localedata.py | 17 ++++++++++++++--- tests/test_localedata.py | 26 ++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/babel/localedata.py b/babel/localedata.py index 2a8c4423d..03381e2df 100644 --- a/babel/localedata.py +++ b/babel/localedata.py @@ -57,13 +57,24 @@ def locale_identifiers(): """Return a list of all locale identifiers for which locale data is available. + This data is cached after the first invocation in `locale_identifiers.cache`. + + Removing the `locale_identifiers.cache` attribute or setting it to `None` + will cause this function to re-read the list from disk. + .. versionadded:: 0.8.1 :return: a list of locale identifiers (strings) """ - return [stem for stem, extension in [ - os.path.splitext(filename) for filename in os.listdir(_dirname) - ] if extension == '.dat' and stem != 'root'] + data = getattr(locale_identifiers, 'cache', None) + if data is None: + locale_identifiers.cache = data = [ + stem + for stem, extension in + (os.path.splitext(filename) for filename in os.listdir(_dirname)) + if extension == '.dat' and stem != 'root' + ] + return data def load(name, merge_inherited=True): diff --git a/tests/test_localedata.py b/tests/test_localedata.py index dceb984be..bffe7633f 100644 --- a/tests/test_localedata.py +++ b/tests/test_localedata.py @@ -17,6 +17,7 @@ from babel import localedata + class MergeResolveTestCase(unittest.TestCase): def test_merge_items(self): @@ -78,6 +79,7 @@ def test_locale_identification(): for l in localedata.locale_identifiers(): assert localedata.exists(l) + def test_unique_ids(): # Check all locale IDs are uniques. all_ids = localedata.locale_identifiers() @@ -93,6 +95,7 @@ def test_mixedcased_locale(): methodcaller(random.choice(['lower', 'upper']))(c) for c in l]) assert localedata.exists(locale_id) + def test_locale_argument_acceptance(): # Testing None input. normalized_locale = localedata.normalize_locale(None) @@ -105,3 +108,26 @@ def test_locale_argument_acceptance(): assert normalized_locale is None locale_exist = localedata.exists(['en_us', None]) assert locale_exist == False + + +def test_locale_identifiers_cache(monkeypatch): + original_listdir = localedata.os.listdir + listdir_calls = [] + def listdir_spy(*args): + rv = original_listdir(*args) + listdir_calls.append((args, rv)) + return rv + monkeypatch.setattr(localedata.os, 'listdir', listdir_spy) + + # In case we've already run some tests... + if hasattr(localedata.locale_identifiers, 'cache'): + del localedata.locale_identifiers.cache + + assert not listdir_calls + assert localedata.locale_identifiers() + assert len(listdir_calls) == 1 + assert localedata.locale_identifiers() is localedata.locale_identifiers.cache + assert len(listdir_calls) == 1 + localedata.locale_identifiers.cache = None + assert localedata.locale_identifiers() + assert len(listdir_calls) == 2 From 4fa0c6e5a2d3ae4c10ca160dabe4e3d169dace81 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 27 May 2019 11:01:38 +0300 Subject: [PATCH 078/139] get_display_name(): Don't attempt to concatenate variant information to None Fixes #601 --- babel/core.py | 2 +- tests/test_core.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/babel/core.py b/babel/core.py index d028c07da..211849bce 100644 --- a/babel/core.py +++ b/babel/core.py @@ -379,7 +379,7 @@ def get_display_name(self, locale=None): locale = self locale = Locale.parse(locale) retval = locale.languages.get(self.language) - if self.territory or self.script or self.variant: + if retval and (self.territory or self.script or self.variant): details = [] if self.script: details.append(locale.scripts.get(self.script)) diff --git a/tests/test_core.py b/tests/test_core.py index f22ab309b..fc637d245 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -317,3 +317,13 @@ def find_class(self, module, name): with open(filename, 'rb') as f: return Unpickler(f).load() + + +def test_issue_601_no_language_name_but_has_variant(): + # kw_GB has a variant for Finnish but no actual language name for Finnish, + # so `get_display_name()` previously crashed with a TypeError as it attempted + # to concatenate " (Finnish)" to None. + # Instead, it's better to return None altogether, as we can't reliably format + # part of a language name. + + assert Locale.parse('fi_FI').get_display_name('kw_GB') == None From 1d7a4da9e1a563e800dc6475bc95e1fa58a07473 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 27 May 2019 11:19:08 +0300 Subject: [PATCH 079/139] Add comparison operators to _NormalizedString Based on @hoangduytranuk's original implementation. Fixes #612 --- babel/messages/pofile.py | 33 ++++++++++++++++++++++-- tests/messages/test_normalized_string.py | 17 ++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 tests/messages/test_normalized_string.py diff --git a/babel/messages/pofile.py b/babel/messages/pofile.py index fe376318a..2943fa2ec 100644 --- a/babel/messages/pofile.py +++ b/babel/messages/pofile.py @@ -16,8 +16,7 @@ from babel.messages.catalog import Catalog, Message from babel.util import wraptext -from babel._compat import text_type - +from babel._compat import text_type, cmp def unescape(string): @@ -99,6 +98,36 @@ def denormalize(self): def __nonzero__(self): return bool(self._strs) + __bool__ = __nonzero__ + + def __repr__(self): + return os.linesep.join(self._strs) + + def __cmp__(self, other): + if not other: + return 1 + + return cmp(text_type(self), text_type(other)) + + def __gt__(self, other): + return self.__cmp__(other) > 0 + + def __lt__(self, other): + return self.__cmp__(other) < 0 + + def __ge__(self, other): + return self.__cmp__(other) >= 0 + + def __le__(self, other): + return self.__cmp__(other) <= 0 + + def __eq__(self, other): + return self.__cmp__(other) == 0 + + def __ne__(self, other): + return self.__cmp__(other) != 0 + + class PoFileParser(object): """Support class to read messages from a ``gettext`` PO (portable object) file diff --git a/tests/messages/test_normalized_string.py b/tests/messages/test_normalized_string.py new file mode 100644 index 000000000..9c95672b4 --- /dev/null +++ b/tests/messages/test_normalized_string.py @@ -0,0 +1,17 @@ +from babel.messages.pofile import _NormalizedString + + +def test_normalized_string(): + ab1 = _NormalizedString('a', 'b ') + ab2 = _NormalizedString('a', ' b') + ac1 = _NormalizedString('a', 'c') + ac2 = _NormalizedString(' a', 'c ') + z = _NormalizedString() + assert ab1 == ab2 and ac1 == ac2 # __eq__ + assert ab1 < ac1 # __lt__ + assert ac1 > ab2 # __gt__ + assert ac1 >= ac2 # __ge__ + assert ab1 <= ab2 # __le__ + assert ab1 != ac1 # __ne__ + assert not z # __nonzero__ / __bool__ + assert sorted([ab1, ab2, ac1]) # the sort order is not stable so we can't really check it, just that we can sort From 509f20915c18f452d6d2d247289423fc0c4b8686 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 27 May 2019 11:24:33 +0300 Subject: [PATCH 080/139] pofile: don't crash when message.locations can't be sorted Fixes #606 --- babel/messages/pofile.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/babel/messages/pofile.py b/babel/messages/pofile.py index 2943fa2ec..9eb3a5cb3 100644 --- a/babel/messages/pofile.py +++ b/babel/messages/pofile.py @@ -581,7 +581,16 @@ def _write_message(message, prefix=''): if not no_location: locs = [] - for filename, lineno in sorted(message.locations): + + # Attempt to sort the locations. If we can't do that, for instance + # because there are mixed integers and Nones or whatnot (see issue #606) + # then give up, but also don't just crash. + try: + locations = sorted(message.locations) + except TypeError: # e.g. "TypeError: unorderable types: NoneType() < int()" + locations = message.locations + + for filename, lineno in locations: if lineno and include_lineno: locs.append(u'%s:%d' % (filename.replace(os.sep, '/'), lineno)) else: From 665c2ac12b95267383e726722d861e54f2d04fee Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 27 May 2019 13:01:55 +0300 Subject: [PATCH 081/139] test_frontend: DRY out path generation --- tests/messages/test_frontend.py | 145 +++++++++++++------------------- 1 file changed, 60 insertions(+), 85 deletions(-) diff --git a/tests/messages/test_frontend.py b/tests/messages/test_frontend.py index 3e3cc8bd5..fa0112c2e 100644 --- a/tests/messages/test_frontend.py +++ b/tests/messages/test_frontend.py @@ -33,16 +33,22 @@ from babel.messages.pofile import read_po, write_po from babel._compat import StringIO - this_dir = os.path.abspath(os.path.dirname(__file__)) +data_dir = os.path.join(this_dir, 'data') +project_dir = os.path.join(data_dir, 'project') +i18n_dir = os.path.join(project_dir, 'i18n') +pot_file = os.path.join(i18n_dir, 'temp.pot') + + +def _po_file(locale): + return os.path.join(i18n_dir, locale, 'LC_MESSAGES', 'messages.po') class CompileCatalogTestCase(unittest.TestCase): def setUp(self): self.olddir = os.getcwd() - self.datadir = os.path.join(this_dir, 'data') - os.chdir(self.datadir) + os.chdir(data_dir) _global_log.threshold = 5 # shut up distutils logging self.dist = Distribution(dict( @@ -71,8 +77,7 @@ class ExtractMessagesTestCase(unittest.TestCase): def setUp(self): self.olddir = os.getcwd() - self.datadir = os.path.join(this_dir, 'data') - os.chdir(self.datadir) + os.chdir(data_dir) _global_log.threshold = 5 # shut up distutils logging self.dist = Distribution(dict( @@ -84,20 +89,13 @@ def setUp(self): self.cmd.initialize_options() def tearDown(self): - pot_file = self._pot_file() if os.path.isfile(pot_file): os.unlink(pot_file) os.chdir(self.olddir) - def _i18n_dir(self): - return os.path.join(self.datadir, 'project', 'i18n') - - def _pot_file(self): - return os.path.join(self._i18n_dir(), 'temp.pot') - def assert_pot_file_exists(self): - assert os.path.isfile(self._pot_file()) + assert os.path.isfile(pot_file) def test_neither_default_nor_custom_keywords(self): self.cmd.output_file = 'dummy' @@ -119,27 +117,27 @@ def test_invalid_file_or_dir_input_path(self): self.assertRaises(DistutilsOptionError, self.cmd.finalize_options) def test_input_paths_is_treated_as_list(self): - self.cmd.input_paths = self.datadir - self.cmd.output_file = self._pot_file() + self.cmd.input_paths = data_dir + self.cmd.output_file = pot_file self.cmd.finalize_options() self.cmd.run() - with open(self._pot_file(), po_file_read_mode) as f: + with open(pot_file, po_file_read_mode) as f: catalog = read_po(f) msg = catalog.get('bar') self.assertEqual(1, len(msg.locations)) self.assertTrue('file1.py' in msg.locations[0][0]) def test_input_paths_handle_spaces_after_comma(self): - self.cmd.input_paths = '%s, %s' % (this_dir, self.datadir) - self.cmd.output_file = self._pot_file() + self.cmd.input_paths = '%s, %s' % (this_dir, data_dir) + self.cmd.output_file = pot_file self.cmd.finalize_options() - self.assertEqual([this_dir, self.datadir], self.cmd.input_paths) + self.assertEqual([this_dir, data_dir], self.cmd.input_paths) def test_input_dirs_is_alias_for_input_paths(self): self.cmd.input_dirs = this_dir - self.cmd.output_file = self._pot_file() + self.cmd.output_file = pot_file self.cmd.finalize_options() # Gets listified in `finalize_options`: assert self.cmd.input_paths == [self.cmd.input_dirs] @@ -147,7 +145,7 @@ def test_input_dirs_is_alias_for_input_paths(self): def test_input_dirs_is_mutually_exclusive_with_input_paths(self): self.cmd.input_dirs = this_dir self.cmd.input_paths = this_dir - self.cmd.output_file = self._pot_file() + self.cmd.output_file = pot_file self.assertRaises(DistutilsOptionError, self.cmd.finalize_options) @freeze_time("1994-11-11") @@ -204,7 +202,7 @@ def test_extraction_with_default_mapping(self): 'year': time.strftime('%Y'), 'date': format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')} - with open(self._pot_file(), po_file_read_mode) as f: + with open(pot_file, po_file_read_mode) as f: actual_content = f.read() self.assertEqual(expected_content, actual_content) @@ -257,7 +255,7 @@ def test_extraction_with_mapping_file(self): 'year': time.strftime('%Y'), 'date': format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')} - with open(self._pot_file(), po_file_read_mode) as f: + with open(pot_file, po_file_read_mode) as f: actual_content = f.read() self.assertEqual(expected_content, actual_content) @@ -315,7 +313,7 @@ def test_extraction_with_mapping_dict(self): 'year': time.strftime('%Y'), 'date': format_datetime(datetime(1994, 11, 11, 00, 00), 'yyyy-MM-dd HH:mmZ', tzinfo=LOCALTZ, locale='en')} - with open(self._pot_file(), po_file_read_mode) as f: + with open(pot_file, po_file_read_mode) as f: actual_content = f.read() self.assertEqual(expected_content, actual_content) @@ -346,7 +344,7 @@ def test_extraction_add_location_file(self): msgstr[1] "" """ - with open(self._pot_file(), po_file_read_mode) as f: + with open(pot_file, po_file_read_mode) as f: actual_content = f.read() self.assertEqual(expected_content, actual_content) @@ -355,8 +353,7 @@ class InitCatalogTestCase(unittest.TestCase): def setUp(self): self.olddir = os.getcwd() - self.datadir = os.path.join(this_dir, 'data') - os.chdir(self.datadir) + os.chdir(data_dir) _global_log.threshold = 5 # shut up distutils logging self.dist = Distribution(dict( @@ -369,19 +366,12 @@ def setUp(self): def tearDown(self): for dirname in ['en_US', 'ja_JP', 'lv_LV']: - locale_dir = os.path.join(self._i18n_dir(), dirname) + locale_dir = os.path.join(i18n_dir, dirname) if os.path.isdir(locale_dir): shutil.rmtree(locale_dir) os.chdir(self.olddir) - def _i18n_dir(self): - return os.path.join(self.datadir, 'project', 'i18n') - - def _po_file(self, locale): - return os.path.join(self._i18n_dir(), locale, 'LC_MESSAGES', - 'messages.po') - def test_no_input_file(self): self.cmd.locale = 'en_US' self.cmd.output_file = 'dummy' @@ -401,7 +391,7 @@ def test_with_output_dir(self): self.cmd.finalize_options() self.cmd.run() - po_file = self._po_file('en_US') + po_file = _po_file('en_US') assert os.path.isfile(po_file) expected_content = r"""# English (United States) translations for TestProject. @@ -453,7 +443,7 @@ def test_keeps_catalog_non_fuzzy(self): self.cmd.finalize_options() self.cmd.run() - po_file = self._po_file('en_US') + po_file = _po_file('en_US') assert os.path.isfile(po_file) expected_content = r"""# English (United States) translations for TestProject. @@ -505,7 +495,7 @@ def test_correct_init_more_than_2_plurals(self): self.cmd.finalize_options() self.cmd.run() - po_file = self._po_file('lv_LV') + po_file = _po_file('lv_LV') assert os.path.isfile(po_file) expected_content = r"""# Latvian (Latvia) translations for TestProject. @@ -559,7 +549,7 @@ def test_correct_init_singular_plural_forms(self): self.cmd.finalize_options() self.cmd.run() - po_file = self._po_file('ja_JP') + po_file = _po_file('ja_JP') assert os.path.isfile(po_file) expected_content = r"""# Japanese (Japan) translations for TestProject. @@ -619,7 +609,7 @@ def test_supports_no_wrap(self): self.cmd.finalize_options() self.cmd.run() - po_file = self._po_file('en_US') + po_file = _po_file('en_US') assert os.path.isfile(po_file) expected_content = r"""# English (United States) translations for TestProject. # Copyright (C) 2007 FooBar, Inc. @@ -679,7 +669,7 @@ def test_supports_width(self): self.cmd.finalize_options() self.cmd.run() - po_file = self._po_file('en_US') + po_file = _po_file('en_US') assert os.path.isfile(po_file) expected_content = r"""# English (United States) translations for TestProject. # Copyright (C) 2007 FooBar, Inc. @@ -726,7 +716,7 @@ def test_supports_width(self): class CommandLineInterfaceTestCase(unittest.TestCase): def setUp(self): - self.datadir = os.path.join(this_dir, 'data') + data_dir = os.path.join(this_dir, 'data') self.orig_working_dir = os.getcwd() self.orig_argv = sys.argv self.orig_stdout = sys.stdout @@ -734,7 +724,7 @@ def setUp(self): sys.argv = ['pybabel'] sys.stdout = StringIO() sys.stderr = StringIO() - os.chdir(self.datadir) + os.chdir(data_dir) self._remove_log_handlers() self.cli = frontend.CommandLineInterface() @@ -745,7 +735,7 @@ def tearDown(self): sys.stdout = self.orig_stdout sys.stderr = self.orig_stderr for dirname in ['lv_LV', 'ja_JP']: - locale_dir = os.path.join(self._i18n_dir(), dirname) + locale_dir = os.path.join(i18n_dir, dirname) if os.path.isdir(locale_dir): shutil.rmtree(locale_dir) self._remove_log_handlers() @@ -772,8 +762,8 @@ def test_usage(self): """, sys.stderr.getvalue().lower()) def _run_init_catalog(self): - i18n_dir = os.path.join(self.datadir, 'project', 'i18n') - pot_path = os.path.join(self.datadir, 'project', 'i18n', 'messages.pot') + i18n_dir = os.path.join(data_dir, 'project', 'i18n') + pot_path = os.path.join(data_dir, 'project', 'i18n', 'messages.pot') init_argv = sys.argv + ['init', '--locale', 'en_US', '-d', i18n_dir, '-i', pot_path] self.cli.run(init_argv) @@ -821,15 +811,11 @@ def test_help(self): update update existing message catalogs from a pot file """, sys.stdout.getvalue().lower()) - def _pot_file(self): - return os.path.join(self._i18n_dir(), 'temp.pot') - def assert_pot_file_exists(self): - assert os.path.isfile(self._pot_file()) + assert os.path.isfile(pot_file) @freeze_time("1994-11-11") def test_extract_with_default_mapping(self): - pot_file = self._pot_file() self.cli.run(sys.argv + ['extract', '--copyright-holder', 'FooBar, Inc.', '--project', 'TestProject', '--version', '0.1', @@ -885,12 +871,11 @@ def test_extract_with_default_mapping(self): @freeze_time("1994-11-11") def test_extract_with_mapping_file(self): - pot_file = self._pot_file() self.cli.run(sys.argv + ['extract', '--copyright-holder', 'FooBar, Inc.', '--project', 'TestProject', '--version', '0.1', '--msgid-bugs-address', 'bugs.address@email.tld', - '--mapping', os.path.join(self.datadir, 'mapping.cfg'), + '--mapping', os.path.join(data_dir, 'mapping.cfg'), '-c', 'TRANSLATOR', '-c', 'TRANSLATORS:', '-o', pot_file, 'project']) self.assert_pot_file_exists() @@ -939,13 +924,12 @@ def test_extract_with_exact_file(self): """Tests that we can call extract with a particular file and only strings from that file get extracted. (Note the absence of strings from file1.py) """ - pot_file = self._pot_file() - file_to_extract = os.path.join(self.datadir, 'project', 'file2.py') + file_to_extract = os.path.join(data_dir, 'project', 'file2.py') self.cli.run(sys.argv + ['extract', '--copyright-holder', 'FooBar, Inc.', '--project', 'TestProject', '--version', '0.1', '--msgid-bugs-address', 'bugs.address@email.tld', - '--mapping', os.path.join(self.datadir, 'mapping.cfg'), + '--mapping', os.path.join(data_dir, 'mapping.cfg'), '-c', 'TRANSLATOR', '-c', 'TRANSLATORS:', '-o', pot_file, file_to_extract]) self.assert_pot_file_exists() @@ -985,11 +969,11 @@ def test_extract_with_exact_file(self): @freeze_time("1994-11-11") def test_init_with_output_dir(self): - po_file = self._po_file('en_US') + po_file = _po_file('en_US') self.cli.run(sys.argv + ['init', '--locale', 'en_US', - '-d', os.path.join(self._i18n_dir()), - '-i', os.path.join(self._i18n_dir(), 'messages.pot')]) + '-d', os.path.join(i18n_dir), + '-i', os.path.join(i18n_dir, 'messages.pot')]) assert os.path.isfile(po_file) expected_content = r"""# English (United States) translations for TestProject. # Copyright (C) 2007 FooBar, Inc. @@ -1031,16 +1015,13 @@ def test_init_with_output_dir(self): actual_content = f.read() self.assertEqual(expected_content, actual_content) - def _i18n_dir(self): - return os.path.join(self.datadir, 'project', 'i18n') - @freeze_time("1994-11-11") def test_init_singular_plural_forms(self): - po_file = self._po_file('ja_JP') + po_file = _po_file('ja_JP') self.cli.run(sys.argv + ['init', '--locale', 'ja_JP', - '-d', os.path.join(self._i18n_dir()), - '-i', os.path.join(self._i18n_dir(), 'messages.pot')]) + '-d', os.path.join(i18n_dir), + '-i', os.path.join(i18n_dir, 'messages.pot')]) assert os.path.isfile(po_file) expected_content = r"""# Japanese (Japan) translations for TestProject. # Copyright (C) 2007 FooBar, Inc. @@ -1083,11 +1064,11 @@ def test_init_singular_plural_forms(self): @freeze_time("1994-11-11") def test_init_more_than_2_plural_forms(self): - po_file = self._po_file('lv_LV') + po_file = _po_file('lv_LV') self.cli.run(sys.argv + ['init', '--locale', 'lv_LV', - '-d', self._i18n_dir(), - '-i', os.path.join(self._i18n_dir(), 'messages.pot')]) + '-d', i18n_dir, + '-i', os.path.join(i18n_dir, 'messages.pot')]) assert os.path.isfile(po_file) expected_content = r"""# Latvian (Latvia) translations for TestProject. # Copyright (C) 2007 FooBar, Inc. @@ -1132,23 +1113,23 @@ def test_init_more_than_2_plural_forms(self): self.assertEqual(expected_content, actual_content) def test_compile_catalog(self): - po_file = self._po_file('de_DE') + po_file = _po_file('de_DE') mo_file = po_file.replace('.po', '.mo') self.cli.run(sys.argv + ['compile', '--locale', 'de_DE', - '-d', self._i18n_dir()]) + '-d', i18n_dir]) assert not os.path.isfile(mo_file), 'Expected no file at %r' % mo_file self.assertEqual("""\ catalog %s is marked as fuzzy, skipping """ % po_file, sys.stderr.getvalue()) def test_compile_fuzzy_catalog(self): - po_file = self._po_file('de_DE') + po_file = _po_file('de_DE') mo_file = po_file.replace('.po', '.mo') try: self.cli.run(sys.argv + ['compile', '--locale', 'de_DE', '--use-fuzzy', - '-d', self._i18n_dir()]) + '-d', i18n_dir]) assert os.path.isfile(mo_file) self.assertEqual("""\ compiling catalog %s to %s @@ -1157,17 +1138,13 @@ def test_compile_fuzzy_catalog(self): if os.path.isfile(mo_file): os.unlink(mo_file) - def _po_file(self, locale): - return os.path.join(self._i18n_dir(), locale, 'LC_MESSAGES', - 'messages.po') - def test_compile_catalog_with_more_than_2_plural_forms(self): - po_file = self._po_file('ru_RU') + po_file = _po_file('ru_RU') mo_file = po_file.replace('.po', '.mo') try: self.cli.run(sys.argv + ['compile', '--locale', 'ru_RU', '--use-fuzzy', - '-d', self._i18n_dir()]) + '-d', i18n_dir]) assert os.path.isfile(mo_file) self.assertEqual("""\ compiling catalog %s to %s @@ -1177,16 +1154,14 @@ def test_compile_catalog_with_more_than_2_plural_forms(self): os.unlink(mo_file) def test_compile_catalog_multidomain(self): - po_foo = os.path.join(self._i18n_dir(), 'de_DE', 'LC_MESSAGES', - 'foo.po') - po_bar = os.path.join(self._i18n_dir(), 'de_DE', 'LC_MESSAGES', - 'bar.po') + po_foo = os.path.join(i18n_dir, 'de_DE', 'LC_MESSAGES', 'foo.po') + po_bar = os.path.join(i18n_dir, 'de_DE', 'LC_MESSAGES', 'bar.po') mo_foo = po_foo.replace('.po', '.mo') mo_bar = po_bar.replace('.po', '.mo') try: self.cli.run(sys.argv + ['compile', '--locale', 'de_DE', '--domain', 'foo bar', '--use-fuzzy', - '-d', self._i18n_dir()]) + '-d', i18n_dir]) for mo_file in [mo_foo, mo_bar]: assert os.path.isfile(mo_file) self.assertEqual("""\ @@ -1204,10 +1179,10 @@ def test_update(self): template.add("1") template.add("2") template.add("3") - tmpl_file = os.path.join(self._i18n_dir(), 'temp-template.pot') + tmpl_file = os.path.join(i18n_dir, 'temp-template.pot') with open(tmpl_file, "wb") as outfp: write_po(outfp, template) - po_file = os.path.join(self._i18n_dir(), 'temp1.po') + po_file = os.path.join(i18n_dir, 'temp1.po') self.cli.run(sys.argv + ['init', '-l', 'fi', '-o', po_file, From ceb074a8f94e4a0d33bdab3ba54803488c0ce682 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 27 May 2019 12:30:01 +0300 Subject: [PATCH 082/139] pybabel compile: exit with code 1 if errors were encountered Fixes #627 --- .gitignore | 3 ++- babel/messages/frontend.py | 14 ++++++++++++-- .../project/i18n/fi_BUGGY/LC_MESSAGES/messages.po | 5 +++++ tests/messages/test_frontend.py | 9 +++++++++ 4 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 tests/messages/data/project/i18n/fi_BUGGY/LC_MESSAGES/messages.po diff --git a/.gitignore b/.gitignore index 50e5838f6..2886dec52 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,5 @@ docs/_build test-env tests/messages/data/project/i18n/en_US tests/messages/data/project/i18n/long_messages.pot -tests/messages/data/project/i18n/temp* \ No newline at end of file +tests/messages/data/project/i18n/temp* +tests/messages/data/project/i18n/fi_BUGGY/LC_MESSAGES/*.mo diff --git a/babel/messages/frontend.py b/babel/messages/frontend.py index f673f900c..9e7c68ca1 100644 --- a/babel/messages/frontend.py +++ b/babel/messages/frontend.py @@ -182,8 +182,13 @@ def finalize_options(self): 'or the base directory') def run(self): + n_errors = 0 for domain in self.domain: - self._run_domain(domain) + for catalog, errors in self._run_domain(domain).items(): + n_errors += len(errors) + if n_errors: + self.log.error('%d errors encountered.' % n_errors) + return (1 if n_errors else 0) def _run_domain(self, domain): po_files = [] @@ -219,6 +224,8 @@ def _run_domain(self, domain): if not po_files: raise DistutilsOptionError('no message catalogs found') + catalogs_and_errors = {} + for idx, (locale, po_file) in enumerate(po_files): mo_file = mo_files[idx] with open(po_file, 'rb') as infile: @@ -241,7 +248,8 @@ def _run_domain(self, domain): self.log.info('catalog %s is marked as fuzzy, skipping', po_file) continue - for message, errors in catalog.check(): + catalogs_and_errors[catalog] = catalog_errors = list(catalog.check()) + for message, errors in catalog_errors: for error in errors: self.log.error( 'error: %s:%d: %s', po_file, message.lineno, error @@ -252,6 +260,8 @@ def _run_domain(self, domain): with open(mo_file, 'wb') as outfile: write_mo(outfile, catalog, use_fuzzy=self.use_fuzzy) + return catalogs_and_errors + class extract_messages(Command): """Message extraction command for use in ``setup.py`` scripts. diff --git a/tests/messages/data/project/i18n/fi_BUGGY/LC_MESSAGES/messages.po b/tests/messages/data/project/i18n/fi_BUGGY/LC_MESSAGES/messages.po new file mode 100644 index 000000000..0a0745b42 --- /dev/null +++ b/tests/messages/data/project/i18n/fi_BUGGY/LC_MESSAGES/messages.po @@ -0,0 +1,5 @@ +msgid "" +msgstr "" + +msgid "bar %(sign)s" +msgstr "tanko %(merkki)s" diff --git a/tests/messages/test_frontend.py b/tests/messages/test_frontend.py index fa0112c2e..deaa66025 100644 --- a/tests/messages/test_frontend.py +++ b/tests/messages/test_frontend.py @@ -1383,3 +1383,12 @@ def test_extract_add_location(): assert isinstance(cmdinst, extract_messages) assert cmdinst.add_location == 'never' assert cmdinst.no_location + + +def test_extract_error_code(monkeypatch, capsys): + monkeypatch.chdir(project_dir) + cmdinst = configure_cli_command("compile --domain=messages --directory i18n --locale fi_BUGGY") + assert cmdinst.run() == 1 + out, err = capsys.readouterr() + # replace hack below for py2/py3 compatibility + assert "unknown named placeholder 'merkki'" in err.replace("u'", "'") From 03215bc33ba4c8ecf7f9966344aae51cc7c420e1 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 27 May 2019 13:50:52 +0300 Subject: [PATCH 083/139] Catalog update: keep user comments from destination by default Closes #418 --- babel/messages/catalog.py | 6 +++++- tests/messages/test_catalog.py | 31 ++++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py index 649d0e2fe..136ef3623 100644 --- a/babel/messages/catalog.py +++ b/babel/messages/catalog.py @@ -700,7 +700,7 @@ def delete(self, id, context=None): if key in self._messages: del self._messages[key] - def update(self, template, no_fuzzy_matching=False, update_header_comment=False): + def update(self, template, no_fuzzy_matching=False, update_header_comment=False, keep_user_comments=True): """Update the catalog based on the given template catalog. >>> from babel.messages import Catalog @@ -780,6 +780,10 @@ def _merge(message, oldkey, newkey): else: oldmsg = remaining.pop(oldkey, None) message.string = oldmsg.string + + if keep_user_comments: + message.user_comments = list(distinct(oldmsg.user_comments)) + if isinstance(message.id, (list, tuple)): if not isinstance(message.string, (list, tuple)): fuzzy = True diff --git a/tests/messages/test_catalog.py b/tests/messages/test_catalog.py index 1b23832a6..f583810c9 100644 --- a/tests/messages/test_catalog.py +++ b/tests/messages/test_catalog.py @@ -15,8 +15,9 @@ import datetime import unittest +from babel._compat import StringIO from babel.dates import format_datetime, UTC -from babel.messages import catalog +from babel.messages import catalog, pofile from babel.util import FixedOffsetTimezone @@ -475,3 +476,31 @@ def test_datetime_parsing(): assert val2.month == 6 assert val2.day == 28 assert val2.tzinfo is None + + +def test_update_catalog_comments(): + # Based on https://web.archive.org/web/20100710131029/http://babel.edgewall.org/attachment/ticket/163/cat-update-comments.py + + catalog = pofile.read_po(StringIO(''' + # A user comment + #. An auto comment + #: main.py:1 + #, fuzzy, python-format + msgid "foo %(name)s" + msgstr "foo %(name)s" + ''')) + + assert all(message.user_comments and message.auto_comments for message in catalog if message.id) + + # NOTE: in the POT file, there are no comments + template = pofile.read_po(StringIO(''' + #: main.py:1 + #, fuzzy, python-format + msgid "bar %(name)s" + msgstr "" + ''')) + + catalog.update(template) + + # Auto comments will be obliterated here + assert all(message.user_comments for message in catalog if message.id) From d7f2e84dedcce223d73229c79f0f1085d4dc84be Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 27 May 2019 14:31:09 +0300 Subject: [PATCH 084/139] py.test: document all_locales marker --- setup.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.cfg b/setup.cfg index 4e877a36f..12585f0d7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,6 +4,8 @@ release = sdist bdist_wheel [tool:pytest] norecursedirs = venv* .* _* scripts {args} doctest_optionflags = ELLIPSIS NORMALIZE_WHITESPACE ALLOW_UNICODE IGNORE_EXCEPTION_DETAIL +markers = + all_locales: parameterize test with all locales [bdist_wheel] universal = 1 From 4b6cd34c5c4436c60747a30d15a915cb2702ca0b Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 27 May 2019 14:32:48 +0300 Subject: [PATCH 085/139] parse_decimal(): assume spaces are equivalent to non-breaking spaces when not in strict mode Fixes #637 --- babel/numbers.py | 11 +++++++++++ tests/test_numbers.py | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/babel/numbers.py b/babel/numbers.py index e5650dd52..3dcd73099 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -673,6 +673,8 @@ def parse_decimal(string, locale=LC_NUMERIC, strict=False): Decimal('1099.98') >>> parse_decimal('1.099,98', locale='de') Decimal('1099.98') + >>> parse_decimal('12 345,123', locale='ru') + Decimal('12345.123') When the given string cannot be parsed, an exception is raised: @@ -704,6 +706,15 @@ def parse_decimal(string, locale=LC_NUMERIC, strict=False): locale = Locale.parse(locale) group_symbol = get_group_symbol(locale) decimal_symbol = get_decimal_symbol(locale) + + if not strict and ( + group_symbol == u'\xa0' and # if the grouping symbol is U+00A0 NO-BREAK SPACE, + group_symbol not in string and # and the string to be parsed does not contain it, + ' ' in string # but it does contain a space instead, + ): + # ... it's reasonable to assume it is taking the place of the grouping symbol. + string = string.replace(' ', group_symbol) + try: parsed = decimal.Decimal(string.replace(group_symbol, '') .replace(decimal_symbol, '.')) diff --git a/tests/test_numbers.py b/tests/test_numbers.py index 99aa96b7e..27649f841 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -679,3 +679,14 @@ def test_numberpattern_repr(): def test_parse_static_pattern(): assert numbers.parse_pattern('Kun') # in the So locale in CLDR 30 # TODO: static patterns might not be correctly `apply()`ed at present + + +def test_parse_decimal_nbsp_heuristics(): + # Re https://github.com/python-babel/babel/issues/637 – + # for locales (of which there are many) that use U+00A0 as the group + # separator in numbers, it's reasonable to assume that input strings + # with plain spaces actually should have U+00A0s instead. + # This heuristic is only applied when strict=False. + n = decimal.Decimal("12345.123") + assert numbers.parse_decimal("12 345.123", locale="fi") == n + assert numbers.parse_decimal(numbers.format_decimal(n, locale="fi"), locale="fi") == n From b294828b4124548d2c156dfb51ea891af262b10a Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 27 May 2019 15:00:21 +0300 Subject: [PATCH 086/139] Update AUTHORS with generate_authors.py --- AUTHORS | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 69e110ecc..6374cd650 100644 --- a/AUTHORS +++ b/AUTHORS @@ -17,6 +17,7 @@ Babel is written and maintained by the Babel team and various contributors: - Michael Birtwell - Jonas Borgström - Kevin Deldycke +- Jon Dufresne - Ville Skyttä - Hugo - Heungsub Lee @@ -24,10 +25,13 @@ Babel is written and maintained by the Babel team and various contributors: - Sachin Paliwal - Alex Willmer - Daniel Neuhäuser +- Cédric Krier +- Luke Plant - Jennifer Wang - Lukas Balaga - sudheesh001 -- Jon Dufresne +- Miro Hrončok +- Changaco - Xavier Fernandez - KO. Mattsson - Sébastien Diemer @@ -41,6 +45,16 @@ Babel is written and maintained by the Babel team and various contributors: - Leonardo Pistone - Jun Omae - Hyunjun Kim +- BT-sschmid +- Alberto Mardegan +- mondeja +- NotAFile +- Julien Palard +- Brian Cappello +- Serban Constantin +- Bryn Truscott +- Chris +- Charly C - PTrottier - xmo-odoo - StevenJ From 8fcffaf9abffff2c57e414e8bc2db4481261cc6f Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 27 May 2019 15:20:09 +0300 Subject: [PATCH 087/139] Distill changelog from git log --- CHANGES | 44 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 622b4c573..3462117fc 100644 --- a/CHANGES +++ b/CHANGES @@ -1,8 +1,8 @@ Babel Changelog =============== -UNRELEASED ----------- +Version 2.7.0 +------------- Possibly incompatible changes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -14,6 +14,46 @@ strange and we'll try to help! * General: Internal uses of ``babel.util.odict`` have been replaced with ``collections.OrderedDict`` from The Python standard library. +Improvements +~~~~~~~~~~~~ + +* CLDR: Upgrade to CLDR 35.1 - Alberto Mardegan, Aarni Koskela (#626, #643) +* General: allow anchoring path patterns to the start of a string - Brian Cappello (#600) +* General: Bumped version requirement on pytz - @chrisbrake (#592) +* Messages: `pybabel compile`: exit with code 1 if errors were encountered - Aarni Koskela (#647) +* Messages: Add omit-header to update_catalog - Cédric Krier (#633) +* Messages: Catalog update: keep user comments from destination by default - Aarni Koskela (#648) +* Messages: Skip empty message when writing mo file - Cédric Krier (#564) +* Messages: Small fixes to avoid crashes on badly formatted .po files - Bryn Truscott (#597) +* Numbers: `parse_decimal()` `strict` argument and `suggestions` - Charly C (#590) +* Numbers: don't repeat suggestions in parse_decimal strict - Serban Constantin (#599) +* Numbers: implement currency formatting with long display names - Luke Plant (#585) +* Numbers: parse_decimal(): assume spaces are equivalent to non-breaking spaces when not in strict mode - Aarni Koskela (#649) +* Performance: Cache locale_identifiers() - Aarni Koskela (#644) + +Bugfixes +~~~~~~~~ + +* CLDR: Skip alt=... for week data (minDays, firstDay, weekendStart, weekendEnd) - Aarni Koskela (#634) +* Dates: Fix wrong weeknumber for 31.12.2018 - BT-sschmid (#621) +* Locale: Avoid KeyError trying to get data on WindowsXP - mondeja (#604) +* Locale: get_display_name(): Don't attempt to concatenate variant information to None - Aarni Koskela (#645) +* Messages: pofile: Add comparison operators to _NormalizedString - Aarni Koskela (#646) +* Messages: pofile: don't crash when message.locations can't be sorted - Aarni Koskela (#646) + +Tooling & docs +~~~~~~~~~~~~~~ + +* Docs: Remove all references to deprecated easy_install - Jon Dufresne (#610) +* Docs: Switch print statement in docs to print function - NotAFile +* Docs: Update all pypi.python.org URLs to pypi.org - Jon Dufresne (#587) +* Docs: Use https URLs throughout project where available - Jon Dufresne (#588) +* Support: Add testing and document support for Python 3.7 - Jon Dufresne (#611) +* Support: Test on Python 3.8-dev - Aarni Koskela (#642) +* Support: Using ABCs from collections instead of collections.abc is deprecated. - Julien Palard (#609) +* Tests: Fix conftest.py compatibility with pytest 4.3 - Miro Hrončok (#635) +* Tests: Update pytest and pytest-cov - Miro Hrončok (#635) + Version 2.6.0 ------------- From 469cce913c0a7d7f028a3701450dc0d27cc18323 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 27 May 2019 15:23:28 +0300 Subject: [PATCH 088/139] Bump version --- babel/__init__.py | 2 +- docs/conf.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/babel/__init__.py b/babel/__init__.py index de44ce6c1..e58942a96 100644 --- a/babel/__init__.py +++ b/babel/__init__.py @@ -21,4 +21,4 @@ negotiate_locale, parse_locale, get_locale_identifier -__version__ = '2.6.0' +__version__ = '2.7.0' diff --git a/docs/conf.py b/docs/conf.py index 63c2d7749..183cc725c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -51,9 +51,9 @@ # built documents. # # The short X.Y version. -version = '2.6' +version = '2.7' # The full version, including alpha/beta/rc tags. -release = '2.6.0' +release = '2.7.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From 6aa30320c2d4173e4475b0dd328e17a1a09767cd Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 27 May 2019 15:24:52 +0300 Subject: [PATCH 089/139] Bump copyright year from 2018 to 2019 --- LICENSE | 2 +- babel/__init__.py | 2 +- babel/core.py | 2 +- babel/dates.py | 10 +++++----- babel/lists.py | 2 +- babel/localedata.py | 2 +- babel/localtime/__init__.py | 2 +- babel/messages/__init__.py | 2 +- babel/messages/catalog.py | 2 +- babel/messages/checkers.py | 2 +- babel/messages/extract.py | 2 +- babel/messages/frontend.py | 2 +- babel/messages/jslexer.py | 2 +- babel/messages/mofile.py | 2 +- babel/messages/plurals.py | 2 +- babel/messages/pofile.py | 2 +- babel/numbers.py | 2 +- babel/plural.py | 2 +- babel/support.py | 2 +- babel/util.py | 2 +- docs/conf.py | 2 +- scripts/dump_data.py | 2 +- scripts/dump_global.py | 2 +- scripts/import_cldr.py | 2 +- tests/messages/test_catalog.py | 2 +- tests/messages/test_checkers.py | 2 +- tests/messages/test_extract.py | 2 +- tests/messages/test_frontend.py | 2 +- tests/messages/test_mofile.py | 2 +- tests/messages/test_plurals.py | 2 +- tests/messages/test_pofile.py | 2 +- tests/test_core.py | 2 +- tests/test_dates.py | 4 ++-- tests/test_localedata.py | 2 +- tests/test_numbers.py | 2 +- tests/test_plural.py | 2 +- tests/test_support.py | 2 +- tests/test_util.py | 2 +- 38 files changed, 43 insertions(+), 43 deletions(-) diff --git a/LICENSE b/LICENSE index b517a5294..10722cc18 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2013-2018 by the Babel Team, see AUTHORS for more information. +Copyright (c) 2013-2019 by the Babel Team, see AUTHORS for more information. All rights reserved. diff --git a/babel/__init__.py b/babel/__init__.py index e58942a96..1132e6f37 100644 --- a/babel/__init__.py +++ b/babel/__init__.py @@ -13,7 +13,7 @@ access to various locale display names, localized number and date formatting, etc. - :copyright: (c) 2013-2018 by the Babel Team. + :copyright: (c) 2013-2019 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/core.py b/babel/core.py index 211849bce..a80807a61 100644 --- a/babel/core.py +++ b/babel/core.py @@ -5,7 +5,7 @@ Core locale representation and locale data access. - :copyright: (c) 2013-2018 by the Babel Team. + :copyright: (c) 2013-2019 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/dates.py b/babel/dates.py index ec86991a0..f1bd66faf 100644 --- a/babel/dates.py +++ b/babel/dates.py @@ -12,7 +12,7 @@ * ``LC_ALL``, and * ``LANG`` - :copyright: (c) 2013-2018 by the Babel Team. + :copyright: (c) 2013-2019 by the Babel Team. :license: BSD, see LICENSE for more details. """ @@ -1506,12 +1506,12 @@ def get_week_number(self, day_of_period, day_of_week=None): if 7 - first_day >= self.locale.min_week_days: week_number += 1 - + if self.locale.first_week_day == 0: - # Correct the weeknumber in case of iso-calendar usage (first_week_day=0). + # Correct the weeknumber in case of iso-calendar usage (first_week_day=0). # If the weeknumber exceeds the maximum number of weeks for the given year - # we must count from zero.For example the above calculation gives week 53 - # for 2018-12-31. By iso-calender definition 2018 has a max of 52 + # we must count from zero.For example the above calculation gives week 53 + # for 2018-12-31. By iso-calender definition 2018 has a max of 52 # weeks, thus the weeknumber must be 53-52=1. max_weeks = date(year=self.value.year, day=28, month=12).isocalendar()[1] if week_number > max_weeks: diff --git a/babel/lists.py b/babel/lists.py index 329437975..ab5a24c40 100644 --- a/babel/lists.py +++ b/babel/lists.py @@ -11,7 +11,7 @@ * ``LC_ALL``, and * ``LANG`` - :copyright: (c) 2015, 2018 by the Babel Team. + :copyright: (c) 2015-2019 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/localedata.py b/babel/localedata.py index 03381e2df..e012abbf2 100644 --- a/babel/localedata.py +++ b/babel/localedata.py @@ -8,7 +8,7 @@ :note: The `Locale` class, which uses this module under the hood, provides a more convenient interface for accessing the locale data. - :copyright: (c) 2013-2018 by the Babel Team. + :copyright: (c) 2013-2019 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/localtime/__init__.py b/babel/localtime/__init__.py index bb4341d60..aefd8a3e7 100644 --- a/babel/localtime/__init__.py +++ b/babel/localtime/__init__.py @@ -6,7 +6,7 @@ Babel specific fork of tzlocal to determine the local timezone of the system. - :copyright: (c) 2013-2018 by the Babel Team. + :copyright: (c) 2013-2019 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/messages/__init__.py b/babel/messages/__init__.py index 7dd6f8b6d..5b69675f3 100644 --- a/babel/messages/__init__.py +++ b/babel/messages/__init__.py @@ -5,7 +5,7 @@ Support for ``gettext`` message catalogs. - :copyright: (c) 2013-2018 by the Babel Team. + :copyright: (c) 2013-2019 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py index 136ef3623..2fcb461a8 100644 --- a/babel/messages/catalog.py +++ b/babel/messages/catalog.py @@ -5,7 +5,7 @@ Data structures for message catalogs. - :copyright: (c) 2013-2018 by the Babel Team. + :copyright: (c) 2013-2019 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/messages/checkers.py b/babel/messages/checkers.py index d04ad70ec..8c1effaf5 100644 --- a/babel/messages/checkers.py +++ b/babel/messages/checkers.py @@ -7,7 +7,7 @@ :since: version 0.9 - :copyright: (c) 2013-2018 by the Babel Team. + :copyright: (c) 2013-2019 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/messages/extract.py b/babel/messages/extract.py index 009bea2b4..db429b2ea 100644 --- a/babel/messages/extract.py +++ b/babel/messages/extract.py @@ -13,7 +13,7 @@ The main entry points into the extraction functionality are the functions `extract_from_dir` and `extract_from_file`. - :copyright: (c) 2013-2018 by the Babel Team. + :copyright: (c) 2013-2019 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/messages/frontend.py b/babel/messages/frontend.py index 9e7c68ca1..475605549 100644 --- a/babel/messages/frontend.py +++ b/babel/messages/frontend.py @@ -5,7 +5,7 @@ Frontends for the message extraction functionality. - :copyright: (c) 2013-2018 by the Babel Team. + :copyright: (c) 2013-2019 by the Babel Team. :license: BSD, see LICENSE for more details. """ from __future__ import print_function diff --git a/babel/messages/jslexer.py b/babel/messages/jslexer.py index 04d02761d..ace0b47e0 100644 --- a/babel/messages/jslexer.py +++ b/babel/messages/jslexer.py @@ -6,7 +6,7 @@ A simple JavaScript 1.5 lexer which is used for the JavaScript extractor. - :copyright: (c) 2013-2018 by the Babel Team. + :copyright: (c) 2013-2019 by the Babel Team. :license: BSD, see LICENSE for more details. """ from collections import namedtuple diff --git a/babel/messages/mofile.py b/babel/messages/mofile.py index 663d93bac..dfd923d23 100644 --- a/babel/messages/mofile.py +++ b/babel/messages/mofile.py @@ -5,7 +5,7 @@ Writing of files in the ``gettext`` MO (machine object) format. - :copyright: (c) 2013-2018 by the Babel Team. + :copyright: (c) 2013-2019 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/messages/plurals.py b/babel/messages/plurals.py index 067ac9826..81234580d 100644 --- a/babel/messages/plurals.py +++ b/babel/messages/plurals.py @@ -5,7 +5,7 @@ Plural form definitions. - :copyright: (c) 2013-2018 by the Babel Team. + :copyright: (c) 2013-2019 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/messages/pofile.py b/babel/messages/pofile.py index 9eb3a5cb3..bbcf7f76c 100644 --- a/babel/messages/pofile.py +++ b/babel/messages/pofile.py @@ -6,7 +6,7 @@ Reading and writing of files in the ``gettext`` PO (portable object) format. - :copyright: (c) 2013-2018 by the Babel Team. + :copyright: (c) 2013-2019 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/numbers.py b/babel/numbers.py index 3dcd73099..6888c9cb4 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -12,7 +12,7 @@ * ``LC_ALL``, and * ``LANG`` - :copyright: (c) 2013-2018 by the Babel Team. + :copyright: (c) 2013-2019 by the Babel Team. :license: BSD, see LICENSE for more details. """ # TODO: diff --git a/babel/plural.py b/babel/plural.py index eb1eef268..1e2b2734b 100644 --- a/babel/plural.py +++ b/babel/plural.py @@ -5,7 +5,7 @@ CLDR Plural support. See UTS #35. - :copyright: (c) 2013-2018 by the Babel Team. + :copyright: (c) 2013-2019 by the Babel Team. :license: BSD, see LICENSE for more details. """ import re diff --git a/babel/support.py b/babel/support.py index 87cb05967..efe41d562 100644 --- a/babel/support.py +++ b/babel/support.py @@ -8,7 +8,7 @@ .. note: the code in this module is not used by Babel itself - :copyright: (c) 2013-2018 by the Babel Team. + :copyright: (c) 2013-2019 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/util.py b/babel/util.py index c443b6902..73a90516f 100644 --- a/babel/util.py +++ b/babel/util.py @@ -5,7 +5,7 @@ Various utility classes and functions. - :copyright: (c) 2013-2018 by the Babel Team. + :copyright: (c) 2013-2019 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/docs/conf.py b/docs/conf.py index 183cc725c..6eed08fe6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -44,7 +44,7 @@ # General information about the project. project = u'Babel' -copyright = u'2018, The Babel Team' +copyright = u'2019, The Babel Team' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/scripts/dump_data.py b/scripts/dump_data.py index 3bf35f2be..e452248d7 100755 --- a/scripts/dump_data.py +++ b/scripts/dump_data.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/scripts/dump_global.py b/scripts/dump_global.py index 641f3c018..8db55f2dc 100755 --- a/scripts/dump_global.py +++ b/scripts/dump_global.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py index ed2e19c04..4188055a6 100755 --- a/scripts/import_cldr.py +++ b/scripts/import_cldr.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/messages/test_catalog.py b/tests/messages/test_catalog.py index f583810c9..f31dca310 100644 --- a/tests/messages/test_catalog.py +++ b/tests/messages/test_catalog.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/messages/test_checkers.py b/tests/messages/test_checkers.py index be4afb341..ec845001e 100644 --- a/tests/messages/test_checkers.py +++ b/tests/messages/test_checkers.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/messages/test_extract.py b/tests/messages/test_extract.py index fa50a4c9e..2f41ddc2c 100644 --- a/tests/messages/test_extract.py +++ b/tests/messages/test_extract.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/messages/test_frontend.py b/tests/messages/test_frontend.py index deaa66025..ad3ea0df3 100644 --- a/tests/messages/test_frontend.py +++ b/tests/messages/test_frontend.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/messages/test_mofile.py b/tests/messages/test_mofile.py index 97af7ae93..b1851f297 100644 --- a/tests/messages/test_mofile.py +++ b/tests/messages/test_mofile.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/messages/test_plurals.py b/tests/messages/test_plurals.py index 2ba15e911..bdca8f6a8 100644 --- a/tests/messages/test_plurals.py +++ b/tests/messages/test_plurals.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/messages/test_pofile.py b/tests/messages/test_pofile.py index a6973d7ec..e77fa6e02 100644 --- a/tests/messages/test_pofile.py +++ b/tests/messages/test_pofile.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/test_core.py b/tests/test_core.py index fc637d245..c146aae9c 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/test_dates.py b/tests/test_dates.py index e8592091d..5be0d16a1 100644 --- a/tests/test_dates.py +++ b/tests/test_dates.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which @@ -804,4 +804,4 @@ def test_russian_week_numbering(): def test_en_gb_first_weekday(): assert Locale.parse('en').first_week_day == 0 # Monday in general assert Locale.parse('en_US').first_week_day == 6 # Sunday in the US - assert Locale.parse('en_GB').first_week_day == 0 # Monday in the UK \ No newline at end of file + assert Locale.parse('en_GB').first_week_day == 0 # Monday in the UK diff --git a/tests/test_localedata.py b/tests/test_localedata.py index bffe7633f..dbacba0d5 100644 --- a/tests/test_localedata.py +++ b/tests/test_localedata.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/test_numbers.py b/tests/test_numbers.py index 27649f841..6e26fe900 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/test_plural.py b/tests/test_plural.py index 546e12a14..c54d07a57 100644 --- a/tests/test_plural.py +++ b/tests/test_plural.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/test_support.py b/tests/test_support.py index 314c18825..b4dd823cd 100644 --- a/tests/test_support.py +++ b/tests/test_support.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/test_util.py b/tests/test_util.py index 0fc59e5c4..a6a4450cf 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2018 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which From 38dfc8dc87f869d4837cbe8338db53ae9e293f8f Mon Sep 17 00:00:00 2001 From: Mario Frasca Date: Mon, 27 May 2019 12:35:28 -0500 Subject: [PATCH 090/139] attempt partial sorting at least see issue #606. if one object has anything that doesn't compare to `int`, bring it to the top, and correctly sort the rest. --- babel/messages/pofile.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/babel/messages/pofile.py b/babel/messages/pofile.py index bbcf7f76c..93b0697c6 100644 --- a/babel/messages/pofile.py +++ b/babel/messages/pofile.py @@ -582,11 +582,13 @@ def _write_message(message, prefix=''): if not no_location: locs = [] - # Attempt to sort the locations. If we can't do that, for instance - # because there are mixed integers and Nones or whatnot (see issue #606) - # then give up, but also don't just crash. + # sort locations by filename and lineno. + # if there's no as lineno, use `-1`. + # if no sorting possible, leave unsorted. + # (see issue #606) try: - locations = sorted(message.locations) + locations = sorted(message.locations, + key=lambda x: (x[0], isinstance(x[1], int) and x[1] or -1)) except TypeError: # e.g. "TypeError: unorderable types: NoneType() < int()" locations = message.locations From 2abad80c9f2fc70bde71f53ab8270ca950100595 Mon Sep 17 00:00:00 2001 From: Romuald Brunet Date: Thu, 18 Jul 2019 14:44:10 +0200 Subject: [PATCH 091/139] Add year along dates in changelog --- CHANGES | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGES b/CHANGES index 3462117fc..ec265c557 100644 --- a/CHANGES +++ b/CHANGES @@ -188,7 +188,7 @@ Internal improvements Version 2.3.4 ------------- -(Bugfix release, released on April 22th) +(Bugfix release, released on April 22th 2016) Bugfixes ~~~~~~~~ @@ -199,7 +199,7 @@ Bugfixes Version 2.3.3 ------------- -(Bugfix release, released on April 12th) +(Bugfix release, released on April 12th 2016) Bugfixes ~~~~~~~~ @@ -209,7 +209,7 @@ Bugfixes Version 2.3.2 ------------- -(Bugfix release, released on April 9th) +(Bugfix release, released on April 9th 2016) Bugfixes ~~~~~~~~ @@ -219,12 +219,12 @@ Bugfixes Version 2.3.1 ------------- -(Bugfix release because of deployment problems, released on April 8th) +(Bugfix release because of deployment problems, released on April 8th 2016) Version 2.3 ----------- -(Feature release, released on April 8th) +(Feature release, released on April 8th 2016) Internal improvements ~~~~~~~~~~~~~~~~~~~~~ From 9680427a75d3c267df111e2db585b26925a336bf Mon Sep 17 00:00:00 2001 From: "Steve (Gadget) Barnes" Date: Fri, 18 Oct 2019 17:07:24 +0100 Subject: [PATCH 092/139] Add install of pytz before import_cldr closes #670 --- docs/installation.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index ce778b04c..c1b7ab9fe 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -79,15 +79,16 @@ Get the git checkout in a new virtualenv and run in development mode:: New python executable in venv/bin/python Installing distribute............done. $ . venv/bin/activate + $ pip install pytz $ python setup.py import_cldr $ pip install --editable . ... Finished processing dependencies for Babel -Make sure to not forget about the ``import_cldr`` step because otherwise -you will be missing the locale data. This custom command will download -the most appropriate CLDR release from the official website and convert it -for Babel. +Make sure to not forget about the ``pip install pytz`` and ``import_cldr`` steps +because otherwise you will be missing the locale data. +The custom setup command will download the most appropriate CLDR release from the +official website and convert it for Babel but will not work without ``pytz``. This will pull also in the dependencies and activate the git head as the current version inside the virtualenv. Then all you have to do is run From 472a3174a7abc314345a914dde6fa2031018d4c4 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 31 Dec 2019 10:13:37 +0200 Subject: [PATCH 093/139] Upgrade freezegun to fix CI failures --- .ci/appveyor.yml | 2 +- .travis.yml | 2 +- tox.ini | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.ci/appveyor.yml b/.ci/appveyor.yml index eb9ba8656..91758f415 100644 --- a/.ci/appveyor.yml +++ b/.ci/appveyor.yml @@ -27,7 +27,7 @@ install: - "python --version" - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" # Build data files - - "pip install --upgrade pytest==4.3.1 pytest-cov==2.6.1 codecov freezegun==0.3.11" + - "pip install --upgrade pytest==4.3.1 pytest-cov==2.6.1 codecov freezegun==0.3.12" - "pip install --editable ." - "python setup.py import_cldr" diff --git a/.travis.yml b/.travis.yml index 174913952..9564d4c65 100644 --- a/.travis.yml +++ b/.travis.yml @@ -40,7 +40,7 @@ matrix: install: - bash .ci/deps.${TRAVIS_OS_NAME}.sh - pip install --upgrade pip - - pip install --upgrade $CDECIMAL pytest==4.3.1 pytest-cov==2.6.1 freezegun==0.3.11 + - pip install --upgrade $CDECIMAL pytest==4.3.1 pytest-cov==2.6.1 freezegun==0.3.12 - pip install --editable . script: diff --git a/tox.ini b/tox.ini index b3f8041f4..eccffea94 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,7 @@ deps = pytest==4.3.1 pytest-cov==2.6.1 cdecimal: m3-cdecimal - freezegun==0.3.11 + freezegun==0.3.12 whitelist_externals = make commands = make clean-cldr test passenv = PYTHON_TEST_FLAGS From 521fd3a3aa7d5605f5b5a10e9c50d85c7aa7af61 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 31 Dec 2019 10:46:45 +0200 Subject: [PATCH 094/139] Test on released Python 3.8 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9564d4c65..9650f674c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,7 +35,7 @@ matrix: - os: linux python: 3.7 - os: linux - python: 3.8-dev + python: 3.8 install: - bash .ci/deps.${TRAVIS_OS_NAME}.sh From 49c68d335f8443ba5fee0a5201dd8c2d033e59ff Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 31 Dec 2019 09:37:50 +0200 Subject: [PATCH 095/139] Download CLDR 36.0 --- scripts/download_import_cldr.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/download_import_cldr.py b/scripts/download_import_cldr.py index f118c6900..434b04f80 100755 --- a/scripts/download_import_cldr.py +++ b/scripts/download_import_cldr.py @@ -13,9 +13,9 @@ from urllib import urlretrieve -URL = 'https://unicode.org/Public/cldr/35.1/core.zip' -FILENAME = 'cldr-core-35.1.zip' -FILESUM = 'e2ede8cb8f9c29157e281ee9e696ce540a72c598841bed595a406b710eea87b0' +URL = 'http://unicode.org/Public/cldr/36/core.zip' +FILENAME = 'cldr-core-36.zip' +FILESUM = '07279e56c1f4266d140b907ef3ec379dce0a99542303a9628562ac5fe460ba43' BLKSIZE = 131072 From fab99b8924ad37e9f04798f464f37e2359ebafeb Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 31 Dec 2019 09:58:03 +0200 Subject: [PATCH 096/139] CLDR import: assume files without revision tags to be new --- scripts/import_cldr.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py index 4188055a6..8993b68e4 100755 --- a/scripts/import_cldr.py +++ b/scripts/import_cldr.py @@ -77,8 +77,10 @@ def error(message, *args): def need_conversion(dst_filename, data_dict, source_filename): with open(source_filename, 'rb') as f: blob = f.read(4096) - version = int(re.search(b'version number="\\$Revision: (\\d+)', - blob).group(1)) + version_match = re.search(b'version number="\\$Revision: (\\d+)', blob) + if not version_match: # CLDR 36.0 was shipped without proper revision numbers + return True + version = int(version_match.group(1)) data_dict['_version'] = version if not os.path.isfile(dst_filename): From d4a7c266ba119ee8564a14575a60948f723a15cb Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 31 Dec 2019 10:02:04 +0200 Subject: [PATCH 097/139] Correct format_unit test based on new Welsh data --- babel/units.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/babel/units.py b/babel/units.py index e58bf81c2..89c491365 100644 --- a/babel/units.py +++ b/babel/units.py @@ -88,12 +88,12 @@ def format_unit(value, measurement_unit, length='long', format=None, locale=LC_N >>> format_unit(1, 'length-meter', locale='ro_RO') u'1 metru' - >>> format_unit(0, 'length-picometer', locale='cy') - u'0 picometr' - >>> format_unit(2, 'length-picometer', locale='cy') - u'2 bicometr' - >>> format_unit(3, 'length-picometer', locale='cy') - u'3 phicometr' + >>> format_unit(0, 'length-mile', locale='cy') + u'0 mi' + >>> format_unit(1, 'length-mile', locale='cy') + u'1 filltir' + >>> format_unit(3, 'length-mile', locale='cy') + u'3 milltir' >>> format_unit(15, 'length-horse', locale='fi') Traceback (most recent call last): From df9c01c6480ef20523418079b6774612e47b8c63 Mon Sep 17 00:00:00 2001 From: He Chen Date: Mon, 19 Aug 2019 16:06:41 -0400 Subject: [PATCH 098/139] fix small decimal with disabled decimal_quantization --- babel/numbers.py | 2 +- tests/test_numbers.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/babel/numbers.py b/babel/numbers.py index 6888c9cb4..cf819fc9a 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -1063,7 +1063,7 @@ def _format_int(self, value, min, max, locale): def _quantize_value(self, value, locale, frac_prec): quantum = get_decimal_quantum(frac_prec[1]) rounded = value.quantize(quantum) - a, sep, b = str(rounded).partition(".") + a, sep, b = "{:f}".format(rounded).partition(".") number = (self._format_int(a, self.int_prec[0], self.int_prec[1], locale) + self._format_frac(b or '0', locale, frac_prec)) diff --git a/tests/test_numbers.py b/tests/test_numbers.py index 6e26fe900..a980a66ad 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -690,3 +690,7 @@ def test_parse_decimal_nbsp_heuristics(): n = decimal.Decimal("12345.123") assert numbers.parse_decimal("12 345.123", locale="fi") == n assert numbers.parse_decimal(numbers.format_decimal(n, locale="fi"), locale="fi") == n + + +def test_very_small_decimal_no_quantization(): + assert numbers.format_decimal(decimal.Decimal('1E-7'), locale='en', decimal_quantization=False) == '0.0000001' From c42bc9e907d638b368d07a326daa841e4a0bd691 Mon Sep 17 00:00:00 2001 From: sebleblanc Date: Tue, 3 Dec 2019 04:45:42 +0000 Subject: [PATCH 099/139] Hardcode "ignore" method The "ignore" method used to force the opening of the file. Some editors (emacs) create symbolic links to use as synchronization locks. Those links have an extension that matches the opened file, but the links themselves do not point to an existing file, thus causing Babel to attempt to open a file that does not exist. This fix skips opening of a file altogether when using the method "ignore" in the mapping file. --- babel/messages/extract.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/babel/messages/extract.py b/babel/messages/extract.py index db429b2ea..e7d7ad70f 100644 --- a/babel/messages/extract.py +++ b/babel/messages/extract.py @@ -236,9 +236,12 @@ def extract_from_file(method, filename, keywords=DEFAULT_KEYWORDS, :returns: list of tuples of the form ``(lineno, message, comments, context)`` :rtype: list[tuple[int, str|tuple[str], list[str], str|None] """ + if method == 'ignore': + return [] + with open(filename, 'rb') as fileobj: - return list(extract(method, fileobj, keywords, comment_tags, options, - strip_comment_tags)) + return list(extract(method, fileobj, keywords, comment_tags, + options, strip_comment_tags)) def extract(method, fileobj, keywords=DEFAULT_KEYWORDS, comment_tags=(), From dc012d4961c8a58c761252053a9954ccdbf4afd2 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 31 Dec 2019 14:21:49 +0200 Subject: [PATCH 100/139] Distill changelog from git log --- CHANGES | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGES b/CHANGES index ec265c557..4441f68e7 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,27 @@ Babel Changelog =============== +Version 2.8.0 +------------- + +Improvements +~~~~~~~~~~~~ + +* CLDR: Upgrade to CLDR 36.0 - Aarni Koskela (#679) +* Messages: Don't even open files with the "ignore" extraction method - @sebleblanc (#678) + +Bugfixes +~~~~~~~~ + +* Numbers: Fix formatting very small decimals when quantization is disabled - Lev Lybin, @miluChen (#662) +* Messages: Attempt to sort all messages – Mario Frasca (#651, #606) + +Docs +~~~~ + +* Add years to changelog - Romuald Brunet +* Note that installation requires pytz - Steve (Gadget) Barnes + Version 2.7.0 ------------- From eb1b24f49760eadb13dee0d505b25f93befa5c97 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 31 Dec 2019 14:22:11 +0200 Subject: [PATCH 101/139] Update authors file --- AUTHORS | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/AUTHORS b/AUTHORS index 6374cd650..31a5cd1be 100644 --- a/AUTHORS +++ b/AUTHORS @@ -45,6 +45,11 @@ Babel is written and maintained by the Babel team and various contributors: - Leonardo Pistone - Jun Omae - Hyunjun Kim +- sebleblanc +- He Chen +- Steve (Gadget) Barnes +- Romuald Brunet +- Mario Frasca - BT-sschmid - Alberto Mardegan - mondeja From 692ad094c8fdf1895b7f56a071f38f8e463a2c62 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 31 Dec 2019 14:23:25 +0200 Subject: [PATCH 102/139] Bump version --- babel/__init__.py | 2 +- docs/conf.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/babel/__init__.py b/babel/__init__.py index 1132e6f37..c10a8bd1f 100644 --- a/babel/__init__.py +++ b/babel/__init__.py @@ -21,4 +21,4 @@ negotiate_locale, parse_locale, get_locale_identifier -__version__ = '2.7.0' +__version__ = '2.8.0' diff --git a/docs/conf.py b/docs/conf.py index 6eed08fe6..83c5bf206 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -51,9 +51,9 @@ # built documents. # # The short X.Y version. -version = '2.7' +version = '2.8' # The full version, including alpha/beta/rc tags. -release = '2.7.0' +release = '2.8.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From 4fa749b918810b52a63b312d82e4003e24db0406 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Mon, 6 Jan 2020 00:37:39 +0100 Subject: [PATCH 103/139] Replace usage of parser.suite with ast.parse Replaced usage of the long-superseded "parser.suite" module in the mako.util package for parsing the python magic encoding comment with the "ast.parse" function introduced many years ago in Python 2.5, as "parser.suite" is emitting deprecation warnings in Python 3.9. Fixes https://github.com/sqlalchemy/mako/issues/310 See also https://github.com/sqlalchemy/mako/commit/2dae7d2c3da73653e6de329dc15c55056a0b9ab6 --- babel/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/babel/util.py b/babel/util.py index 73a90516f..c371badbd 100644 --- a/babel/util.py +++ b/babel/util.py @@ -68,8 +68,8 @@ def parse_encoding(fp): m = PYTHON_MAGIC_COMMENT_re.match(line1) if not m: try: - import parser - parser.suite(line1.decode('latin-1')) + import ast + ast.parse(line1.decode('latin-1')) except (ImportError, SyntaxError, UnicodeEncodeError): # Either it's a real syntax error, in which case the source is # not valid python source, or line2 is a continuation of line1, From f4f6653e6aa053724d2c6dc0ee71dcb928013352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niklas=20Hamb=C3=BCchen?= Date: Tue, 28 Jan 2020 02:46:25 +0100 Subject: [PATCH 104/139] Introduce invariant that _invalid_pofile() takes unicode line. This makes debugging and reasoning about the code easier; otherwise it is surprising that sometimes `line` is a unicode and sometimes not. So far, when it was not, it could either be only `""` or `'Algo esta mal'`; thus this commit makes those two u"" strings. In all other cases, it was guaranteed that it's unicode, because all code paths leading to `_invalid_pofile()` went through if not isinstance(line, text_type): line = line.decode(self.catalog.charset) before. --- babel/messages/pofile.py | 3 ++- tests/messages/test_pofile.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/babel/messages/pofile.py b/babel/messages/pofile.py index 93b0697c6..f6771bedf 100644 --- a/babel/messages/pofile.py +++ b/babel/messages/pofile.py @@ -178,7 +178,7 @@ def _add_message(self): string = ['' for _ in range(self.catalog.num_plurals)] for idx, translation in self.translations: if idx >= self.catalog.num_plurals: - self._invalid_pofile("", self.offset, "msg has more translations than num_plurals of catalog") + self._invalid_pofile(u"", self.offset, "msg has more translations than num_plurals of catalog") continue string[idx] = translation.denormalize() string = tuple(string) @@ -319,6 +319,7 @@ def parse(self, fileobj): self._add_message() def _invalid_pofile(self, line, lineno, msg): + assert isinstance(line, text_type) if self.abort_invalid: raise PoFileError(msg, self.catalog, line, lineno) print("WARNING:", msg) diff --git a/tests/messages/test_pofile.py b/tests/messages/test_pofile.py index e77fa6e02..214ddf5d5 100644 --- a/tests/messages/test_pofile.py +++ b/tests/messages/test_pofile.py @@ -480,7 +480,7 @@ def test_abort_invalid_po_file(self): def test_invalid_pofile_with_abort_flag(self): parser = pofile.PoFileParser(None, abort_invalid=True) lineno = 10 - line = 'Algo esta mal' + line = u'Algo esta mal' msg = 'invalid file' with self.assertRaises(pofile.PoFileError) as e: parser._invalid_pofile(line, lineno, msg) From da7f31143847659b6b74d802618b03438aceb350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niklas=20Hamb=C3=BCchen?= Date: Tue, 28 Jan 2020 00:37:22 +0100 Subject: [PATCH 105/139] Fix unicode printing error on Python 2 without TTY. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Until now, on Python 2.7, `python setup.py test | cat` crashed in the test runner with ====================================================================== ERROR: test_abort_invalid_po_file (tests.messages.test_pofile.ReadPoTestCase) ---------------------------------------------------------------------- Traceback (most recent call last): File "src/babel/tests/messages/test_pofile.py", line 458, in test_abort_invalid_po_file output = pofile.read_po(buf, locale='fr', abort_invalid=False) File "src/babel/babel/messages/pofile.py", line 377, in read_po parser.parse(fileobj) File "src/babel/babel/messages/pofile.py", line 310, in parse self._process_message_line(lineno, line) File "src/babel/babel/messages/pofile.py", line 210, in _process_message_line self._process_keyword_line(lineno, line, obsolete) File "src/babel/babel/messages/pofile.py", line 222, in _process_keyword_line self._invalid_pofile(line, lineno, "Start of line didn't match any expected keyword.") File "src/babel/babel/messages/pofile.py", line 325, in _invalid_pofile print(u"WARNING: Problem on line {0}: {1}".format(lineno + 1, line)) UnicodeEncodeError: 'ascii' codec can't encode character u'\xe0' in position 84: ordinal not in range(128) The test suite would show this when printing the `à` in the test pofile contents Pour toute question, veuillez communiquer avec Fulano à nadie@blah.com But this bug is not confined to the test suite only. Any call to `read_po()` with invalid .po file could trigger it in non-test code when `sys.stdout.encoding` is `None`, which is the default for Python 2 when `sys.stdout.isatty()` is false (as induced e.g. by `| cat`). The fix is to `repr()` the line when printing the WARNING. --- babel/messages/pofile.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/babel/messages/pofile.py b/babel/messages/pofile.py index f6771bedf..b86dd4052 100644 --- a/babel/messages/pofile.py +++ b/babel/messages/pofile.py @@ -323,7 +323,10 @@ def _invalid_pofile(self, line, lineno, msg): if self.abort_invalid: raise PoFileError(msg, self.catalog, line, lineno) print("WARNING:", msg) - print(u"WARNING: Problem on line {0}: {1}".format(lineno + 1, line)) + # `line` is guaranteed to be unicode so u"{}"-interpolating would always + # succeed, but on Python < 2 if not in a TTY, `sys.stdout.encoding` + # is `None`, unicode may not be printable so we `repr()` to ASCII. + print(u"WARNING: Problem on line {0}: {1}".format(lineno + 1, repr(line))) def read_po(fileobj, locale=None, domain=None, ignore_obsolete=False, charset=None, abort_invalid=False): From 0cfa69e087a24364ba788ff9d862949b65f0ff12 Mon Sep 17 00:00:00 2001 From: CyanNani123 Date: Mon, 13 Jan 2020 23:13:01 +0100 Subject: [PATCH 106/139] catalog.rst: Add __iter__ to Catalog documentation The declaration of __iter__ under the special-members directive makes it visible in the documentation. The docstring describing __iter__ already exists. Closes https://github.com/python-babel/babel/issues/128 --- docs/api/messages/catalog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/api/messages/catalog.rst b/docs/api/messages/catalog.rst index 8a905bcd9..8cb6375e3 100644 --- a/docs/api/messages/catalog.rst +++ b/docs/api/messages/catalog.rst @@ -12,6 +12,7 @@ Catalogs .. autoclass:: Catalog :members: + :special-members: __iter__ Messages -------- From 4ce6f13412705949f6d74579f15cad15cf00fed7 Mon Sep 17 00:00:00 2001 From: Tyler Kennedy Date: Tue, 25 Feb 2020 17:05:43 -0500 Subject: [PATCH 107/139] Update license.rst The documentation refers to the license as the license for _flask_ instead of Babel. --- docs/license.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/license.rst b/docs/license.rst index a619b5746..7c93ab426 100644 --- a/docs/license.rst +++ b/docs/license.rst @@ -19,7 +19,7 @@ Authors General License Definitions --------------------------- -The following section contains the full license texts for Flask and the +The following section contains the full license texts for Babel and the documentation. - "AUTHORS" hereby refers to all the authors listed in the From 167b71421f113e2210e4deefef5020402492e5be Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Tue, 5 May 2020 09:58:01 +0200 Subject: [PATCH 108/139] stop using deprecated ElementTree methods "getchildren()" and "getiterator()" Both methods were removed in Python 3.9 as mentioned in the release notes: > Methods getchildren() and getiterator() of classes ElementTree and Element in > the ElementTree module have been removed. They were deprecated in Python 3.2. > Use iter(x) or list(x) instead of x.getchildren() and x.iter() or > list(x.iter()) instead of x.getiterator(). --- scripts/import_cldr.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py index 8993b68e4..2ed3af91e 100755 --- a/scripts/import_cldr.py +++ b/scripts/import_cldr.py @@ -598,7 +598,7 @@ def parse_calendar_months(data, calendar): for width in ctxt.findall('monthWidth'): width_type = width.attrib['type'] widths = ctxts.setdefault(width_type, {}) - for elem in width.getiterator(): + for elem in width.iter(): if elem.tag == 'month': _import_type_text(widths, elem, int(elem.attrib['type'])) elif elem.tag == 'alias': @@ -616,7 +616,7 @@ def parse_calendar_days(data, calendar): for width in ctxt.findall('dayWidth'): width_type = width.attrib['type'] widths = ctxts.setdefault(width_type, {}) - for elem in width.getiterator(): + for elem in width.iter(): if elem.tag == 'day': _import_type_text(widths, elem, weekdays[elem.attrib['type']]) elif elem.tag == 'alias': @@ -634,7 +634,7 @@ def parse_calendar_quarters(data, calendar): for width in ctxt.findall('quarterWidth'): width_type = width.attrib['type'] widths = ctxts.setdefault(width_type, {}) - for elem in width.getiterator(): + for elem in width.iter(): if elem.tag == 'quarter': _import_type_text(widths, elem, int(elem.attrib['type'])) elif elem.tag == 'alias': @@ -649,7 +649,7 @@ def parse_calendar_eras(data, calendar): for width in calendar.findall('eras/*'): width_type = NAME_MAP[width.tag] widths = eras.setdefault(width_type, {}) - for elem in width.getiterator(): + for elem in width.iter(): if elem.tag == 'era': _import_type_text(widths, elem, type=int(elem.attrib.get('type'))) elif elem.tag == 'alias': @@ -676,7 +676,7 @@ def parse_calendar_periods(data, calendar): def parse_calendar_date_formats(data, calendar): date_formats = data.setdefault('date_formats', {}) for format in calendar.findall('dateFormats'): - for elem in format.getiterator(): + for elem in format.iter(): if elem.tag == 'dateFormatLength': type = elem.attrib.get('type') if _should_skip_elem(elem, type, date_formats): @@ -696,7 +696,7 @@ def parse_calendar_date_formats(data, calendar): def parse_calendar_time_formats(data, calendar): time_formats = data.setdefault('time_formats', {}) for format in calendar.findall('timeFormats'): - for elem in format.getiterator(): + for elem in format.iter(): if elem.tag == 'timeFormatLength': type = elem.attrib.get('type') if _should_skip_elem(elem, type, time_formats): @@ -717,7 +717,7 @@ def parse_calendar_datetime_skeletons(data, calendar): datetime_formats = data.setdefault('datetime_formats', {}) datetime_skeletons = data.setdefault('datetime_skeletons', {}) for format in calendar.findall('dateTimeFormats'): - for elem in format.getiterator(): + for elem in format.iter(): if elem.tag == 'dateTimeFormatLength': type = elem.attrib.get('type') if _should_skip_elem(elem, type, datetime_formats): @@ -880,7 +880,7 @@ def parse_interval_formats(data, tree): interval_formats[None] = elem.text elif elem.tag == "intervalFormatItem": skel_data = interval_formats.setdefault(elem.attrib["id"], {}) - for item_sub in elem.getchildren(): + for item_sub in elem: if item_sub.tag == "greatestDifference": skel_data[item_sub.attrib["id"]] = split_interval_pattern(item_sub.text) else: @@ -903,7 +903,7 @@ def parse_currency_formats(data, tree): type = '%s:%s' % (type, curr_length_type) if _should_skip_elem(elem, type, currency_formats): continue - for child in elem.getiterator(): + for child in elem.iter(): if child.tag == 'alias': currency_formats[type] = Alias( _translate_alias(['currency_formats', elem.attrib['type']], From 7bdaa28a55e8d8228d5434effa4b1473ab7b3669 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Tue, 5 May 2020 08:05:56 +0000 Subject: [PATCH 109/139] fix tests when using Python 3.9a6 In Python 3.9a6 integer values for future flags were changed to prevent collision with compiler flags. We need to retrieve these at runtime so the test suite works with Python <= 3.8 as well as Python 3.9. --- tests/test_util.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/tests/test_util.py b/tests/test_util.py index a6a4450cf..b9343aaab 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -11,6 +11,7 @@ # individuals. For the exact contribution history, see the revision # history and logs, available at http://babel.edgewall.org/log/. +import __future__ import unittest import pytest @@ -20,6 +21,12 @@ from babel.util import parse_future_flags +class _FF: + division = __future__.division.compiler_flag + print_function = __future__.print_function.compiler_flag + with_statement = __future__.with_statement.compiler_flag + unicode_literals = __future__.unicode_literals.compiler_flag + def test_distinct(): assert list(util.distinct([1, 2, 1, 3, 4, 4])) == [1, 2, 3, 4] assert list(util.distinct('foobar')) == ['f', 'o', 'b', 'a', 'r'] @@ -70,25 +77,25 @@ def test_parse_encoding_non_ascii(): from __future__ import print_function, division, with_statement, unicode_literals -''', 0x10000 | 0x2000 | 0x8000 | 0x20000), +''', _FF.print_function | _FF.division | _FF.with_statement | _FF.unicode_literals), (''' from __future__ import print_function, division print('hello') -''', 0x10000 | 0x2000), +''', _FF.print_function | _FF.division), (''' from __future__ import print_function, division, unknown,,,,, print 'hello' -''', 0x10000 | 0x2000), +''', _FF.print_function | _FF.division), (''' from __future__ import ( print_function, division) -''', 0x10000 | 0x2000), +''', _FF.print_function | _FF.division), (''' from __future__ import \\ print_function, \\ division -''', 0x10000 | 0x2000), +''', _FF.print_function | _FF.division), ]) def test_parse_future(source, result): fp = BytesIO(source.encode('latin-1')) From 2a826bb1ad9996a1c1e2d3e86a493d01d7f12c09 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Tue, 5 May 2020 21:32:52 +0000 Subject: [PATCH 110/139] simplify iteration code in "import_cldr.py" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As Miro Hrončok pointed out we don't need ".iter()" in the script. --- scripts/import_cldr.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py index 2ed3af91e..7ea6481a2 100755 --- a/scripts/import_cldr.py +++ b/scripts/import_cldr.py @@ -598,7 +598,7 @@ def parse_calendar_months(data, calendar): for width in ctxt.findall('monthWidth'): width_type = width.attrib['type'] widths = ctxts.setdefault(width_type, {}) - for elem in width.iter(): + for elem in width: if elem.tag == 'month': _import_type_text(widths, elem, int(elem.attrib['type'])) elif elem.tag == 'alias': @@ -616,7 +616,7 @@ def parse_calendar_days(data, calendar): for width in ctxt.findall('dayWidth'): width_type = width.attrib['type'] widths = ctxts.setdefault(width_type, {}) - for elem in width.iter(): + for elem in width: if elem.tag == 'day': _import_type_text(widths, elem, weekdays[elem.attrib['type']]) elif elem.tag == 'alias': @@ -634,7 +634,7 @@ def parse_calendar_quarters(data, calendar): for width in ctxt.findall('quarterWidth'): width_type = width.attrib['type'] widths = ctxts.setdefault(width_type, {}) - for elem in width.iter(): + for elem in width: if elem.tag == 'quarter': _import_type_text(widths, elem, int(elem.attrib['type'])) elif elem.tag == 'alias': @@ -649,7 +649,7 @@ def parse_calendar_eras(data, calendar): for width in calendar.findall('eras/*'): width_type = NAME_MAP[width.tag] widths = eras.setdefault(width_type, {}) - for elem in width.iter(): + for elem in width: if elem.tag == 'era': _import_type_text(widths, elem, type=int(elem.attrib.get('type'))) elif elem.tag == 'alias': @@ -676,7 +676,7 @@ def parse_calendar_periods(data, calendar): def parse_calendar_date_formats(data, calendar): date_formats = data.setdefault('date_formats', {}) for format in calendar.findall('dateFormats'): - for elem in format.iter(): + for elem in format: if elem.tag == 'dateFormatLength': type = elem.attrib.get('type') if _should_skip_elem(elem, type, date_formats): @@ -696,7 +696,7 @@ def parse_calendar_date_formats(data, calendar): def parse_calendar_time_formats(data, calendar): time_formats = data.setdefault('time_formats', {}) for format in calendar.findall('timeFormats'): - for elem in format.iter(): + for elem in format: if elem.tag == 'timeFormatLength': type = elem.attrib.get('type') if _should_skip_elem(elem, type, time_formats): @@ -717,7 +717,7 @@ def parse_calendar_datetime_skeletons(data, calendar): datetime_formats = data.setdefault('datetime_formats', {}) datetime_skeletons = data.setdefault('datetime_skeletons', {}) for format in calendar.findall('dateTimeFormats'): - for elem in format.iter(): + for elem in format: if elem.tag == 'dateTimeFormatLength': type = elem.attrib.get('type') if _should_skip_elem(elem, type, datetime_formats): From e7e4265d9a037ac38bba99f8513fb9e48a1081ba Mon Sep 17 00:00:00 2001 From: Brad Martin Date: Sun, 10 May 2020 12:02:16 -0600 Subject: [PATCH 111/139] docs/numbers.rst : update parse_number comments (#708) * docs/numbers.rst : test format of revised comments * docs/numbers.rst : test final doc changes * docs/numbers.rst : refine format/language * docs/numbers.rst : refine language/format * docs/numbers.rst : refine language/format * docs/numbers.rst : experiment with spacing --- docs/numbers.rst | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/numbers.rst b/docs/numbers.rst index df834eaf8..058d79e18 100644 --- a/docs/numbers.rst +++ b/docs/numbers.rst @@ -160,4 +160,21 @@ Examples: ... NumberFormatError: '2,109,998' is not a valid decimal number -.. note:: Number parsing is not properly implemented yet +Note: as of version 2.8.0, the ``parse_number`` function has limited +functionality. It can remove group symbols of certain locales from numeric +strings, but may behave unexpectedly until its logic handles more encoding +issues and other special cases. + +Examples: + +.. code-block:: pycon + + >>> parse_number('1,099', locale='en_US') + 1099 + >>> parse_number('1.099.024', locale='de') + 1099024 + >>> parse_number('123' + u'\xa0' + '4567', locale='ru') + 1234567 + >>> parse_number('123 4567', locale='ru') + ... + NumberFormatError: '123 4567' is not a valid number From e0e6aa614546855ccb76637b8d910382b6e94dba Mon Sep 17 00:00:00 2001 From: Abdullah Javed Nesar Date: Tue, 22 Sep 2020 17:44:27 +0530 Subject: [PATCH 112/139] Added group_separator feature in number formatting (#726) --- babel/numbers.py | 58 ++++++++++++++++++++++++++++++------------- tests/test_numbers.py | 30 ++++++++++++++++++++++ 2 files changed, 71 insertions(+), 17 deletions(-) diff --git a/babel/numbers.py b/babel/numbers.py index cf819fc9a..bbc5e2129 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -373,7 +373,7 @@ def get_decimal_quantum(precision): def format_decimal( - number, format=None, locale=LC_NUMERIC, decimal_quantization=True): + number, format=None, locale=LC_NUMERIC, decimal_quantization=True, group_separator=True): u"""Return the given decimal number formatted for a specific locale. >>> format_decimal(1.2345, locale='en_US') @@ -401,19 +401,25 @@ def format_decimal( u'1.235' >>> format_decimal(1.2346, locale='en_US', decimal_quantization=False) u'1.2346' + >>> format_decimal(12345.67, locale='fr_CA', group_separator=False) + u'12345,67' + >>> format_decimal(12345.67, locale='en_US', group_separator=True) + u'12,345.67' :param number: the number to format :param format: :param locale: the `Locale` object or locale identifier :param decimal_quantization: Truncate and round high-precision numbers to the format pattern. Defaults to `True`. + :param group_separator: Boolean to switch group separator on/off in a locale's + number format. """ locale = Locale.parse(locale) if not format: format = locale.decimal_formats.get(format) pattern = parse_pattern(format) return pattern.apply( - number, locale, decimal_quantization=decimal_quantization) + number, locale, decimal_quantization=decimal_quantization, group_separator=group_separator) class UnknownCurrencyFormatError(KeyError): @@ -422,7 +428,7 @@ class UnknownCurrencyFormatError(KeyError): def format_currency( number, currency, format=None, locale=LC_NUMERIC, currency_digits=True, - format_type='standard', decimal_quantization=True): + format_type='standard', decimal_quantization=True, group_separator=True): u"""Return formatted currency value. >>> format_currency(1099.98, 'USD', locale='en_US') @@ -472,6 +478,12 @@ def format_currency( ... UnknownCurrencyFormatError: "'unknown' is not a known currency format type" + >>> format_currency(101299.98, 'USD', locale='en_US', group_separator=False) + u'$101299.98' + + >>> format_currency(101299.98, 'USD', locale='en_US', group_separator=True) + u'$101,299.98' + You can also pass format_type='name' to use long display names. The order of the number and currency name, along with the correct localized plural form of the currency name, is chosen according to locale: @@ -500,12 +512,14 @@ def format_currency( :param format_type: the currency format type to use :param decimal_quantization: Truncate and round high-precision numbers to the format pattern. Defaults to `True`. + :param group_separator: Boolean to switch group separator on/off in a locale's + number format. """ if format_type == 'name': return _format_currency_long_name(number, currency, format=format, locale=locale, currency_digits=currency_digits, - decimal_quantization=decimal_quantization) + decimal_quantization=decimal_quantization, group_separator=group_separator) locale = Locale.parse(locale) if format: pattern = parse_pattern(format) @@ -518,12 +532,12 @@ def format_currency( return pattern.apply( number, locale, currency=currency, currency_digits=currency_digits, - decimal_quantization=decimal_quantization) + decimal_quantization=decimal_quantization, group_separator=group_separator) def _format_currency_long_name( number, currency, format=None, locale=LC_NUMERIC, currency_digits=True, - format_type='standard', decimal_quantization=True): + format_type='standard', decimal_quantization=True, group_separator=True): # Algorithm described here: # https://www.unicode.org/reports/tr35/tr35-numbers.html#Currencies locale = Locale.parse(locale) @@ -552,13 +566,13 @@ def _format_currency_long_name( number_part = pattern.apply( number, locale, currency=currency, currency_digits=currency_digits, - decimal_quantization=decimal_quantization) + decimal_quantization=decimal_quantization, group_separator=group_separator) return unit_pattern.format(number_part, display_name) def format_percent( - number, format=None, locale=LC_NUMERIC, decimal_quantization=True): + number, format=None, locale=LC_NUMERIC, decimal_quantization=True, group_separator=True): """Return formatted percent value for a specific locale. >>> format_percent(0.34, locale='en_US') @@ -582,18 +596,26 @@ def format_percent( >>> format_percent(23.9876, locale='en_US', decimal_quantization=False) u'2,398.76%' + >>> format_percent(229291.1234, locale='pt_BR', group_separator=False) + u'22929112%' + + >>> format_percent(229291.1234, locale='pt_BR', group_separator=True) + u'22.929.112%' + :param number: the percent number to format :param format: :param locale: the `Locale` object or locale identifier :param decimal_quantization: Truncate and round high-precision numbers to the format pattern. Defaults to `True`. + :param group_separator: Boolean to switch group separator on/off in a locale's + number format. """ locale = Locale.parse(locale) if not format: format = locale.percent_formats.get(format) pattern = parse_pattern(format) return pattern.apply( - number, locale, decimal_quantization=decimal_quantization) + number, locale, decimal_quantization=decimal_quantization, group_separator=group_separator) def format_scientific( @@ -913,6 +935,7 @@ def apply( currency_digits=True, decimal_quantization=True, force_frac=None, + group_separator=True, ): """Renders into a string a number following the defined pattern. @@ -952,8 +975,8 @@ def apply( if self.exp_prec: value, exp, exp_sign = self.scientific_notation_elements(value, locale) - # Adjust the precision of the fractionnal part and force it to the - # currency's if neccessary. + # Adjust the precision of the fractional part and force it to the + # currency's if necessary. if force_frac: # TODO (3.x?): Remove this parameter warnings.warn('The force_frac parameter to NumberPattern.apply() is deprecated.', DeprecationWarning) @@ -975,7 +998,7 @@ def apply( # Render scientific notation. if self.exp_prec: number = ''.join([ - self._quantize_value(value, locale, frac_prec), + self._quantize_value(value, locale, frac_prec, group_separator), get_exponential_symbol(locale), exp_sign, self._format_int( @@ -993,7 +1016,7 @@ def apply( # A normal number pattern. else: - number = self._quantize_value(value, locale, frac_prec) + number = self._quantize_value(value, locale, frac_prec, group_separator) retval = ''.join([ self.prefix[is_negative], @@ -1060,13 +1083,14 @@ def _format_int(self, value, min, max, locale): gsize = self.grouping[1] return value + ret - def _quantize_value(self, value, locale, frac_prec): + def _quantize_value(self, value, locale, frac_prec, group_separator): quantum = get_decimal_quantum(frac_prec[1]) rounded = value.quantize(quantum) a, sep, b = "{:f}".format(rounded).partition(".") - number = (self._format_int(a, self.int_prec[0], - self.int_prec[1], locale) + - self._format_frac(b or '0', locale, frac_prec)) + integer_part = a + if group_separator: + integer_part = self._format_int(a, self.int_prec[0], self.int_prec[1], locale) + number = integer_part + self._format_frac(b or '0', locale, frac_prec) return number def _format_frac(self, value, locale, force_frac=None): diff --git a/tests/test_numbers.py b/tests/test_numbers.py index a980a66ad..3db5f3307 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -153,6 +153,36 @@ def test_formatting_of_very_small_decimals(self): fmt = numbers.format_decimal(number, format="@@@", locale='en_US') self.assertEqual('0.000000700', fmt) + def test_group_separator(self): + self.assertEqual('29567.12', numbers.format_decimal(29567.12, + locale='en_US', group_separator=False)) + self.assertEqual('29567,12', numbers.format_decimal(29567.12, + locale='fr_CA', group_separator=False)) + self.assertEqual('29567,12', numbers.format_decimal(29567.12, + locale='pt_BR', group_separator=False)) + self.assertEqual(u'$1099.98', numbers.format_currency(1099.98, 'USD', + locale='en_US', group_separator=False)) + self.assertEqual(u'101299,98\xa0€', numbers.format_currency(101299.98, 'EUR', + locale='fr_CA', group_separator=False)) + self.assertEqual('101299.98 euros', numbers.format_currency(101299.98, 'EUR', + locale='en_US', group_separator=False, format_type='name')) + self.assertEqual(u'25123412\xa0%', numbers.format_percent(251234.1234, locale='sv_SE', group_separator=False)) + + self.assertEqual(u'29,567.12', numbers.format_decimal(29567.12, + locale='en_US', group_separator=True)) + self.assertEqual(u'29\u202f567,12', numbers.format_decimal(29567.12, + locale='fr_CA', group_separator=True)) + self.assertEqual(u'29.567,12', numbers.format_decimal(29567.12, + locale='pt_BR', group_separator=True)) + self.assertEqual(u'$1,099.98', numbers.format_currency(1099.98, 'USD', + locale='en_US', group_separator=True)) + self.assertEqual(u'101\u202f299,98\xa0\u20ac', numbers.format_currency(101299.98, 'EUR', + locale='fr_CA', group_separator=True)) + self.assertEqual(u'101,299.98 euros', numbers.format_currency(101299.98, 'EUR', + locale='en_US', group_separator=True, + format_type='name')) + self.assertEqual(u'25\xa0123\xa0412\xa0%', numbers.format_percent(251234.1234, locale='sv_SE', group_separator=True)) + class NumberParsingTestCase(unittest.TestCase): From 044416707f9effbe77582779a09ae9e37a0f6ffd Mon Sep 17 00:00:00 2001 From: Nikiforov Konstantin Date: Wed, 30 Sep 2020 20:17:16 +0500 Subject: [PATCH 113/139] LazyProxy: Handle AttributeError in specified func (#724) Fixes #723 Co-authored-by: Aarni Koskela --- babel/support.py | 12 ++++++++++-- tests/test_support.py | 11 +++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/babel/support.py b/babel/support.py index efe41d562..47f812d8a 100644 --- a/babel/support.py +++ b/babel/support.py @@ -165,7 +165,7 @@ class LazyProxy(object): Hello, universe! Hello, world! """ - __slots__ = ['_func', '_args', '_kwargs', '_value', '_is_cache_enabled'] + __slots__ = ['_func', '_args', '_kwargs', '_value', '_is_cache_enabled', '_attribute_error'] def __init__(self, func, *args, **kwargs): is_cache_enabled = kwargs.pop('enable_cache', True) @@ -175,11 +175,17 @@ def __init__(self, func, *args, **kwargs): object.__setattr__(self, '_kwargs', kwargs) object.__setattr__(self, '_is_cache_enabled', is_cache_enabled) object.__setattr__(self, '_value', None) + object.__setattr__(self, '_attribute_error', None) @property def value(self): if self._value is None: - value = self._func(*self._args, **self._kwargs) + try: + value = self._func(*self._args, **self._kwargs) + except AttributeError as error: + object.__setattr__(self, '_attribute_error', error) + raise + if not self._is_cache_enabled: return value object.__setattr__(self, '_value', value) @@ -249,6 +255,8 @@ def __delattr__(self, name): delattr(self.value, name) def __getattr__(self, name): + if self._attribute_error is not None: + raise self._attribute_error return getattr(self.value, name) def __setattr__(self, name, value): diff --git a/tests/test_support.py b/tests/test_support.py index b4dd823cd..1b74ae8bc 100644 --- a/tests/test_support.py +++ b/tests/test_support.py @@ -279,6 +279,17 @@ def first(xs): self.assertEqual(2, proxy.value) self.assertEqual(1, proxy_deepcopy.value) + def test_handle_attribute_error(self): + + def raise_attribute_error(): + raise AttributeError('message') + + proxy = support.LazyProxy(raise_attribute_error) + with pytest.raises(AttributeError) as exception: + proxy.value + + self.assertEqual('message', str(exception.value)) + def test_format_date(): fmt = support.Format('en_US') From 3e6d9a70e11041ad6737630281098866462ed1e3 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 1 Oct 2020 15:54:08 +0300 Subject: [PATCH 114/139] Switch downloader to CLDR 37 --- scripts/download_import_cldr.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/download_import_cldr.py b/scripts/download_import_cldr.py index 434b04f80..531a04c62 100755 --- a/scripts/download_import_cldr.py +++ b/scripts/download_import_cldr.py @@ -13,9 +13,9 @@ from urllib import urlretrieve -URL = 'http://unicode.org/Public/cldr/36/core.zip' -FILENAME = 'cldr-core-36.zip' -FILESUM = '07279e56c1f4266d140b907ef3ec379dce0a99542303a9628562ac5fe460ba43' +URL = 'http://unicode.org/Public/cldr/37/core.zip' +FILENAME = 'cldr-core-37.zip' +FILESUM = 'ba93f5ba256a61a6f8253397c6c4b1a9b9e77531f013cc7ffa7977b5f7e4da57' BLKSIZE = 131072 From 462444f35756551f83809dab7f7424a907033a9e Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 1 Oct 2020 16:27:02 +0300 Subject: [PATCH 115/139] Adapt things to new compound pattern format --- babel/units.py | 9 ++++++--- scripts/import_cldr.py | 20 +++++++++++++++++--- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/babel/units.py b/babel/units.py index 89c491365..07637358c 100644 --- a/babel/units.py +++ b/babel/units.py @@ -75,8 +75,10 @@ def format_unit(value, measurement_unit, length='long', format=None, locale=LC_N u'12 metri' >>> format_unit(15.5, 'length-mile', locale='fi_FI') u'15,5 mailia' - >>> format_unit(1200, 'pressure-inch-hg', locale='nb') - u'1\\xa0200 tommer kvikks\\xf8lv' + >>> format_unit(1200, 'pressure-millimeter-ofhg', locale='nb') + u'1\\xa0200 millimeter kvikks\\xf8lv' + >>> format_unit(270, 'ton', locale='en') + u'270 tons' Number formats may be overridden with the ``format`` parameter. @@ -271,6 +273,7 @@ def format_compound_unit( else: # Bare denominator formatted_denominator = format_decimal(denominator_value, format=format, locale=locale) - per_pattern = locale._data["compound_unit_patterns"].get("per", {}).get(length, "{0}/{1}") + # TODO: this doesn't support "compound_variations" (or "prefix"), and will fall back to the "x/y" representation + per_pattern = locale._data["compound_unit_patterns"].get("per", {}).get(length, {}).get("compound", "{0}/{1}") return per_pattern.format(formatted_numerator, formatted_denominator) diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py index 7ea6481a2..aab2888e1 100755 --- a/scripts/import_cldr.py +++ b/scripts/import_cldr.py @@ -853,9 +853,23 @@ def parse_unit_patterns(data, tree): for unit in elem.findall('compoundUnit'): unit_type = unit.attrib['type'] - compound_patterns.setdefault(unit_type, {})[unit_length_type] = ( - _text(unit.find('compoundUnitPattern')) - ) + compound_unit_info = {} + compound_variations = {} + for child in unit.getchildren(): + if child.tag == "unitPrefixPattern": + compound_unit_info['prefix'] = _text(child) + elif child.tag == "compoundUnitPattern": + compound_variations[None] = _text(child) + elif child.tag == "compoundUnitPattern1": + compound_variations[child.attrib.get('count')] = _text(child) + if compound_variations: + compound_variation_values = set(compound_variations.values()) + if len(compound_variation_values) == 1: + # shortcut: if all compound variations are the same, only store one + compound_unit_info['compound'] = next(iter(compound_variation_values)) + else: + compound_unit_info['compound_variations'] = compound_variations + compound_patterns.setdefault(unit_type, {})[unit_length_type] = compound_unit_info def parse_date_fields(data, tree): From 31abefa1009ee8f176a7fcd9311935ea806bc1a0 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 2 Oct 2020 10:56:37 +0300 Subject: [PATCH 116/139] Correct default timedelta format to 'long' Augments 9327e0824a1bbed538e73d42b971988f8214b490 --- babel/support.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/babel/support.py b/babel/support.py index 47f812d8a..8d905ed64 100644 --- a/babel/support.py +++ b/babel/support.py @@ -79,7 +79,7 @@ def time(self, time=None, format='medium'): return format_time(time, format, tzinfo=self.tzinfo, locale=self.locale) def timedelta(self, delta, granularity='second', threshold=.85, - format='medium', add_direction=False): + format='long', add_direction=False): """Return a time delta according to the rules of the given locale. >>> from datetime import timedelta From acf1caeed35c180ce7c14f822a23ab297931feab Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 2 Oct 2020 10:56:59 +0300 Subject: [PATCH 117/139] Skip deprecated l*gettext functions on Python 3.8+ --- tests/test_support.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/test_support.py b/tests/test_support.py index 1b74ae8bc..966cb4e62 100644 --- a/tests/test_support.py +++ b/tests/test_support.py @@ -17,6 +17,7 @@ import tempfile import unittest import pytest +import sys from datetime import date, datetime, timedelta from babel import support @@ -26,6 +27,7 @@ get_arg_spec = (inspect.getargspec if PY2 else inspect.getfullargspec) +SKIP_LGETTEXT = sys.version_info >= (3, 8) @pytest.mark.usefixtures("os_environ") class TranslationsTestCase(unittest.TestCase): @@ -76,6 +78,7 @@ def test_upgettext(self): self.assertEqualTypeToo(u'VohCTX', self.translations.upgettext('foo', 'foo')) + @pytest.mark.skipif(SKIP_LGETTEXT, reason="lgettext is deprecated") def test_lpgettext(self): self.assertEqualTypeToo(b'Voh', self.translations.lgettext('foo')) self.assertEqualTypeToo(b'VohCTX', self.translations.lpgettext('foo', @@ -105,6 +108,7 @@ def test_unpgettext(self): self.translations.unpgettext('foo', 'foo1', 'foos1', 2)) + @pytest.mark.skipif(SKIP_LGETTEXT, reason="lgettext is deprecated") def test_lnpgettext(self): self.assertEqualTypeToo(b'Voh1', self.translations.lngettext('foo1', 'foos1', 1)) @@ -129,6 +133,7 @@ def test_dupgettext(self): self.assertEqualTypeToo( u'VohCTXD', self.translations.dupgettext('messages1', 'foo', 'foo')) + @pytest.mark.skipif(SKIP_LGETTEXT, reason="lgettext is deprecated") def test_ldpgettext(self): self.assertEqualTypeToo( b'VohD', self.translations.ldgettext('messages1', 'foo')) @@ -159,6 +164,7 @@ def test_dunpgettext(self): u'VohsCTXD1', self.translations.dunpgettext('messages1', 'foo', 'foo1', 'foos1', 2)) + @pytest.mark.skipif(SKIP_LGETTEXT, reason="lgettext is deprecated") def test_ldnpgettext(self): self.assertEqualTypeToo( b'VohD1', self.translations.ldngettext('messages1', 'foo1', 'foos1', 1)) @@ -197,7 +203,11 @@ def setUp(self): self.null_translations = support.NullTranslations(fp=fp) def method_names(self): - return [name for name in dir(self.translations) if 'gettext' in name] + names = [name for name in dir(self.translations) if 'gettext' in name] + if SKIP_LGETTEXT: + # Remove deprecated l*gettext functions + names = [name for name in names if not name.startswith('l')] + return names def test_same_methods(self): for name in self.method_names(): From 2b615f7ea7d3c1383e26d1cea402f43895750843 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 2 Oct 2020 12:29:48 +0300 Subject: [PATCH 118/139] Ignore lack of coverage on lines that e.g. raise warnings --- .coveragerc | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000..a3d8ae65e --- /dev/null +++ b/.coveragerc @@ -0,0 +1,5 @@ +[report] +exclude_lines = + NotImplemented + pragma: no cover + warnings.warn \ No newline at end of file From 78c42824a61427aa73ebfa6ad5e35da16030671d Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 2 Oct 2020 12:35:56 +0300 Subject: [PATCH 119/139] Use 'if not' instead of 'if ... is False' for no_fuzzy_matching Matches the behavior elsewhere in the same function Fixes #698 --- babel/messages/catalog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py index 2fcb461a8..ed9e9a062 100644 --- a/babel/messages/catalog.py +++ b/babel/messages/catalog.py @@ -807,7 +807,7 @@ def _merge(message, oldkey, newkey): if key in messages: _merge(message, key, key) else: - if no_fuzzy_matching is False: + if not no_fuzzy_matching: # do some fuzzy matching with difflib if isinstance(key, tuple): matchkey = key[0] # just the msgid, no context From 77a3d2f2a4b897c5a9d09a2cbe7159bf7ef37f0c Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sun, 1 Nov 2020 12:37:36 -0800 Subject: [PATCH 120/139] Remove deprecated 'sudo: false' from Travis configuraiton https://blog.travis-ci.com/2018-11-19-required-linux-infrastructure-migration --- .travis.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9650f674c..0aebbf982 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,6 @@ dist: xenial language: python -# Use travis docker infrastructure for greater speed -sudo: false - cache: directories: - cldr From 53153419972aef6e5c06c6cc1c58896de6b2a281 Mon Sep 17 00:00:00 2001 From: Alessio Bogon Date: Tue, 10 Nov 2020 10:34:57 +0100 Subject: [PATCH 121/139] Handle ZoneInfo objects in get_timezone_location, get_timezone_name (#741) Fixes #740 --- .travis.yml | 2 +- babel/dates.py | 26 +++++---- tests/test_dates.py | 129 ++++++++++++++++++++++++++++++-------------- tox.ini | 1 + 4 files changed, 108 insertions(+), 50 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0aebbf982..e7401cc4f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,7 +37,7 @@ matrix: install: - bash .ci/deps.${TRAVIS_OS_NAME}.sh - pip install --upgrade pip - - pip install --upgrade $CDECIMAL pytest==4.3.1 pytest-cov==2.6.1 freezegun==0.3.12 + - pip install --upgrade $CDECIMAL pytest==4.3.1 pytest-cov==2.6.1 freezegun==0.3.12 'backports.zoneinfo;python_version>="3.6" and python_version<"3.9"' - pip install --editable . script: diff --git a/babel/dates.py b/babel/dates.py index f1bd66faf..1775029fa 100644 --- a/babel/dates.py +++ b/babel/dates.py @@ -76,6 +76,21 @@ def _get_dt_and_tzinfo(dt_or_tzinfo): return dt, tzinfo +def _get_tz_name(dt_or_tzinfo): + """ + Get the timezone name out of a time, datetime, or tzinfo object. + + :rtype: str + """ + dt, tzinfo = _get_dt_and_tzinfo(dt_or_tzinfo) + if hasattr(tzinfo, 'zone'): # pytz object + return tzinfo.zone + elif hasattr(tzinfo, 'key') and tzinfo.key is not None: # ZoneInfo object + return tzinfo.key + else: + return tzinfo.tzname(dt or datetime.utcnow()) + + def _get_datetime(instant): """ Get a datetime out of an "instant" (date, time, datetime, number). @@ -500,13 +515,9 @@ def get_timezone_location(dt_or_tzinfo=None, locale=LC_TIME, return_city=False): :return: the localized timezone name using location format """ - dt, tzinfo = _get_dt_and_tzinfo(dt_or_tzinfo) locale = Locale.parse(locale) - if hasattr(tzinfo, 'zone'): - zone = tzinfo.zone - else: - zone = tzinfo.tzname(dt or datetime.utcnow()) + zone = _get_tz_name(dt_or_tzinfo) # Get the canonical time-zone code zone = get_global('zone_aliases').get(zone, zone) @@ -619,10 +630,7 @@ def get_timezone_name(dt_or_tzinfo=None, width='long', uncommon=False, dt, tzinfo = _get_dt_and_tzinfo(dt_or_tzinfo) locale = Locale.parse(locale) - if hasattr(tzinfo, 'zone'): - zone = tzinfo.zone - else: - zone = tzinfo.tzname(dt) + zone = _get_tz_name(dt_or_tzinfo) if zone_variant is None: if dt is None: diff --git a/tests/test_dates.py b/tests/test_dates.py index 5be0d16a1..423f737de 100644 --- a/tests/test_dates.py +++ b/tests/test_dates.py @@ -24,6 +24,23 @@ from babel.util import FixedOffsetTimezone +@pytest.fixture(params=["pytz.timezone", "zoneinfo.ZoneInfo"]) +def timezone_getter(request): + if request.param == "pytz.timezone": + return timezone + elif request.param == "zoneinfo.ZoneInfo": + try: + import zoneinfo + except ImportError: + try: + from backports import zoneinfo + except ImportError: + pytest.skip("zoneinfo not available") + return zoneinfo.ZoneInfo + else: + raise NotImplementedError + + class DateTimeFormatTestCase(unittest.TestCase): def test_quarter_format(self): @@ -583,8 +600,8 @@ def test_get_timezone_gmt(): assert dates.get_timezone_gmt(dt, 'long', locale='fr_FR') == u'UTC-07:00' -def test_get_timezone_location(): - tz = timezone('America/St_Johns') +def test_get_timezone_location(timezone_getter): + tz = timezone_getter('America/St_Johns') assert (dates.get_timezone_location(tz, locale='de_DE') == u"Kanada (St. John\u2019s) Zeit") assert (dates.get_timezone_location(tz, locale='en') == @@ -592,51 +609,83 @@ def test_get_timezone_location(): assert (dates.get_timezone_location(tz, locale='en', return_city=True) == u'St. John’s') - tz = timezone('America/Mexico_City') + tz = timezone_getter('America/Mexico_City') assert (dates.get_timezone_location(tz, locale='de_DE') == u'Mexiko (Mexiko-Stadt) Zeit') - tz = timezone('Europe/Berlin') + tz = timezone_getter('Europe/Berlin') assert (dates.get_timezone_location(tz, locale='de_DE') == u'Deutschland (Berlin) Zeit') -def test_get_timezone_name(): - dt = time(15, 30, tzinfo=timezone('America/Los_Angeles')) - assert (dates.get_timezone_name(dt, locale='en_US') == - u'Pacific Standard Time') - assert (dates.get_timezone_name(dt, locale='en_US', return_zone=True) == - u'America/Los_Angeles') - assert dates.get_timezone_name(dt, width='short', locale='en_US') == u'PST' - - tz = timezone('America/Los_Angeles') - assert dates.get_timezone_name(tz, locale='en_US') == u'Pacific Time' - assert dates.get_timezone_name(tz, 'short', locale='en_US') == u'PT' - - tz = timezone('Europe/Berlin') - assert (dates.get_timezone_name(tz, locale='de_DE') == - u'Mitteleurop\xe4ische Zeit') - assert (dates.get_timezone_name(tz, locale='pt_BR') == - u'Hor\xe1rio da Europa Central') - - tz = timezone('America/St_Johns') - assert dates.get_timezone_name(tz, locale='de_DE') == u'Neufundland-Zeit' - - tz = timezone('America/Los_Angeles') - assert dates.get_timezone_name(tz, locale='en', width='short', - zone_variant='generic') == u'PT' - assert dates.get_timezone_name(tz, locale='en', width='short', - zone_variant='standard') == u'PST' - assert dates.get_timezone_name(tz, locale='en', width='short', - zone_variant='daylight') == u'PDT' - assert dates.get_timezone_name(tz, locale='en', width='long', - zone_variant='generic') == u'Pacific Time' - assert dates.get_timezone_name(tz, locale='en', width='long', - zone_variant='standard') == u'Pacific Standard Time' - assert dates.get_timezone_name(tz, locale='en', width='long', - zone_variant='daylight') == u'Pacific Daylight Time' - - localnow = datetime.utcnow().replace(tzinfo=timezone('UTC')).astimezone(dates.LOCALTZ) +@pytest.mark.parametrize( + "tzname, params, expected", + [ + ("America/Los_Angeles", {"locale": "en_US"}, u"Pacific Time"), + ("America/Los_Angeles", {"width": "short", "locale": "en_US"}, u"PT"), + ("Europe/Berlin", {"locale": "de_DE"}, u"Mitteleurop\xe4ische Zeit"), + ("Europe/Berlin", {"locale": "pt_BR"}, u"Hor\xe1rio da Europa Central"), + ("America/St_Johns", {"locale": "de_DE"}, u"Neufundland-Zeit"), + ( + "America/Los_Angeles", + {"locale": "en", "width": "short", "zone_variant": "generic"}, + u"PT", + ), + ( + "America/Los_Angeles", + {"locale": "en", "width": "short", "zone_variant": "standard"}, + u"PST", + ), + ( + "America/Los_Angeles", + {"locale": "en", "width": "short", "zone_variant": "daylight"}, + u"PDT", + ), + ( + "America/Los_Angeles", + {"locale": "en", "width": "long", "zone_variant": "generic"}, + u"Pacific Time", + ), + ( + "America/Los_Angeles", + {"locale": "en", "width": "long", "zone_variant": "standard"}, + u"Pacific Standard Time", + ), + ( + "America/Los_Angeles", + {"locale": "en", "width": "long", "zone_variant": "daylight"}, + u"Pacific Daylight Time", + ), + ("Europe/Berlin", {"locale": "en_US"}, u"Central European Time"), + ], +) +def test_get_timezone_name_tzinfo(timezone_getter, tzname, params, expected): + tz = timezone_getter(tzname) + assert dates.get_timezone_name(tz, **params) == expected + + +@pytest.mark.parametrize("timezone_getter", ["pytz.timezone"], indirect=True) +@pytest.mark.parametrize( + "tzname, params, expected", + [ + ("America/Los_Angeles", {"locale": "en_US"}, u"Pacific Standard Time"), + ( + "America/Los_Angeles", + {"locale": "en_US", "return_zone": True}, + u"America/Los_Angeles", + ), + ("America/Los_Angeles", {"width": "short", "locale": "en_US"}, u"PST"), + ], +) +def test_get_timezone_name_time_pytz(timezone_getter, tzname, params, expected): + """pytz (by design) can't determine if the time is in DST or not, + so it will always return Standard time""" + dt = time(15, 30, tzinfo=timezone_getter(tzname)) + assert dates.get_timezone_name(dt, **params) == expected + + +def test_get_timezone_name_misc(timezone_getter): + localnow = datetime.utcnow().replace(tzinfo=timezone_getter('UTC')).astimezone(dates.LOCALTZ) assert (dates.get_timezone_name(None, locale='en_US') == dates.get_timezone_name(localnow, locale='en_US')) diff --git a/tox.ini b/tox.ini index eccffea94..c069c5942 100644 --- a/tox.ini +++ b/tox.ini @@ -7,6 +7,7 @@ deps = pytest-cov==2.6.1 cdecimal: m3-cdecimal freezegun==0.3.12 + backports.zoneinfo;python_version>"3.6" and python_version<"3.9" whitelist_externals = make commands = make clean-cldr test passenv = PYTHON_TEST_FLAGS From 6e29f11234a046fca4716b8804f8cd62c7b51166 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 11 Nov 2020 10:35:35 +0200 Subject: [PATCH 122/139] Py.test 6 support (#752) * Support Py.test 6+ * Run CI on Py.test 6 on new Pythons --- .travis.yml | 18 +++++++++++++++++- conftest.py | 3 +++ tox.ini | 5 +++-- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index e7401cc4f..7f7331be5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,33 +11,49 @@ matrix: include: - os: linux python: 2.7 + env: + - PYTEST_VERSION=4.3.1 - os: linux python: 2.7 env: - CDECIMAL=m3-cdecimal + - PYTEST_VERSION=4.3.1 - os: linux dist: trusty python: pypy + env: + - PYTEST_VERSION=4.3.1 - os: linux dist: trusty python: pypy3 + env: + - PYTEST_VERSION=6.1.2 - os: linux python: 3.4 + env: + - PYTEST_VERSION=4.3.1 - os: linux python: 3.5 env: - PYTHON_TEST_FLAGS=-bb + - PYTEST_VERSION=6.1.2 - os: linux python: 3.6 + env: + - PYTEST_VERSION=6.1.2 - os: linux python: 3.7 + env: + - PYTEST_VERSION=6.1.2 - os: linux python: 3.8 + env: + - PYTEST_VERSION=6.1.2 install: - bash .ci/deps.${TRAVIS_OS_NAME}.sh - pip install --upgrade pip - - pip install --upgrade $CDECIMAL pytest==4.3.1 pytest-cov==2.6.1 freezegun==0.3.12 'backports.zoneinfo;python_version>="3.6" and python_version<"3.9"' + - pip install --upgrade $CDECIMAL pytest==$PYTEST_VERSION pytest-cov freezegun==0.3.12 'backports.zoneinfo;python_version>="3.6" and python_version<"3.9"' - pip install --editable . script: diff --git a/conftest.py b/conftest.py index 32bd1362a..bd9f2d32d 100644 --- a/conftest.py +++ b/conftest.py @@ -8,4 +8,7 @@ def pytest_collect_file(path, parent): if babel_path.common(path) == babel_path: if path.ext == ".py": + # TODO: remove check when dropping support for old Pytest + if hasattr(DoctestModule, "from_parent"): + return DoctestModule.from_parent(parent, fspath=path) return DoctestModule(path, parent) diff --git a/tox.ini b/tox.ini index c069c5942..27faf5bbb 100644 --- a/tox.ini +++ b/tox.ini @@ -3,8 +3,9 @@ envlist = py27, pypy, py34, py35, py36, py37, pypy3, py27-cdecimal [testenv] deps = - pytest==4.3.1 - pytest-cov==2.6.1 + pytest==4.3.1;python_version<"3.5" + pytest==6.1.2;python_version>="3.5" + pytest-cov cdecimal: m3-cdecimal freezegun==0.3.12 backports.zoneinfo;python_version>"3.6" and python_version<"3.9" From 99cc2c6fba08939dc8693ac0ae53c6046cc92459 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 11 Nov 2020 10:35:35 +0200 Subject: [PATCH 123/139] Py.test 6 support (#752) * Support Py.test 6+ * Run CI on Py.test 6 on new Pythons Cherry-pick of commit 6e29f11 --- .travis.yml | 18 +++++++++++++++++- conftest.py | 3 +++ tox.ini | 5 +++-- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9650f674c..495aa2721 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,33 +14,49 @@ matrix: include: - os: linux python: 2.7 + env: + - PYTEST_VERSION=4.3.1 - os: linux python: 2.7 env: - CDECIMAL=m3-cdecimal + - PYTEST_VERSION=4.3.1 - os: linux dist: trusty python: pypy + env: + - PYTEST_VERSION=4.3.1 - os: linux dist: trusty python: pypy3 + env: + - PYTEST_VERSION=6.1.2 - os: linux python: 3.4 + env: + - PYTEST_VERSION=4.3.1 - os: linux python: 3.5 env: - PYTHON_TEST_FLAGS=-bb + - PYTEST_VERSION=6.1.2 - os: linux python: 3.6 + env: + - PYTEST_VERSION=6.1.2 - os: linux python: 3.7 + env: + - PYTEST_VERSION=6.1.2 - os: linux python: 3.8 + env: + - PYTEST_VERSION=6.1.2 install: - bash .ci/deps.${TRAVIS_OS_NAME}.sh - pip install --upgrade pip - - pip install --upgrade $CDECIMAL pytest==4.3.1 pytest-cov==2.6.1 freezegun==0.3.12 + - pip install --upgrade $CDECIMAL pytest==$PYTEST_VERSION pytest-cov freezegun==0.3.12 - pip install --editable . script: diff --git a/conftest.py b/conftest.py index 32bd1362a..bd9f2d32d 100644 --- a/conftest.py +++ b/conftest.py @@ -8,4 +8,7 @@ def pytest_collect_file(path, parent): if babel_path.common(path) == babel_path: if path.ext == ".py": + # TODO: remove check when dropping support for old Pytest + if hasattr(DoctestModule, "from_parent"): + return DoctestModule.from_parent(parent, fspath=path) return DoctestModule(path, parent) diff --git a/tox.ini b/tox.ini index eccffea94..cda03a292 100644 --- a/tox.ini +++ b/tox.ini @@ -3,8 +3,9 @@ envlist = py27, pypy, py34, py35, py36, py37, pypy3, py27-cdecimal [testenv] deps = - pytest==4.3.1 - pytest-cov==2.6.1 + pytest==4.3.1;python_version<"3.5" + pytest==6.1.2;python_version>="3.5" + pytest-cov cdecimal: m3-cdecimal freezegun==0.3.12 whitelist_externals = make From d77fe5d3275f5cf9774fa6c9bf097b466ddb6473 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 11 Nov 2020 11:27:00 +0200 Subject: [PATCH 124/139] Distill changelog --- CHANGES | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGES b/CHANGES index 4441f68e7..c694ec8c7 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,16 @@ Babel Changelog =============== +Version 2.8.1 +------------- + +This is solely a patch release to make running tests on Py.test 6+ possible. + +Bugfixes +~~~~~~~~ + +* Support Py.test 6 - Aarni Koskela (#747, #750, #752) + Version 2.8.0 ------------- From bd836a4ac51958c083ff13ea02f8c18b7c0f4e2d Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 11 Nov 2020 11:27:58 +0200 Subject: [PATCH 125/139] Bump copyright year from 2019 to 2020 --- LICENSE | 2 +- babel/__init__.py | 2 +- babel/core.py | 2 +- babel/dates.py | 2 +- babel/lists.py | 2 +- babel/localedata.py | 2 +- babel/localtime/__init__.py | 2 +- babel/messages/__init__.py | 2 +- babel/messages/catalog.py | 2 +- babel/messages/checkers.py | 2 +- babel/messages/extract.py | 2 +- babel/messages/frontend.py | 2 +- babel/messages/jslexer.py | 2 +- babel/messages/mofile.py | 2 +- babel/messages/plurals.py | 2 +- babel/messages/pofile.py | 2 +- babel/numbers.py | 2 +- babel/plural.py | 2 +- babel/support.py | 2 +- babel/util.py | 2 +- docs/conf.py | 2 +- scripts/dump_data.py | 2 +- scripts/dump_global.py | 2 +- scripts/import_cldr.py | 2 +- tests/messages/test_catalog.py | 2 +- tests/messages/test_checkers.py | 2 +- tests/messages/test_extract.py | 2 +- tests/messages/test_frontend.py | 2 +- tests/messages/test_mofile.py | 2 +- tests/messages/test_plurals.py | 2 +- tests/messages/test_pofile.py | 2 +- tests/test_core.py | 2 +- tests/test_dates.py | 2 +- tests/test_localedata.py | 2 +- tests/test_numbers.py | 2 +- tests/test_plural.py | 2 +- tests/test_support.py | 2 +- tests/test_util.py | 2 +- 38 files changed, 38 insertions(+), 38 deletions(-) diff --git a/LICENSE b/LICENSE index 10722cc18..7e2f12e46 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2013-2019 by the Babel Team, see AUTHORS for more information. +Copyright (c) 2013-2020 by the Babel Team, see AUTHORS for more information. All rights reserved. diff --git a/babel/__init__.py b/babel/__init__.py index c10a8bd1f..12674e051 100644 --- a/babel/__init__.py +++ b/babel/__init__.py @@ -13,7 +13,7 @@ access to various locale display names, localized number and date formatting, etc. - :copyright: (c) 2013-2019 by the Babel Team. + :copyright: (c) 2013-2020 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/core.py b/babel/core.py index a80807a61..a0c35b4ea 100644 --- a/babel/core.py +++ b/babel/core.py @@ -5,7 +5,7 @@ Core locale representation and locale data access. - :copyright: (c) 2013-2019 by the Babel Team. + :copyright: (c) 2013-2020 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/dates.py b/babel/dates.py index f1bd66faf..dea365a7f 100644 --- a/babel/dates.py +++ b/babel/dates.py @@ -12,7 +12,7 @@ * ``LC_ALL``, and * ``LANG`` - :copyright: (c) 2013-2019 by the Babel Team. + :copyright: (c) 2013-2020 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/lists.py b/babel/lists.py index ab5a24c40..e0bd7ed7f 100644 --- a/babel/lists.py +++ b/babel/lists.py @@ -11,7 +11,7 @@ * ``LC_ALL``, and * ``LANG`` - :copyright: (c) 2015-2019 by the Babel Team. + :copyright: (c) 2015-2020 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/localedata.py b/babel/localedata.py index e012abbf2..f4771d1fd 100644 --- a/babel/localedata.py +++ b/babel/localedata.py @@ -8,7 +8,7 @@ :note: The `Locale` class, which uses this module under the hood, provides a more convenient interface for accessing the locale data. - :copyright: (c) 2013-2019 by the Babel Team. + :copyright: (c) 2013-2020 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/localtime/__init__.py b/babel/localtime/__init__.py index aefd8a3e7..8c9203a7b 100644 --- a/babel/localtime/__init__.py +++ b/babel/localtime/__init__.py @@ -6,7 +6,7 @@ Babel specific fork of tzlocal to determine the local timezone of the system. - :copyright: (c) 2013-2019 by the Babel Team. + :copyright: (c) 2013-2020 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/messages/__init__.py b/babel/messages/__init__.py index 5b69675f3..77c79621b 100644 --- a/babel/messages/__init__.py +++ b/babel/messages/__init__.py @@ -5,7 +5,7 @@ Support for ``gettext`` message catalogs. - :copyright: (c) 2013-2019 by the Babel Team. + :copyright: (c) 2013-2020 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py index 2fcb461a8..b9961086c 100644 --- a/babel/messages/catalog.py +++ b/babel/messages/catalog.py @@ -5,7 +5,7 @@ Data structures for message catalogs. - :copyright: (c) 2013-2019 by the Babel Team. + :copyright: (c) 2013-2020 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/messages/checkers.py b/babel/messages/checkers.py index 8c1effaf5..29add9fce 100644 --- a/babel/messages/checkers.py +++ b/babel/messages/checkers.py @@ -7,7 +7,7 @@ :since: version 0.9 - :copyright: (c) 2013-2019 by the Babel Team. + :copyright: (c) 2013-2020 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/messages/extract.py b/babel/messages/extract.py index e7d7ad70f..8f53fa298 100644 --- a/babel/messages/extract.py +++ b/babel/messages/extract.py @@ -13,7 +13,7 @@ The main entry points into the extraction functionality are the functions `extract_from_dir` and `extract_from_file`. - :copyright: (c) 2013-2019 by the Babel Team. + :copyright: (c) 2013-2020 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/messages/frontend.py b/babel/messages/frontend.py index 475605549..0b65a7c6b 100644 --- a/babel/messages/frontend.py +++ b/babel/messages/frontend.py @@ -5,7 +5,7 @@ Frontends for the message extraction functionality. - :copyright: (c) 2013-2019 by the Babel Team. + :copyright: (c) 2013-2020 by the Babel Team. :license: BSD, see LICENSE for more details. """ from __future__ import print_function diff --git a/babel/messages/jslexer.py b/babel/messages/jslexer.py index ace0b47e0..f56c80d98 100644 --- a/babel/messages/jslexer.py +++ b/babel/messages/jslexer.py @@ -6,7 +6,7 @@ A simple JavaScript 1.5 lexer which is used for the JavaScript extractor. - :copyright: (c) 2013-2019 by the Babel Team. + :copyright: (c) 2013-2020 by the Babel Team. :license: BSD, see LICENSE for more details. """ from collections import namedtuple diff --git a/babel/messages/mofile.py b/babel/messages/mofile.py index dfd923d23..1ab3e4e9c 100644 --- a/babel/messages/mofile.py +++ b/babel/messages/mofile.py @@ -5,7 +5,7 @@ Writing of files in the ``gettext`` MO (machine object) format. - :copyright: (c) 2013-2019 by the Babel Team. + :copyright: (c) 2013-2020 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/messages/plurals.py b/babel/messages/plurals.py index 81234580d..4755cfcf9 100644 --- a/babel/messages/plurals.py +++ b/babel/messages/plurals.py @@ -5,7 +5,7 @@ Plural form definitions. - :copyright: (c) 2013-2019 by the Babel Team. + :copyright: (c) 2013-2020 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/messages/pofile.py b/babel/messages/pofile.py index 93b0697c6..aff886d8e 100644 --- a/babel/messages/pofile.py +++ b/babel/messages/pofile.py @@ -6,7 +6,7 @@ Reading and writing of files in the ``gettext`` PO (portable object) format. - :copyright: (c) 2013-2019 by the Babel Team. + :copyright: (c) 2013-2020 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/numbers.py b/babel/numbers.py index cf819fc9a..e246baed7 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -12,7 +12,7 @@ * ``LC_ALL``, and * ``LANG`` - :copyright: (c) 2013-2019 by the Babel Team. + :copyright: (c) 2013-2020 by the Babel Team. :license: BSD, see LICENSE for more details. """ # TODO: diff --git a/babel/plural.py b/babel/plural.py index 1e2b2734b..3c9a3e614 100644 --- a/babel/plural.py +++ b/babel/plural.py @@ -5,7 +5,7 @@ CLDR Plural support. See UTS #35. - :copyright: (c) 2013-2019 by the Babel Team. + :copyright: (c) 2013-2020 by the Babel Team. :license: BSD, see LICENSE for more details. """ import re diff --git a/babel/support.py b/babel/support.py index efe41d562..a8709e46d 100644 --- a/babel/support.py +++ b/babel/support.py @@ -8,7 +8,7 @@ .. note: the code in this module is not used by Babel itself - :copyright: (c) 2013-2019 by the Babel Team. + :copyright: (c) 2013-2020 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/util.py b/babel/util.py index 73a90516f..35039c41f 100644 --- a/babel/util.py +++ b/babel/util.py @@ -5,7 +5,7 @@ Various utility classes and functions. - :copyright: (c) 2013-2019 by the Babel Team. + :copyright: (c) 2013-2020 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/docs/conf.py b/docs/conf.py index 83c5bf206..41d1049b8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -44,7 +44,7 @@ # General information about the project. project = u'Babel' -copyright = u'2019, The Babel Team' +copyright = u'2020, The Babel Team' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/scripts/dump_data.py b/scripts/dump_data.py index e452248d7..57439b98f 100755 --- a/scripts/dump_data.py +++ b/scripts/dump_data.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/scripts/dump_global.py b/scripts/dump_global.py index 8db55f2dc..a27b31617 100755 --- a/scripts/dump_global.py +++ b/scripts/dump_global.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py index 8993b68e4..f0558c239 100755 --- a/scripts/import_cldr.py +++ b/scripts/import_cldr.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/messages/test_catalog.py b/tests/messages/test_catalog.py index f31dca310..2d9e31d00 100644 --- a/tests/messages/test_catalog.py +++ b/tests/messages/test_catalog.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/messages/test_checkers.py b/tests/messages/test_checkers.py index ec845001e..bf813bc05 100644 --- a/tests/messages/test_checkers.py +++ b/tests/messages/test_checkers.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/messages/test_extract.py b/tests/messages/test_extract.py index 2f41ddc2c..38979cd30 100644 --- a/tests/messages/test_extract.py +++ b/tests/messages/test_extract.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/messages/test_frontend.py b/tests/messages/test_frontend.py index ad3ea0df3..38165b354 100644 --- a/tests/messages/test_frontend.py +++ b/tests/messages/test_frontend.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/messages/test_mofile.py b/tests/messages/test_mofile.py index b1851f297..395b9fb1a 100644 --- a/tests/messages/test_mofile.py +++ b/tests/messages/test_mofile.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/messages/test_plurals.py b/tests/messages/test_plurals.py index bdca8f6a8..62c770870 100644 --- a/tests/messages/test_plurals.py +++ b/tests/messages/test_plurals.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/messages/test_pofile.py b/tests/messages/test_pofile.py index e77fa6e02..c8125f9d6 100644 --- a/tests/messages/test_pofile.py +++ b/tests/messages/test_pofile.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/test_core.py b/tests/test_core.py index c146aae9c..53b9d1875 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/test_dates.py b/tests/test_dates.py index 5be0d16a1..16fda9cbc 100644 --- a/tests/test_dates.py +++ b/tests/test_dates.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/test_localedata.py b/tests/test_localedata.py index dbacba0d5..83cd66994 100644 --- a/tests/test_localedata.py +++ b/tests/test_localedata.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/test_numbers.py b/tests/test_numbers.py index a980a66ad..bda7bffba 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/test_plural.py b/tests/test_plural.py index c54d07a57..1a9fbe507 100644 --- a/tests/test_plural.py +++ b/tests/test_plural.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/test_support.py b/tests/test_support.py index b4dd823cd..f9a552d16 100644 --- a/tests/test_support.py +++ b/tests/test_support.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/test_util.py b/tests/test_util.py index a6a4450cf..5b897aa7d 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2019 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which From efa0d6d3d7f632ff398705d4f767d953226c16fd Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 11 Nov 2020 11:28:06 +0200 Subject: [PATCH 126/139] Bump version to 2.8.1 --- babel/__init__.py | 2 +- docs/conf.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/babel/__init__.py b/babel/__init__.py index 12674e051..cbcb4fb2f 100644 --- a/babel/__init__.py +++ b/babel/__init__.py @@ -21,4 +21,4 @@ negotiate_locale, parse_locale, get_locale_identifier -__version__ = '2.8.0' +__version__ = '2.8.1' diff --git a/docs/conf.py b/docs/conf.py index 41d1049b8..7b5584a1a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,7 +53,7 @@ # The short X.Y version. version = '2.8' # The full version, including alpha/beta/rc tags. -release = '2.8.0' +release = '2.8.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From cef9189ce68c35f5790bf9ebb3ca13719bded696 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 11 Nov 2020 14:42:18 +0200 Subject: [PATCH 127/139] Distill changelog --- AUTHORS | 9 ++++++++- CHANGES | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 31a5cd1be..9cf8f4e7d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -25,12 +25,13 @@ Babel is written and maintained by the Babel team and various contributors: - Sachin Paliwal - Alex Willmer - Daniel Neuhäuser +- Miro Hrončok - Cédric Krier - Luke Plant - Jennifer Wang - Lukas Balaga - sudheesh001 -- Miro Hrončok +- Niklas Hambüchen - Changaco - Xavier Fernandez - KO. Mattsson @@ -45,6 +46,12 @@ Babel is written and maintained by the Babel team and various contributors: - Leonardo Pistone - Jun Omae - Hyunjun Kim +- Alessio Bogon +- Nikiforov Konstantin +- Abdullah Javed Nesar +- Brad Martin +- Tyler Kennedy +- CyanNani123 - sebleblanc - He Chen - Steve (Gadget) Barnes diff --git a/CHANGES b/CHANGES index c694ec8c7..fbc73801f 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,41 @@ Babel Changelog =============== +Version 2.9.0 +------------- + +Upcoming version support changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* This version, Babel 2.9, is the last version of Babel to support Python 2.7, Python 3.4, and Python 3.5. + +Improvements +~~~~~~~~~~~~ + +* CLDR: Use CLDR 37 – Aarni Koskela (#734) +* Dates: Handle ZoneInfo objects in get_timezone_location, get_timezone_name - Alessio Bogon (#741) +* Numbers: Add group_separator feature in number formatting - Abdullah Javed Nesar (#726) + +Bugfixes +~~~~~~~~ + +* Dates: Correct default Format().timedelta format to 'long' to mute deprecation warnings – Aarni Koskela +* Import: Simplify iteration code in "import_cldr.py" – Felix Schwarz +* Import: Stop using deprecated ElementTree methods "getchildren()" and "getiterator()" – Felix Schwarz +* Messages: Fix unicode printing error on Python 2 without TTY. – Niklas Hambüchen +* Messages: Introduce invariant that _invalid_pofile() takes unicode line. – Niklas Hambüchen +* Tests: fix tests when using Python 3.9 – Felix Schwarz +* Tests: Remove deprecated 'sudo: false' from Travis configuration – Jon Dufresne +* Tests: Support Py.test 6.x – Aarni Koskela +* Utilities: LazyProxy: Handle AttributeError in specified func – Nikiforov Konstantin (#724) +* Utilities: Replace usage of parser.suite with ast.parse – Miro Hrončok + +Documentation +~~~~~~~~~~~~~ + +* Update parse_number comments – Brad Martin (#708) +* Add __iter__ to Catalog documentation – @CyanNani123 + Version 2.8.1 ------------- From 04090802f404ceca98844279750a1db0b0a4636d Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 11 Nov 2020 15:14:52 +0200 Subject: [PATCH 128/139] Fix deprecated .getchildren() call Augments 167b71421f113e2210e4deefef5020402492e5be --- scripts/import_cldr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py index 2977c6e40..8e5eebb95 100755 --- a/scripts/import_cldr.py +++ b/scripts/import_cldr.py @@ -855,7 +855,7 @@ def parse_unit_patterns(data, tree): unit_type = unit.attrib['type'] compound_unit_info = {} compound_variations = {} - for child in unit.getchildren(): + for child in unit: if child.tag == "unitPrefixPattern": compound_unit_info['prefix'] = _text(child) elif child.tag == "compoundUnitPattern": From 49e20930995a9feab35c918abe0cae2554e7e408 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 11 Nov 2020 14:42:51 +0200 Subject: [PATCH 129/139] Travis: Test on Python 3.9 --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7f7331be5..1bebe7f87 100644 --- a/.travis.yml +++ b/.travis.yml @@ -49,7 +49,10 @@ matrix: python: 3.8 env: - PYTEST_VERSION=6.1.2 - + - os: linux + python: 3.9 + env: + - PYTEST_VERSION=6.1.2 install: - bash .ci/deps.${TRAVIS_OS_NAME}.sh - pip install --upgrade pip From 9f6ea69f49fad04ece0611b856e0debfac2ae805 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 11 Nov 2020 14:43:28 +0200 Subject: [PATCH 130/139] Bump version to 2.9.0 --- babel/__init__.py | 2 +- docs/conf.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/babel/__init__.py b/babel/__init__.py index cbcb4fb2f..ecb18b546 100644 --- a/babel/__init__.py +++ b/babel/__init__.py @@ -21,4 +21,4 @@ negotiate_locale, parse_locale, get_locale_identifier -__version__ = '2.8.1' +__version__ = '2.9.0' diff --git a/docs/conf.py b/docs/conf.py index 7b5584a1a..1c21c67cc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -51,9 +51,9 @@ # built documents. # # The short X.Y version. -version = '2.8' +version = '2.9' # The full version, including alpha/beta/rc tags. -release = '2.8.1' +release = '2.9.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From 9a9d3c60ec05b346e87f2241b7ce976ed1a6bf0b Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 11 Nov 2020 14:11:38 +0200 Subject: [PATCH 131/139] Use Freezegun in test_format_current_moment The earlier patch resulted in `unsupported operand type(s) for -: 'datetime' and 'datetime'` on Pypy3. --- tests/test_dates.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/tests/test_dates.py b/tests/test_dates.py index 48ed05c49..8e693d34c 100644 --- a/tests/test_dates.py +++ b/tests/test_dates.py @@ -15,6 +15,7 @@ from datetime import date, datetime, time, timedelta import unittest +import freezegun import pytest import pytz from pytz import timezone @@ -809,19 +810,10 @@ def test_zh_TW_format(): assert dates.format_time(datetime(2016, 4, 8, 12, 34, 56), locale='zh_TW') == u'\u4e0b\u534812:34:56' -def test_format_current_moment(monkeypatch): - import datetime as datetime_module +def test_format_current_moment(): frozen_instant = datetime.utcnow() - - class frozen_datetime(datetime): - - @classmethod - def utcnow(cls): - return frozen_instant - - # Freeze time! Well, some of it anyway. - monkeypatch.setattr(datetime_module, "datetime", frozen_datetime) - assert dates.format_datetime(locale="en_US") == dates.format_datetime(frozen_instant, locale="en_US") + with freezegun.freeze_time(time_to_freeze=frozen_instant): + assert dates.format_datetime(locale="en_US") == dates.format_datetime(frozen_instant, locale="en_US") @pytest.mark.all_locales From 613dc1700f91c3d40b081948c0dd6023d8ece057 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 10 Nov 2020 14:54:31 +0200 Subject: [PATCH 132/139] Make the import warnings about unsupported number systems less verbose --- scripts/import_cldr.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py index 8e5eebb95..32e6abcb4 100755 --- a/scripts/import_cldr.py +++ b/scripts/import_cldr.py @@ -392,6 +392,7 @@ def _process_local_datas(sup, srcdir, destdir, force=False, dump_json=False): ])) data['locale_id'] = locale_id + data['unsupported_number_systems'] = set() if locale_id in plural_rules: data['plural_form'] = plural_rules[locale_id] @@ -432,6 +433,13 @@ def _process_local_datas(sup, srcdir, destdir, force=False, dump_json=False): parse_character_order(data, tree) parse_measurement_systems(data, tree) + unsupported_number_systems_string = ', '.join(sorted(data.pop('unsupported_number_systems'))) + if unsupported_number_systems_string: + log('%s: unsupported number systems were ignored: %s' % ( + locale_id, + unsupported_number_systems_string, + )) + write_datafile(data_filename, data, dump_json=dump_json) @@ -440,21 +448,14 @@ def _should_skip_number_elem(data, elem): Figure out whether the numbering-containing element `elem` is in a currently non-supported (i.e. currently non-Latin) numbering system. - If it is, a warning is raised. - - :param data: The root data element, for formatting the warning. + :param data: The root data element, for stashing the warning. :param elem: Element with `numberSystem` key :return: Boolean """ number_system = elem.get('numberSystem', 'latn') if number_system != 'latn': - log('%s: Unsupported number system "%s" in <%s numberSystem="%s">' % ( - data['locale_id'], - number_system, - elem.tag, - number_system, - )) + data['unsupported_number_systems'].add(number_system) return True return False From 156b7fb9f377ccf58c71cf01dc69fb10c7b69314 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 11 Nov 2020 15:24:16 +0200 Subject: [PATCH 133/139] Quiesce CLDR download progress bar if requested (or not a TTY) --- scripts/download_import_cldr.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/download_import_cldr.py b/scripts/download_import_cldr.py index 531a04c62..805772a16 100755 --- a/scripts/download_import_cldr.py +++ b/scripts/download_import_cldr.py @@ -75,12 +75,13 @@ def main(): cldr_path = os.path.join(repo, 'cldr', os.path.splitext(FILENAME)[0]) zip_path = os.path.join(cldr_dl_path, FILENAME) changed = False + show_progress = (False if os.environ.get("BABEL_CLDR_NO_DOWNLOAD_PROGRESS") else sys.stdout.isatty()) while not is_good_file(zip_path): log('Downloading \'%s\'', FILENAME) if os.path.isfile(zip_path): os.remove(zip_path) - urlretrieve(URL, zip_path, reporthook) + urlretrieve(URL, zip_path, (reporthook if show_progress else None)) changed = True print() common_path = os.path.join(cldr_path, 'common') From d1bbc08e845d03d8e1f0dfa0e04983d755f39cb5 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 11 Nov 2020 15:59:41 +0200 Subject: [PATCH 134/139] import_cldr: use logging; add -q option --- scripts/import_cldr.py | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py index 32e6abcb4..0dbd39369 100755 --- a/scripts/import_cldr.py +++ b/scripts/import_cldr.py @@ -17,6 +17,7 @@ import os import re import sys +import logging try: from xml.etree import cElementTree as ElementTree @@ -62,16 +63,7 @@ def _text(elem): 'timeFormats': 'time_formats' } - -def log(message, *args): - if args: - message = message % args - sys.stderr.write(message + '\r\n') - sys.stderr.flush() - - -def error(message, *args): - log('ERROR: %s' % message, *args) +log = logging.getLogger("import_cldr") def need_conversion(dst_filename, data_dict, source_filename): @@ -182,10 +174,19 @@ def main(): '-j', '--json', dest='dump_json', action='store_true', default=False, help='also export debugging JSON dumps of locale data' ) + parser.add_option( + '-q', '--quiet', dest='quiet', action='store_true', default=bool(os.environ.get('BABEL_CLDR_QUIET')), + help='quiesce info/warning messages', + ) options, args = parser.parse_args() if len(args) != 1: parser.error('incorrect number of arguments') + + logging.basicConfig( + level=(logging.ERROR if options.quiet else logging.INFO), + ) + return process_data( srcdir=args[0], destdir=BABEL_PACKAGE_ROOT, @@ -383,8 +384,10 @@ def _process_local_datas(sup, srcdir, destdir, force=False, dump_json=False): territory = '001' # world regions = territory_containment.get(territory, []) - log('Processing %s (Language = %s; Territory = %s)', - filename, language, territory) + log.info( + 'Processing %s (Language = %s; Territory = %s)', + filename, language, territory, + ) locale_id = '_'.join(filter(None, [ language, @@ -435,7 +438,7 @@ def _process_local_datas(sup, srcdir, destdir, force=False, dump_json=False): unsupported_number_systems_string = ', '.join(sorted(data.pop('unsupported_number_systems'))) if unsupported_number_systems_string: - log('%s: unsupported number systems were ignored: %s' % ( + log.warning('%s: unsupported number systems were ignored: %s' % ( locale_id, unsupported_number_systems_string, )) @@ -687,7 +690,7 @@ def parse_calendar_date_formats(data, calendar): text_type(elem.findtext('dateFormat/pattern')) ) except ValueError as e: - error(e) + log.error(e) elif elem.tag == 'alias': date_formats = Alias(_translate_alias( ['date_formats'], elem.attrib['path']) @@ -707,7 +710,7 @@ def parse_calendar_time_formats(data, calendar): text_type(elem.findtext('timeFormat/pattern')) ) except ValueError as e: - error(e) + log.error(e) elif elem.tag == 'alias': time_formats = Alias(_translate_alias( ['time_formats'], elem.attrib['path']) @@ -726,7 +729,7 @@ def parse_calendar_datetime_skeletons(data, calendar): try: datetime_formats[type] = text_type(elem.findtext('dateTimeFormat/pattern')) except ValueError as e: - error(e) + log.error(e) elif elem.tag == 'alias': datetime_formats = Alias(_translate_alias( ['datetime_formats'], elem.attrib['path']) From 58de8342f865df88697a4a166191e880e3c84d82 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 10 Nov 2020 16:19:04 +0200 Subject: [PATCH 135/139] Replace Travis + Appveyor with GitHub Actions (WIP) --- .ci/appveyor.yml | 38 -------------------- .ci/deploy.linux.sh | 4 --- .ci/deploy.osx.sh | 4 --- .ci/deps.linux.sh | 4 --- .ci/deps.osx.sh | 11 ------ .ci/run_with_env.cmd | 47 ------------------------ .github/workflows/test.yml | 38 ++++++++++++++++++++ .travis.yml | 74 -------------------------------------- setup.py | 8 ++--- tox.ini | 29 ++++++++------- 10 files changed, 58 insertions(+), 199 deletions(-) delete mode 100644 .ci/appveyor.yml delete mode 100644 .ci/deploy.linux.sh delete mode 100644 .ci/deploy.osx.sh delete mode 100644 .ci/deps.linux.sh delete mode 100644 .ci/deps.osx.sh delete mode 100644 .ci/run_with_env.cmd create mode 100644 .github/workflows/test.yml delete mode 100644 .travis.yml diff --git a/.ci/appveyor.yml b/.ci/appveyor.yml deleted file mode 100644 index 91758f415..000000000 --- a/.ci/appveyor.yml +++ /dev/null @@ -1,38 +0,0 @@ -# From https://github.com/ogrisel/python-appveyor-demo/blob/master/appveyor.yml - -environment: - global: - # SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the - # /E:ON and /V:ON options are not enabled in the batch script intepreter - # See: https://stackoverflow.com/a/13751649 - CMD_IN_ENV: "cmd /E:ON /V:ON /C .\\.ci\\run_with_env.cmd" - - matrix: - - PYTHON: "C:\\Python35" - PYTHON_VERSION: "3.5.x" - PYTHON_ARCH: "32" - - - PYTHON: "C:\\Python35-x64" - PYTHON_VERSION: "3.5.x" - PYTHON_ARCH: "64" - -branches: # Only build official branches, PRs are built anyway. - only: - - master - - /release.*/ - -install: - - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - # Check that we have the expected version and architecture for Python - - "python --version" - - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" - # Build data files - - "pip install --upgrade pytest==4.3.1 pytest-cov==2.6.1 codecov freezegun==0.3.12" - - "pip install --editable ." - - "python setup.py import_cldr" - -build: false # Not a C# project, build stuff at the test step instead. - -test_script: - - "%CMD_IN_ENV% python -m pytest --cov=babel" - - "codecov" diff --git a/.ci/deploy.linux.sh b/.ci/deploy.linux.sh deleted file mode 100644 index 4d59382d7..000000000 --- a/.ci/deploy.linux.sh +++ /dev/null @@ -1,4 +0,0 @@ -set -x -set -e - -bash <(curl -s https://codecov.io/bash) diff --git a/.ci/deploy.osx.sh b/.ci/deploy.osx.sh deleted file mode 100644 index c44550eff..000000000 --- a/.ci/deploy.osx.sh +++ /dev/null @@ -1,4 +0,0 @@ -set -x -set -e - -echo "Due to a bug in codecov, coverage cannot be deployed for Mac builds." diff --git a/.ci/deps.linux.sh b/.ci/deps.linux.sh deleted file mode 100644 index 13cc9e1ef..000000000 --- a/.ci/deps.linux.sh +++ /dev/null @@ -1,4 +0,0 @@ -set -x -set -e - -echo "No dependencies to install for linux." diff --git a/.ci/deps.osx.sh b/.ci/deps.osx.sh deleted file mode 100644 index b52a84f6d..000000000 --- a/.ci/deps.osx.sh +++ /dev/null @@ -1,11 +0,0 @@ -set -e -set -x - -# Install packages with brew -brew update >/dev/null -brew outdated pyenv || brew upgrade --quiet pyenv - -# Install required python version for this build -pyenv install -ks $PYTHON_VERSION -pyenv global $PYTHON_VERSION -python --version diff --git a/.ci/run_with_env.cmd b/.ci/run_with_env.cmd deleted file mode 100644 index 0f5b8e097..000000000 --- a/.ci/run_with_env.cmd +++ /dev/null @@ -1,47 +0,0 @@ -:: To build extensions for 64 bit Python 3, we need to configure environment -:: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of: -:: MS Windows SDK for Windows 7 and .NET Framework 4 (SDK v7.1) -:: -:: To build extensions for 64 bit Python 2, we need to configure environment -:: variables to use the MSVC 2008 C++ compilers from GRMSDKX_EN_DVD.iso of: -:: MS Windows SDK for Windows 7 and .NET Framework 3.5 (SDK v7.0) -:: -:: 32 bit builds do not require specific environment configurations. -:: -:: Note: this script needs to be run with the /E:ON and /V:ON flags for the -:: cmd interpreter, at least for (SDK v7.0) -:: -:: More details at: -:: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows -:: https://stackoverflow.com/a/13751649 -:: -:: Author: Olivier Grisel -:: License: CC0 1.0 Universal: https://creativecommons.org/publicdomain/zero/1.0/ -@ECHO OFF - -SET COMMAND_TO_RUN=%* -SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows - -SET MAJOR_PYTHON_VERSION="%PYTHON_VERSION:~0,1%" -IF %MAJOR_PYTHON_VERSION% == "2" ( - SET WINDOWS_SDK_VERSION="v7.0" -) ELSE IF %MAJOR_PYTHON_VERSION% == "3" ( - SET WINDOWS_SDK_VERSION="v7.1" -) ELSE ( - ECHO Unsupported Python version: "%MAJOR_PYTHON_VERSION%" - EXIT 1 -) - -IF "%PYTHON_ARCH%"=="64" ( - ECHO Configuring Windows SDK %WINDOWS_SDK_VERSION% for Python %MAJOR_PYTHON_VERSION% on a 64 bit architecture - SET DISTUTILS_USE_SDK=1 - SET MSSdk=1 - "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% - "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release - ECHO Executing: %COMMAND_TO_RUN% - call %COMMAND_TO_RUN% || EXIT 1 -) ELSE ( - ECHO Using default MSVC build environment for 32 bit architecture - ECHO Executing: %COMMAND_TO_RUN% - call %COMMAND_TO_RUN% || EXIT 1 -) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..e9c411862 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,38 @@ +name: Test + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-18.04, windows-2019, macos-10.15] + python-version: [3.6, 3.7, 3.8, 3.9, pypy3] + exclude: + - os: windows-2019 + python-version: pypy3 + # TODO: Remove this; see: + # https://github.com/actions/setup-python/issues/151 + # https://github.com/tox-dev/tox/issues/1704 + # https://foss.heptapod.net/pypy/pypy/-/issues/3331 + env: + BABEL_CLDR_NO_DOWNLOAD_PROGRESS: "1" + BABEL_CLDR_QUIET: "1" + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip setuptools wheel + python -m pip install tox tox-gh-actions==2.1.0 + - name: Run test via Tox + run: tox --skip-missing-interpreters + - uses: codecov/codecov-action@v1 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 1bebe7f87..000000000 --- a/.travis.yml +++ /dev/null @@ -1,74 +0,0 @@ -dist: xenial -language: python - -cache: - directories: - - cldr - - "$HOME/.cache/pip" - - "$HOME/.pyenv" - -matrix: - include: - - os: linux - python: 2.7 - env: - - PYTEST_VERSION=4.3.1 - - os: linux - python: 2.7 - env: - - CDECIMAL=m3-cdecimal - - PYTEST_VERSION=4.3.1 - - os: linux - dist: trusty - python: pypy - env: - - PYTEST_VERSION=4.3.1 - - os: linux - dist: trusty - python: pypy3 - env: - - PYTEST_VERSION=6.1.2 - - os: linux - python: 3.4 - env: - - PYTEST_VERSION=4.3.1 - - os: linux - python: 3.5 - env: - - PYTHON_TEST_FLAGS=-bb - - PYTEST_VERSION=6.1.2 - - os: linux - python: 3.6 - env: - - PYTEST_VERSION=6.1.2 - - os: linux - python: 3.7 - env: - - PYTEST_VERSION=6.1.2 - - os: linux - python: 3.8 - env: - - PYTEST_VERSION=6.1.2 - - os: linux - python: 3.9 - env: - - PYTEST_VERSION=6.1.2 -install: - - bash .ci/deps.${TRAVIS_OS_NAME}.sh - - pip install --upgrade pip - - pip install --upgrade $CDECIMAL pytest==$PYTEST_VERSION pytest-cov freezegun==0.3.12 'backports.zoneinfo;python_version>="3.6" and python_version<"3.9"' - - pip install --editable . - -script: - - make test-cov - - bash .ci/deploy.${TRAVIS_OS_NAME}.sh - -notifications: - email: false - irc: - channels: - - "chat.freenode.net#pocoo" - on_success: change - on_failure: always - use_notice: true - skip_join: true diff --git a/setup.py b/setup.py index 0032a3a05..adf3bb5d9 100755 --- a/setup.py +++ b/setup.py @@ -44,18 +44,16 @@ def run(self): 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Libraries :: Python Modules', ], - python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', + python_requires='>=3.6', packages=['babel', 'babel.messages', 'babel.localtime'], include_package_data=True, install_requires=[ diff --git a/tox.ini b/tox.ini index 27faf5bbb..14b450ff8 100644 --- a/tox.ini +++ b/tox.ini @@ -1,20 +1,25 @@ [tox] -envlist = py27, pypy, py34, py35, py36, py37, pypy3, py27-cdecimal +envlist = + py{36,37,38,39} + pypy3 [testenv] deps = - pytest==4.3.1;python_version<"3.5" - pytest==6.1.2;python_version>="3.5" + pytest pytest-cov - cdecimal: m3-cdecimal freezegun==0.3.12 - backports.zoneinfo;python_version>"3.6" and python_version<"3.9" + backports.zoneinfo;python_version<"3.9" + tzdata;sys_platform == 'win32' whitelist_externals = make -commands = make clean-cldr test -passenv = PYTHON_TEST_FLAGS +commands = make clean-cldr test-cov +passenv = + BABEL_* + PYTHON_* -[pep8] -ignore = E501,E731,W503 - -[flake8] -ignore = E501,E731,W503 +[gh-actions] +python = + pypy3: pypy3 + 3.6: py36 + 3.7: py37 + 3.8: py38 + 3.9: py39 From 3a700b5b8b53606fd98ef8294a56f9510f7290f8 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 28 Apr 2021 10:33:40 +0300 Subject: [PATCH 136/139] Run locale identifiers through `os.path.basename()` --- babel/localedata.py | 2 ++ tests/test_localedata.py | 30 +++++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/babel/localedata.py b/babel/localedata.py index f4771d1fd..11085490a 100644 --- a/babel/localedata.py +++ b/babel/localedata.py @@ -47,6 +47,7 @@ def exists(name): """ if not name or not isinstance(name, string_types): return False + name = os.path.basename(name) if name in _cache: return True file_found = os.path.exists(os.path.join(_dirname, '%s.dat' % name)) @@ -102,6 +103,7 @@ def load(name, merge_inherited=True): :raise `IOError`: if no locale data file is found for the given locale identifer, or one of the locales it inherits from """ + name = os.path.basename(name) _cache_lock.acquire() try: data = _cache.get(name) diff --git a/tests/test_localedata.py b/tests/test_localedata.py index 83cd66994..9cb4282e4 100644 --- a/tests/test_localedata.py +++ b/tests/test_localedata.py @@ -11,11 +11,17 @@ # individuals. For the exact contribution history, see the revision # history and logs, available at http://babel.edgewall.org/log/. +import os +import pickle +import sys +import tempfile import unittest import random from operator import methodcaller -from babel import localedata +import pytest + +from babel import localedata, Locale, UnknownLocaleError class MergeResolveTestCase(unittest.TestCase): @@ -131,3 +137,25 @@ def listdir_spy(*args): localedata.locale_identifiers.cache = None assert localedata.locale_identifiers() assert len(listdir_calls) == 2 + + +def test_locale_name_cleanup(): + """ + Test that locale identifiers are cleaned up to avoid directory traversal. + """ + no_exist_name = os.path.join(tempfile.gettempdir(), "babel%d.dat" % random.randint(1, 99999)) + with open(no_exist_name, "wb") as f: + pickle.dump({}, f) + + try: + name = os.path.splitext(os.path.relpath(no_exist_name, localedata._dirname))[0] + except ValueError: + if sys.platform == "win32": + pytest.skip("unable to form relpath") + raise + + assert not localedata.exists(name) + with pytest.raises(IOError): + localedata.load(name) + with pytest.raises(UnknownLocaleError): + Locale(name) From 5caf717ceca4bd235552362b4fbff88983c75d8c Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 28 Apr 2021 11:47:42 +0300 Subject: [PATCH 137/139] Disallow special filenames on Windows --- babel/localedata.py | 24 +++++++++++++++++++++--- tests/test_localedata.py | 9 +++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/babel/localedata.py b/babel/localedata.py index 11085490a..782b7afa0 100644 --- a/babel/localedata.py +++ b/babel/localedata.py @@ -13,6 +13,8 @@ """ import os +import re +import sys import threading from itertools import chain @@ -22,6 +24,7 @@ _cache = {} _cache_lock = threading.RLock() _dirname = os.path.join(os.path.dirname(__file__), 'locale-data') +_windows_reserved_name_re = re.compile("^(con|prn|aux|nul|com[0-9]|lpt[0-9])$", re.I) def normalize_locale(name): @@ -38,6 +41,22 @@ def normalize_locale(name): return locale_id +def resolve_locale_filename(name): + """ + Resolve a locale identifier to a `.dat` path on disk. + """ + + # Clean up any possible relative paths. + name = os.path.basename(name) + + # Ensure we're not left with one of the Windows reserved names. + if sys.platform == "win32" and _windows_reserved_name_re.match(os.path.splitext(name)[0]): + raise ValueError("Name %s is invalid on Windows" % name) + + # Build the path. + return os.path.join(_dirname, '%s.dat' % name) + + def exists(name): """Check whether locale data is available for the given locale. @@ -47,10 +66,9 @@ def exists(name): """ if not name or not isinstance(name, string_types): return False - name = os.path.basename(name) if name in _cache: return True - file_found = os.path.exists(os.path.join(_dirname, '%s.dat' % name)) + file_found = os.path.exists(resolve_locale_filename(name)) return True if file_found else bool(normalize_locale(name)) @@ -121,7 +139,7 @@ def load(name, merge_inherited=True): else: parent = '_'.join(parts[:-1]) data = load(parent).copy() - filename = os.path.join(_dirname, '%s.dat' % name) + filename = resolve_locale_filename(name) with open(filename, 'rb') as fileobj: if name != 'root' and merge_inherited: merge(data, pickle.load(fileobj)) diff --git a/tests/test_localedata.py b/tests/test_localedata.py index 9cb4282e4..c852c1b69 100644 --- a/tests/test_localedata.py +++ b/tests/test_localedata.py @@ -159,3 +159,12 @@ def test_locale_name_cleanup(): localedata.load(name) with pytest.raises(UnknownLocaleError): Locale(name) + + +@pytest.mark.skipif(sys.platform != "win32", reason="windows-only test") +def test_reserved_locale_names(): + for name in ("con", "aux", "nul", "prn", "com8", "lpt5"): + with pytest.raises(ValueError): + localedata.load(name) + with pytest.raises(ValueError): + Locale(name) From 60b33e083801109277cb068105251e76d0b7c14e Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 28 Apr 2021 22:04:48 +0300 Subject: [PATCH 138/139] Become 2.9.1 * Update copyright year * Update changelog --- CHANGES | 9 +++++++++ LICENSE | 2 +- babel/__init__.py | 4 ++-- babel/core.py | 2 +- babel/dates.py | 2 +- babel/lists.py | 2 +- babel/localedata.py | 2 +- babel/localtime/__init__.py | 2 +- babel/messages/__init__.py | 2 +- babel/messages/catalog.py | 2 +- babel/messages/checkers.py | 2 +- babel/messages/extract.py | 2 +- babel/messages/frontend.py | 2 +- babel/messages/jslexer.py | 2 +- babel/messages/mofile.py | 2 +- babel/messages/plurals.py | 2 +- babel/messages/pofile.py | 2 +- babel/numbers.py | 2 +- babel/plural.py | 2 +- babel/support.py | 2 +- babel/util.py | 2 +- docs/conf.py | 4 ++-- scripts/dump_data.py | 2 +- scripts/dump_global.py | 2 +- scripts/import_cldr.py | 2 +- tests/messages/test_catalog.py | 2 +- tests/messages/test_checkers.py | 2 +- tests/messages/test_extract.py | 2 +- tests/messages/test_frontend.py | 2 +- tests/messages/test_mofile.py | 2 +- tests/messages/test_plurals.py | 2 +- tests/messages/test_pofile.py | 2 +- tests/test_core.py | 2 +- tests/test_dates.py | 2 +- tests/test_localedata.py | 2 +- tests/test_numbers.py | 2 +- tests/test_plural.py | 2 +- tests/test_support.py | 2 +- tests/test_util.py | 2 +- 39 files changed, 49 insertions(+), 40 deletions(-) diff --git a/CHANGES b/CHANGES index fbc73801f..e3c54bfc8 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,15 @@ Babel Changelog =============== +Version 2.9.1 +------------- + +Bugfixes +~~~~~~~~ + +* The internal locale-data loading functions now validate the name of the locale file to be loaded and only + allow files within Babel's data directory. Thank you to Chris Lyne of Tenable, Inc. for discovering the issue! + Version 2.9.0 ------------- diff --git a/LICENSE b/LICENSE index 7e2f12e46..693e1a187 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2013-2020 by the Babel Team, see AUTHORS for more information. +Copyright (c) 2013-2021 by the Babel Team, see AUTHORS for more information. All rights reserved. diff --git a/babel/__init__.py b/babel/__init__.py index ecb18b546..3e20e4bd1 100644 --- a/babel/__init__.py +++ b/babel/__init__.py @@ -13,7 +13,7 @@ access to various locale display names, localized number and date formatting, etc. - :copyright: (c) 2013-2020 by the Babel Team. + :copyright: (c) 2013-2021 by the Babel Team. :license: BSD, see LICENSE for more details. """ @@ -21,4 +21,4 @@ negotiate_locale, parse_locale, get_locale_identifier -__version__ = '2.9.0' +__version__ = '2.9.1' diff --git a/babel/core.py b/babel/core.py index a0c35b4ea..a323a7295 100644 --- a/babel/core.py +++ b/babel/core.py @@ -5,7 +5,7 @@ Core locale representation and locale data access. - :copyright: (c) 2013-2020 by the Babel Team. + :copyright: (c) 2013-2021 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/dates.py b/babel/dates.py index d62d00ba4..75e8f3501 100644 --- a/babel/dates.py +++ b/babel/dates.py @@ -12,7 +12,7 @@ * ``LC_ALL``, and * ``LANG`` - :copyright: (c) 2013-2020 by the Babel Team. + :copyright: (c) 2013-2021 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/lists.py b/babel/lists.py index e0bd7ed7f..8368b27a6 100644 --- a/babel/lists.py +++ b/babel/lists.py @@ -11,7 +11,7 @@ * ``LC_ALL``, and * ``LANG`` - :copyright: (c) 2015-2020 by the Babel Team. + :copyright: (c) 2015-2021 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/localedata.py b/babel/localedata.py index 782b7afa0..438afb643 100644 --- a/babel/localedata.py +++ b/babel/localedata.py @@ -8,7 +8,7 @@ :note: The `Locale` class, which uses this module under the hood, provides a more convenient interface for accessing the locale data. - :copyright: (c) 2013-2020 by the Babel Team. + :copyright: (c) 2013-2021 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/localtime/__init__.py b/babel/localtime/__init__.py index 8c9203a7b..bd3954951 100644 --- a/babel/localtime/__init__.py +++ b/babel/localtime/__init__.py @@ -6,7 +6,7 @@ Babel specific fork of tzlocal to determine the local timezone of the system. - :copyright: (c) 2013-2020 by the Babel Team. + :copyright: (c) 2013-2021 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/messages/__init__.py b/babel/messages/__init__.py index 77c79621b..7d2587f63 100644 --- a/babel/messages/__init__.py +++ b/babel/messages/__init__.py @@ -5,7 +5,7 @@ Support for ``gettext`` message catalogs. - :copyright: (c) 2013-2020 by the Babel Team. + :copyright: (c) 2013-2021 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py index af1b6573c..a19a3e6d8 100644 --- a/babel/messages/catalog.py +++ b/babel/messages/catalog.py @@ -5,7 +5,7 @@ Data structures for message catalogs. - :copyright: (c) 2013-2020 by the Babel Team. + :copyright: (c) 2013-2021 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/messages/checkers.py b/babel/messages/checkers.py index 29add9fce..cba911d72 100644 --- a/babel/messages/checkers.py +++ b/babel/messages/checkers.py @@ -7,7 +7,7 @@ :since: version 0.9 - :copyright: (c) 2013-2020 by the Babel Team. + :copyright: (c) 2013-2021 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/messages/extract.py b/babel/messages/extract.py index 8f53fa298..64497762c 100644 --- a/babel/messages/extract.py +++ b/babel/messages/extract.py @@ -13,7 +13,7 @@ The main entry points into the extraction functionality are the functions `extract_from_dir` and `extract_from_file`. - :copyright: (c) 2013-2020 by the Babel Team. + :copyright: (c) 2013-2021 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/messages/frontend.py b/babel/messages/frontend.py index 0b65a7c6b..c5eb1dea9 100644 --- a/babel/messages/frontend.py +++ b/babel/messages/frontend.py @@ -5,7 +5,7 @@ Frontends for the message extraction functionality. - :copyright: (c) 2013-2020 by the Babel Team. + :copyright: (c) 2013-2021 by the Babel Team. :license: BSD, see LICENSE for more details. """ from __future__ import print_function diff --git a/babel/messages/jslexer.py b/babel/messages/jslexer.py index f56c80d98..c57b1213f 100644 --- a/babel/messages/jslexer.py +++ b/babel/messages/jslexer.py @@ -6,7 +6,7 @@ A simple JavaScript 1.5 lexer which is used for the JavaScript extractor. - :copyright: (c) 2013-2020 by the Babel Team. + :copyright: (c) 2013-2021 by the Babel Team. :license: BSD, see LICENSE for more details. """ from collections import namedtuple diff --git a/babel/messages/mofile.py b/babel/messages/mofile.py index 1ab3e4e9c..8d3cfc905 100644 --- a/babel/messages/mofile.py +++ b/babel/messages/mofile.py @@ -5,7 +5,7 @@ Writing of files in the ``gettext`` MO (machine object) format. - :copyright: (c) 2013-2020 by the Babel Team. + :copyright: (c) 2013-2021 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/messages/plurals.py b/babel/messages/plurals.py index 4755cfcf9..91ba9e1b1 100644 --- a/babel/messages/plurals.py +++ b/babel/messages/plurals.py @@ -5,7 +5,7 @@ Plural form definitions. - :copyright: (c) 2013-2020 by the Babel Team. + :copyright: (c) 2013-2021 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/messages/pofile.py b/babel/messages/pofile.py index b8cb46976..be33b831d 100644 --- a/babel/messages/pofile.py +++ b/babel/messages/pofile.py @@ -6,7 +6,7 @@ Reading and writing of files in the ``gettext`` PO (portable object) format. - :copyright: (c) 2013-2020 by the Babel Team. + :copyright: (c) 2013-2021 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/numbers.py b/babel/numbers.py index 0303f0542..0fcc07e15 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -12,7 +12,7 @@ * ``LC_ALL``, and * ``LANG`` - :copyright: (c) 2013-2020 by the Babel Team. + :copyright: (c) 2013-2021 by the Babel Team. :license: BSD, see LICENSE for more details. """ # TODO: diff --git a/babel/plural.py b/babel/plural.py index 3c9a3e614..e705e9b8d 100644 --- a/babel/plural.py +++ b/babel/plural.py @@ -5,7 +5,7 @@ CLDR Plural support. See UTS #35. - :copyright: (c) 2013-2020 by the Babel Team. + :copyright: (c) 2013-2021 by the Babel Team. :license: BSD, see LICENSE for more details. """ import re diff --git a/babel/support.py b/babel/support.py index d6fc56cfb..4be9ed37f 100644 --- a/babel/support.py +++ b/babel/support.py @@ -8,7 +8,7 @@ .. note: the code in this module is not used by Babel itself - :copyright: (c) 2013-2020 by the Babel Team. + :copyright: (c) 2013-2021 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/babel/util.py b/babel/util.py index a5d40c5de..a8fbac1d9 100644 --- a/babel/util.py +++ b/babel/util.py @@ -5,7 +5,7 @@ Various utility classes and functions. - :copyright: (c) 2013-2020 by the Babel Team. + :copyright: (c) 2013-2021 by the Babel Team. :license: BSD, see LICENSE for more details. """ diff --git a/docs/conf.py b/docs/conf.py index 1c21c67cc..962792fbd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -44,7 +44,7 @@ # General information about the project. project = u'Babel' -copyright = u'2020, The Babel Team' +copyright = u'2021, The Babel Team' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -53,7 +53,7 @@ # The short X.Y version. version = '2.9' # The full version, including alpha/beta/rc tags. -release = '2.9.0' +release = '2.9.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/scripts/dump_data.py b/scripts/dump_data.py index 57439b98f..ac295b2d7 100755 --- a/scripts/dump_data.py +++ b/scripts/dump_data.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2021 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/scripts/dump_global.py b/scripts/dump_global.py index a27b31617..c9e1d3008 100755 --- a/scripts/dump_global.py +++ b/scripts/dump_global.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2021 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py index 0dbd39369..7876d5208 100755 --- a/scripts/import_cldr.py +++ b/scripts/import_cldr.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2021 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/messages/test_catalog.py b/tests/messages/test_catalog.py index 2d9e31d00..661999648 100644 --- a/tests/messages/test_catalog.py +++ b/tests/messages/test_catalog.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2021 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/messages/test_checkers.py b/tests/messages/test_checkers.py index bf813bc05..49abb51b0 100644 --- a/tests/messages/test_checkers.py +++ b/tests/messages/test_checkers.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2021 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/messages/test_extract.py b/tests/messages/test_extract.py index 38979cd30..ac7f0a642 100644 --- a/tests/messages/test_extract.py +++ b/tests/messages/test_extract.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2021 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/messages/test_frontend.py b/tests/messages/test_frontend.py index 38165b354..70580215e 100644 --- a/tests/messages/test_frontend.py +++ b/tests/messages/test_frontend.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2021 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/messages/test_mofile.py b/tests/messages/test_mofile.py index 395b9fb1a..fb672a80c 100644 --- a/tests/messages/test_mofile.py +++ b/tests/messages/test_mofile.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2021 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/messages/test_plurals.py b/tests/messages/test_plurals.py index 62c770870..5e490f374 100644 --- a/tests/messages/test_plurals.py +++ b/tests/messages/test_plurals.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2021 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/messages/test_pofile.py b/tests/messages/test_pofile.py index 2db7f6715..be1172a88 100644 --- a/tests/messages/test_pofile.py +++ b/tests/messages/test_pofile.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2021 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/test_core.py b/tests/test_core.py index 53b9d1875..558322e00 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2021 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/test_dates.py b/tests/test_dates.py index 8e693d34c..44efa7fbc 100644 --- a/tests/test_dates.py +++ b/tests/test_dates.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2021 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/test_localedata.py b/tests/test_localedata.py index c852c1b69..735678f80 100644 --- a/tests/test_localedata.py +++ b/tests/test_localedata.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2021 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/test_numbers.py b/tests/test_numbers.py index 739fd1fb7..11e61d37d 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2021 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/test_plural.py b/tests/test_plural.py index 1a9fbe507..bea8115ce 100644 --- a/tests/test_plural.py +++ b/tests/test_plural.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2021 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/test_support.py b/tests/test_support.py index 456425e3e..a683591dc 100644 --- a/tests/test_support.py +++ b/tests/test_support.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2021 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which diff --git a/tests/test_util.py b/tests/test_util.py index dbd6378a0..b29278e00 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007-2011 Edgewall Software, 2013-2020 the Babel team +# Copyright (C) 2007-2011 Edgewall Software, 2013-2021 the Babel team # All rights reserved. # # This software is licensed as described in the file LICENSE, which From a99fa2474c808b51ebdabea18db871e389751559 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 28 Apr 2021 22:26:16 +0300 Subject: [PATCH 139/139] Use 2.9.0's setup.py for 2.9.1 --- setup.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index adf3bb5d9..0032a3a05 100755 --- a/setup.py +++ b/setup.py @@ -44,16 +44,18 @@ def run(self): 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Libraries :: Python Modules', ], - python_requires='>=3.6', + python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', packages=['babel', 'babel.messages', 'babel.localtime'], include_package_data=True, install_requires=[