[Python-checkins] bpo-10716: Migrating pydoc to html5. (GH-28651)

JulienPalard webhook-mailer at python.org
Sat Oct 9 03:36:59 EDT 2021


https://github.com/python/cpython/commit/c91b6f57f3f75b482e4a9d30ad2afe37892a8ceb
commit: c91b6f57f3f75b482e4a9d30ad2afe37892a8ceb
branch: main
author: Julien Palard <julien at palard.fr>
committer: JulienPalard <julien at palard.fr>
date: 2021-10-09T09:36:50+02:00
summary:

bpo-10716: Migrating pydoc to html5. (GH-28651)

files:
A Misc/NEWS.d/next/Library/2021-10-08-04-11-55.bpo-10716.QSRVK2.rst
M Lib/cgitb.py
M Lib/pydoc.py
M Lib/pydoc_data/_pydoc.css
M Lib/test/test_docxmlrpc.py
M Lib/test/test_pydoc.py
M Lib/xmlrpc/server.py

diff --git a/Lib/cgitb.py b/Lib/cgitb.py
index 17ddda376884d..ec156843099d3 100644
--- a/Lib/cgitb.py
+++ b/Lib/cgitb.py
@@ -31,6 +31,7 @@
 import time
 import tokenize
 import traceback
+from html import escape as html_escape
 
 def reset():
     """Return a string that resets the CGI and browser to a known state."""
@@ -105,10 +106,16 @@ def html(einfo, context=5):
         etype = etype.__name__
     pyver = 'Python ' + sys.version.split()[0] + ': ' + sys.executable
     date = time.ctime(time.time())
-    head = '<body bgcolor="#f0f0f8">' + pydoc.html.heading(
-        '<big><big>%s</big></big>' %
-        strong(pydoc.html.escape(str(etype))),
-        '#ffffff', '#6622aa', pyver + '<br>' + date) + '''
+    head = f'''
+<body bgcolor="#f0f0f8">
+<table width="100%" cellspacing=0 cellpadding=2 border=0 summary="heading">
+<tr bgcolor="#6622aa">
+<td valign=bottom> <br>
+<font color="#ffffff" face="helvetica, arial"> <br>
+<big><big><strong>{html_escape(str(etype))}</strong></big></big></font></td>
+<td align=right valign=bottom>
+<font color="#ffffff" face="helvetica, arial">{pyver}<br>{date}</font></td>
+</tr></table>
 <p>A problem occurred in a Python script.  Here is the sequence of
 function calls leading up to the error, in the order they occurred.</p>'''
 
diff --git a/Lib/pydoc.py b/Lib/pydoc.py
index 34a608760338e..3a2ff218f8319 100755
--- a/Lib/pydoc.py
+++ b/Lib/pydoc.py
@@ -542,7 +542,7 @@ def repr_string(self, x, level):
             # needed to make any special characters, so show a raw string.
             return 'r' + testrepr[0] + self.escape(test) + testrepr[0]
         return re.sub(r'((\\[\\abfnrtv\'"]|\\[0-9]..|\\x..|\\u....)+)',
-                      r'<font color="#c040c0">\1</font>',
+                      r'<span class="repr">\1</span>',
                       self.escape(testrepr))
 
     repr_str = repr_string
@@ -567,49 +567,48 @@ class HTMLDoc(Doc):
     def page(self, title, contents):
         """Format an HTML page."""
         return '''\
-<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
-<html><head><title>Python: %s</title>
-<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-</head><body bgcolor="#f0f0f8">
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>Python: %s</title>
+</head><body>
 %s
 </body></html>''' % (title, contents)
 
-    def heading(self, title, fgcol, bgcol, extras=''):
+    def heading(self, title, extras=''):
         """Format a page heading."""
         return '''
-<table width="100%%" cellspacing=0 cellpadding=2 border=0 summary="heading">
-<tr bgcolor="%s">
-<td valign=bottom> <br>
-<font color="%s" face="helvetica, arial"> <br>%s</font></td
-><td align=right valign=bottom
-><font color="%s" face="helvetica, arial">%s</font></td></tr></table>
-    ''' % (bgcol, fgcol, title, fgcol, extras or ' ')
-
-    def section(self, title, fgcol, bgcol, contents, width=6,
+<table class="heading">
+<tr class="heading-text decor">
+<td class="title"> <br>%s</td>
+<td class="extra">%s</td></tr></table>
+    ''' % (title, extras or ' ')
+
+    def section(self, title, cls, contents, width=6,
                 prelude='', marginalia=None, gap=' '):
         """Format a section with a heading."""
         if marginalia is None:
-            marginalia = '<tt>' + ' ' * width + '</tt>'
+            marginalia = '<span class="code">' + ' ' * width + '</span>'
         result = '''<p>
-<table width="100%%" cellspacing=0 cellpadding=2 border=0 summary="section">
-<tr bgcolor="%s">
-<td colspan=3 valign=bottom> <br>
-<font color="%s" face="helvetica, arial">%s</font></td></tr>
-    ''' % (bgcol, fgcol, title)
+<table class="section">
+<tr class="decor %s-decor heading-text">
+<td class="section-title" colspan=3> <br>%s</td></tr>
+    ''' % (cls, title)
         if prelude:
             result = result + '''
