From 5980c48e265d65aebf7aa56f6339185d59aa7056 Mon Sep 17 00:00:00 2001 From: Alex Chan Date: Sun, 16 Oct 2016 12:25:39 +0100 Subject: [PATCH 1/2] Add a temporary_locale context manager. Addresses #1347. --- pelican/tests/test_utils.py | 115 +++++++++++++++++++++--------------- pelican/utils.py | 32 ++++++---- 2 files changed, 87 insertions(+), 60 deletions(-) diff --git a/pelican/tests/test_utils.py b/pelican/tests/test_utils.py index 8dfc0b9b..0621dc0c 100644 --- a/pelican/tests/test_utils.py +++ b/pelican/tests/test_utils.py @@ -370,33 +370,30 @@ class TestUtils(LoggedTestCase): locale_available('Turkish'), 'Turkish locale needed') def test_strftime_locale_dependent_turkish(self): - # store current locale - old_locale = locale.setlocale(locale.LC_ALL) - if platform == 'win32': - locale.setlocale(locale.LC_ALL, str('Turkish')) + new_locale = 'Turkish' else: - locale.setlocale(locale.LC_ALL, str('tr_TR.UTF-8')) + new_locale = 'tr_TR.UTF-8' - d = utils.SafeDatetime(2012, 8, 29) + with utils.temporary_locale(new_locale): - # simple - self.assertEqual(utils.strftime(d, '%d %B %Y'), '29 Ağustos 2012') - self.assertEqual(utils.strftime(d, '%A, %d %B %Y'), - 'Çarşamba, 29 Ağustos 2012') + d = utils.SafeDatetime(2012, 8, 29) - # with text - self.assertEqual( - utils.strftime(d, 'Yayınlanma tarihi: %A, %d %B %Y'), - 'Yayınlanma tarihi: Çarşamba, 29 Ağustos 2012') + # simple + self.assertEqual(utils.strftime(d, '%d %B %Y'), '29 Ağustos 2012') + self.assertEqual(utils.strftime(d, '%A, %d %B %Y'), + 'Çarşamba, 29 Ağustos 2012') - # non-ascii format candidate (someone might pass it… for some reason) - self.assertEqual( - utils.strftime(d, '%Y yılında %üretim artışı'), - '2012 yılında %üretim artışı') + # with text + self.assertEqual( + utils.strftime(d, 'Yayınlanma tarihi: %A, %d %B %Y'), + 'Yayınlanma tarihi: Çarşamba, 29 Ağustos 2012') - # restore locale back - locale.setlocale(locale.LC_ALL, old_locale) + # non-ascii format candidate (someone might pass it… for + # some reason) + self.assertEqual( + utils.strftime(d, '%Y yılında %üretim artışı'), + '2012 yılında %üretim artışı') # test the output of utils.strftime in a different locale # French locale @@ -404,34 +401,31 @@ class TestUtils(LoggedTestCase): locale_available('French'), 'French locale needed') def test_strftime_locale_dependent_french(self): - # store current locale - old_locale = locale.setlocale(locale.LC_ALL) - if platform == 'win32': - locale.setlocale(locale.LC_ALL, str('French')) + new_locale = 'French' else: - locale.setlocale(locale.LC_ALL, str('fr_FR.UTF-8')) + new_locale = 'fr_FR.UTF-8' - d = utils.SafeDatetime(2012, 8, 29) + with utils.temporary_locale(new_locale): - # simple - self.assertEqual(utils.strftime(d, '%d %B %Y'), '29 août 2012') + d = utils.SafeDatetime(2012, 8, 29) - # depending on OS, the first letter is m or M - self.assertTrue(utils.strftime(d, '%A') in ('mercredi', 'Mercredi')) + # simple + self.assertEqual(utils.strftime(d, '%d %B %Y'), '29 août 2012') - # with text - self.assertEqual( - utils.strftime(d, 'Écrit le %d %B %Y'), - 'Écrit le 29 août 2012') + # depending on OS, the first letter is m or M + self.assertTrue(utils.strftime(d, '%A') in ('mercredi', 'Mercredi')) - # non-ascii format candidate (someone might pass it… for some reason) - self.assertEqual( - utils.strftime(d, '%écrits en %Y'), - '%écrits en 2012') + # with text + self.assertEqual( + utils.strftime(d, 'Écrit le %d %B %Y'), + 'Écrit le 29 août 2012') - # restore locale back - locale.setlocale(locale.LC_ALL, old_locale) + # non-ascii format candidate (someone might pass it… for + # some reason) + self.assertEqual( + utils.strftime(d, '%écrits en %Y'), + '%écrits en 2012') def test_maybe_pluralize(self): self.assertEqual( @@ -573,13 +567,13 @@ class TestDateFormatter(unittest.TestCase): self.assertEqual( u'jeudi, 14 août 2014', df(date, date_format="%A, %d %B %Y").lower()) - # Let us now set the global locale to C: - locale.setlocale(locale.LC_ALL, str('C')) - # DateFormatter should still work as expected - # since it is the whole point of DateFormatter - # (This is where pre-2014/4/15 code fails on macos10) - df_date = df(date, date_format="%A, %d %B %Y").lower() - self.assertEqual(u'jeudi, 14 août 2014', df_date) + + with utils.temporary_locale(str('C')): + # DateFormatter should still work as expected + # since it is the whole point of DateFormatter + # (This is where pre-2014/4/15 code fails on macos10) + df_date = df(date, date_format="%A, %d %B %Y").lower() + self.assertEqual(u'jeudi, 14 août 2014', df_date) @unittest.skipUnless(locale_available('fr_FR.UTF-8') or locale_available('French'), @@ -650,3 +644,30 @@ class TestDateFormatter(unittest.TestCase): with utils.pelican_open(output_path) as output_file: self.assertEqual(output_file, utils.strftime(self.date, 'date = %A, %d %B %Y')) + + @unittest.skipUnless( + (locale_available('tr_TR.UTF-8') or locale_available('Turkish')) and + (locale_available('fr_FR.UTF-8') or locale_available('French')), + 'French and Turkish locales needed' + ) + def test_temporary_locale(self): + ''' + Test the temporary_locale context manager. Use it to temporarily + set the locale to Turkish (or French, if we are already using the + Turkish locale). + ''' + if platform == 'win32': + new_locale = 'Turkish' + alt_locale = 'French' + else: + new_locale = 'tr_TR.UTF-8' + alt_locale = 'fr_FR.UTF-8' + + # If our locale is already Turkish, we will use French instead + if locale.setlocale(locale.LC_ALL) == new_locale: + new_locale = alt_locale + + self.assertNotEqual(locale.setlocale(locale.LC_ALL), new_locale) + with utils.temporary_locale(new_locale): + self.assertEqual(locale.setlocale(locale.LC_ALL), new_locale) + self.assertNotEqual(locale.setlocale(locale.LC_ALL), new_locale) diff --git a/pelican/utils.py b/pelican/utils.py index f1558fda..b6d466de 100644 --- a/pelican/utils.py +++ b/pelican/utils.py @@ -120,19 +120,8 @@ class DateFormatter(object): self.locale = locale.setlocale(locale.LC_TIME) def __call__(self, date, date_format): - old_lc_time = locale.setlocale(locale.LC_TIME) - old_lc_ctype = locale.setlocale(locale.LC_CTYPE) - - locale.setlocale(locale.LC_TIME, self.locale) - # on OSX, encoding from LC_CTYPE determines the unicode output in PY3 - # make sure it's same as LC_TIME - locale.setlocale(locale.LC_CTYPE, self.locale) - - formatted = strftime(date, date_format) - - locale.setlocale(locale.LC_TIME, old_lc_time) - locale.setlocale(locale.LC_CTYPE, old_lc_ctype) - return formatted + with temporary_locale(self.locale): + return strftime(date, date_format) def python_2_unicode_compatible(klass): @@ -824,3 +813,20 @@ def maybe_pluralize(count, singular, plural): if count == 1: selection = singular return '{} {}'.format(count, selection) + + +@contextmanager +def temporary_locale(temp_locale=None): + ''' + Enable code to run in a context with a temporary locale. + + Resets the locale back when context exits. Can set a temporary + locale if provided. + ''' + orig_locale = locale.setlocale(locale.LC_ALL) + + if temp_locale is not None: + locale.setlocale(locale.LC_ALL, temp_locale) + + yield + locale.setlocale(locale.LC_ALL, orig_locale) From 6aef1eee41c5e63653321d7366ed135497d20240 Mon Sep 17 00:00:00 2001 From: Alex Chan Date: Sun, 16 Oct 2016 12:49:52 +0100 Subject: [PATCH 2/2] Start using parameterized tests in `test_utils.py` --- pelican/tests/test_utils.py | 256 ++++++++++++++++-------------------- tox.ini | 1 + 2 files changed, 112 insertions(+), 145 deletions(-) diff --git a/pelican/tests/test_utils.py b/pelican/tests/test_utils.py index 0621dc0c..4e840968 100644 --- a/pelican/tests/test_utils.py +++ b/pelican/tests/test_utils.py @@ -9,6 +9,7 @@ import time from sys import platform from tempfile import mkdtemp +from nose_parameterized import parameterized import pytz from pelican import utils @@ -106,114 +107,84 @@ class TestUtils(LoggedTestCase): for item in invalid_dates: self.assertRaises(ValueError, utils.get_date, item) - def test_slugify(self): - - samples = (('this is a test', 'this-is-a-test'), - ('this is a test', 'this-is-a-test'), - ('this → is ← a ↑ test', 'this-is-a-test'), - ('this--is---a test', 'this-is-a-test'), - ('unicode測試許功蓋,你看到了嗎?', - 'unicodece-shi-xu-gong-gai-ni-kan-dao-liao-ma'), - ('大飯原発4号機、18日夜起動へ', - 'da-fan-yuan-fa-4hao-ji-18ri-ye-qi-dong-he'),) - - for value, expected in samples: - self.assertEqual(utils.slugify(value), expected) - - def test_slugify_substitute(self): - - samples = (('C++ is based on C', 'cpp-is-based-on-c'), - ('C+++ test C+ test', 'cpp-test-c-test'), - ('c++, c#, C#, C++', 'cpp-c-sharp-c-sharp-cpp'), - ('c++-streams', 'cpp-streams'),) + @parameterized.expand([ + ('this is a test', 'this-is-a-test'), + ('this is a test', 'this-is-a-test'), + ('this → is ← a ↑ test', 'this-is-a-test'), + ('this--is---a test', 'this-is-a-test'), + ('unicode測試許功蓋,你看到了嗎?', + 'unicodece-shi-xu-gong-gai-ni-kan-dao-liao-ma'), + ('大飯原発4号機、18日夜起動へ', + 'da-fan-yuan-fa-4hao-ji-18ri-ye-qi-dong-he'), + ]) + def test_slugify(self, value, expected): + self.assertEqual(utils.slugify(value), expected) + @parameterized.expand([ + ('C++ is based on C', 'cpp-is-based-on-c'), + ('C+++ test C+ test', 'cpp-test-c-test'), + ('c++, c#, C#, C++', 'cpp-c-sharp-c-sharp-cpp'), + ('c++-streams', 'cpp-streams'), + ]) + def test_slugify_substitute(self, value, expected): subs = (('C++', 'CPP'), ('C#', 'C-SHARP')) - for value, expected in samples: - self.assertEqual(utils.slugify(value, subs), expected) - - def test_slugify_substitute_and_keeping_non_alphanum(self): - - samples = (('Fedora QA', 'fedora.qa'), - ('C++ is used by Fedora QA', 'cpp is used by fedora.qa'), - ('C++ is based on C', 'cpp-is-based-on-c'), - ('C+++ test C+ test', 'cpp-test-c-test'),) + self.assertEqual(utils.slugify(value, subs), expected) + @parameterized.expand([ + ('Fedora QA', 'fedora.qa'), + ('C++ is used by Fedora QA', 'cpp is used by fedora.qa'), + ('C++ is based on C', 'cpp-is-based-on-c'), + ('C+++ test C+ test', 'cpp-test-c-test'), + ]) + def test_slugify_substitute_keeping_non_alphanum(self, value, expected): subs = (('Fedora QA', 'fedora.qa', True), ('c++', 'cpp'),) - for value, expected in samples: - self.assertEqual(utils.slugify(value, subs), expected) + self.assertEqual(utils.slugify(value, subs), expected) - def test_get_relative_path(self): + @parameterized.expand([ + (os.path.join('test', 'test.html'), os.pardir), + (os.path.join('test', 'test', 'test.html'), + os.path.join(os.pardir, os.pardir)), + ('test.html', os.curdir), + (os.path.join('/test', 'test.html'), os.pardir), + (os.path.join('/test', 'test', 'test.html'), + os.path.join(os.pardir, os.pardir)), + ('/test.html', os.curdir), + ]) + def test_get_relative_path(self, value, expected): + self.assertEqual(utils.get_relative_path(value), expected) - samples = ((os.path.join('test', 'test.html'), os.pardir), - (os.path.join('test', 'test', 'test.html'), - os.path.join(os.pardir, os.pardir)), - ('test.html', os.curdir), - (os.path.join('/test', 'test.html'), os.pardir), - (os.path.join('/test', 'test', 'test.html'), - os.path.join(os.pardir, os.pardir)), - ('/test.html', os.curdir),) + @parameterized.expand([ + # Plain text + ('short string', 'short string'), + ('word ' * 100, 'word ' * 20 + '…'), - for value, expected in samples: - self.assertEqual(utils.get_relative_path(value), expected) - - def test_truncate_html_words(self): - # Plain text. - self.assertEqual( - utils.truncate_html_words('short string', 20), - 'short string') - self.assertEqual( - utils.truncate_html_words('word ' * 100, 20), - 'word ' * 20 + '…') - - # Words enclosed or intervaled by HTML tags. - self.assertEqual( - utils.truncate_html_words('

