Skip to content

Commit 4adc99e

Browse files
feat: Add 'symbols' link to pydoc's html menu bar
- Added 'symbols' navigation link alongside 'topics' and 'keywords' - Refactored html_topicindex() to handle topics, keywords, and symbols - Added _getsymbol() method to Helper class for symbol lookup - Restored escape() method in HTMLRepr and HTMLDoc classes - Updated test suite to include symbols link verification - Modernized string formatting using f-strings Original patch by Ron Adam. Co-authored-by: Ron Adam <ron3200@gmail.com>
1 parent f575dd9 commit 4adc99e

File tree

3 files changed

+71
-46
lines changed

3 files changed

+71
-46
lines changed

Lib/pydoc.py

Lines changed: 67 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ class or function within a module or module in a package. If the
7575
import warnings
7676
from annotationlib import Format
7777
from collections import deque
78+
from html import escape as html_escape
7879
from reprlib import Repr
7980
from traceback import format_exception_only
8081

@@ -608,26 +609,26 @@ def repr1(self, x, level):
608609
methodname = 'repr_' + '_'.join(type(x).__name__.split())
609610
if hasattr(self, methodname):
610611
return getattr(self, methodname)(x, level)
611-
return self.escape(cram(stripid(repr(x)), self.maxother))
612+
return html_escape(cram(stripid(repr(x)), self.maxother))
612613

613614
def repr_string(self, x, level):
614615
test = cram(x, self.maxstring)
615616
testrepr = repr(test)
616617
if '\\' in test and '\\' not in replace(testrepr, r'\\', ''):
617618
# Backslashes are only literal in the string and are never
618619
# needed to make any special characters, so show a raw string.
619-
return 'r' + testrepr[0] + self.escape(test) + testrepr[0]
620+
return 'r' + testrepr[0] + html_escape(test) + testrepr[0]
620621
return re.sub(r'((\\[\\abfnrtv\'"]|\\[0-9]..|\\x..|\\u....)+)',
621622
r'<span class="repr">\1</span>',
622-
self.escape(testrepr))
623+
html_escape(testrepr, quote=False))
623624

624625
repr_str = repr_string
625626

626627
def repr_instance(self, x, level):
627628
try:
628-
return self.escape(cram(stripid(repr(x)), self.maxstring))
629+
return html_escape(cram(stripid(repr(x)), self.maxstring))
629630
except:
630-
return self.escape('<%s instance>' % x.__class__.__name__)
631+
return html_escape('<%s instance>' % x.__class__.__name__)
631632

632633
repr_unicode = repr_string
633634

@@ -689,7 +690,7 @@ def bigsection(self, title, *args):
689690

690691
def preformat(self, text):
691692
"""Format literal preformatted text."""
692-
text = self.escape(text.expandtabs())
693+
text = html_escape(text.expandtabs(), quote=False)
693694
return replace(text, '\n\n', '\n \n', '\n\n', '\n \n',
694695
' ', '&nbsp;', '\n', '<br>\n')
695696