-<tr bgcolor="%s"><td rowspan=2>%s</td>
-<td colspan=2>%s</td></tr>
-<tr><td>%s</td>''' % (bgcol, marginalia, prelude, gap)
+<tr><td class="decor %s-decor" rowspan=2>%s</td>
+<td class="decor %s-decor" colspan=2>%s</td></tr>
+<tr><td>%s</td>''' % (cls, marginalia, cls, prelude, gap)
         else:
             result = result + '''
-<tr><td bgcolor="%s">%s</td><td>%s</td>''' % (bgcol, marginalia, gap)
+<tr><td class="decor %s-decor">%s</td><td>%s</td>''' % (cls, marginalia, gap)
 
-        return result + '\n<td width="100%%">%s</td></tr></table>' % contents
+        return result + '\n<td class="singlecolumn">%s</td></tr></table>' % contents
 
     def bigsection(self, title, *args):
         """Format a section with a big heading."""
-        title = '<big><strong>%s</strong></big>' % title
+        title = '<strong class="bigsection">%s</strong>' % title
         return self.section(title, *args)
 
     def preformat(self, text):
@@ -618,19 +617,19 @@ def preformat(self, text):
         return replace(text, '\n\n', '\n \n', '\n\n', '\n \n',
                              ' ', ' ', '\n', '<br>\n')
 
-    def multicolumn(self, list, format, cols=4):
+    def multicolumn(self, list, format):
         """Format a list of items into a multi-column list."""
         result = ''
-        rows = (len(list)+cols-1)//cols
-        for col in range(cols):
-            result = result + '<td width="%d%%" valign=top>' % (100//cols)
+        rows = (len(list) + 3) // 4
+        for col in range(4):
+            result = result + '<td class="multicolumn">'
             for i in range(rows*col, rows*col+rows):
                 if i < len(list):
                     result = result + format(list[i]) + '<br>\n'
             result = result + '</td>'
-        return '<table width="100%%" summary="list"><tr>%s</tr></table>' % result
+        return '<table><tr>%s</tr></table>' % result
 
-    def grey(self, text): return '<font color="#909090">%s</font>' % text
+    def grey(self, text): return '<span class="grey">%s</span>' % text
 
     def namelink(self, name, *dicts):
         """Make a link for an identifier, given name-to-URL mappings."""
@@ -719,14 +718,14 @@ def formattree(self, tree, modname, parent=None):
         for entry in tree:
             if type(entry) is type(()):
                 c, bases = entry
-                result = result + '<dt><font face="helvetica, arial">'
+                result = result + '<dt class="heading-text">'
                 result = result + self.classlink(c, modname)
                 if bases and bases != (parent,):
                     parents = []
                     for base in bases:
                         parents.append(self.classlink(base, modname))
                     result = result + '(' + ', '.join(parents) + ')'
-                result = result + '\n</font></dt>'
+                result = result + '\n</dt>'
             elif type(entry) is type([]):
                 result = result + '<dd>\n%s</dd>\n' % self.formattree(
                     entry, modname, c)
@@ -743,10 +742,10 @@ def docmodule(self, object, name=None, mod=None, *ignored):
         links = []
         for i in range(len(parts)-1):
             links.append(
-                '<a href="%s.html"><font color="#ffffff">%s</font></a>' %
+                '<a href="%s.html" class="white">%s</a>' %
                 ('.'.join(parts[:i+1]), parts[i]))
         linkedname = '.'.join(links + parts[-1:])
-        head = '<big><big><strong>%s</strong></big></big>' % linkedname
+        head = '<strong class="title">%s</strong>' % linkedname
         try:
             path = inspect.getabsfile(object)
             url = urllib.parse.quote(path)
@@ -768,9 +767,7 @@ def docmodule(self, object, name=None, mod=None, *ignored):
             docloc = '<br><a href="%(docloc)s">Module Reference</a>' % locals()
         else:
             docloc = ''
-        result = self.heading(
-            head, '#ffffff', '#7799ee',
-            '<a href=".">index</a><br>' + filelink + docloc)
+        result = self.heading(head, '<a href=".">index</a><br>' + filelink + docloc)
 
         modules = inspect.getmembers(object, inspect.ismodule)
 
@@ -805,7 +802,7 @@ def docmodule(self, object, name=None, mod=None, *ignored):
                 data.append((key, value))
 
         doc = self.markup(getdoc(object), self.preformat, fdict, cdict)
-        doc = doc and '<tt>%s</tt>' % doc
+        doc = doc and '<span class="code">%s</span>' % doc
         result = result + '<p>%s</p>\n' % doc
 
         if hasattr(object, '__path__'):
@@ -815,12 +812,12 @@ def docmodule(self, object, name=None, mod=None, *ignored):
             modpkgs.sort()
             contents = self.multicolumn(modpkgs, self.modpkglink)
             result = result + self.bigsection(
-                'Package Contents', '#ffffff', '#aa55cc', contents)
+                'Package Contents', 'pkg-content', contents)
         elif modules:
             contents = self.multicolumn(
                 modules, lambda t: self.modulelink(t[1]))
             result = result + self.bigsection(
-                'Modules', '#ffffff', '#aa55cc', contents)
+                'Modules', 'pkg-content', contents)
 
         if classes:
             classlist = [value for (key, value) in classes]
@@ -829,27 +826,25 @@ def docmodule(self, object, name=None, mod=None, *ignored):
             for key, value in classes:
                 contents.append(self.document(value, key, name, fdict, cdict))
             result = result + self.bigsection(
-                'Classes', '#ffffff', '#ee77aa', ' '.join(contents))
+                'Classes', 'index', ' '.join(contents))
         if funcs:
             contents = []
             for key, value in funcs:
                 contents.append(self.document(value, key, name, fdict, cdict))
             result = result + self.bigsection(
-                'Functions', '#ffffff', '#eeaa77', ' '.join(contents))
+                'Functions', 'functions', ' '.join(contents))
         if data:
             contents = []
             for key, value in data:
                 contents.append(self.document(value, key))
             result = result + self.bigsection(
-                'Data', '#ffffff', '#55aa55', '<br>\n'.join(contents))
+                'Data', 'data', '<br>\n'.join(contents))
         if hasattr(object, '__author__'):
             contents = self.markup(str(object.__author__), self.preformat)
-            result = result + self.bigsection(
-                'Author', '#ffffff', '#7799ee', contents)
+            result = result + self.bigsection('Author', 'author', contents)
         if hasattr(object, '__credits__'):
             contents = self.markup(str(object.__credits__), self.preformat)
-            result = result + self.bigsection(
-                'Credits', '#ffffff', '#7799ee', contents)
+            result = result + self.bigsection('Credits', 'credits', contents)
 
         return result
 
@@ -923,7 +918,7 @@ def spilldata(msg, attrs, predicate):
                     else:
                         doc = self.markup(getdoc(value), self.preformat,
                                           funcs, classes, mdict)
-                        doc = '<dd><tt>%s</tt>' % doc
+                        doc = '<dd><span class="code">%s</span>' % doc
                         push('<dl><dt>%s%s</dl>\n' % (base, doc))
                     push('\n')
             return attrs
@@ -1011,9 +1006,9 @@ def spilldata(msg, attrs, predicate):
         if decl:
             doc = decl + (doc or '')
         doc = self.markup(doc, self.preformat, funcs, classes, mdict)
-        doc = doc and '<tt>%s<br> </tt>' % doc
+        doc = doc and '<span class="code">%s<br> </span>' % doc
 
-        return self.section(title, '#000000', '#ffc8d8', contents, 3, doc)
+        return self.section(title, 'title', contents, 3, doc)
 
     def formatvalue(self, object):
         """Format an argument default value as text."""
@@ -1074,14 +1069,14 @@ def docroutine(self, object, name=None, mod=None,
             argspec = '(...)'
 
         decl = asyncqualifier + title + self.escape(argspec) + (note and
-               self.grey('<font face="helvetica, arial">%s</font>' % note))
+               self.grey('<span class="heading-text">%s</span>' % note))
 
         if skipdocs:
             return '<dl><dt>%s</dt></dl>\n' % decl
         else:
             doc = self.markup(
                 getdoc(object), self.preformat, funcs, classes, methods)
-            doc = doc and '<dd><tt>%s</tt></dd>' % doc
+            doc = doc and '<dd><span class="code">%s</span></dd>' % doc
             return '<dl><dt>%s</dt>%s</dl>\n' % (decl, doc)
 
     def docdata(self, object, name=None, mod=None, cl=None):
@@ -1093,7 +1088,7 @@ def docdata(self, object, name=None, mod=None, cl=None):
             push('<dl><dt><strong>%s</strong></dt>\n' % name)
         doc = self.markup(getdoc(object), self.preformat)
         if doc:
-            push('<dd><tt>%s</tt></dd>\n' % doc)
+            push('<dd><span class="code">%s</span></dd>\n' % doc)
         push('</dl>\n')
 
         return ''.join(results)
@@ -1118,7 +1113,7 @@ def index(self, dir, shadowed=None):
 
         modpkgs.sort()
         contents = self.multicolumn(modpkgs, self.modpkglink)
-        return self.bigsection(dir, '#ffffff', '#ee77aa', contents)
+        return self.bigsection(dir, 'index', contents)
 
 # -------------------------------------------- text documentation generator
 
@@ -2446,10 +2441,12 @@ def page(self, title, contents):
                 '<link rel="stylesheet" type="text/css" href="%s">' %
                 css_path)
             return '''\
