Skip to content

Commit 4156e4e

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 4156e4e

3 files changed

Lines changed: 74 additions & 60 deletions

File tree

Lib/pydoc.py

Lines changed: 70 additions & 59 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

@@ -184,20 +185,9 @@ def _getowndoc(obj):
184185
return None
185186

186187
def _getdoc(object):
187-
"""Get the documentation string for an object.
188-
189-
All tabs are expanded to spaces. To clean up docstrings that are
190-
indented to line up with blocks of code, any whitespace than can be
191-
uniformly removed from the second line onwards is removed."""
192-
doc = _getowndoc(object)
193-
if doc is None:
194-
try:
195-
doc = _finddoc(object)
196-
except (AttributeError, TypeError):
197-
return None
198-
if not isinstance(doc, str):
199-
return None
200-
return inspect.cleandoc(doc)
188+
return inspect.getdoc(object,
189+
fallback_to_class_doc=False,
190+
inherit_class_doc=False)
201191

202192
def getdoc(object):
203193
"""Get the doc string or comments for an object."""
@@ -608,26 +598,26 @@ def repr1(self, x, level):
608598
methodname = 'repr_' + '_'.join(type(x).__name__.split())
609599
if hasattr(self, methodname):
610600
return getattr(self, methodname)(x, level)
611-
return self.escape(cram(stripid(repr(x)), self.maxother))
601+
return html_escape(cram(stripid(repr(x)), self.maxother))
612602

613603
def repr_string(self, x, level):
614604
test = cram(x, self.maxstring)
615605
testrepr = repr(test)
616606
if '\\' in test and '\\' not in replace(testrepr, r'\\', ''):
617607
# Backslashes are only literal in the string and are never
618608
# needed to make any special characters, so show a raw string.
619-
return 'r' + testrepr[0] + self.escape(test) + testrepr[0]
609+
return 'r' + testrepr[0] + html_escape(test) + testrepr[0]
620610
return re.sub(r'((\\[\\abfnrtv\'"]|\\[0-9]..|\\x..|\\u....)+)',
621611
r'<span class="repr">\1</span>',
622-
self.escape(testrepr))
612+
html_escape(testrepr, quote=False))
623613

624614
repr_str = repr_string
625615

626616
def repr_instance(self, x, level):
627617
try:
628-
return self.escape(cram(stripid(repr(x)), self.maxstring))
618+
return html_escape(cram(stripid(repr(x)), self.maxstring))
629619
except:
630-
return self.escape('<%s instance>' % x.__class__.__name__)
620+
return html_escape('<%s instance>' % x.__class__.__name__)
631621

632622
repr_unicode = repr_string
633623

@@ -689,7 +679,7 @@ def bigsection(self, title, *args):
689679

690680
def preformat(self, text):
691681
"""Format literal preformatted text."""
692-
text = self.escape(text.expandtabs())
682+
text = html_escape(text.expandtabs(), quote=False)
693683
return replace(text, '\n\n', '\n \n', '\n\n', '\n \n',
694684
' ', '&nbsp;', '\n', '<br>\n')
695685

