From 875fd3b1e2ca4ec3e96e94aa3e383b8f9cd0c12c Mon Sep 17 00:00:00 2001 From: Holden Nelson Date: Thu, 23 Dec 2021 11:37:33 -0700 Subject: [PATCH] Add ability to run code with temporary locale. Fix #1347 --- RELEASE.md | 3 + pelican/tests/test_generators.py | 168 +++++++++++++++---------------- pelican/tests/test_settings.py | 9 +- pelican/tests/test_utils.py | 133 ++++++++++++------------ pelican/utils.py | 23 +++-- 5 files changed, 175 insertions(+), 161 deletions(-) create mode 100644 RELEASE.md diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 00000000..0627990d --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,3 @@ +Release type: minor + +Add ability to run code with temporary locale. diff --git a/pelican/tests/test_generators.py b/pelican/tests/test_generators.py index 1bc8aff0..2e251bc7 100644 --- a/pelican/tests/test_generators.py +++ b/pelican/tests/test_generators.py @@ -12,6 +12,8 @@ from pelican.tests.support import (can_symlink, get_context, get_settings, unittest) from pelican.writers import Writer +from pelican.utils import temporary_locale + CUR_DIR = os.path.dirname(__file__) CONTENT_DIR = os.path.join(CUR_DIR, 'content') @@ -410,93 +412,91 @@ class TestArticlesGenerator(unittest.TestCase): Test that the context of a generated period_archive is passed 'period' : a tuple of year, month, day according to the time period """ - old_locale = locale.setlocale(locale.LC_ALL) - locale.setlocale(locale.LC_ALL, 'C') - settings = get_settings() + with temporary_locale('C'): + settings = get_settings() - settings['YEAR_ARCHIVE_SAVE_AS'] = 'posts/{date:%Y}/index.html' - settings['YEAR_ARCHIVE_URL'] = 'posts/{date:%Y}/' - settings['CACHE_PATH'] = self.temp_cache - context = get_context(settings) - generator = ArticlesGenerator( - context=context, settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) - generator.generate_context() - write = MagicMock() - generator.generate_period_archives(write) - dates = [d for d in generator.dates if d.date.year == 1970] - articles = [d for d in generator.articles if d.date.year == 1970] - self.assertEqual(len(dates), 1) - # among other things it must have at least been called with this - context["period"] = (1970,) - context["period_num"] = (1970,) - write.assert_called_with("posts/1970/index.html", - generator.get_template("period_archives"), - context, blog=True, articles=articles, - dates=dates, template_name='period_archives', - url="posts/1970/", - all_articles=generator.articles) + settings['YEAR_ARCHIVE_SAVE_AS'] = 'posts/{date:%Y}/index.html' + settings['YEAR_ARCHIVE_URL'] = 'posts/{date:%Y}/' + settings['CACHE_PATH'] = self.temp_cache + context = get_context(settings) + generator = ArticlesGenerator( + context=context, settings=settings, + path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + generator.generate_context() + write = MagicMock() + generator.generate_period_archives(write) + dates = [d for d in generator.dates if d.date.year == 1970] + articles = [d for d in generator.articles if d.date.year == 1970] + self.assertEqual(len(dates), 1) + # among other things it must have at least been called with this + context["period"] = (1970,) + context["period_num"] = (1970,) + write.assert_called_with("posts/1970/index.html", + generator.get_template("period_archives"), + context, blog=True, articles=articles, + dates=dates, template_name='period_archives', + url="posts/1970/", + all_articles=generator.articles) - settings['MONTH_ARCHIVE_SAVE_AS'] = \ - 'posts/{date:%Y}/{date:%b}/index.html' - settings['MONTH_ARCHIVE_URL'] = \ - 'posts/{date:%Y}/{date:%b}/' - context = get_context(settings) - generator = ArticlesGenerator( - context=context, settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) - generator.generate_context() - write = MagicMock() - generator.generate_period_archives(write) - dates = [d for d in generator.dates - if d.date.year == 1970 and d.date.month == 1] - articles = [d for d in generator.articles - if d.date.year == 1970 and d.date.month == 1] - self.assertEqual(len(dates), 1) - context["period"] = (1970, "January") - context["period_num"] = (1970, 1) - # among other things it must have at least been called with this - write.assert_called_with("posts/1970/Jan/index.html", - generator.get_template("period_archives"), - context, blog=True, articles=articles, - dates=dates, template_name='period_archives', - url="posts/1970/Jan/", - all_articles=generator.articles) + settings['MONTH_ARCHIVE_SAVE_AS'] = \ + 'posts/{date:%Y}/{date:%b}/index.html' + settings['MONTH_ARCHIVE_URL'] = \ + 'posts/{date:%Y}/{date:%b}/' + context = get_context(settings) + generator = ArticlesGenerator( + context=context, settings=settings, + path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + generator.generate_context() + write = MagicMock() + generator.generate_period_archives(write) + dates = [d for d in generator.dates + if d.date.year == 1970 and d.date.month == 1] + articles = [d for d in generator.articles + if d.date.year == 1970 and d.date.month == 1] + self.assertEqual(len(dates), 1) + context["period"] = (1970, "January") + context["period_num"] = (1970, 1) + # among other things it must have at least been called with this + write.assert_called_with("posts/1970/Jan/index.html", + generator.get_template("period_archives"), + context, blog=True, articles=articles, + dates=dates, template_name='period_archives', + url="posts/1970/Jan/", + all_articles=generator.articles) - settings['DAY_ARCHIVE_SAVE_AS'] = \ - 'posts/{date:%Y}/{date:%b}/{date:%d}/index.html' - settings['DAY_ARCHIVE_URL'] = \ - 'posts/{date:%Y}/{date:%b}/{date:%d}/' - context = get_context(settings) - generator = ArticlesGenerator( - context=context, settings=settings, - path=CONTENT_DIR, theme=settings['THEME'], output_path=None) - generator.generate_context() - write = MagicMock() - generator.generate_period_archives(write) - dates = [ - d for d in generator.dates if - d.date.year == 1970 and - d.date.month == 1 and - d.date.day == 1 - ] - articles = [ - d for d in generator.articles if - d.date.year == 1970 and - d.date.month == 1 and - d.date.day == 1 - ] - self.assertEqual(len(dates), 1) - context["period"] = (1970, "January", 1) - context["period_num"] = (1970, 1, 1) - # among other things it must have at least been called with this - write.assert_called_with("posts/1970/Jan/01/index.html", - generator.get_template("period_archives"), - context, blog=True, articles=articles, - dates=dates, template_name='period_archives', - url="posts/1970/Jan/01/", - all_articles=generator.articles) - locale.setlocale(locale.LC_ALL, old_locale) + settings['DAY_ARCHIVE_SAVE_AS'] = \ + 'posts/{date:%Y}/{date:%b}/{date:%d}/index.html' + settings['DAY_ARCHIVE_URL'] = \ + 'posts/{date:%Y}/{date:%b}/{date:%d}/' + context = get_context(settings) + generator = ArticlesGenerator( + context=context, settings=settings, + path=CONTENT_DIR, theme=settings['THEME'], output_path=None) + generator.generate_context() + write = MagicMock() + generator.generate_period_archives(write) + dates = [ + d for d in generator.dates if + d.date.year == 1970 and + d.date.month == 1 and + d.date.day == 1 + ] + articles = [ + d for d in generator.articles if + d.date.year == 1970 and + d.date.month == 1 and + d.date.day == 1 + ] + self.assertEqual(len(dates), 1) + context["period"] = (1970, "January", 1) + context["period_num"] = (1970, 1, 1) + # among other things it must have at least been called with this + write.assert_called_with("posts/1970/Jan/01/index.html", + generator.get_template("period_archives"), + context, blog=True, articles=articles, + dates=dates, template_name='period_archives', + url="posts/1970/Jan/01/", + all_articles=generator.articles) def test_nonexistent_template(self): """Attempt to load a non-existent template""" diff --git a/pelican/tests/test_settings.py b/pelican/tests/test_settings.py index 83203ae5..aa702e72 100644 --- a/pelican/tests/test_settings.py +++ b/pelican/tests/test_settings.py @@ -10,6 +10,7 @@ from pelican.settings import (DEFAULT_CONFIG, DEFAULT_THEME, coerce_overrides, configure_settings, handle_deprecated_settings, read_settings) from pelican.tests.support import unittest +from pelican.utils import temporary_locale class TestSettingsConfiguration(unittest.TestCase): @@ -143,11 +144,11 @@ class TestSettingsConfiguration(unittest.TestCase): # Test that the default locale is set if not specified in settings # Reset locale to Python's default locale - locale.setlocale(locale.LC_ALL, 'C') - self.assertEqual(self.settings['LOCALE'], DEFAULT_CONFIG['LOCALE']) + with temporary_locale('C'): + self.assertEqual(self.settings['LOCALE'], DEFAULT_CONFIG['LOCALE']) - configure_settings(self.settings) - self.assertEqual(locale.getlocale(), locale.getdefaultlocale()) + configure_settings(self.settings) + self.assertEqual(locale.getlocale(), locale.getdefaultlocale()) def test_invalid_settings_throw_exception(self): # Test that the path name is valid diff --git a/pelican/tests/test_utils.py b/pelican/tests/test_utils.py index 710e14ed..5c7fd90d 100644 --- a/pelican/tests/test_utils.py +++ b/pelican/tests/test_utils.py @@ -535,33 +535,25 @@ 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) + temp_locale = 'Turkish' if platform == 'win32' else 'tr_TR.UTF-8' - if platform == 'win32': - locale.setlocale(locale.LC_ALL, 'Turkish') - else: - locale.setlocale(locale.LC_ALL, 'tr_TR.UTF-8') + with utils.temporary_locale(temp_locale): + d = utils.SafeDatetime(2012, 8, 29) - d = utils.SafeDatetime(2012, 8, 29) + # 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') - # 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') - # 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ışı') - - # 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 @@ -569,34 +561,26 @@ 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) + temp_locale = 'French' if platform == 'win32' else 'fr_FR.UTF-8' - if platform == 'win32': - locale.setlocale(locale.LC_ALL, 'French') - else: - locale.setlocale(locale.LC_ALL, 'fr_FR.UTF-8') + with utils.temporary_locale(temp_locale): + d = utils.SafeDatetime(2012, 8, 29) - d = utils.SafeDatetime(2012, 8, 29) + # simple + self.assertEqual(utils.strftime(d, '%d %B %Y'), '29 août 2012') - # simple - self.assertEqual(utils.strftime(d, '%d %B %Y'), '29 août 2012') + # depending on OS, the first letter is m or M + self.assertTrue(utils.strftime(d, '%A') in ('mercredi', 'Mercredi')) - # depending on OS, the first letter is m or M - self.assertTrue(utils.strftime(d, '%A') in ('mercredi', 'Mercredi')) + # with text + self.assertEqual( + utils.strftime(d, 'Écrit le %d %B %Y'), + 'Écrit le 29 août 2012') - # with text - self.assertEqual( - utils.strftime(d, 'Écrit le %d %B %Y'), - 'Écrit le 29 août 2012') - - # non-ascii format candidate (someone might pass it… for some reason) - self.assertEqual( - utils.strftime(d, '%écrits en %Y'), - '%écrits en 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( @@ -609,6 +593,23 @@ class TestUtils(LoggedTestCase): utils.maybe_pluralize(2, 'Article', 'Articles'), '2 Articles') + def test_temporary_locale(self): + # test with default LC category + orig_locale = locale.setlocale(locale.LC_ALL) + + with utils.temporary_locale('C'): + self.assertEqual(locale.setlocale(locale.LC_ALL), 'C') + + self.assertEqual(locale.setlocale(locale.LC_ALL), orig_locale) + + # test with custom LC category + orig_locale = locale.setlocale(locale.LC_TIME) + + with utils.temporary_locale('C', locale.LC_TIME): + self.assertEqual(locale.setlocale(locale.LC_TIME), 'C') + + self.assertEqual(locale.setlocale(locale.LC_TIME), orig_locale) + class TestCopy(unittest.TestCase): '''Tests the copy utility''' @@ -724,27 +725,27 @@ class TestDateFormatter(unittest.TestCase): def test_french_strftime(self): # This test tries to reproduce an issue that # occurred with python3.3 under macos10 only - if platform == 'win32': - locale.setlocale(locale.LC_ALL, 'French') - else: - locale.setlocale(locale.LC_ALL, 'fr_FR.UTF-8') - date = utils.SafeDatetime(2014, 8, 14) - # we compare the lower() dates since macos10 returns - # "Jeudi" for %A whereas linux reports "jeudi" - self.assertEqual( - 'jeudi, 14 août 2014', - utils.strftime(date, date_format="%A, %d %B %Y").lower()) - df = utils.DateFormatter() - self.assertEqual( - 'jeudi, 14 août 2014', - df(date, date_format="%A, %d %B %Y").lower()) + temp_locale = 'French' if platform == 'win32' else 'fr_FR.UTF-8' + + with utils.temporary_locale(temp_locale): + date = utils.SafeDatetime(2014, 8, 14) + # we compare the lower() dates since macos10 returns + # "Jeudi" for %A whereas linux reports "jeudi" + self.assertEqual( + 'jeudi, 14 août 2014', + utils.strftime(date, date_format="%A, %d %B %Y").lower()) + df = utils.DateFormatter() + self.assertEqual( + '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, '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('jeudi, 14 août 2014', df_date) + with utils.temporary_locale('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('jeudi, 14 août 2014', df_date) @unittest.skipUnless(locale_available('fr_FR.UTF-8') or locale_available('French'), diff --git a/pelican/utils.py b/pelican/utils.py index 4d025657..be9c07c1 100644 --- a/pelican/utils.py +++ b/pelican/utils.py @@ -111,18 +111,14 @@ class DateFormatter: 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) + with temporary_locale(self.locale, locale.LC_TIME), \ + temporary_locale(self.locale, locale.LC_CTYPE): - formatted = strftime(date, date_format) + formatted = strftime(date, date_format) - locale.setlocale(locale.LC_TIME, old_lc_time) - locale.setlocale(locale.LC_CTYPE, old_lc_ctype) return formatted @@ -982,3 +978,16 @@ def maybe_pluralize(count, singular, plural): if count == 1: selection = singular return '{} {}'.format(count, selection) + + +@contextmanager +def temporary_locale(temp_locale=None, lc_category=locale.LC_ALL): + ''' + Enable code to run in a context with a temporary locale + Resets the locale back when exiting context. + ''' + orig_locale = locale.setlocale(lc_category) + if temp_locale: + locale.setlocale(lc_category, temp_locale) + yield + locale.setlocale(lc_category, orig_locale)