-<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
-<html><head><title>Pydoc: %s</title>
-<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-%s</head><body bgcolor="#f0f0f8">%s<div style="clear:both;padding-top:.5em;">%s</div>
+<!DOCTYPE>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>Pydoc: %s</title>
+%s</head><body>%s<div style="clear:both;padding-top:.5em;">%s</div>
 </body></html>''' % (title, css_link, html_navbar(), contents)
 
 
@@ -2489,22 +2486,21 @@ def bltinlink(name):
             return '<a href="%s.html">%s</a>' % (name, name)
 
         heading = html.heading(
-            '<big><big><strong>Index of Modules</strong></big></big>',
-            '#ffffff', '#7799ee')
+            '<strong class="title">Index of Modules</strong>'
+        )
         names = [name for name in sys.builtin_module_names
                  if name != '__main__']
         contents = html.multicolumn(names, bltinlink)
         contents = [heading, '<p>' + html.bigsection(
-            'Built-in Modules', '#ffffff', '#ee77aa', contents)]
+            'Built-in Modules', 'index', contents)]
 
         seen = {}
         for dir in sys.path:
             contents.append(html.index(dir, seen))
 
         contents.append(
-            '<p align=right><font color="#909090" face="helvetica,'
-            'arial"><strong>pydoc</strong> by Ka-Ping Yee'
-            '<ping at lfw.org></font>')
+            '<p align=right class="heading-text grey"><strong>pydoc</strong> by Ka-Ping Yee'
+            '<ping at lfw.org></p>')
         return 'Index of Modules', ''.join(contents)
 
     def html_search(key):
@@ -2529,12 +2525,12 @@ def bltinlink(name):
 
         results = []
         heading = html.heading(
-            '<big><big><strong>Search Results</strong></big></big>',
-            '#ffffff', '#7799ee')
+            '<strong class="title">Search Results</strong>',
+        )
         for name, desc in search_result:
             results.append(bltinlink(name) + desc)
         contents = heading + html.bigsection(
-            'key = %s' % key, '#ffffff', '#ee77aa', '<br>'.join(results))
+            'key = %s' % key, 'index', '<br>'.join(results))
         return 'Search Results', contents
 
     def html_topics():
@@ -2544,20 +2540,20 @@ def bltinlink(name):
             return '<a href="topic?key=%s">%s</a>' % (name, name)
 
         heading = html.heading(
-            '<big><big><strong>INDEX</strong></big></big>',
-            '#ffffff', '#7799ee')
+            '<strong class="title">INDEX</strong>',
+        )
         names = sorted(Helper.topics.keys())
 
         contents = html.multicolumn(names, bltinlink)
         contents = heading + html.bigsection(
-            'Topics', '#ffffff', '#ee77aa', contents)
+            'Topics', 'index', contents)
         return 'Topics', contents
 
     def html_keywords():
         """Index of keywords."""
         heading = html.heading(
-            '<big><big><strong>INDEX</strong></big></big>',
-            '#ffffff', '#7799ee')
+            '<strong class="title">INDEX</strong>',
+        )
         names = sorted(Helper.keywords.keys())
 
         def bltinlink(name):
@@ -2565,7 +2561,7 @@ def bltinlink(name):
 
         contents = html.multicolumn(names, bltinlink)
         contents = heading + html.bigsection(
-            'Keywords', '#ffffff', '#ee77aa', contents)
+            'Keywords', 'index', contents)
         return 'Keywords', contents
 
     def html_topicpage(topic):
@@ -2578,10 +2574,10 @@ def html_topicpage(topic):
         else:
             title = 'TOPIC'
         heading = html.heading(
-            '<big><big><strong>%s</strong></big></big>' % title,
-            '#ffffff', '#7799ee')
+            '<strong class="title">%s</strong>' % title,
+        )
         contents = '<pre>%s</pre>' % html.markup(contents)
-        contents = html.bigsection(topic , '#ffffff','#ee77aa', contents)
+        contents = html.bigsection(topic , 'index', contents)
         if xrefs:
             xrefs = sorted(xrefs.split())
 
@@ -2589,8 +2585,7 @@ def bltinlink(name):
                 return '<a href="topic?key=%s">%s</a>' % (name, name)
 
             xrefs = html.multicolumn(xrefs, bltinlink)
-            xrefs = html.section('Related help topics: ',
-                                 '#ffffff', '#ee77aa', xrefs)
+            xrefs = html.section('Related help topics: ', 'index', xrefs)
         return ('%s %s' % (title, topic),
                 ''.join((heading, contents, xrefs)))
 
@@ -2604,12 +2599,11 @@ def html_getobj(url):
 
     def html_error(url, exc):
         heading = html.heading(
-            '<big><big><strong>Error</strong></big></big>',
-            '#ffffff', '#7799ee')
+            '<strong class="title">Error</strong>',
+        )
         contents = '<br>'.join(html.escape(line) for line in
                                format_exception_only(type(exc), exc))
-        contents = heading + html.bigsection(url, '#ffffff', '#bb0000',
-                                             contents)
+        contents = heading + html.bigsection(url, 'error', contents)
         return "Error - %s" % url, contents
 
     def get_html_page(url):
diff --git a/Lib/pydoc_data/_pydoc.css b/Lib/pydoc_data/_pydoc.css
index f036ef37a5aba..a6aa2e4c1a021 100644
--- a/Lib/pydoc_data/_pydoc.css
+++ b/Lib/pydoc_data/_pydoc.css
@@ -4,3 +4,109 @@
     Contents of this file are subject to change without notice.
 
 */
+
+body {
+    background-color: #f0f0f8;
+}
+
+table.heading tr {
+    background-color: #7799ee;
+}
+
+.decor {
+    color: #ffffff;
+}
+
+.title-decor {
+    background-color: #ffc8d8;
+    color: #000000;
+}
+
+.pkg-content-decor {
+    background-color: #aa55cc;
+}
+
+.index-decor {
+    background-color: #ee77aa;
+}
+
+.functions-decor {
+    background-color: #eeaa77;
+}
+
+.data-decor {
+    background-color: #55aa55;
+}
+
+.author-decor {
+    background-color: #7799ee;
+}
+
+.credits-decor {
+    background-color: #7799ee;
+}
+
+.error-decor {
+    background-color: #bb0000;
+}
+
+.grey {
+    color: #909090;
+}
+
+.white {
+    color: #ffffff;
+}
+
+.repr {
+    color: #c040c0;
+}
+
+table.heading tr td.title {
+    vertical-align: bottom;
+}
+
+table.heading tr td.extra {
+    vertical-align: bottom;
+    text-align: right;
+}
+
+.heading-text {
+    font-family: helvetica, arial;
+}
+
+.bigsection {
+    font-size: larger;
+}
+
+.title {
+    font-size: x-large;
+}
+
+.code {
+    font-family: monospace;
+}
+
+table {
+    width: 100%;
+    border-spacing : 0;
+    border-collapse : collapse;
+    border: 0;
+}
+
+td {
+    padding: 2;
+}
+
+td.section-title {
+    vertical-align: bottom;
+}
+
+td.multicolumn {
+    width: 25%;
+    vertical-align: bottom;
+}
+
+td.singlecolumn {
+    width: 100%;
+}
diff --git a/Lib/test/test_docxmlrpc.py b/Lib/test/test_docxmlrpc.py
index 7725250288311..9a06be4585502 100644
--- a/Lib/test/test_docxmlrpc.py
+++ b/Lib/test/test_docxmlrpc.py
@@ -90,7 +90,17 @@ def test_valid_get_response(self):
         response = self.client.getresponse()
 
         self.assertEqual(response.status, 200)
-        self.assertEqual(response.getheader("Content-type"), "text/html")
+        self.assertEqual(response.getheader("Content-type"), "text/html; charset=UTF-8")
+
+        # Server raises an exception if we don't start to read the data
+        response.read()
+
+    def test_get_css(self):
+        self.client.request("GET", "/pydoc.css")
+        response = self.client.getresponse()
+
+        self.assertEqual(response.status, 200)
+        self.assertEqual(response.getheader("Content-type"), "text/css; charset=UTF-8")
 
         # Server raises an exception if we don't start to read the data
         response.read()
diff --git a/Lib/test/test_pydoc.py b/Lib/test/test_pydoc.py
index 25ac1fb59d4c4..0a7d72c768424 100644
--- a/Lib/test/test_pydoc.py
+++ b/Lib/test/test_pydoc.py
@@ -132,128 +132,70 @@ class C(builtins.object)
 expected_text_data_docstrings = tuple('\n     |      ' + s if s else ''
                                       for s in expected_data_docstrings)
 
-expected_html_pattern = """
-<table width="100%%" cellspacing=0 cellpadding=2 border=0 summary="heading">
-<tr bgcolor="#7799ee">
-<td valign=bottom> <br>
-<font color="#ffffff" face="helvetica, arial"> <br><big><big><strong><a href="test.html"><font color="#ffffff">test</font></a>.pydoc_mod</strong></big></big> (version 1.2.3.4)</font></td
-><td align=right valign=bottom
-><font color="#ffffff" face="helvetica, arial"><a href=".">index</a><br><a href="file:%s">%s</a>%s</font></td></tr></table>
-    <p><tt>This is a test module for test_pydoc</tt></p>
-<p>
-<table width="100%%" cellspacing=0 cellpadding=2 border=0 summary="section">
-<tr bgcolor="#ee77aa">
-<td colspan=3 valign=bottom> <br>
-<font color="#ffffff" face="helvetica, arial"><big><strong>Classes</strong></big></font></td></tr>
-\x20\x20\x20\x20
-<tr><td bgcolor="#ee77aa"><tt>      </tt></td><td> </td>
-<td width="100%%"><dl>
-<dt><font face="helvetica, arial"><a href="builtins.html#object">builtins.object</a>
-</font></dt><dd>
-<dl>
-<dt><font face="helvetica, arial"><a href="test.pydoc_mod.html#A">A</a>
-</font></dt><dt><font face="helvetica, arial"><a href="test.pydoc_mod.html#B">B</a>
-</font></dt><dt><font face="helvetica, arial"><a href="test.pydoc_mod.html#C">C</a>
-</font></dt></dl>
-</dd>
-</dl>
- <p>
-<table width="100%%" cellspacing=0 cellpadding=2 border=0 summary="section">
-<tr bgcolor="#ffc8d8">
-<td colspan=3 valign=bottom> <br>
-<font color="#000000" face="helvetica, arial"><a name="A">class <strong>A</strong></a>(<a href="builtins.html#object">builtins.object</a>)</font></td></tr>
-\x20\x20\x20\x20
-<tr bgcolor="#ffc8d8"><td rowspan=2><tt>   </tt></td>
-<td colspan=2><tt>Hello and goodbye<br> </tt></td></tr>
-<tr><td> </td>
-<td width="100%%">Methods defined here:<br>
-<dl><dt><a name="A-__init__"><strong>__init__</strong></a>()</dt><dd><tt>Wow, I have no function!</tt></dd></dl>
-
-<hr>
-Data descriptors defined here:<br>
-<dl><dt><strong>__dict__</strong></dt>
-<dd><tt>%s</tt></dd>
-</dl>
-<dl><dt><strong>__weakref__</strong></dt>
-<dd><tt>%s</tt></dd>
-</dl>
-</td></tr></table> <p>
-<table width="100%%" cellspacing=0 cellpadding=2 border=0 summary="section">
-<tr bgcolor="#ffc8d8">
-<td colspan=3 valign=bottom> <br>
-<font color="#000000" face="helvetica, arial"><a name="B">class <strong>B</strong></a>(<a href="builtins.html#object">builtins.object</a>)</font></td></tr>
-\x20\x20\x20\x20
-<tr><td bgcolor="#ffc8d8"><tt>   </tt></td><td> </td>
-<td width="100%%">Data descriptors defined here:<br>
-<dl><dt><strong>__dict__</strong></dt>
-<dd><tt>%s</tt></dd>
-</dl>
-<dl><dt><strong>__weakref__</strong></dt>
-<dd><tt>%s</tt></dd>
-</dl>
-<hr>
-Data and other attributes defined here:<br>
-<dl><dt><strong>NO_MEANING</strong> = 'eggs'</dl>
-
-<dl><dt><strong>__annotations__</strong> = {'NO_MEANING': <class 'str'>}</dl>
-
-</td></tr></table> <p>
-<table width="100%%" cellspacing=0 cellpadding=2 border=0 summary="section">
-<tr bgcolor="#ffc8d8">
-<td colspan=3 valign=bottom> <br>
-<font color="#000000" face="helvetica, arial"><a name="C">class <strong>C</strong></a>(<a href="builtins.html#object">builtins.object</a>)</font></td></tr>
-\x20\x20\x20\x20
-<tr><td bgcolor="#ffc8d8"><tt>   </tt></td><td> </td>
-<td width="100%%">Methods defined here:<br>
-<dl><dt><a name="C-get_answer"><strong>get_answer</strong></a>(self)</dt><dd><tt>Return <a href="#C-say_no">say_no</a>()</tt></dd></dl>
-
-<dl><dt><a name="C-is_it_true"><strong>is_it_true</strong></a>(self)</dt><dd><tt>Return self.<a href="#C-get_answer">get_answer</a>()</tt></dd></dl>
-
-<dl><dt><a name="C-say_no"><strong>say_no</strong></a>(self)</dt></dl>
-
-<hr>
-Data descriptors defined here:<br>
-<dl><dt><strong>__dict__</strong></dt>
-<dd><tt>dictionary for instance variables (if defined)</tt></dd>
-</dl>
-<dl><dt><strong>__weakref__</strong></dt>
-<dd><tt>list of weak references to the object (if defined)</tt></dd>
-</dl>
-</td></tr></table></td></tr></table><p>
-<table width="100%%" cellspacing=0 cellpadding=2 border=0 summary="section">
-<tr bgcolor="#eeaa77">
-<td colspan=3 valign=bottom> <br>
-<font color="#ffffff" face="helvetica, arial"><big><strong>Functions</strong></big></font></td></tr>
-\x20\x20\x20\x20
-<tr><td bgcolor="#eeaa77"><tt>      </tt></td><td> </td>
-<td width="100%%"><dl><dt><a name="-doc_func"><strong>doc_func</strong></a>()</dt><dd><tt>This function solves all of the world's problems:<br>
-hunger<br>
-lack of Python<br>
-war</tt></dd></dl>
- <dl><dt><a name="-nodoc_func"><strong>nodoc_func</strong></a>()</dt></dl>
-</td></tr></table><p>
-<table width="100%%" cellspacing=0 cellpadding=2 border=0 summary="section">
-<tr bgcolor="#55aa55">
-<td colspan=3 valign=bottom> <br>
-<font color="#ffffff" face="helvetica, arial"><big><strong>Data</strong></big></font></td></tr>
-\x20\x20\x20\x20
-<tr><td bgcolor="#55aa55"><tt>      </tt></td><td> </td>
-<td width="100%%"><strong>__xyz__</strong> = 'X, Y and Z'</td></tr></table><p>
-<table width="100%%" cellspacing=0 cellpadding=2 border=0 summary="section">
-<tr bgcolor="#7799ee">
-<td colspan=3 valign=bottom> <br>
-<font color="#ffffff" face="helvetica, arial"><big><strong>Author</strong></big></font></td></tr>
-\x20\x20\x20\x20
-<tr><td bgcolor="#7799ee"><tt>      </tt></td><td> </td>
-<td width="100%%">Benjamin Peterson</td></tr></table><p>
-<table width="100%%" cellspacing=0 cellpadding=2 border=0 summary="section">
-<tr bgcolor="#7799ee">
-<td colspan=3 valign=bottom> <br>
-<font color="#ffffff" face="helvetica, arial"><big><strong>Credits</strong></big></font></td></tr>
-\x20\x20\x20\x20
-<tr><td bgcolor="#7799ee"><tt>      </tt></td><td> </td>
-<td width="100%%">Nobody</td></tr></table>
-""".strip() # ' <- emacs turd
+html2text_of_expected = """
+test.pydoc_mod (version 1.2.3.4)
+This is a test module for test_pydoc
+
+Classes
+    builtins.object
+    A
+    B
+    C
+
+class A(builtins.object)
+    Hello and goodbye
+
+    Methods defined here:
+        __init__()
+            Wow, I have no function!
+
+    Data descriptors defined here:
+        __dict__
+            dictionary for instance variables (if defined)
+        __weakref__
+            list of weak references to the object (if defined)
+
+class B(builtins.object)
+    Data descriptors defined here:
+        __dict__
+            dictionary for instance variables (if defined)
+        __weakref__
+            list of weak references to the object (if defined)
+    Data and other attributes defined here:
+        NO_MEANING = 'eggs'
+        __annotations__ = {'NO_MEANING': <class 'str'>}
+
+
+class C(builtins.object)
+    Methods defined here:
+        get_answer(self)
+            Return say_no()
+        is_it_true(self)
+            Return self.get_answer()
+        say_no(self)
+    Data descriptors defined here:
+        __dict__
+            dictionary for instance variables (if defined)
+        __weakref__
+             list of weak references to the object (if defined)
+
+Functions
+    doc_func()
+        This function solves all of the world's problems:
+        hunger
+        lack of Python
+        war
+    nodoc_func()
+
+Data
+    __xyz__ = 'X, Y and Z'
+
+Author
+    Benjamin Peterson
+
+Credits
+    Nobody
+"""
 
 expected_html_data_docstrings = tuple(s.replace(' ', ' ')
                                       for s in expected_data_docstrings)
@@ -394,6 +336,16 @@ def get_html_title(text):
     return title
 
 
+def html2text(html):
+    """A quick and dirty implementation of html2text.
+
+    Tailored for pydoc tests only.
+    """
+    return pydoc.replace(
+        re.sub("<.*?>", "", html),
+        " ", " ", ">", ">", "<", "<")
+
+
 class PydocBaseTest(unittest.TestCase):
 
     def _restricted_walk_packages(self, walk_packages, path=None):
@@ -434,12 +386,16 @@ class PydocDocTest(unittest.TestCase):
     @requires_docstrings
     def test_html_doc(self):
         result, doc_loc = get_pydoc_html(pydoc_mod)
+        text_result = html2text(result)
+        expected_lines = [line.strip() for line in html2text_of_expected if line]
+        for line in expected_lines:
+            self.assertIn(line, text_result)
         mod_file = inspect.getabsfile(pydoc_mod)
         mod_url = urllib.parse.quote(mod_file)
-        expected_html = expected_html_pattern % (
-                        (mod_url, mod_file, doc_loc) +
-                        expected_html_data_docstrings)
-        self.assertEqual(result, expected_html)
+        self.assertIn(mod_url, result)
+        self.assertIn(mod_file, result)
+        self.assertIn(doc_loc, result)
+
 
     @unittest.skipIf(sys.flags.optimize >= 2,
                      "Docstrings are omitted with -O2 and above")
@@ -845,47 +801,39 @@ class B(A)
 ''' % __name__)
 
         doc = pydoc.render_doc(B, renderer=pydoc.HTMLDoc())