' + 'word ' * 100 + '

', 20), - '

' + 'word ' * 20 + '…

') - self.assertEqual( - utils.truncate_html_words( - '' + 'word ' * 100 + '', 20), - '' + 'word ' * 20 + '…') - self.assertEqual( - utils.truncate_html_words('
' + 'word ' * 100, 20), - '
' + 'word ' * 20 + '…') - self.assertEqual( - utils.truncate_html_words('' + 'word ' * 100, 20), - '' + 'word ' * 20 + '…') + # Words enclosed or intervaled by HTML tags + ('

' + 'word ' * 100 + '

', '

' + 'word ' * 20 + '…

'), + ('' + 'word ' * 100 + '', + '' + 'word ' * 20 + '…'), + ('
' + 'word ' * 100, '
' + 'word ' * 20 + '…'), + ('' + 'word ' * 100, + '' + 'word ' * 20 + '…'), # Words with hypens and apostrophes. - self.assertEqual( - utils.truncate_html_words("a-b " * 100, 20), - "a-b " * 20 + '…') - self.assertEqual( - utils.truncate_html_words("it's " * 100, 20), - "it's " * 20 + '…') + ("a-b " * 100, "a-b " * 20 + '…'), + ("it's " * 100, "it's " * 20 + '…'), # Words with HTML entity references. - self.assertEqual( - utils.truncate_html_words("é " * 100, 20), - "é " * 20 + '…') - self.assertEqual( - utils.truncate_html_words("café " * 100, 20), - "café " * 20 + '…') - self.assertEqual( - utils.truncate_html_words("èlite " * 100, 20), - "èlite " * 20 + '…') - self.assertEqual( - utils.truncate_html_words("cafetiére " * 100, 20), - "cafetiére " * 20 + '…') - self.assertEqual( - utils.truncate_html_words("∫dx " * 100, 20), - "∫dx " * 20 + '…') + ("é " * 100, "é " * 20 + '…'), + ("café " * 100, "café " * 20 + '…'), + ("èlite " * 100, "èlite " * 20 + '…'), + ("cafetiére " * 100, "cafetiére " * 20 + '…'), + ("∫dx " * 100, "∫dx " * 20 + '…'), # Words with HTML character references inside and outside # the ASCII range. - self.assertEqual( - utils.truncate_html_words("é " * 100, 20), - "é " * 20 + '…') - self.assertEqual( - utils.truncate_html_words("∫dx " * 100, 20), - "∫dx " * 20 + '…') + ("é " * 100, "é " * 20 + '…'), + ("∫dx " * 100, "∫dx " * 20 + '…'), + ]) + def test_truncate_html_words(self, html_words, expected): + self.assertEqual(utils.truncate_html_words(html_words, 20), expected) def test_process_translations(self): # create a bunch of articles @@ -327,80 +298,76 @@ class TestUtils(LoggedTestCase): utils.clean_output_dir(test_directory, retention) self.assertFalse(os.path.exists(test_directory)) - def test_strftime(self): - d = utils.SafeDatetime(2012, 8, 29) - + @parameterized.expand([ # simple formatting - self.assertEqual(utils.strftime(d, '%d/%m/%y'), '29/08/12') - self.assertEqual(utils.strftime(d, '%d/%m/%Y'), '29/08/2012') + ('%d/%m/%y', '29/08/12'), + ('%d/%m/%Y', '29/08/2012'), # RFC 3339 - self.assertEqual( - utils.strftime(d, '%Y-%m-%dT%H:%M:%SZ'), - '2012-08-29T00:00:00Z') + ('%Y-%m-%dT%H:%M:%SZ', '2012-08-29T00:00:00Z'), # % escaped - self.assertEqual(utils.strftime(d, '%d%%%m%%%y'), '29%08%12') - self.assertEqual(utils.strftime(d, '%d %% %m %% %y'), '29 % 08 % 12') + ('%d%%%m%%%y', '29%08%12'), + ('%d %% %m %% %y', '29 % 08 % 12'), + # not valid % formatter - self.assertEqual(utils.strftime(d, '10% reduction in %Y'), - '10% reduction in 2012') - self.assertEqual(utils.strftime(d, '%10 reduction in %Y'), - '%10 reduction in 2012') + ('10% reduction in %Y', '10% reduction in 2012'), + ('%10 reduction in %Y', '%10 reduction in 2012'), # with text - self.assertEqual(utils.strftime(d, 'Published in %d-%m-%Y'), - 'Published in 29-08-2012') + ('Published in %d-%m-%Y', 'Published in 29-08-2012'), # with non-ascii text - self.assertEqual( - utils.strftime(d, '%d/%m/%Y Øl trinken beim Besäufnis'), - '29/08/2012 Øl trinken beim Besäufnis') + ('%d/%m/%Y Øl trinken beim Besäufnis', + '29/08/2012 Øl trinken beim Besäufnis'), # alternative formatting options - self.assertEqual(utils.strftime(d, '%-d/%-m/%y'), '29/8/12') - self.assertEqual(utils.strftime(d, '%-H:%-M:%-S'), '0:0:0') + ('%-d/%-m/%y', '29/8/12'), + ('%-H:%-M:%-S', '0:0:0'), + ]) + def test_strftime(self, fmt_string, expected): + d = utils.SafeDatetime(2012, 8, 29) + self.assertEqual(utils.strftime(d, fmt_string), expected) d = utils.SafeDatetime(2012, 8, 9) self.assertEqual(utils.strftime(d, '%-d/%-m/%y'), '9/8/12') - # test the output of utils.strftime in a different locale - # Turkish locale @unittest.skipUnless(locale_available('tr_TR.UTF-8') or locale_available('Turkish'), 'Turkish locale needed') - def test_strftime_locale_dependent_turkish(self): + @parameterized.expand([ + # simple + ('%d %B %Y', '29 Ağustos 2012'), + ('%A, %d %B %Y', 'Çarşamba, 29 Ağustos 2012'), + + # with text + ('Yayınlanma tarihi: %A, %d %B %Y', + 'Yayınlanma tarihi: Çarşamba, 29 Ağustos 2012') + + # non-ascii format candidate (someone might pass it… for + # some reason) + ('%Y yılında %üretim artışı', '2012 yılında %üretim artışı'), + ]) + def test_strftime_locale_dependent_turkish(self, fmt_string, expected): + ''' + Test the output of utils.strftime in a different locale (Turkish). + ''' if platform == 'win32': new_locale = 'Turkish' else: new_locale = 'tr_TR.UTF-8' with utils.temporary_locale(new_locale): - d = utils.SafeDatetime(2012, 8, 29) + self.assertEqual(utils.strftime(d, fmt_string), expected) - # simple - self.assertEqual(utils.strftime(d, '%d %B %Y'), '29 Ağustos 2012') - self.assertEqual(utils.strftime(d, '%A, %d %B %Y'), - 'Çarşamba, 29 Ağustos 2012') - - # with text - self.assertEqual( - utils.strftime(d, 'Yayınlanma tarihi: %A, %d %B %Y'), - 'Yayınlanma tarihi: Çarşamba, 29 Ağustos 2012') - - # non-ascii format candidate (someone might pass it… for - # some reason) - self.assertEqual( - utils.strftime(d, '%Y yılında %üretim artışı'), - '2012 yılında %üretim artışı') - - # test the output of utils.strftime in a different locale - # French locale @unittest.skipUnless(locale_available('fr_FR.UTF-8') or locale_available('French'), 'French locale needed') def test_strftime_locale_dependent_french(self): + ''' + Test the output of utils.strftime in a different locale (French). + ''' if platform == 'win32': new_locale = 'French' else: @@ -427,16 +394,15 @@ class TestUtils(LoggedTestCase): utils.strftime(d, '%écrits en %Y'), '%écrits en 2012') - def test_maybe_pluralize(self): + @parameterized.expand([ + (0, 'Article', 'Articles', '0 Articles'), + (1, 'Article', 'Articles', '1 Article'), + (2, 'Article', 'Articles', '2 Articles'), + ]) + def test_maybe_pluralize(self, quantity, singular, plural, expected): self.assertEqual( - utils.maybe_pluralize(0, 'Article', 'Articles'), - '0 Articles') - self.assertEqual( - utils.maybe_pluralize(1, 'Article', 'Articles'), - '1 Article') - self.assertEqual( - utils.maybe_pluralize(2, 'Article', 'Articles'), - '2 Articles') + utils.maybe_pluralize(quantity, singular, plural), + expected) class TestCopy(unittest.TestCase): diff --git a/tox.ini b/tox.ini index 1e04df53..85b5b39b 100644 --- a/tox.ini +++ b/tox.ini @@ -13,6 +13,7 @@ deps = -rrequirements/developer.pip nose nose-cov + nose-parameterized coveralls pygments==2.1.3