@@ -767,7 +757,7 @@ def filelink(self, url, path):
767757
def markup(self, text, escape=None, funcs={}, classes={}, methods={}):
768758
"""Mark up some plain text, given a context of symbols to look for.
769759
Each context dictionary maps object names to anchor names."""
770-
escape = escape or self.escape
760+
escape = escape or html_escape
771761
results = []
772762
here = 0
773763
pattern = re.compile(r'\b((http|https|ftp)://\S+[\w/]|'
@@ -850,9 +840,9 @@ def docmodule(self, object, name=None, mod=None, *ignored):
850840
version = str(object.__version__)
851841
if version[:11] == '$' + 'Revision: ' and version[-1:] == '$':
852842
version = version[11:-1].strip()
853-
info.append('version %s' % self.escape(version))
843+
info.append('version %s' % html_escape(version))
854844
if hasattr(object, '__date__'):
855-
info.append(self.escape(str(object.__date__)))
845+
info.append(html_escape(str(object.__date__)))
856846
if info:
857847
head = head + ' (%s)' % ', '.join(info)
858848
docloc = self.getdocloc(object)
@@ -2212,6 +2202,11 @@ def showsymbol(self, symbol):
22122202
topic, _, xrefs = target.partition(' ')
22132203
self.showtopic(topic, xrefs)
22142204

2205+
def _getsymbol(self, symbol):
2206+
target = self.symbols[symbol]
2207+
topic, _, xrefs = target.partition(' ')
2208+
return self._gettopic(topic, xrefs)
2209+
22152210
def listmodules(self, key=''):
22162211
if key:
22172212
self.output.write('''
@@ -2377,6 +2372,7 @@ def _start_server(urlhandler, hostname, port):
23772372
import email.message
23782373
import select
23792374
import threading
2375+
from urllib.parse import unquote
23802376

23812377
class DocHandler(http.server.BaseHTTPRequestHandler):
23822378

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

2495+
def filelink(self, url, path):
2496+
return ('<a href="getfile?key=%s">%s</a>' %
2497+
(html_escape(url), html_escape(path)))
24992498

25002499
html = _HTMLDoc()
25012500

25022501
def html_navbar():
2503-
version = html.escape("%s [%s, %s]" % (platform.python_version(),
2502+
version = html_escape("%s [%s, %s]" % (platform.python_version(),
25042503
platform.python_build()[0],
25052504
platform.python_compiler()))
25062505
return """
@@ -2512,6 +2511,7 @@ def html_navbar():
25122511
<a href="index.html">Module Index</a>
25132512
: <a href="topics.html">Topics</a>
25142513
: <a href="keywords.html">Keywords</a>
2514+
: <a href="symbols.html">Symbols</a>
25152515
</div>
25162516
<div>
25172517
<form action="get" style='display:inline;'>
@@ -2524,7 +2524,7 @@ def html_navbar():
25242524
</form>
25252525
</div>
25262526
</div>
2527-
""" % (version, html.escape(platform.platform(terse=True)))
2527+
""" % (version, html_escape(platform.platform(terse=True)))
25282528

25292529
def html_index():
25302530
"""Module Index page."""
@@ -2580,7 +2580,20 @@ def bltinlink(name):
25802580
'key = %s' % key, 'index', '<br>'.join(results))
25812581
return 'Search Results', contents
25822582

2583-
def html_topics():
2583+
def html_getfile(path):
2584+
"""Get and display a source file listing safely."""
2585+
path = urllib.parse.unquote(path)
2586+
with tokenize.open(path) as fp:
2587+
lines = html_escape(fp.read())
2588+
body = '<pre>%s</pre>' % lines
2589+
heading = html.heading(
2590+
'<strong class="title">File Listing</strong>',
2591+
)
2592+
contents = heading + html.bigsection(
2593+
'File: %s' % path, 'index', body)
2594+
return 'getfile %s' % path, contents
2595+
2596+
def html_topicindex(title):
25842597
"""Index of topic texts available."""
25852598

25862599
def bltinlink(name):
@@ -2589,51 +2602,48 @@ def bltinlink(name):
25892602
heading = html.heading(
25902603
'<strong class="title">INDEX</strong>',
25912604
)
2592-
names = sorted(Helper.topics.keys())
25932605

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)
2606+
keys = {
2607+
'topics': Helper.topics.keys,
2608+
'keywords': Helper.keywords.keys,
2609+
'symbols': Helper.symbols.keys,
2610+
}
2611+
names = sorted(keys[title]())
26082612

26092613
contents = html.multicolumn(names, bltinlink)
26102614
contents = heading + html.bigsection(
2611-
'Keywords', 'index', contents)
2612-
return 'Keywords', contents
2615+
title.capitalize(), 'index', contents)
2616+
return title.capitalize(), contents
26132617

26142618
def html_topicpage(topic):
26152619
"""Topic or keyword help page."""
26162620
buf = io.StringIO()
26172621
htmlhelp = Helper(buf, buf)
2618-
contents, xrefs = htmlhelp._gettopic(topic)
26192622
if topic in htmlhelp.keywords:
26202623
title = 'KEYWORD'
2621-
else:
2624+
contents, xrefs = htmlhelp._gettopic(topic)
2625+
elif topic in htmlhelp.topics:
26222626
title = 'TOPIC'
2627+
contents, xrefs = htmlhelp._gettopic(topic)
2628+
elif topic in htmlhelp.symbols:
2629+
title = 'SYMBOL'
2630+
contents, xrefs = htmlhelp._getsymbol(topic)
2631+
else:
2632+
raise ValueError(f'could not find topic {topic!r}')
26232633
heading = html.heading(
2624-
'<strong class="title">%s</strong>' % title,
2634+
f'<strong class="title">{title}</strong>',
26252635
)
2626-
contents = '<pre>%s</pre>' % html.markup(contents)
2636+
contents = f'<pre>{html.markup(contents)}</pre>'
26272637
contents = html.bigsection(topic , 'index', contents)
26282638
if xrefs:
26292639
xrefs = sorted(xrefs.split())
26302640

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

26342644
xrefs = html.multicolumn(xrefs, bltinlink)
26352645
xrefs = html.section('Related help topics: ', 'index', xrefs)
2636-
return ('%s %s' % (title, topic),
2646+
return (f'{title} {topic}',
26372647
''.join((heading, contents, xrefs)))
26382648

26392649
def html_getobj(url):
@@ -2648,7 +2658,7 @@ def html_error(url, exc):
26482658
heading = html.heading(
26492659
'<strong class="title">Error</strong>',
26502660
)
2651-
contents = '<br>'.join(html.escape(line) for line in
2661+
contents = '<br>'.join(html_escape(line) for line in
26522662
format_exception_only(type(exc), exc))
26532663
contents = heading + html.bigsection(url, 'error', contents)
26542664
return "Error - %s" % url, contents
@@ -2661,21 +2671,21 @@ def get_html_page(url):
26612671
try:
26622672
if url in ("", "index"):
26632673
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":
2674+
elif url in ("topics", "keywords", "symbols"):
2675+
title, content = html_topicindex(url)
2676+
elif '?key=' in url:
2677+
op, _, url = url.partition('?key=')
2678+
if op == "search":
26712679
title, content = html_search(url)
2672-
elif op == "topic?key":
2680+
elif op == "getfile":
2681+
title, content = html_getfile(url)
2682+
elif op == "topic":
26732683
# try topics first, then objects.
26742684
try:
26752685
title, content = html_topicpage(url)
26762686
except ValueError:
26772687
title, content = html_getobj(url)
2678-
elif op == "get?key":
2688+
elif op == "get":
26792689
# try objects first, then topics.
26802690
if url in ("", "index"):
26812691
title, content = html_index()
@@ -2870,5 +2880,6 @@ class BadUsage(Exception): pass
28702880
it names a directory, documentation is written for all the contents.
28712881
""".format(cmd=cmd, sep=os.sep))
28722882

2883+
28732884
if __name__ == '__main__':
28742885
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)