-        self.assertEqual(doc, '''\
-Python Library Documentation: class B in module %s
+        expected_text = """
+Python Library Documentation
 
-<p>
-<table width="100%%" cellspacing=0 cellpadding=2 border=0 summary="section">
-<tr bgcolor="#ffc8d8">
-<td colspan=3 valign=bottom> <br>
-<font color="#000000" face="helvetica, arial"><a name="B">class <strong>B</strong></a>(A)</font></td></tr>
-\x20\x20\x20\x20
-<tr><td bgcolor="#ffc8d8"><tt>   </tt></td><td> </td>
-<td width="100%%"><dl><dt>Method resolution order:</dt>
-<dd>B</dd>
-<dd>A</dd>
-<dd><a href="builtins.html#object">builtins.object</a></dd>
-</dl>
-<hr>
-Methods defined here:<br>
-<dl><dt><a name="B-b_size"><strong>b_size</strong></a> = <a href="#B-a_size">a_size</a>(self)</dt></dl>
-
-<dl><dt><a name="B-itemconfig"><strong>itemconfig</strong></a> = <a href="#B-itemconfigure">itemconfigure</a>(self, tagOrId, cnf=None, **kw)</dt></dl>
-
-<dl><dt><a name="B-itemconfigure"><strong>itemconfigure</strong></a>(self, tagOrId, cnf=None, **kw)</dt><dd><tt>Configure resources of an item TAGORID.</tt></dd></dl>
-
-<hr>
-Methods inherited from A:<br>
-<dl><dt><a name="B-a_size"><strong>a_size</strong></a>(self)</dt><dd><tt>Return size</tt></dd></dl>
-
-<dl><dt><a name="B-lift"><strong>lift</strong></a> = <a href="#B-tkraise">tkraise</a>(self, aboveThis=None)</dt></dl>
-
-<dl><dt><a name="B-tkraise"><strong>tkraise</strong></a>(self, aboveThis=None)</dt><dd><tt>Raise this widget in the stacking order.</tt></dd></dl>
-
-<hr>
-Data descriptors inherited from A:<br>
-<dl><dt><strong>__dict__</strong></dt>
-<dd><tt>dictionary for instance variables (if defined)</tt></dd>
-</dl>
-<dl><dt><strong>__weakref__</strong></dt>
-<dd><tt>list of weak references to the object (if defined)</tt></dd>
-</dl>
-</td></tr></table>\
-''' % __name__)
+class B in module test.test_pydoc
+class B(A)
+    Method resolution order:
+        B
+        A
+        builtins.object
+
+    Methods defined here:
+        b_size = a_size(self)
+        itemconfig = itemconfigure(self, tagOrId, cnf=None, **kw)
+        itemconfigure(self, tagOrId, cnf=None, **kw)
+            Configure resources of an item TAGORID.
+
+    Methods inherited from A:
+        a_size(self)
+            Return size
+        lift = tkraise(self, aboveThis=None)
+        tkraise(self, aboveThis=None)
+            Raise this widget in the stacking order.
+
+    Data descriptors inherited from A:
+        __dict__
+            dictionary for instance variables (if defined)
+        __weakref__
+            list of weak references to the object (if defined)
+"""
+        as_text = html2text(doc)
+        expected_lines = [line.strip() for line in expected_text.split("\n") if line]
+        for expected_line in expected_lines:
+            self.assertIn(expected_line, as_text)
 
 
 class PydocImportTest(PydocBaseTest):
diff --git a/Lib/xmlrpc/server.py b/Lib/xmlrpc/server.py
index 69a260f5b12c0..e22e480a829ff 100644
--- a/Lib/xmlrpc/server.py
+++ b/Lib/xmlrpc/server.py
@@ -440,7 +440,7 @@ class SimpleXMLRPCRequestHandler(BaseHTTPRequestHandler):
 
     # Class attribute listing the accessible path components;
     # paths not on this list will result in a 404 error.
-    rpc_paths = ('/', '/RPC2')
+    rpc_paths = ('/', '/RPC2', '/pydoc.css')
 
     #if not None, encode responses larger than this, if possible
     encode_threshold = 1400 #a common MTU
@@ -801,7 +801,7 @@ def docserver(self, server_name, package_documentation, methods):
 
         server_name = self.escape(server_name)
         head = '<big><big><strong>%s</strong></big></big>' % server_name
-        result = self.heading(head, '#ffffff', '#7799ee')
+        result = self.heading(head)
 
         doc = self.markup(package_documentation, self.preformat, fdict)
         doc = doc and '<tt>%s</tt>' % doc
@@ -812,10 +812,25 @@ def docserver(self, server_name, package_documentation, methods):
         for key, value in method_items:
             contents.append(self.docroutine(value, key, funcs=fdict))
         result = result + self.bigsection(
-            'Methods', '#ffffff', '#eeaa77', ''.join(contents))
+            'Methods', 'functions', ''.join(contents))
 
         return result
 
+
+    def page(self, title, contents):
+        """Format an HTML page."""
+        css_path = "/pydoc.css"
+        css_link = (
+            '<link rel="stylesheet" type="text/css" href="%s">' %
+            css_path)
+        return '''\
+<!DOCTYPE>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>Python: %s</title>
+%s</head><body>%s</body></html>''' % (title, css_link, contents)
+
 class XMLRPCDocGenerator:
     """Generates documentation for an XML-RPC server.
 
