]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-137634: Modernize `calendar.HTMLCalendar` output (#137635)
authorWulian233 <1055917385@qq.com>
Tue, 14 Oct 2025 10:25:12 +0000 (18:25 +0800)
committerGitHub <noreply@github.com>
Tue, 14 Oct 2025 10:25:12 +0000 (13:25 +0300)
Doc/whatsnew/3.15.rst
Lib/calendar.py
Lib/test/test_calendar.py
Misc/NEWS.d/next/Library/2025-08-11-14-18-32.gh-issue-137634.M7iBG6.rst [new file with mode: 0644]

index a6be27162965ea1dd8fd300a17b4bb3f57d5cf8c..5446cd1fdabde4cef79dff4767d708a35770acd0 100644 (file)
@@ -294,6 +294,14 @@ New modules
 Improved modules
 ================
 
+calendar
+--------
+
+* Calendar pages generated by the :class:`calendar.HTMLCalendar` class now support
+  dark mode and have been migrated to the HTML5 standard for improved accessibility.
+  (Contributed by Jiahao Li and Hugo van Kemenade in :gh:`137634`.)
+
+
 collections
 -----------
 
index 678c7be5aac094e84610212e9ec657c6ecceab33..ed6b74b248042eb98dcc0dea5c7787205342aa7c 100644 (file)
@@ -498,30 +498,29 @@ class HTMLCalendar(Calendar):
         """
         if day == 0:
             # day outside month
-            return '<td class="%s">&nbsp;</td>' % self.cssclass_noday
+            return f'<td class="{self.cssclass_noday}">&nbsp;</td>'
         else:
-            return '<td class="%s">%d</td>' % (self.cssclasses[weekday], day)
+            return f'<td class="{self.cssclasses[weekday]}">{day}</td>'
 
     def formatweek(self, theweek):
         """
         Return a complete week as a table row.
         """
         s = ''.join(self.formatday(d, wd) for (d, wd) in theweek)
-        return '<tr>%s</tr>' % s
+        return f'<tr>{s}</tr>'
 
     def formatweekday(self, day):
         """
         Return a weekday name as a table header.
         """
-        return '<th class="%s">%s</th>' % (
-            self.cssclasses_weekday_head[day], day_abbr[day])
+        return f'<th class="{self.cssclasses_weekday_head[day]}">{day_abbr[day]}</th>'
 
     def formatweekheader(self):
         """
         Return a header for a week as a table row.
         """
         s = ''.join(self.formatweekday(i) for i in self.iterweekdays())
-        return '<tr>%s</tr>' % s
+        return f'<tr>{s}</tr>'
 
     def formatmonthname(self, theyear, themonth, withyear=True):
         """
@@ -529,11 +528,10 @@ class HTMLCalendar(Calendar):
         """
         _validate_month(themonth)
         if withyear:
-            s = '%s %s' % (standalone_month_name[themonth], theyear)
+            s = f'{standalone_month_name[themonth]} {theyear}'
         else:
             s = standalone_month_name[themonth]
-        return '<tr><th colspan="7" class="%s">%s</th></tr>' % (
-            self.cssclass_month_head, s)
+        return f'<tr><th colspan="7" class="{self.cssclass_month_head}">{s}</th></tr>'
 
     def formatmonth(self, theyear, themonth, withyear=True):
         """
@@ -541,8 +539,7 @@ class HTMLCalendar(Calendar):
         """
         v = []
         a = v.append
-        a('<table border="0" cellpadding="0" cellspacing="0" class="%s">' % (
-            self.cssclass_month))
+        a(f'<table class="{self.cssclass_month}">')
         a('\n')
         a(self.formatmonthname(theyear, themonth, withyear=withyear))
         a('\n')
@@ -562,11 +559,9 @@ class HTMLCalendar(Calendar):
         v = []
         a = v.append
         width = max(width, 1)
-        a('<table border="0" cellpadding="0" cellspacing="0" class="%s">' %
-          self.cssclass_year)
+        a(f'<table class="{self.cssclass_year}">')
         a('\n')
-        a('<tr><th colspan="%d" class="%s">%s</th></tr>' % (
-            width, self.cssclass_year_head, theyear))
+        a(f'<tr><th colspan="{width}" class="{self.cssclass_year_head}">{theyear}</th></tr>')
         for i in range(JANUARY, JANUARY+12, width):
             # months in this row
             months = range(i, min(i+width, 13))
@@ -587,14 +582,19 @@ class HTMLCalendar(Calendar):
             encoding = 'utf-8'
         v = []
         a = v.append
-        a('<?xml version="1.0" encoding="%s"?>\n' % encoding)
-        a('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n')
-        a('<html>\n')
+        a('<!DOCTYPE html>\n')
+        a('<html lang="en">\n')
         a('<head>\n')
-        a('<meta http-equiv="Content-Type" content="text/html; charset=%s" />\n' % encoding)
+        a(f'<meta charset="{encoding}">\n')
+        a('<meta name="viewport" content="width=device-width, initial-scale=1">\n')
+        a(f'<title>Calendar for {theyear}</title>\n')
+        a('<style>\n')
+        a(':root { color-scheme: light dark; }\n')
+        a('table.year { border: solid; }\n')
+        a('table.year > tbody > tr > td { border: solid; vertical-align: top; }\n')
+        a('</style>\n')
         if css is not None:
-            a('<link rel="stylesheet" type="text/css" href="%s" />\n' % css)
-        a('<title>Calendar for %d</title>\n' % theyear)
+            a(f'<link rel="stylesheet" href="{css}">\n')
         a('</head>\n')
         a('<body>\n')
         a(self.formatyear(theyear, width))
index c129b3e51ba13da6ce2e16696ec5173284810115..020f9d61cae3cf6ed210d362b27d9ab5ed09ce8d 100644 (file)
@@ -113,18 +113,25 @@ Mo Tu We Th Fr Sa Su      Mo Tu We Th Fr Sa Su      Mo Tu We Th Fr Sa Su
 
 default_format = dict(year="year", month="month", encoding="ascii")
 
+result_2004_css = """<style>
+:root { color-scheme: light dark; }
+table.year { border: solid; }
+table.year > tbody > tr > td { border: solid; vertical-align: top; }
+</style>"""
+
 result_2004_html = """\
-<?xml version="1.0" encoding="{encoding}"?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html>
+<!DOCTYPE html>
+<html lang="en">
 <head>
-<meta http-equiv="Content-Type" content="text/html; charset={encoding}" />
-<link rel="stylesheet" type="text/css" href="calendar.css" />
+<meta charset="{encoding}">
+<meta name="viewport" content="width=device-width, initial-scale=1">
 <title>Calendar for 2004</title>
+{css_styles}
+<link rel="stylesheet" href="calendar.css">
 </head>
 <body>
-<table border="0" cellpadding="0" cellspacing="0" class="{year}">
-<tr><th colspan="3" class="{year}">2004</th></tr><tr><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
+<table class="{year}">
+<tr><th colspan="3" class="{year}">2004</th></tr><tr><td><table class="{month}">
 <tr><th colspan="7" class="{month}">January</th></tr>
 <tr><th class="mon">Mon</th><th class="tue">Tue</th><th class="wed">Wed</th><th class="thu">Thu</th><th class="fri">Fri</th><th class="sat">Sat</th><th class="sun">Sun</th></tr>
 <tr><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="thu">1</td><td class="fri">2</td><td class="sat">3</td><td class="sun">4</td></tr>
@@ -133,7 +140,7 @@ result_2004_html = """\
 <tr><td class="mon">19</td><td class="tue">20</td><td class="wed">21</td><td class="thu">22</td><td class="fri">23</td><td class="sat">24</td><td class="sun">25</td></tr>
 <tr><td class="mon">26</td><td class="tue">27</td><td class="wed">28</td><td class="thu">29</td><td class="fri">30</td><td class="sat">31</td><td class="noday">&nbsp;</td></tr>
 </table>
-</td><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
+</td><td><table class="{month}">
 <tr><th colspan="7" class="{month}">February</th></tr>
 <tr><th class="mon">Mon</th><th class="tue">Tue</th><th class="wed">Wed</th><th class="thu">Thu</th><th class="fri">Fri</th><th class="sat">Sat</th><th class="sun">Sun</th></tr>
 <tr><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="sun">1</td></tr>
@@ -142,7 +149,7 @@ result_2004_html = """\
 <tr><td class="mon">16</td><td class="tue">17</td><td class="wed">18</td><td class="thu">19</td><td class="fri">20</td><td class="sat">21</td><td class="sun">22</td></tr>
 <tr><td class="mon">23</td><td class="tue">24</td><td class="wed">25</td><td class="thu">26</td><td class="fri">27</td><td class="sat">28</td><td class="sun">29</td></tr>
 </table>
-</td><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
+</td><td><table class="{month}">
 <tr><th colspan="7" class="{month}">March</th></tr>
 <tr><th class="mon">Mon</th><th class="tue">Tue</th><th class="wed">Wed</th><th class="thu">Thu</th><th class="fri">Fri</th><th class="sat">Sat</th><th class="sun">Sun</th></tr>
 <tr><td class="mon">1</td><td class="tue">2</td><td class="wed">3</td><td class="thu">4</td><td class="fri">5</td><td class="sat">6</td><td class="sun">7</td></tr>
@@ -151,7 +158,7 @@ result_2004_html = """\
 <tr><td class="mon">22</td><td class="tue">23</td><td class="wed">24</td><td class="thu">25</td><td class="fri">26</td><td class="sat">27</td><td class="sun">28</td></tr>
 <tr><td class="mon">29</td><td class="tue">30</td><td class="wed">31</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td></tr>
 </table>
-</td></tr><tr><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
+</td></tr><tr><td><table class="{month}">
 <tr><th colspan="7" class="{month}">April</th></tr>
 <tr><th class="mon">Mon</th><th class="tue">Tue</th><th class="wed">Wed</th><th class="thu">Thu</th><th class="fri">Fri</th><th class="sat">Sat</th><th class="sun">Sun</th></tr>
 <tr><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="thu">1</td><td class="fri">2</td><td class="sat">3</td><td class="sun">4</td></tr>
@@ -160,7 +167,7 @@ result_2004_html = """\
 <tr><td class="mon">19</td><td class="tue">20</td><td class="wed">21</td><td class="thu">22</td><td class="fri">23</td><td class="sat">24</td><td class="sun">25</td></tr>
 <tr><td class="mon">26</td><td class="tue">27</td><td class="wed">28</td><td class="thu">29</td><td class="fri">30</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td></tr>
 </table>
-</td><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
+</td><td><table class="{month}">
 <tr><th colspan="7" class="{month}">May</th></tr>
 <tr><th class="mon">Mon</th><th class="tue">Tue</th><th class="wed">Wed</th><th class="thu">Thu</th><th class="fri">Fri</th><th class="sat">Sat</th><th class="sun">Sun</th></tr>
 <tr><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="sat">1</td><td class="sun">2</td></tr>
@@ -170,7 +177,7 @@ result_2004_html = """\
 <tr><td class="mon">24</td><td class="tue">25</td><td class="wed">26</td><td class="thu">27</td><td class="fri">28</td><td class="sat">29</td><td class="sun">30</td></tr>
 <tr><td class="mon">31</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td></tr>
 </table>
-</td><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
+</td><td><table class="{month}">
 <tr><th colspan="7" class="{month}">June</th></tr>
 <tr><th class="mon">Mon</th><th class="tue">Tue</th><th class="wed">Wed</th><th class="thu">Thu</th><th class="fri">Fri</th><th class="sat">Sat</th><th class="sun">Sun</th></tr>
 <tr><td class="noday">&nbsp;</td><td class="tue">1</td><td class="wed">2</td><td class="thu">3</td><td class="fri">4</td><td class="sat">5</td><td class="sun">6</td></tr>
@@ -179,7 +186,7 @@ result_2004_html = """\
 <tr><td class="mon">21</td><td class="tue">22</td><td class="wed">23</td><td class="thu">24</td><td class="fri">25</td><td class="sat">26</td><td class="sun">27</td></tr>
 <tr><td class="mon">28</td><td class="tue">29</td><td class="wed">30</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td></tr>
 </table>
-</td></tr><tr><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
+</td></tr><tr><td><table class="{month}">
 <tr><th colspan="7" class="{month}">July</th></tr>
 <tr><th class="mon">Mon</th><th class="tue">Tue</th><th class="wed">Wed</th><th class="thu">Thu</th><th class="fri">Fri</th><th class="sat">Sat</th><th class="sun">Sun</th></tr>
 <tr><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="thu">1</td><td class="fri">2</td><td class="sat">3</td><td class="sun">4</td></tr>
@@ -188,7 +195,7 @@ result_2004_html = """\
 <tr><td class="mon">19</td><td class="tue">20</td><td class="wed">21</td><td class="thu">22</td><td class="fri">23</td><td class="sat">24</td><td class="sun">25</td></tr>
 <tr><td class="mon">26</td><td class="tue">27</td><td class="wed">28</td><td class="thu">29</td><td class="fri">30</td><td class="sat">31</td><td class="noday">&nbsp;</td></tr>
 </table>
-</td><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
+</td><td><table class="{month}">
 <tr><th colspan="7" class="{month}">August</th></tr>
 <tr><th class="mon">Mon</th><th class="tue">Tue</th><th class="wed">Wed</th><th class="thu">Thu</th><th class="fri">Fri</th><th class="sat">Sat</th><th class="sun">Sun</th></tr>
 <tr><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="sun">1</td></tr>
@@ -198,7 +205,7 @@ result_2004_html = """\
 <tr><td class="mon">23</td><td class="tue">24</td><td class="wed">25</td><td class="thu">26</td><td class="fri">27</td><td class="sat">28</td><td class="sun">29</td></tr>
 <tr><td class="mon">30</td><td class="tue">31</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td></tr>
 </table>
-</td><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
+</td><td><table class="{month}">
 <tr><th colspan="7" class="{month}">September</th></tr>
 <tr><th class="mon">Mon</th><th class="tue">Tue</th><th class="wed">Wed</th><th class="thu">Thu</th><th class="fri">Fri</th><th class="sat">Sat</th><th class="sun">Sun</th></tr>
 <tr><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="wed">1</td><td class="thu">2</td><td class="fri">3</td><td class="sat">4</td><td class="sun">5</td></tr>
@@ -207,7 +214,7 @@ result_2004_html = """\
 <tr><td class="mon">20</td><td class="tue">21</td><td class="wed">22</td><td class="thu">23</td><td class="fri">24</td><td class="sat">25</td><td class="sun">26</td></tr>
 <tr><td class="mon">27</td><td class="tue">28</td><td class="wed">29</td><td class="thu">30</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td></tr>
 </table>
-</td></tr><tr><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
+</td></tr><tr><td><table class="{month}">
 <tr><th colspan="7" class="{month}">October</th></tr>
 <tr><th class="mon">Mon</th><th class="tue">Tue</th><th class="wed">Wed</th><th class="thu">Thu</th><th class="fri">Fri</th><th class="sat">Sat</th><th class="sun">Sun</th></tr>
 <tr><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="fri">1</td><td class="sat">2</td><td class="sun">3</td></tr>
@@ -216,7 +223,7 @@ result_2004_html = """\
 <tr><td class="mon">18</td><td class="tue">19</td><td class="wed">20</td><td class="thu">21</td><td class="fri">22</td><td class="sat">23</td><td class="sun">24</td></tr>
 <tr><td class="mon">25</td><td class="tue">26</td><td class="wed">27</td><td class="thu">28</td><td class="fri">29</td><td class="sat">30</td><td class="sun">31</td></tr>
 </table>
-</td><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
+</td><td><table class="{month}">
 <tr><th colspan="7" class="{month}">November</th></tr>
 <tr><th class="mon">Mon</th><th class="tue">Tue</th><th class="wed">Wed</th><th class="thu">Thu</th><th class="fri">Fri</th><th class="sat">Sat</th><th class="sun">Sun</th></tr>
 <tr><td class="mon">1</td><td class="tue">2</td><td class="wed">3</td><td class="thu">4</td><td class="fri">5</td><td class="sat">6</td><td class="sun">7</td></tr>
@@ -225,7 +232,7 @@ result_2004_html = """\
 <tr><td class="mon">22</td><td class="tue">23</td><td class="wed">24</td><td class="thu">25</td><td class="fri">26</td><td class="sat">27</td><td class="sun">28</td></tr>
 <tr><td class="mon">29</td><td class="tue">30</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td></tr>
 </table>
-</td><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
+</td><td><table class="{month}">
 <tr><th colspan="7" class="{month}">December</th></tr>
 <tr><th class="mon">Mon</th><th class="tue">Tue</th><th class="wed">Wed</th><th class="thu">Thu</th><th class="fri">Fri</th><th class="sat">Sat</th><th class="sun">Sun</th></tr>
 <tr><td class="noday">&nbsp;</td><td class="noday">&nbsp;</td><td class="wed">1</td><td class="thu">2</td><td class="fri">3</td><td class="sat">4</td><td class="sun">5</td></tr>
@@ -385,10 +392,12 @@ class OutputTestCase(unittest.TestCase):
         cal = calendar.HTMLCalendar()
         format_ = default_format.copy()
         format_["encoding"] = req or 'utf-8'
+        format_with_css = {**format_, "css_styles": result_2004_css}
+        formatted_html = result_2004_html.format(**format_with_css)
         output = cal.formatyearpage(2004, encoding=req)
         self.assertEqual(
             output,
-            result_2004_html.format(**format_).encode(res)
+            formatted_html.encode(res)
         )
 
     def test_output(self):
@@ -1183,7 +1192,7 @@ class CommandLineTestCase(unittest.TestCase):
             output = run('--type', 'text', '2004')
             self.assertEqual(output, conv(result_2004_text))
             output = run('--type', 'html', '2004')
-            self.assertStartsWith(output, b'<?xml ')
+            self.assertStartsWith(output, b'<!DOCTYPE html>')
             self.assertIn(b'<title>Calendar for 2004</title>', output)
 
     def test_html_output_current_year(self):
@@ -1196,15 +1205,16 @@ class CommandLineTestCase(unittest.TestCase):
     def test_html_output_year_encoding(self):
         for run in self.runners:
             output = run('-t', 'html', '--encoding', 'ascii', '2004')
-            self.assertEqual(output, result_2004_html.format(**default_format).encode('ascii'))
+            format_with_css = default_format.copy()
+            format_with_css["css_styles"] = result_2004_css
+            self.assertEqual(output, result_2004_html.format(**format_with_css).encode('ascii'))
 
     def test_html_output_year_css(self):
         self.assertFailure('-t', 'html', '-c')
         self.assertFailure('-t', 'html', '--css')
         for run in self.runners:
             output = run('-t', 'html', '--css', 'custom.css', '2004')
-            self.assertIn(b'<link rel="stylesheet" type="text/css" '
-                          b'href="custom.css" />', output)
+            self.assertIn(b'<link rel="stylesheet" href="custom.css">', output)
 
 
 class MiscTestCase(unittest.TestCase):
@@ -1258,7 +1268,7 @@ class TestSubClassingCase(unittest.TestCase):
 
     def test_format_year(self):
         self.assertIn(
-            ('<table border="0" cellpadding="0" cellspacing="0" class="%s">' %
+            ('<table class="%s">' %
              self.cal.cssclass_year), self.cal.formatyear(2017))
 
     def test_format_year_head(self):
diff --git a/Misc/NEWS.d/next/Library/2025-08-11-14-18-32.gh-issue-137634.M7iBG6.rst b/Misc/NEWS.d/next/Library/2025-08-11-14-18-32.gh-issue-137634.M7iBG6.rst
new file mode 100644 (file)
index 0000000..e1d7c72
--- /dev/null
@@ -0,0 +1,2 @@
+Calendar pages generated by the :class:`calendar.HTMLCalendar` class now support
+dark mode and have been migrated to the HTML5 standard for improved accessibility.