Skip to content

Commit 5ce8a9a

Browse files
committed
Modernizing calendar.HTMLCalendar for HTML Output
1 parent b36d23f commit 5ce8a9a

File tree

4 files changed

+68
-45
lines changed

4 files changed

+68
-45
lines changed

Doc/whatsnew/3.15.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,13 @@ New modules
214214
Improved modules
215215
================
216216

217+
calendar
218+
--------
219+
220+
* Calendar pages generated by the :class:`calendar.HTMLCalendar` class now support
221+
dark mode, and migrated the output to the HTML5 standard.
222+
(Contributed by Jiahao Li in :gh:`137634`.)
223+
217224
dbm
218225
---
219226

Lib/calendar.py

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -490,51 +490,48 @@ def formatday(self, day, weekday):
490490
"""
491491
if day == 0:
492492
# day outside month
493-
return '<td class="%s">&nbsp;</td>' % self.cssclass_noday
493+
return f'<td class="{self.cssclass_noday}">&nbsp;</td>'
494494
else:
495-
return '<td class="%s">%d</td>' % (self.cssclasses[weekday], day)
495+
return f'<td class="{self.cssclasses[weekday]}">{day}</td>'
496496

497497
def formatweek(self, theweek):
498498
"""
499499
Return a complete week as a table row.
500500
"""
501501
s = ''.join(self.formatday(d, wd) for (d, wd) in theweek)
502-
return '<tr>%s</tr>' % s
502+
return f'<tr>{s}</tr>'
503503

504504
def formatweekday(self, day):
505505
"""
506506
Return a weekday name as a table header.
507507
"""
508-
return '<th class="%s">%s</th>' % (
509-
self.cssclasses_weekday_head[day], day_abbr[day])
508+
return f'<th class="{self.cssclasses_weekday_head[day]}">{day_abbr[day]}</th>'
510509

511510
def formatweekheader(self):
512511
"""
513512
Return a header for a week as a table row.
514513
"""
515514
s = ''.join(self.formatweekday(i) for i in self.iterweekdays())
516-
return '<tr>%s</tr>' % s
515+
return f'<tr>{s}</tr>'
517516

518517
def formatmonthname(self, theyear, themonth, withyear=True):
519518
"""
520519
Return a month name as a table row.
521520
"""
522521
_validate_month(themonth)
523522
if withyear:
524-
s = '%s %s' % (standalone_month_name[themonth], theyear)
523+
s = f'{standalone_month_name[themonth]} {theyear}'
525524
else:
526525
s = standalone_month_name[themonth]
527-
return '<tr><th colspan="7" class="%s">%s</th></tr>' % (
528-
self.cssclass_month_head, s)
526+
return f'<tr><th colspan="7" class="{self.cssclass_month_head}">{s}</th></tr>'
529527

530528
def formatmonth(self, theyear, themonth, withyear=True):
531529
"""
532530
Return a formatted month as a table.
533531
"""
534532
v = []
535533
a = v.append
536-
a('<table border="0" cellpadding="0" cellspacing="0" class="%s">' % (
537-
self.cssclass_month))
534+
a(f'<table class="{self.cssclass_month}">')
538535
a('\n')
539536
a(self.formatmonthname(theyear, themonth, withyear=withyear))
540537
a('\n')
@@ -554,11 +551,9 @@ def formatyear(self, theyear, width=3):
554551
v = []
555552
a = v.append
556553
width = max(width, 1)
557-
a('<table border="0" cellpadding="0" cellspacing="0" class="%s">' %
558-
self.cssclass_year)
554+
a(f'<table class="{self.cssclass_year}">')
559555
a('\n')
560-
a('<tr><th colspan="%d" class="%s">%s</th></tr>' % (
561-
width, self.cssclass_year_head, theyear))
556+
a(f'<tr><th colspan="{width}" class="{self.cssclass_year_head}">{theyear}</th></tr>')
562557
for i in range(JANUARY, JANUARY+12, width):
563558
# months in this row
564559
months = range(i, min(i+width, 13))
@@ -579,14 +574,21 @@ def formatyearpage(self, theyear, width=3, css='calendar.css', encoding=None):
579574
encoding = 'utf-8'
580575
v = []
581576
a = v.append
582-
a('<?xml version="1.0" encoding="%s"?>\n' % encoding)
583-
a('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n')
584-
a('<html>\n')
577+
a('<!DOCTYPE html>\n')
578+
a('<html lang="en">\n')
585579
a('<head>\n')
586-
a('<meta http-equiv="Content-Type" content="text/html; charset=%s" />\n' % encoding)
580+
a(f'<meta charset="{encoding}">\n')
581+
a('<meta name="viewport" content="width=device-width, initial-scale=1">\n')
582+
a(f'<title>Calendar for {theyear}</title>\n')
583+
a('<style>\n')
584+
a('@media (prefers-color-scheme: dark) {\n')
585+
a(' body { background-color: #121212; color: #e0e0e0; }\n')
586+
a(' table.year, table.month { border-color: #444; }\n')
587+
a(' td, th { border-color: #444; }\n')
588+
a('}\n')
589+
a('</style>\n')
587590
if css is not None:
588-
a('<link rel="stylesheet" type="text/css" href="%s" />\n' % css)
589-
a('<title>Calendar for %d</title>\n' % theyear)
591+
a(f'<link rel="stylesheet" href="{css}">\n')
590592
a('</head>\n')
591593
a('<body>\n')
592594
a(self.formatyear(theyear, width))

Lib/test/test_calendar.py

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -113,18 +113,27 @@
113113

114114
default_format = dict(year="year", month="month", encoding="ascii")
115115

116+
result_2004_css = """<style>
117+
@media (prefers-color-scheme: dark) {
118+
body { background-color: #121212; color: #e0e0e0; }
119+
table.year, table.month { border-color: #444; }
120+
td, th { border-color: #444; }
121+
}
122+
</style>"""
123+
116124
result_2004_html = """\
117-
<?xml version="1.0" encoding="{encoding}"?>
118-
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
119-
<html>
125+
<!DOCTYPE html>
126+
<html lang="en">
120127
<head>
121-
<meta http-equiv="Content-Type" content="text/html; charset={encoding}" />
122-
<link rel="stylesheet" type="text/css" href="calendar.css" />
128+
<meta charset="{encoding}">
129+
<meta name="viewport" content="width=device-width, initial-scale=1">
123130
<title>Calendar for 2004</title>
131+
{css_styles}
132+
<link rel="stylesheet" href="calendar.css">
124133
</head>
125134
<body>
126-
<table border="0" cellpadding="0" cellspacing="0" class="{year}">
127-
<tr><th colspan="3" class="{year}">2004</th></tr><tr><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
135+
<table class="{year}">
136+
<tr><th colspan="3" class="{year}">2004</th></tr><tr><td><table class="{month}">
128137
<tr><th colspan="7" class="{month}">January</th></tr>
129138
<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>
130139
<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 +142,7 @@
133142
<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>
134143
<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>
135144
</table>
136-
</td><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
145+
</td><td><table class="{month}">
137146
<tr><th colspan="7" class="{month}">February</th></tr>
138147
<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>
139148
<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 +151,7 @@
142151
<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>
143152
<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>
144153
</table>
145-
</td><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
154+
</td><td><table class="{month}">
146155
<tr><th colspan="7" class="{month}">March</th></tr>
147156
<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>
148157
<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 +160,7 @@
151160
<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>
152161
<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>
153162
</table>
154-
</td></tr><tr><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
163+
</td></tr><tr><td><table class="{month}">
155164
<tr><th colspan="7" class="{month}">April</th></tr>
156165
<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>
157166
<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 +169,7 @@
160169
<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>
161170
<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>
162171
</table>
163-
</td><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
172+
</td><td><table class="{month}">
164173
<tr><th colspan="7" class="{month}">May</th></tr>
165174
<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>
166175
<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 +179,7 @@
170179
<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>
171180
<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>
172181
</table>
173-
</td><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
182+
</td><td><table class="{month}">
174183
<tr><th colspan="7" class="{month}">June</th></tr>
175184
<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>
176185
<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 +188,7 @@
179188
<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>
180189
<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>
181190
</table>
182-
</td></tr><tr><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
191+
</td></tr><tr><td><table class="{month}">
183192
<tr><th colspan="7" class="{month}">July</th></tr>
184193
<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>
185194
<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 +197,7 @@
188197
<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>
189198
<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>
190199
</table>
191-
</td><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
200+
</td><td><table class="{month}">
192201
<tr><th colspan="7" class="{month}">August</th></tr>
193202
<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>
194203
<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 +207,7 @@
198207
<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>
199208
<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>
200209
</table>
201-
</td><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
210+
</td><td><table class="{month}">
202211
<tr><th colspan="7" class="{month}">September</th></tr>
203212
<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>
204213
<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 +216,7 @@
207216
<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>
208217
<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>
209218
</table>
210-
</td></tr><tr><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
219+
</td></tr><tr><td><table class="{month}">
211220
<tr><th colspan="7" class="{month}">October</th></tr>
212221
<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>
213222
<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 +225,7 @@
216225
<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>
217226
<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>
218227
</table>
219-
</td><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
228+
</td><td><table class="{month}">
220229
<tr><th colspan="7" class="{month}">November</th></tr>
221230
<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>
222231
<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 +234,7 @@
225234
<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>
226235
<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>
227236
</table>
228-
</td><td><table border="0" cellpadding="0" cellspacing="0" class="{month}">
237+
</td><td><table class="{month}">
229238
<tr><th colspan="7" class="{month}">December</th></tr>
230239
<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>
231240
<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 +394,12 @@ def check_htmlcalendar_encoding(self, req, res):
385394
cal = calendar.HTMLCalendar()
386395
format_ = default_format.copy()
387396
format_["encoding"] = req or 'utf-8'
397+
format_with_css = {**format_, "css_styles": result_2004_css}
398+
formatted_html = result_2004_html.format(**format_with_css)
388399
output = cal.formatyearpage(2004, encoding=req)
389400
self.assertEqual(
390401
output,
391-
result_2004_html.format(**format_).encode(res)
402+
formatted_html.encode(res)
392403
)
393404

394405
def test_output(self):
@@ -1132,7 +1143,7 @@ def test_option_type(self):
11321143
output = run('--type', 'text', '2004')
11331144
self.assertEqual(output, conv(result_2004_text))
11341145
output = run('--type', 'html', '2004')
1135-
self.assertStartsWith(output, b'<?xml ')
1146+
self.assertStartsWith(output, b'<!DOCTYPE html>')
11361147
self.assertIn(b'<title>Calendar for 2004</title>', output)
11371148

11381149
def test_html_output_current_year(self):
@@ -1145,15 +1156,16 @@ def test_html_output_current_year(self):
11451156
def test_html_output_year_encoding(self):
11461157
for run in self.runners:
11471158
output = run('-t', 'html', '--encoding', 'ascii', '2004')
1148-
self.assertEqual(output, result_2004_html.format(**default_format).encode('ascii'))
1159+
format_with_css = default_format.copy()
1160+
format_with_css["css_styles"] = result_2004_css
1161+
self.assertEqual(output, result_2004_html.format(**format_with_css).encode('ascii'))
11491162

11501163
def test_html_output_year_css(self):
11511164
self.assertFailure('-t', 'html', '-c')
11521165
self.assertFailure('-t', 'html', '--css')
11531166
for run in self.runners:
11541167
output = run('-t', 'html', '--css', 'custom.css', '2004')
1155-
self.assertIn(b'<link rel="stylesheet" type="text/css" '
1156-
b'href="custom.css" />', output)
1168+
self.assertIn(b'<link rel="stylesheet" href="custom.css">', output)
11571169

11581170

11591171
class MiscTestCase(unittest.TestCase):
@@ -1207,7 +1219,7 @@ def test_formatweek_head(self):
12071219

12081220
def test_format_year(self):
12091221
self.assertIn(
1210-
('<table border="0" cellpadding="0" cellspacing="0" class="%s">' %
1222+
('<table class="%s">' %
12111223
self.cal.cssclass_year), self.cal.formatyear(2017))
12121224

12131225
def test_format_year_head(self):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Calendar pages generated by the :class:`calendar.HTMLCalendar` class now
2+
support dark mode, and migrated the output to the HTML5 standard.

0 commit comments

Comments
 (0)