@@ -907,6 +922,12 @@ class DocXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
     for documentation.
     """
 
+    def _get_css(self, url):
+        path_here = os.path.dirname(os.path.realpath(__file__))
+        css_path = os.path.join(path_here, "..", "pydoc_data", "_pydoc.css")
+        with open(css_path, mode="rb") as fp:
+            return fp.read()
+
     def do_GET(self):
         """Handles the HTTP GET request.
 
@@ -918,9 +939,15 @@ def do_GET(self):
             self.report_404()
             return
 
-        response = self.server.generate_html_documentation().encode('utf-8')
+        if self.path.endswith('.css'):
+            content_type = 'text/css'
+            response = self._get_css(self.path)
+        else:
+            content_type = 'text/html'
+            response = self.server.generate_html_documentation().encode('utf-8')
+
         self.send_response(200)
-        self.send_header("Content-type", "text/html")
+        self.send_header('Content-Type', '%s; charset=UTF-8' % content_type)
         self.send_header("Content-length", str(len(response)))
         self.end_headers()
         self.wfile.write(response)
diff --git a/Misc/NEWS.d/next/Library/2021-10-08-04-11-55.bpo-10716.QSRVK2.rst b/Misc/NEWS.d/next/Library/2021-10-08-04-11-55.bpo-10716.QSRVK2.rst
new file mode 100644
index 0000000000000..8ec944960211a
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2021-10-08-04-11-55.bpo-10716.QSRVK2.rst
@@ -0,0 +1,3 @@
+Migrated pydoc to HTML5 (without changing the look of it). Side effect is to
+update xmlrpc's ``ServerHTMLDoc`` which now uses the CSS too. cgitb now
+relies less on pydoc (as it can't use the CSS file).



More information about the Python-checkins mailing list