Add ability to run code with temporary locale. Fix #1347

This commit is contained in:
Holden Nelson 2021-12-23 11:37:33 -07:00
commit 875fd3b1e2
5 changed files with 178 additions and 164 deletions

3
RELEASE.md Normal file
View file

@ -0,0 +1,3 @@
Release type: minor
Add ability to run code with temporary locale.

View file

@ -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"""

View file

@ -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

View file

@ -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'),

View file

@ -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)