Skip to content

Commit 90614a6

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 90614a6

3 files changed

Lines changed: 74 additions & 135 deletions

File tree

Lib/pydoc.py

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

@@ -108,96 +109,10 @@ def pathdirs():
108109
normdirs.append(normdir)
109110
return dirs
110111

111-
def _findclass(func):
112-
cls = sys.modules.get(func.__module__)
113-
if cls is None:
114-
return None
115-
for name in func.__qualname__.split('.')[:-1]:
116-
cls = getattr(cls, name)
117-
if not inspect.isclass(cls):
118-
return None
119-
return cls
120-
121-
def _finddoc(obj):
122-
if inspect.ismethod(obj):
123-
name = obj.__func__.__name__
124-
self = obj.__self__
125-
if (inspect.isclass(self) and
126-
getattr(getattr(self, name, None), '__func__') is obj.__func__):
127-
# classmethod
128-
cls = self
129-
else:
130-
cls = self.__class__
131-
elif inspect.isfunction(obj):
132-
name = obj.__name__
133-
cls = _findclass(obj)
134-
if cls is None or getattr(cls, name) is not obj:
135-
return None
136-
elif inspect.isbuiltin(obj):
137-
name = obj.__name__
138-
self = obj.__self__
139-
if (inspect.isclass(self) and
140-
self.__qualname__ + '.' + name == obj.__qualname__):
141-
# classmethod
142-
cls = self
143-
else:
144-
cls = self.__class__
145-
# Should be tested before isdatadescriptor().
146-
elif isinstance(obj, property):
147-
name = obj.__name__
148-
cls = _findclass(obj.fget)
149-
if cls is None or getattr(cls, name) is not obj:
150-
return None
151-
elif inspect.ismethoddescriptor(obj) or inspect.isdatadescriptor(obj):
152-
name = obj.__name__
153-
cls = obj.__objclass__
154-
if getattr(cls, name) is not obj:
155-
return None
156-
if inspect.ismemberdescriptor(obj):
157-
slots = getattr(cls, '__slots__', None)
158-
if isinstance(slots, dict) and name in slots:
159-
return slots[name]
160-
else:
161-
return None
162-
for base in cls.__mro__:
163-
try:
164-
doc = _getowndoc(getattr(base, name))
165-
except AttributeError:
166-
continue
167-
if doc is not None:
168-
return doc
169-
return None
170-
171-
def _getowndoc(obj):
172-
"""Get the documentation string for an object if it is not
173-
inherited from its class."""
174-
try:
175-
doc = object.__getattribute__(obj, '__doc__')
176-
if doc is None:
177-
return None
178-
if obj is not type:
179-
typedoc = type(obj).__doc__
180-
if isinstance(typedoc, str) and typedoc == doc:
181-
return None
182-
return doc
183-
except AttributeError:
184-
return None
185-
186112
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)
113+
return inspect.getdoc(object,
114+
fallback_to_class_doc=False,
115+
inherit_class_doc=False)
201116

202117
def getdoc(object):
203118
"""Get the doc string or comments for an object."""
@@ -608,26 +523,26 @@ def repr1(self, x, level):
608523
methodname = 'repr_' + '_'.join(type(x).__name__.split())
609524
if hasattr(self, methodname):
610525
return getattr(self, methodname)(x, level)
611-
return self.escape(cram(stripid(repr(x)), self.maxother))
526+
return html_escape(cram(stripid(repr(x)), self.maxother))
612527

613528
def repr_string(self, x, level):
614529
test = cram(x, self.maxstring)
615530
testrepr = repr(test)
616531
if '\\' in test and '\\' not in replace(testrepr, r'\\', ''):
617532
# Backslashes are only literal in the string and are never
618533
# needed to make any special characters, so show a raw string.
619-
return 'r' + testrepr[0] + self.escape(test) + testrepr[0]
534+
return 'r' + testrepr[0] + html_escape(test) + testrepr[0]
620535
return re.sub(r'((\\[\\abfnrtv\'"]|\\[0-9]..|\\x..|\\u....)+)',
621536
r'<span class="repr">\1</span>',
622-
self.escape(testrepr))
537+
html_escape(testrepr, quote=False))
623538

624539
repr_str = repr_string
625540

626541
def repr_instance(self, x, level):
627542
try:
628-
return self.escape(cram(stripid(repr(x)), self.maxstring))
543+
return html_escape(cram(stripid(repr(x)), self.maxstring))
629544
except:
630-
return self.escape('<%s instance>' % x.__class__.__name__)
545+
return html_escape('<%s instance>' % x.__class__.__name__)
631546

