gh-69714: Make `calendar` module fully tested (#93655)

There are 3 paths to use `locale` argument in
`calendar.Locale{Text|HTML}Calendar.__init__(..., locale=None)`:
(1) `locale=None` -- denotes the "default locale"[1]
(2) `locale=""` -- denotes the native environment
(3) `locale=other_valid_locale` -- denotes a custom locale

So far case (2) is covered and case (1) is in 78935daf5a (same branch).
This commit adds a remaining case (3).

[1] In the current implementation, this translates into the following
approach:

GET current locale
IF current locale == "C" THEN
  SET current locale TO ""
  GET current locale
ENDIF

* Remove unreachable code (and increase test coverage)

This condition cannot be true. `_locale.setlocale()` from the C module
raises `locale.Error` instead of returning `None` for
`different_locale.__enter__` (where `self.oldlocale` is set).

* Expand the try clause to calls to `LocaleTextCalendar.formatmonthname()`.

This method temporarily changes the current locale to the given locale,
so `_locale.setlocale()` may raise `local.Error`.


Co-authored-by: Rohit Mediratta <rohitm@gmail.com>
Co-authored-by: Jessica McKellar <jesstess@mit.edu>
Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com>
Co-authored-by: Irit Katriel <1055913+iritkatriel@users.noreply.github.com>
This commit is contained in:
Bart Skowron 2023-07-22 15:20:40 +02:00 committed by GitHub
parent 463b56da12
commit 2aba047f0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 204 additions and 54 deletions

View File

@ -585,8 +585,6 @@ class different_locale:
_locale.setlocale(_locale.LC_TIME, self.locale) _locale.setlocale(_locale.LC_TIME, self.locale)
def __exit__(self, *args): def __exit__(self, *args):
if self.oldlocale is None:
return
_locale.setlocale(_locale.LC_TIME, self.oldlocale) _locale.setlocale(_locale.LC_TIME, self.oldlocale)
@ -690,7 +688,7 @@ def timegm(tuple):
return seconds return seconds
def main(args): def main(args=None):
import argparse import argparse
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
textgroup = parser.add_argument_group('text only arguments') textgroup = parser.add_argument_group('text only arguments')
@ -747,7 +745,7 @@ def main(args):
help="month number (1-12, text only)" help="month number (1-12, text only)"
) )
options = parser.parse_args(args[1:]) options = parser.parse_args(args)
if options.locale and not options.encoding: if options.locale and not options.encoding:
parser.error("if --locale is specified --encoding is required") parser.error("if --locale is specified --encoding is required")
@ -756,6 +754,9 @@ def main(args):
locale = options.locale, options.encoding locale = options.locale, options.encoding
if options.type == "html": if options.type == "html":
if options.month:
parser.error("incorrect number of arguments")
sys.exit(1)
if options.locale: if options.locale:
cal = LocaleHTMLCalendar(locale=locale) cal = LocaleHTMLCalendar(locale=locale)
else: else:
@ -767,11 +768,8 @@ def main(args):
write = sys.stdout.buffer.write write = sys.stdout.buffer.write
if options.year is None: if options.year is None:
write(cal.formatyearpage(datetime.date.today().year, **optdict)) write(cal.formatyearpage(datetime.date.today().year, **optdict))
elif options.month is None:
write(cal.formatyearpage(options.year, **optdict))
else: else:
parser.error("incorrect number of arguments") write(cal.formatyearpage(options.year, **optdict))
sys.exit(1)
else: else:
if options.locale: if options.locale:
cal = LocaleTextCalendar(locale=locale) cal = LocaleTextCalendar(locale=locale)
@ -795,4 +793,4 @@ def main(args):
if __name__ == "__main__": if __name__ == "__main__":
main(sys.argv) main()

View File