@@ -767,7 +768,7 @@ def filelink(self, url, path):
767768
def markup(self, text, escape=None, funcs={}, classes={}, methods={}):
768769
"""Mark up some plain text, given a context of symbols to look for.
769770
Each context dictionary maps object names to anchor names."""
770-
escape = escape or self.escape
771+
escape = escape or html_escape
771772
results = []
772773
here = 0
773774
pattern = re.compile(r'\b((http|https|ftp)://\S+[\w/]|'
@@ -850,9 +851,9 @@ def docmodule(self, object, name=None, mod=None, *ignored):
850851
version = str(object.__version__)
851852
if version[:11] == '$' + 'Revision: ' and version[-1:] == '$':
852853
version = version[11:-1].strip()
853-
info.append('version %s' % self.escape(version))
854+
info.append('version %s' % html_escape(version))
854855
if hasattr(object, '__date__'):
855-
info.append(self.escape(str(object.__date__)))
856+
info.append(html_escape(str(object.__date__)))
856857
if info:
857858
head = head + ' (%s)' % ', '.join(info)
858859
docloc = self.getdocloc(object)
@@ -2212,6 +2213,11 @@ def showsymbol(self, symbol):
22122213
topic, _, xrefs = target.partition(' ')
22132214
self.showtopic(topic, xrefs)
22142215

2216+
def _getsymbol(self, symbol):
2217+
target = self.symbols[symbol]
2218+
topic, _, xrefs = target.partition(' ')
2219+
return self._gettopic(topic, xrefs)
2220+
22152221
def listmodules(self, key=''):
22162222
if key:
22172223
self.output.write('''
@@ -2377,6 +2383,7 @@ def _start_server(urlhandler, hostname, port):
23772383
import email.message
23782384
import select
23792385
import threading
2386+
from urllib.parse import unquote
23802387

23812388
class DocHandler(http.server.BaseHTTPRequestHandler):
23822389

@@ -2496,11 +2503,14 @@ def page(self, title, contents):
24962503
%s</head><body>%s<div style="clear:both;padding-top:.5em;">%s</div>
24972504
</body></html>''' % (title, css_link, html_navbar(), contents)
24982505

2506+
def filelink(self, url, path):
2507+
return ('<a href="getfile?key=%s">%s</a>' %
2508+
(html_escape(url), html_escape(path)))
24992509

25002510
html = _HTMLDoc()
25012511

25022512
def html_navbar():
2503-
version = html.escape("%s [%s, %s]" % (platform.python_version(),
2513+
version = html_escape("%s [%s, %s]" % (platform.python_version(),
25042514
platform.python_build()[0],
25052515
platform.python_compiler()))
25062516
return """
@@ -2512,6 +2522,7 @@ def html_navbar():
25122522
<a href="index.html">Module Index</a>
25132523
: <a href="topics.html">Topics</a>
25142524
: <a href="keywords.html">Keywords</a>
2525+
: <a href="symbols.html">Symbols</a>
25152526
</div>
25162527
<div>
25172528
<form action="get" style='display:inline;'>
@@ -2524,7 +2535,7 @@ def html_navbar():
25242535
</form>
25252536
</div>
25262537
</div>
2527-
""" % (version, html.escape(platform.platform(terse=True)))
2538+
""" % (version, html_escape(platform.platform(terse=True)))
25282539

25292540
def html_index():
25302541
"""Module Index page."""
@@ -2580,7 +2591,20 @@ def bltinlink(name):
25802591
'key = %s' % key, 'index', '<br>'.join(results))
25812592
return 'Search Results', contents
25822593

2583-
def html_topics():
2594+
def html_getfile(path):
2595+
"""Get and display a source file listing safely."""
2596+
path = urllib.parse.unquote(path)
2597+
with tokenize.open(path) as fp:
2598+
lines = html_escape(fp.read())
2599+
body = '<pre>%s</pre>' % lines
2600+
heading = html.heading(
2601+
'<strong class="title">File Listing</strong>',
2602+
)
2603+
contents = heading + html.bigsection(
2604+
'File: %s' % path, 'index', body)
2605+
return 'getfile %s' % path, contents
2606+
2607+
def html_topicindex(title):
25842608
"""Index of topic texts available."""
25852609

25862610
def bltinlink(name):
@@ -2589,51 +2613,48 @@ def bltinlink(name):
25892613
heading = html.heading(
25902614
'<strong class="title">INDEX</strong>',
25912615
)
2592-
names = sorted(Helper.topics.keys())
25932616

2594-
contents = html.multicolumn(names, bltinlink)
2595-
contents = heading + html.bigsection(
2596-
'Topics', 'index', contents)
2597-
return 'Topics', contents
2598-
2599-
def html_keywords():
2600-
"""Index of keywords."""
2601-
heading = html.heading(
2602-
'<strong class="title">INDEX</strong>',
2603-
)
2604-
names = sorted(Helper.keywords.keys())
2605-
2606-
def bltinlink(name):
2607-
return '<a href="topic?key=%s">%s</a>' % (name, name)
2617+
keys = {
2618+
'topics': Helper.topics.keys,
2619+
'keywords': Helper.keywords.keys,
2620+
'symbols': Helper.symbols.keys,
2621+
}
2622+
names = sorted(keys[title]())
26082623

26092624
contents = html.multicolumn(names, bltinlink)
26102625
contents = heading + html.bigsection(
2611-
'Keywords', 'index', contents)
2612-
return 'Keywords', contents
2626+
title.capitalize(), 'index', contents)
2627+
return title.capitalize(), contents
26132628

26142629
def html_topicpage(topic):
26152630
"""Topic or keyword help page."""
26162631
buf = io.StringIO()
26172632
htmlhelp = Helper(buf, buf)
2618-
contents, xrefs = htmlhelp._gettopic(topic)
26192633
if topic in htmlhelp.keywords:
26202634
title = 'KEYWORD'
2621-
else:
2635+
contents, xrefs = htmlhelp._gettopic(topic)
2636+
elif topic in htmlhelp.topics:
26222637
title = 'TOPIC'
2638+
contents, xrefs = htmlhelp._gettopic(topic)
2639+
elif topic in htmlhelp.symbols:
2640+
title = 'SYMBOL'
2641+
contents, xrefs = htmlhelp._getsymbol(topic)
2642+
else:
2643+
raise ValueError(f'could not find topic {topic!r}')
26232644
heading = html.heading(
2624-
'<strong class="title">%s</strong>' % title,
2645+
f'<strong class="title">{title}</strong>',
26252646
)
2626-
contents = '<pre>%s</pre>' % html.markup(contents)
2647+
contents = f'<pre>{html.markup(contents)}</pre>'
26272648
contents = html.bigsection(topic , 'index', contents)
26282649
if xrefs:
26292650
xrefs = sorted(xrefs.split())
26302651

26312652
def bltinlink(name):
2632-
return '<a href="topic?key=%s">%s</a>' % (name, name)
2653+
return f'<a href="topic?key={html_escape(name)}>{html_escape(name)}</a>'
26332654

26342655
xrefs = html.multicolumn(xrefs, bltinlink)
26352656
xrefs = html.section('Related help topics: ', 'index', xrefs)
2636-
return ('%s %s' % (title, topic),
2657+
return (f'{title} {topic}',
26372658
''.join((heading, contents, xrefs)))
26382659

26392660
def html_getobj(url):
@@ -2648,7 +2669,7 @@ def html_error(url, exc):
26482669
heading = html.heading(
26492670
'<strong class="title">Error</strong>',
26502671
)
2651-
contents = '<br>'.join(html.escape(line) for line in
2672+
contents = '<br>'.join(html_escape(line) for line in
26522673
format_exception_only(type(exc), exc))
26532674
contents = heading + html.bigsection(url, 'error', contents)
26542675
return "Error - %s" % url, contents
@@ -2661,21 +2682,21 @@ def get_html_page(url):
26612682
try:
26622683
if url in ("", "index"):
26632684
title, content = html_index()
2664-
elif url == "topics":
2665-
title, content = html_topics()
2666-
elif url == "keywords":
2667-
title, content = html_keywords()
2668-
elif '=' in url:
2669-
op, _, url = url.partition('=')
2670-
if op == "search?key":
2685+
elif url in ("topics", "keywords", "symbols"):
2686+
title, content = html_topicindex(url)
2687+
elif '?key=' in url:
2688+
op, _, url = url.partition('?key=')
2689+
if op == "search":
26712690
title, content = html_search(url)
2672-
elif op == "topic?key":
2691+
elif op == "getfile":
2692+
title, content = html_getfile(url)
2693+
elif op == "topic":
26732694
# try topics first, then objects.
26742695
try:
26752696
title, content = html_topicpage(url)
26762697
except ValueError:
26772698
title, content = html_getobj(url)
2678-
elif op == "get?key":
2699+
elif op == "get":
26792700
# try objects first, then topics.
26802701
if url in ("", "index"):
26812702
title, content = html_index()
@@ -2870,5 +2891,6 @@ class BadUsage(Exception): pass
28702891
it names a directory, documentation is written for all the contents.
28712892
""".format(cmd=cmd, sep=os.sep))
28722893

2894+
28732895
if __name__ == '__main__':
28742896
cli()

Lib/test/test_pydoc/test_pydoc.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1413,7 +1413,7 @@ def test_special_form(self):
14131413
self.assertIn('NoReturn = typing.NoReturn', doc)
14141414
self.assertIn(typing.NoReturn.__doc__.strip().splitlines()[0], doc)
14151415
else:
1416-
self.assertIn('NoReturn = class _SpecialForm(_Final)', doc)
1416+
self.assertIn('NoReturn = class _SpecialForm(_Final, _NotIterable)', doc)
14171417

14181418
def test_typing_pydoc(self):
14191419
def foo(data: typing.List[typing.Any],
@@ -2102,6 +2102,7 @@ def test_url_requests(self):
21022102
("index", "Pydoc: Index of Modules"),
21032103
("topics", "Pydoc: Topics"),
21042104
("keywords", "Pydoc: Keywords"),
2105+
("symbols", "Pydoc: Symbols"),
21052106
("pydoc", "Pydoc: module pydoc"),
21062107
("get?key=pydoc", "Pydoc: module pydoc"),
21072108
("search?key=pydoc", "Pydoc: Search Results"),
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add 'symbols' link to pydoc's html menu bar. Original patch by Ron Adam.
2+
Enhanced by Sanyam Khurana.

0 commit comments

Comments
 (0)