632547
repr_unicode = repr_string
633548

@@ -689,7 +604,7 @@ def bigsection(self, title, *args):
689604

690605
def preformat(self, text):
691606
"""Format literal preformatted text."""
692-
text = self.escape(text.expandtabs())
607+
text = html_escape(text.expandtabs(), quote=False)
693608
return replace(text, '\n\n', '\n \n', '\n\n', '\n \n',
694609
' ', '&nbsp;', '\n', '<br>\n')
695610

@@ -767,7 +682,7 @@ def filelink(self, url, path):
767682
def markup(self, text, escape=None, funcs={}, classes={}, methods={}):
768683
"""Mark up some plain text, given a context of symbols to look for.
769684
Each context dictionary maps object names to anchor names."""
770-
escape = escape or self.escape
685+
escape = escape or html_escape
771686
results = []
772687
here = 0
773688
pattern = re.compile(r'\b((http|https|ftp)://\S+[\w/]|'
@@ -850,9 +765,9 @@ def docmodule(self, object, name=None, mod=None, *ignored):
850765
version = str(object.__version__)
851766
if version[:11] == '$' + 'Revision: ' and version[-1:] == '$':
852767
version = version[11:-1].strip()
853-
info.append('version %s' % self.escape(version))
768+
info.append('version %s' % html_escape(version))
854769
if hasattr(object, '__date__'):
855-
info.append(self.escape(str(object.__date__)))
770+
info.append(html_escape(str(object.__date__)))
856771
if info:
857772
head = head + ' (%s)' % ', '.join(info)
858773
docloc = self.getdocloc(object)
@@ -2212,6 +2127,11 @@ def showsymbol(self, symbol):
22122127
topic, _, xrefs = target.partition(' ')
22132128
self.showtopic(topic, xrefs)
22142129

2130+
def _getsymbol(self, symbol):
2131+
target = self.symbols[symbol]
2132+
topic, _, xrefs = target.partition(' ')
2133+
return self._gettopic(topic, xrefs)
2134+
22152135
def listmodules(self, key=''):
22162136
if key:
22172137
self.output.write('''
@@ -2377,6 +2297,7 @@ def _start_server(urlhandler, hostname, port):
23772297
import email.message
23782298
import select
23792299
import threading
2300+
from urllib.parse import unquote
23802301

23812302
class DocHandler(http.server.BaseHTTPRequestHandler):
23822303

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

2420+
def filelink(self, url, path):
2421+
return ('<a href="getfile?key=%s">%s</a>' %
2422+
(html_escape(url), html_escape(path)))
24992423

25002424
html = _HTMLDoc()
25012425

25022426
def html_navbar():
2503-
version = html.escape("%s [%s, %s]" % (platform.python_version(),
2427+
version = html_escape("%s [%s, %s]" % (platform.python_version(),
25042428
platform.python_build()[0],
25052429
platform.python_compiler()))
25062430
return """
@@ -2512,6 +2436,7 @@ def html_navbar():
25122436
<a href="index.html">Module Index</a>
25132437
: <a href="topics.html">Topics</a>
25142438
: <a href="keywords.html">Keywords</a>
2439+
: <a href="symbols.html">Symbols</a>
25152440
</div>
25162441
<div>
25172442
<form action="get" style='display:inline;'>
@@ -2524,7 +2449,7 @@ def html_navbar():
25242449
</form>
25252450
</div>
25262451
</div>
2527-
""" % (version, html.escape(platform.platform(terse=True)))
2452+
""" % (version, html_escape(platform.platform(terse=True)))
25282453

25292454
def html_index():
25302455
"""Module Index page."""
@@ -2580,7 +2505,20 @@ def bltinlink(name):
25802505
'key = %s' % key, 'index', '<br>'.join(results))
25812506
return 'Search Results', contents
25822507

2583-
def html_topics():
2508+
def html_getfile(path):
2509+
"""Get and display a source file listing safely."""
2510+
path = urllib.parse.unquote(path)
2511+
with tokenize.open(path) as fp:
2512+
lines = html_escape(fp.read())
2513+
body = '<pre>%s</pre>' % lines
2514+
heading = html.heading(
2515+
'<strong class="title">File Listing</strong>',
2516+
)
2517+
contents = heading + html.bigsection(
2518+
'File: %s' % path, 'index', body)
2519+
return 'getfile %s' % path, contents
2520+
2521+
def html_topicindex(title):
25842522
"""Index of topic texts available."""
25852523

25862524
def bltinlink(name):
@@ -2589,51 +2527,48 @@ def bltinlink(name):
25892527
heading = html.heading(
25902528
'<strong class="title">INDEX</strong>',
25912529
)
2592-
names = sorted(Helper.topics.keys())
2593-
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())
26052530

2606-
def bltinlink(name):
2607-
return '<a href="topic?key=%s">%s</a>' % (name, name)
2531+
keys = {
2532+
'topics': Helper.topics.keys,
2533+
'keywords': Helper.keywords.keys,
2534+
'symbols': Helper.symbols.keys,
2535+
}
2536+
names = sorted(keys[title]())
26082537

26092538
contents = html.multicolumn(names, bltinlink)
26102539
contents = heading + html.bigsection(
2611-
'Keywords', 'index', contents)
2612-
return 'Keywords', contents
2540+
title.capitalize(), 'index', contents)
2541+
return title.capitalize(), contents
26132542

26142543
def html_topicpage(topic):
26152544
"""Topic or keyword help page."""
26162545
buf = io.StringIO()
26172546
htmlhelp = Helper(buf, buf)
2618-
contents, xrefs = htmlhelp._gettopic(topic)
26192547
if topic in htmlhelp.keywords:
26202548
title = 'KEYWORD'
2621-
else:
2549+
contents, xrefs = htmlhelp._gettopic(topic)
2550+
elif topic in htmlhelp.topics:
26222551
title = 'TOPIC'
2552+
contents, xrefs = htmlhelp._gettopic(topic)
2553+
elif topic in htmlhelp.symbols:
2554+
title = 'SYMBOL'
2555+
contents, xrefs = htmlhelp._getsymbol(topic)
2556+
else:
2557+
raise ValueError(f'could not find topic {topic!r}')
26232558
heading = html.heading(
2624-
'<strong class="title">%s</strong>' % title,
2559+
f'<strong class="title">{title}</strong>',
26252560
)
2626-
contents = '<pre>%s</pre>' % html.markup(contents)
2561+
contents = f'<pre>{html.markup(contents)}</pre>'
26272562
contents = html.bigsection(topic , 'index', contents)
26282563
if xrefs:
26292564
xrefs = sorted(xrefs.split())
26302565

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

26342569
xrefs = html.multicolumn(xrefs, bltinlink)
26352570
xrefs = html.section('Related help topics: ', 'index', xrefs)
2636-
return ('%s %s' % (title, topic),
2571+
return (f'{title} {topic}',
26372572
''.join((heading, contents, xrefs)))
26382573

26392574
def html_getobj(url):
@@ -2648,7 +2583,7 @@ def html_error(url, exc):
26482583
heading = html.heading(
26492584
'<strong class="title">Error</strong>',
26502585
)
2651-
contents = '<br>'.join(html.escape(line) for line in
2586+
contents = '<br>'.join(html_escape(line) for line in
26522587
format_exception_only(type(exc), exc))
26532588
contents = heading + html.bigsection(url, 'error', contents)
26542589
return "Error - %s" % url, contents
@@ -2661,21 +2596,21 @@ def get_html_page(url):
26612596
try:
26622597
if url in ("", "index"):
26632598
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":
2599+
elif url in ("topics", "keywords", "symbols"):
2600+
title, content = html_topicindex(url)
2601+
elif '?key=' in url:
2602+
op, _, url = url.partition('?key=')
2603+
if op == "search":
26712604
title, content = html_search(url)
2672-
elif op == "topic?key":
2605+
elif op == "getfile":
2606+
title, content = html_getfile(url)
2607+
elif op == "topic":
26732608
# try topics first, then objects.
26742609
try:
26752610
title, content = html_topicpage(url)
26762611
except ValueError:
26772612
title, content = html_getobj(url)
2678-
elif op == "get?key":
2613+
elif op == "get":
26792614
# try objects first, then topics.
26802615
if url in ("", "index"):
26812616
title, content = html_index()
@@ -2870,5 +2805,6 @@ class BadUsage(Exception): pass
28702805
it names a directory, documentation is written for all the contents.
28712806
""".format(cmd=cmd, sep=os.sep))
28722807

2808+
28732809
if __name__ == '__main__':
28742810
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)