@ -3,11 +3,13 @@ import unittest
from test import support from test import support
from test.support.script_helper import assert_python_ok, assert_python_failure from test.support.script_helper import assert_python_ok, assert_python_failure
import time import contextlib
import locale
import sys
import datetime import datetime
import io
import locale
import os import os
import sys
import time
# From https://en.wikipedia.org/wiki/Leap_year_starting_on_Saturday # From https://en.wikipedia.org/wiki/Leap_year_starting_on_Saturday
result_0_02_text = """\ result_0_02_text = """\
@ -549,26 +551,92 @@ class CalendarTestCase(unittest.TestCase):
# verify it "acts like a sequence" in two forms of iteration # verify it "acts like a sequence" in two forms of iteration
self.assertEqual(value[::-1], list(reversed(value))) self.assertEqual(value[::-1], list(reversed(value)))
def test_locale_calendars(self): def test_locale_text_calendar(self):
try:
cal = calendar.LocaleTextCalendar(locale='')
local_weekday = cal.formatweekday(1, 10)
local_weekday_abbr = cal.formatweekday(1, 3)
local_month = cal.formatmonthname(2010, 10, 10)
except locale.Error:
# cannot set the system default locale -- skip rest of test
raise unittest.SkipTest('cannot set the system default locale')
self.assertIsInstance(local_weekday, str)
self.assertIsInstance(local_weekday_abbr, str)
self.assertIsInstance(local_month, str)
self.assertEqual(len(local_weekday), 10)
self.assertEqual(len(local_weekday_abbr), 3)
self.assertGreaterEqual(len(local_month), 10)
cal = calendar.LocaleTextCalendar(locale=None)
local_weekday = cal.formatweekday(1, 10)
local_weekday_abbr = cal.formatweekday(1, 3)
local_month = cal.formatmonthname(2010, 10, 10)
self.assertIsInstance(local_weekday, str)
self.assertIsInstance(local_weekday_abbr, str)
self.assertIsInstance(local_month, str)
self.assertEqual(len(local_weekday), 10)
self.assertEqual(len(local_weekday_abbr), 3)
self.assertGreaterEqual(len(local_month), 10)
cal = calendar.LocaleTextCalendar(locale='C')
local_weekday = cal.formatweekday(1, 10)
local_weekday_abbr = cal.formatweekday(1, 3)
local_month = cal.formatmonthname(2010, 10, 10)
self.assertIsInstance(local_weekday, str)
self.assertIsInstance(local_weekday_abbr, str)
self.assertIsInstance(local_month, str)
self.assertEqual(len(local_weekday), 10)
self.assertEqual(len(local_weekday_abbr), 3)
self.assertGreaterEqual(len(local_month), 10)
def test_locale_html_calendar(self):
try:
cal = calendar.LocaleHTMLCalendar(locale='')
local_weekday = cal.formatweekday(1)
local_month = cal.formatmonthname(2010, 10)
except locale.Error:
# cannot set the system default locale -- skip rest of test
raise unittest.SkipTest('cannot set the system default locale')
self.assertIsInstance(local_weekday, str)
self.assertIsInstance(local_month, str)
cal = calendar.LocaleHTMLCalendar(locale=None)
local_weekday = cal.formatweekday(1)
local_month = cal.formatmonthname(2010, 10)
self.assertIsInstance(local_weekday, str)
self.assertIsInstance(local_month, str)
cal = calendar.LocaleHTMLCalendar(locale='C')
local_weekday = cal.formatweekday(1)
local_month = cal.formatmonthname(2010, 10)
self.assertIsInstance(local_weekday, str)
self.assertIsInstance(local_month, str)
def test_locale_calendars_reset_locale_properly(self):
# ensure that Locale{Text,HTML}Calendar resets the locale properly # ensure that Locale{Text,HTML}Calendar resets the locale properly
# (it is still not thread-safe though) # (it is still not thread-safe though)
old_october = calendar.TextCalendar().formatmonthname(2010, 10, 10) old_october = calendar.TextCalendar().formatmonthname(2010, 10, 10)
try: try:
cal = calendar.LocaleTextCalendar(locale='') cal = calendar.LocaleTextCalendar(locale='')
local_weekday = cal.formatweekday(1, 10) local_weekday = cal.formatweekday(1, 10)
local_weekday_abbr = cal.formatweekday(1, 3)
local_month = cal.formatmonthname(2010, 10, 10) local_month = cal.formatmonthname(2010, 10, 10)
except locale.Error: except locale.Error:
# cannot set the system default locale -- skip rest of test # cannot set the system default locale -- skip rest of test
raise unittest.SkipTest('cannot set the system default locale') raise unittest.SkipTest('cannot set the system default locale')
self.assertIsInstance(local_weekday, str) self.assertIsInstance(local_weekday, str)
self.assertIsInstance(local_weekday_abbr, str)
self.assertIsInstance(local_month, str) self.assertIsInstance(local_month, str)
self.assertEqual(len(local_weekday), 10) self.assertEqual(len(local_weekday), 10)
self.assertEqual(len(local_weekday_abbr), 3)
self.assertGreaterEqual(len(local_month), 10) self.assertGreaterEqual(len(local_month), 10)
cal = calendar.LocaleHTMLCalendar(locale='') cal = calendar.LocaleHTMLCalendar(locale='')
local_weekday = cal.formatweekday(1) local_weekday = cal.formatweekday(1)
local_month = cal.formatmonthname(2010, 10) local_month = cal.formatmonthname(2010, 10)
self.assertIsInstance(local_weekday, str) self.assertIsInstance(local_weekday, str)
self.assertIsInstance(local_month, str) self.assertIsInstance(local_month, str)
new_october = calendar.TextCalendar().formatmonthname(2010, 10, 10) new_october = calendar.TextCalendar().formatmonthname(2010, 10, 10)
self.assertEqual(old_october, new_october) self.assertEqual(old_october, new_october)
@ -589,6 +657,21 @@ class CalendarTestCase(unittest.TestCase):
except locale.Error: except locale.Error:
raise unittest.SkipTest('cannot set the en_US locale') raise unittest.SkipTest('cannot set the en_US locale')
def test_locale_calendar_formatmonthname(self):
try:
# formatmonthname uses the same month names regardless of the width argument.
cal = calendar.LocaleTextCalendar(locale='en_US')
# For too short widths, a full name (with year) is used.
self.assertEqual(cal.formatmonthname(2022, 6, 2, withyear=False), "June")
self.assertEqual(cal.formatmonthname(2022, 6, 2, withyear=True), "June 2022")
self.assertEqual(cal.formatmonthname(2022, 6, 3, withyear=False), "June")
self.assertEqual(cal.formatmonthname(2022, 6, 3, withyear=True), "June 2022")
# For long widths, a centered name is used.
self.assertEqual(cal.formatmonthname(2022, 6, 10, withyear=False), " June ")
self.assertEqual(cal.formatmonthname(2022, 6, 15, withyear=True), " June 2022 ")
except locale.Error:
raise unittest.SkipTest('cannot set the en_US locale')
def test_locale_html_calendar_custom_css_class_month_name(self): def test_locale_html_calendar_custom_css_class_month_name(self):
try: try:
cal = calendar.LocaleHTMLCalendar(locale='') cal = calendar.LocaleHTMLCalendar(locale='')
@ -847,46 +930,104 @@ def conv(s):
return s.replace('\n', os.linesep).encode() return s.replace('\n', os.linesep).encode()
class CommandLineTestCase(unittest.TestCase): class CommandLineTestCase(unittest.TestCase):
def run_ok(self, *args): def setUp(self):
self.runners = [self.run_cli_ok, self.run_cmd_ok]
@contextlib.contextmanager
def captured_stdout_with_buffer(self):
orig_stdout = sys.stdout
buffer = io.BytesIO()
sys.stdout = io.TextIOWrapper(buffer)
try:
yield sys.stdout
finally:
sys.stdout.flush()
sys.stdout.buffer.seek(0)
sys.stdout = orig_stdout
@contextlib.contextmanager
def captured_stderr_with_buffer(self):
orig_stderr = sys.stderr
buffer = io.BytesIO()
sys.stderr = io.TextIOWrapper(buffer)
try:
yield sys.stderr
finally:
sys.stderr.flush()
sys.stderr.buffer.seek(0)
sys.stderr = orig_stderr
def run_cli_ok(self, *args):
with self.captured_stdout_with_buffer() as stdout:
calendar.main(args)
return stdout.buffer.read()
def run_cmd_ok(self, *args):
return assert_python_ok('-m', 'calendar', *args)[1] return assert_python_ok('-m', 'calendar', *args)[1]
def assertFailure(self, *args): def assertCLIFails(self, *args):
with self.captured_stderr_with_buffer() as stderr:
self.assertRaises(SystemExit, calendar.main, args)
stderr = stderr.buffer.read()
self.assertIn(b'usage:', stderr)
return stderr
def assertCmdFails(self, *args):
rc, stdout, stderr = assert_python_failure('-m', 'calendar', *args) rc, stdout, stderr = assert_python_failure('-m', 'calendar', *args)
self.assertIn(b'usage:', stderr) self.assertIn(b'usage:', stderr)
self.assertEqual(rc, 2) self.assertEqual(rc, 2)
return rc, stdout, stderr
def assertFailure(self, *args):
self.assertCLIFails(*args)
self.assertCmdFails(*args)
def test_help(self): def test_help(self):
stdout = self.run_ok('-h') stdout = self.run_cmd_ok('-h')
self.assertIn(b'usage:', stdout) self.assertIn(b'usage:', stdout)
self.assertIn(b'calendar.py', stdout) self.assertIn(b'calendar.py', stdout)
self.assertIn(b'--help', stdout) self.assertIn(b'--help', stdout)
# special case: stdout but sys.exit()
with self.captured_stdout_with_buffer() as output:
self.assertRaises(SystemExit, calendar.main, ['-h'])
output = output.buffer.read()
self.assertIn(b'usage:', output)
self.assertIn(b'--help', output)
def test_illegal_arguments(self): def test_illegal_arguments(self):
self.assertFailure('-z') self.assertFailure('-z')
self.assertFailure('spam') self.assertFailure('spam')
self.assertFailure('2004', 'spam') self.assertFailure('2004', 'spam')
self.assertFailure('2004', '1', 'spam')
self.assertFailure('2004', '1', '1')
self.assertFailure('2004', '1', '1', 'spam')
self.assertFailure('-t', 'html', '2004', '1') self.assertFailure('-t', 'html', '2004', '1')
def test_output_current_year(self): def test_output_current_year(self):
stdout = self.run_ok() for run in self.runners:
year = datetime.datetime.now().year output = run()
self.assertIn((' %s' % year).encode(), stdout) year = datetime.datetime.now().year
self.assertIn(b'January', stdout) self.assertIn(conv(' %s' % year), output)
self.assertIn(b'Mo Tu We Th Fr Sa Su', stdout) self.assertIn(b'January', output)
self.assertIn(b'Mo Tu We Th Fr Sa Su', output)
def test_output_year(self): def test_output_year(self):
stdout = self.run_ok('2004') for run in self.runners:
self.assertEqual(stdout, conv(result_2004_text)) output = run('2004')
self.assertEqual(output, conv(result_2004_text))
def test_output_month(self): def test_output_month(self):
stdout = self.run_ok('2004', '1') for run in self.runners:
self.assertEqual(stdout, conv(result_2004_01_text)) output = run('2004', '1')
self.assertEqual(output, conv(result_2004_01_text))
def test_option_encoding(self): def test_option_encoding(self):
self.assertFailure('-e') self.assertFailure('-e')
self.assertFailure('--encoding') self.assertFailure('--encoding')
stdout = self.run_ok('--encoding', 'utf-16-le', '2004') for run in self.runners:
self.assertEqual(stdout, result_2004_text.encode('utf-16-le')) output = run('--encoding', 'utf-16-le', '2004')
self.assertEqual(output, result_2004_text.encode('utf-16-le'))
def test_option_locale(self): def test_option_locale(self):
self.assertFailure('-L') self.assertFailure('-L')
@ -904,66 +1045,75 @@ class CommandLineTestCase(unittest.TestCase):
locale.setlocale(locale.LC_TIME, oldlocale) locale.setlocale(locale.LC_TIME, oldlocale)
except (locale.Error, ValueError): except (locale.Error, ValueError):
self.skipTest('cannot set the system default locale') self.skipTest('cannot set the system default locale')
stdout = self.run_ok('--locale', lang, '--encoding', enc, '2004') for run in self.runners:
self.assertIn('2004'.encode(enc), stdout) for type in ('text', 'html'):
output = run(
'--type', type, '--locale', lang, '--encoding', enc, '2004'
)
self.assertIn('2004'.encode(enc), output)
def test_option_width(self): def test_option_width(self):
self.assertFailure('-w') self.assertFailure('-w')
self.assertFailure('--width') self.assertFailure('--width')
self.assertFailure('-w', 'spam') self.assertFailure('-w', 'spam')
stdout = self.run_ok('--width', '3', '2004') for run in self.runners:
self.assertIn(b'Mon Tue Wed Thu Fri Sat Sun', stdout) output = run('--width', '3', '2004')
self.assertIn(b'Mon Tue Wed Thu Fri Sat Sun', output)
def test_option_lines(self): def test_option_lines(self):
self.assertFailure('-l') self.assertFailure('-l')
self.assertFailure('--lines') self.assertFailure('--lines')
self.assertFailure('-l', 'spam') self.assertFailure('-l', 'spam')
stdout = self.run_ok('--lines', '2', '2004') for run in self.runners:
self.assertIn(conv('December\n\nMo Tu We'), stdout) output = run('--lines', '2', '2004')
self.assertIn(conv('December\n\nMo Tu We'), output)
def test_option_spacing(self): def test_option_spacing(self):
self.assertFailure('-s') self.assertFailure('-s')
self.assertFailure('--spacing') self.assertFailure('--spacing')
self.assertFailure('-s', 'spam') self.assertFailure('-s', 'spam')
stdout = self.run_ok('--spacing', '8', '2004') for run in self.runners:
self.assertIn(b'Su Mo', stdout) output = run('--spacing', '8', '2004')
self.assertIn(b'Su Mo', output)
def test_option_months(self): def test_option_months(self):
self.assertFailure('-m') self.assertFailure('-m')
self.assertFailure('--month') self.assertFailure('--month')
self.assertFailure('-m', 'spam') self.assertFailure('-m', 'spam')
stdout = self.run_ok('--months', '1', '2004') for run in self.runners:
self.assertIn(conv('\nMo Tu We Th Fr Sa Su\n'), stdout) output = run('--months', '1', '2004')
self.assertIn(conv('\nMo Tu We Th Fr Sa Su\n'), output)
def test_option_type(self): def test_option_type(self):
self.assertFailure('-t') self.assertFailure('-t')
self.assertFailure('--type') self.assertFailure('--type')
self.assertFailure('-t', 'spam') self.assertFailure('-t', 'spam')
stdout = self.run_ok('--type', 'text', '2004') for run in self.runners:
self.assertEqual(stdout, conv(result_2004_text)) output = run('--type', 'text', '2004')
stdout = self.run_ok('--type', 'html', '2004') self.assertEqual(output, conv(result_2004_text))
self.assertEqual(stdout[:6], b'<?xml ') output = run('--type', 'html', '2004')
self.assertIn(b'<title>Calendar for 2004</title>', stdout) self.assertEqual(output[:6], b'<?xml ')
self.assertIn(b'<title>Calendar for 2004</title>', output)
def test_html_output_current_year(self): def test_html_output_current_year(self):
stdout = self.run_ok('--type', 'html') for run in self.runners:
year = datetime.datetime.now().year output = run('--type', 'html')
self.assertIn(('<title>Calendar for %s</title>' % year).encode(), year = datetime.datetime.now().year
stdout) self.assertIn(('<title>Calendar for %s</title>' % year).encode(), output)
self.assertIn(b'<tr><th colspan="7" class="month">January</th></tr>', self.assertIn(b'<tr><th colspan="7" class="month">January</th></tr>', output)
stdout)
def test_html_output_year_encoding(self): def test_html_output_year_encoding(self):
stdout = self.run_ok('-t', 'html', '--encoding', 'ascii', '2004') for run in self.runners:
self.assertEqual(stdout, output = run('-t', 'html', '--encoding', 'ascii', '2004')
result_2004_html.format(**default_format).encode('ascii')) self.assertEqual(output, result_2004_html.format(**default_format).encode('ascii'))
def test_html_output_year_css(self): def test_html_output_year_css(self):
self.assertFailure('-t', 'html', '-c') self.assertFailure('-t', 'html', '-c')
self.assertFailure('-t', 'html', '--css') self.assertFailure('-t', 'html', '--css')
stdout = self.run_ok('-t', 'html', '--css', 'custom.css', '2004') for run in self.runners:
self.assertIn(b'<link rel="stylesheet" type="text/css" ' output = run('-t', 'html', '--css', 'custom.css', '2004')
b'href="custom.css" />', stdout) self.assertIn(b'<link rel="stylesheet" type="text/css" '
b'href="custom.css" />', output)
class MiscTestCase(unittest.TestCase): class MiscTestCase(unittest.TestCase):

View File

@ -1700,6 +1700,7 @@ Ngalim Siregar
Kragen Sitaker Kragen Sitaker
Kaartic Sivaraam Kaartic Sivaraam
Stanisław Skonieczny Stanisław Skonieczny
Bart Skowron
Roman Skurikhin Roman Skurikhin
Ville Skyttä Ville Skyttä
Michael Sloan Michael Sloan

View File

@ -0,0 +1 @@
Add additional tests to :mod:`calendar` to achieve full test coverage.