From fad2ff7ae3cd0ea8b974db5fe42de7383da679c1 Mon Sep 17 00:00:00 2001 From: boxydog <93335439+boxydog@users.noreply.github.com> Date: Sat, 28 Oct 2023 17:40:40 -0500 Subject: [PATCH] Add unit test utilities temporary_locale and TestCaseWithCLocale (#3224) --- pelican/tests/support.py | 13 +++ pelican/tests/test_generators.py | 30 ++----- pelican/tests/test_importer.py | 28 ++----- pelican/tests/test_utils.py | 133 ++++++++++++++++--------------- pelican/utils.py | 26 ++++-- 5 files changed, 111 insertions(+), 119 deletions(-) diff --git a/pelican/tests/support.py b/pelican/tests/support.py index 720e4d0e..3e4da785 100644 --- a/pelican/tests/support.py +++ b/pelican/tests/support.py @@ -254,3 +254,16 @@ class LoggedTestCase(unittest.TestCase): actual, count, msg='expected {} occurrences of {!r}, but found {}'.format( count, msg, actual)) + + +class TestCaseWithCLocale(unittest.TestCase): + """Set locale to C for each test case, then restore afterward. + + Use utils.temporary_locale if you want a context manager ("with" statement). + """ + def setUp(self): + self.old_locale = locale.setlocale(locale.LC_ALL) + locale.setlocale(locale.LC_ALL, 'C') + + def tearDown(self): + locale.setlocale(locale.LC_ALL, self.old_locale) diff --git a/pelican/tests/test_generators.py b/pelican/tests/test_generators.py index ac271c1c..05c37269 100644 --- a/pelican/tests/test_generators.py +++ b/pelican/tests/test_generators.py @@ -1,4 +1,3 @@ -import locale import os import sys from shutil import copy, rmtree @@ -9,26 +8,21 @@ from pelican.generators import (ArticlesGenerator, Generator, PagesGenerator, PelicanTemplateNotFound, StaticGenerator, TemplatePagesGenerator) from pelican.tests.support import (can_symlink, get_context, get_settings, - unittest) + unittest, TestCaseWithCLocale) from pelican.writers import Writer - CUR_DIR = os.path.dirname(__file__) CONTENT_DIR = os.path.join(CUR_DIR, 'content') -class TestGenerator(unittest.TestCase): +class TestGenerator(TestCaseWithCLocale): def setUp(self): - self.old_locale = locale.setlocale(locale.LC_ALL) - locale.setlocale(locale.LC_ALL, 'C') + super().setUp() self.settings = get_settings() self.settings['READERS'] = {'asc': None} self.generator = Generator(self.settings.copy(), self.settings, CUR_DIR, self.settings['THEME'], None) - def tearDown(self): - locale.setlocale(locale.LC_ALL, self.old_locale) - def test_include_path(self): self.settings['IGNORE_FILES'] = {'ignored1.rst', 'ignored2.rst'} @@ -408,8 +402,6 @@ class TestArticlesGenerator(unittest.TestCase): def test_period_archives_context(self): """Test correctness of the period_archives context values.""" - old_locale = locale.setlocale(locale.LC_ALL) - locale.setlocale(locale.LC_ALL, 'C') settings = get_settings() settings['CACHE_PATH'] = self.temp_cache @@ -532,15 +524,11 @@ class TestArticlesGenerator(unittest.TestCase): self.assertEqual(sample_archive['dates'][0].title, dates[0].title) self.assertEqual(sample_archive['dates'][0].date, dates[0].date) - locale.setlocale(locale.LC_ALL, old_locale) - def test_period_in_timeperiod_archive(self): """ 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() settings['YEAR_ARCHIVE_SAVE_AS'] = 'posts/{date:%Y}/index.html' @@ -625,7 +613,6 @@ class TestArticlesGenerator(unittest.TestCase): dates=dates, template_name='period_archives', url="posts/1970/Jan/01/", all_articles=generator.articles) - locale.setlocale(locale.LC_ALL, old_locale) def test_nonexistent_template(self): """Attempt to load a non-existent template""" @@ -926,20 +913,18 @@ class TestPageGenerator(unittest.TestCase): context['static_links']) -class TestTemplatePagesGenerator(unittest.TestCase): +class TestTemplatePagesGenerator(TestCaseWithCLocale): TEMPLATE_CONTENT = "foo: {{ foo }}" def setUp(self): + super().setUp() self.temp_content = mkdtemp(prefix='pelicantests.') self.temp_output = mkdtemp(prefix='pelicantests.') - self.old_locale = locale.setlocale(locale.LC_ALL) - locale.setlocale(locale.LC_ALL, 'C') def tearDown(self): rmtree(self.temp_content) rmtree(self.temp_output) - locale.setlocale(locale.LC_ALL, self.old_locale) def test_generate_output(self): @@ -1299,18 +1284,15 @@ class TestStaticGenerator(unittest.TestCase): self.assertTrue(os.path.isfile(self.endfile)) -class TestJinja2Environment(unittest.TestCase): +class TestJinja2Environment(TestCaseWithCLocale): def setUp(self): self.temp_content = mkdtemp(prefix='pelicantests.') self.temp_output = mkdtemp(prefix='pelicantests.') - self.old_locale = locale.setlocale(locale.LC_ALL) - locale.setlocale(locale.LC_ALL, 'C') def tearDown(self): rmtree(self.temp_content) rmtree(self.temp_output) - locale.setlocale(locale.LC_ALL, self.old_locale) def _test_jinja2_helper(self, additional_settings, content, expected): settings = get_settings() diff --git a/pelican/tests/test_importer.py b/pelican/tests/test_importer.py index 0d9586f0..870d3001 100644 --- a/pelican/tests/test_importer.py +++ b/pelican/tests/test_importer.py @@ -1,4 +1,3 @@ -import locale import os import re from posixpath import join as posix_join @@ -6,7 +5,7 @@ from unittest.mock import patch from pelican.settings import DEFAULT_CONFIG from pelican.tests.support import (mute, skipIfNoExecutable, temporary_folder, - unittest) + unittest, TestCaseWithCLocale) from pelican.tools.pelican_import import (blogger2fields, build_header, build_markdown_header, decode_wp_content, @@ -16,7 +15,6 @@ from pelican.tools.pelican_import import (blogger2fields, build_header, ) from pelican.utils import path_to_file_url, slugify - CUR_DIR = os.path.abspath(os.path.dirname(__file__)) BLOGGER_XML_SAMPLE = os.path.join(CUR_DIR, 'content', 'bloggerexport.xml') WORDPRESS_XML_SAMPLE = os.path.join(CUR_DIR, 'content', 'wordpressexport.xml') @@ -38,19 +36,9 @@ except ImportError: LXML = False -class TestWithOsDefaults(unittest.TestCase): - """Set locale to C and timezone to UTC for tests, then restore.""" - def setUp(self): - self.old_locale = locale.setlocale(locale.LC_ALL) - locale.setlocale(locale.LC_ALL, 'C') - - def tearDown(self): - locale.setlocale(locale.LC_ALL, self.old_locale) - - @skipIfNoExecutable(['pandoc', '--version']) @unittest.skipUnless(BeautifulSoup, 'Needs BeautifulSoup module') -class TestBloggerXmlImporter(TestWithOsDefaults): +class TestBloggerXmlImporter(TestCaseWithCLocale): def setUp(self): super().setUp() @@ -95,7 +83,7 @@ class TestBloggerXmlImporter(TestWithOsDefaults): @skipIfNoExecutable(['pandoc', '--version']) @unittest.skipUnless(BeautifulSoup, 'Needs BeautifulSoup module') -class TestWordpressXmlImporter(TestWithOsDefaults): +class TestWordpressXmlImporter(TestCaseWithCLocale): def setUp(self): super().setUp() @@ -433,15 +421,11 @@ class TestBuildHeader(unittest.TestCase): @unittest.skipUnless(BeautifulSoup, 'Needs BeautifulSoup module') @unittest.skipUnless(LXML, 'Needs lxml module') -class TestWordpressXMLAttachements(unittest.TestCase): +class TestWordpressXMLAttachements(TestCaseWithCLocale): def setUp(self): - self.old_locale = locale.setlocale(locale.LC_ALL) - locale.setlocale(locale.LC_ALL, 'C') + super().setUp() self.attachments = get_attachments(WORDPRESS_XML_SAMPLE) - def tearDown(self): - locale.setlocale(locale.LC_ALL, self.old_locale) - def test_recognise_attachments(self): self.assertTrue(self.attachments) self.assertTrue(len(self.attachments.keys()) == 3) @@ -485,7 +469,7 @@ class TestWordpressXMLAttachements(unittest.TestCase): directory) -class TestTumblrImporter(TestWithOsDefaults): +class TestTumblrImporter(TestCaseWithCLocale): @patch("pelican.tools.pelican_import._get_tumblr_posts") def test_posts(self, get): def get_posts(api_key, blogname, offset=0): diff --git a/pelican/tests/test_utils.py b/pelican/tests/test_utils.py index d8296285..40aff005 100644 --- a/pelican/tests/test_utils.py +++ b/pelican/tests/test_utils.py @@ -484,33 +484,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 @@ -518,34 +510,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( @@ -558,6 +542,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''' @@ -673,27 +674,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 3c67369b..e1bed154 100644 --- a/pelican/utils.py +++ b/pelican/utils.py @@ -116,18 +116,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 @@ -872,3 +868,19 @@ 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. + + Use tests.support.TestCaseWithCLocale if you want every unit test in a + class to use the C locale. + ''' + orig_locale = locale.setlocale(lc_category) + if temp_locale: + locale.setlocale(lc_category, temp_locale) + yield + locale.setlocale(lc_category, orig_locale)