diff --git a/pelican/tests/test_utils.py b/pelican/tests/test_utils.py
index 040707ca..2c65e3f2 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):
fr_articles = []
@@ -343,122 +314,111 @@ 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):
- # store current locale
- old_locale = locale.setlocale(locale.LC_ALL)
-
- if platform == 'win32':
- locale.setlocale(locale.LC_ALL, str('Turkish'))
- else:
- locale.setlocale(locale.LC_ALL, str('tr_TR.UTF-8'))
-
- d = utils.SafeDatetime(2012, 8, 29)
-
+ @parameterized.expand([
# 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 %B %Y', '29 Ağustos 2012'),
+ ('%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')
+ ('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ışı')
+ # 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'
- # restore locale back
- locale.setlocale(locale.LC_ALL, old_locale)
+ with utils.temporary_locale(new_locale):
+ d = utils.SafeDatetime(2012, 8, 29)
+ self.assertEqual(utils.strftime(d, fmt_string), expected)
- # 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):
- # store current locale
- old_locale = locale.setlocale(locale.LC_ALL)
-
+ '''
+ Test the output of utils.strftime in a different locale (French).
+ '''
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
+ # 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')
+
+ # non-ascii format candidate (someone might pass it… for
+ # some reason)
+ self.assertEqual(
+ utils.strftime(d, '%écrits en %Y'),
+ '%écrits en 2012')
+
+ @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.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)
-
- def test_maybe_pluralize(self):
- 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):
@@ -589,13 +549,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'),
@@ -666,3 +626,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 9d780039..f4e24ef9 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):
@@ -838,3 +827,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)
diff --git a/tox.ini b/tox.ini
index cb400ea7..86ee995d 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