diff --git a/Lib/pydoc.py b/Lib/pydoc.py index 69c83e08511..4973b8fb179 100644 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -75,6 +75,7 @@ class or function within a module or module in a package. If the import warnings from annotationlib import Format from collections import deque +from html import escape as html_escape from reprlib import Repr from traceback import format_exception_only @@ -552,7 +553,7 @@ def repr1(self, x, level): methodname = 'repr_' + '_'.join(type(x).__name__.split()) if hasattr(self, methodname): return getattr(self, methodname)(x, level) - return self.escape(cram(stripid(repr(x)), self.maxother)) + return html_escape(cram(stripid(repr(x)), self.maxother)) def repr_string(self, x, level): test = cram(x, self.maxstring) @@ -560,18 +561,18 @@ def repr_string(self, x, level): if '\\' in test and '\\' not in replace(testrepr, r'\\', ''): # Backslashes are only literal in the string and are never # needed to make any special characters, so show a raw string. - return 'r' + testrepr[0] + self.escape(test) + testrepr[0] + return 'r' + testrepr[0] + html_escape(test) + testrepr[0] return re.sub(r'((\\[\\abfnrtv\'"]|\\[0-9]..|\\x..|\\u....)+)', r'\1', - self.escape(testrepr)) + html_escape(testrepr, quote=False)) repr_str = repr_string def repr_instance(self, x, level): try: - return self.escape(cram(stripid(repr(x)), self.maxstring)) + return html_escape(cram(stripid(repr(x)), self.maxstring)) except: - return self.escape('<%s instance>' % x.__class__.__name__) + return html_escape('<%s instance>' % x.__class__.__name__) repr_unicode = repr_string @@ -633,7 +634,7 @@ def bigsection(self, title, *args): def preformat(self, text): """Format literal preformatted text.""" - text = self.escape(text.expandtabs()) + text = html_escape(text.expandtabs(), quote=False) return replace(text, '\n\n', '\n \n', '\n\n', '\n \n', ' ', ' ', '\n', '
\n') @@ -711,7 +712,7 @@ def filelink(self, url, path): def markup(self, text, escape=None, funcs={}, classes={}, methods={}): """Mark up some plain text, given a context of symbols to look for. Each context dictionary maps object names to anchor names.""" - escape = escape or self.escape + escape = escape or html_escape results = [] here = 0 pattern = re.compile(r'\b((http|https|ftp)://\S+[\w/]|' @@ -794,9 +795,9 @@ def docmodule(self, object, name=None, mod=None, *ignored): if version := self._get_version(object): if version[:11] == '$' + 'Revision: ' and version[-1:] == '$': version = version[11:-1].strip() - info.append('version %s' % self.escape(version)) + info.append('version %s' % html_escape(version)) if hasattr(object, '__date__'): - info.append(self.escape(str(object.__date__))) + info.append(html_escape(str(object.__date__))) if info: head = head + ' (%s)' % ', '.join(info) docloc = self.getdocloc(object) @@ -2156,6 +2157,11 @@ def showsymbol(self, symbol): topic, _, xrefs = target.partition(' ') self.showtopic(topic, xrefs) + def _getsymbol(self, symbol): + target = self.symbols[symbol] + topic, _, xrefs = target.partition(' ') + return self._gettopic(topic, xrefs) + def listmodules(self, key=''): if key: self.output.write(''' @@ -2321,6 +2327,7 @@ def _start_server(urlhandler, hostname, port): import email.message import select import threading + from urllib.parse import unquote class DocHandler(http.server.BaseHTTPRequestHandler): @@ -2440,11 +2447,14 @@ def page(self, title, contents): %s%s
%s
''' % (title, css_link, html_navbar(), contents) + def filelink(self, url, path): + return ('%s' % + (html_escape(url), html_escape(path))) html = _HTMLDoc() def html_navbar(): - version = html.escape("%s [%s, %s]" % (platform.python_version(), + version = html_escape("%s [%s, %s]" % (platform.python_version(), platform.python_build()[0], platform.python_compiler())) return """ @@ -2456,6 +2466,7 @@ def html_navbar(): Module Index : Topics : Keywords + : Symbols
@@ -2468,7 +2479,7 @@ def html_navbar():
- """ % (version, html.escape(platform.platform(terse=True))) + """ % (version, html_escape(platform.platform(terse=True))) def html_index(): """Module Index page.""" @@ -2524,7 +2535,20 @@ def bltinlink(name): 'key = %s' % key, 'index', '
'.join(results)) return 'Search Results', contents - def html_topics(): + def html_getfile(path): + """Get and display a source file listing safely.""" + path = urllib.parse.unquote(path) + with tokenize.open(path) as fp: + lines = html_escape(fp.read()) + body = '
%s
' % lines + heading = html.heading( + 'File Listing', + ) + contents = heading + html.bigsection( + 'File: %s' % path, 'index', body) + return 'getfile %s' % path, contents + + def html_topicindex(title): """Index of topic texts available.""" def bltinlink(name): @@ -2533,51 +2557,48 @@ def bltinlink(name): heading = html.heading( 'INDEX', ) - names = sorted(Helper.topics.keys()) + + keys = { + 'topics': Helper.topics.keys, + 'keywords': Helper.keywords.keys, + 'symbols': Helper.symbols.keys, + } + names = sorted(keys[title]()) contents = html.multicolumn(names, bltinlink) contents = heading + html.bigsection( - 'Topics', 'index', contents) - return 'Topics', contents - - def html_keywords(): - """Index of keywords.""" - heading = html.heading( - 'INDEX', - ) - names = sorted(Helper.keywords.keys()) - - def bltinlink(name): - return '%s' % (name, name) - - contents = html.multicolumn(names, bltinlink) - contents = heading + html.bigsection( - 'Keywords', 'index', contents) - return 'Keywords', contents + title.capitalize(), 'index', contents) + return title.capitalize(), contents def html_topicpage(topic): """Topic or keyword help page.""" buf = io.StringIO() htmlhelp = Helper(buf, buf) - contents, xrefs = htmlhelp._gettopic(topic) if topic in htmlhelp.keywords: title = 'KEYWORD' - else: + contents, xrefs = htmlhelp._gettopic(topic) + elif topic in htmlhelp.topics: title = 'TOPIC' + contents, xrefs = htmlhelp._gettopic(topic) + elif topic in htmlhelp.symbols: + title = 'SYMBOL' + contents, xrefs = htmlhelp._getsymbol(topic) + else: + raise ValueError(f'could not find topic {topic!r}') heading = html.heading( - '%s' % title, + f'{title}', ) - contents = '
%s
' % html.markup(contents) + contents = f'
{html.markup(contents)}
' contents = html.bigsection(topic , 'index', contents) if xrefs: xrefs = sorted(xrefs.split()) def bltinlink(name): - return '%s' % (name, name) + return f'Error', ) - contents = '
'.join(html.escape(line) for line in + contents = '
'.join(html_escape(line) for line in format_exception_only(type(exc), exc)) contents = heading + html.bigsection(url, 'error', contents) return "Error - %s" % url, contents @@ -2605,21 +2626,21 @@ def get_html_page(url): try: if url in ("", "index"): title, content = html_index() - elif url == "topics": - title, content = html_topics() - elif url == "keywords": - title, content = html_keywords() - elif '=' in url: - op, _, url = url.partition('=') - if op == "search?key": + elif url in ("topics", "keywords", "symbols"): + title, content = html_topicindex(url) + elif '?key=' in url: + op, _, url = url.partition('?key=') + if op == "search": title, content = html_search(url) - elif op == "topic?key": + elif op == "getfile": + title, content = html_getfile(url) + elif op == "topic": # try topics first, then objects. try: title, content = html_topicpage(url) except ValueError: title, content = html_getobj(url) - elif op == "get?key": + elif op == "get": # try objects first, then topics. if url in ("", "index"): title, content = html_index() @@ -2814,5 +2835,6 @@ class BadUsage(Exception): pass it names a directory, documentation is written for all the contents. """.format(cmd=cmd, sep=os.sep)) + if __name__ == '__main__': cli() diff --git a/Lib/test/test_pydoc/test_pydoc.py b/Lib/test/test_pydoc/test_pydoc.py index 0e113006cfa..989a9cb4219 100644 --- a/Lib/test/test_pydoc/test_pydoc.py +++ b/Lib/test/test_pydoc/test_pydoc.py @@ -2128,6 +2128,7 @@ def test_url_requests(self): ("index", "Pydoc: Index of Modules"), ("topics", "Pydoc: Topics"), ("keywords", "Pydoc: Keywords"), + ("symbols", "Pydoc: Symbols"), ("pydoc", "Pydoc: module pydoc"), ("get?key=pydoc", "Pydoc: module pydoc"), ("search?key=pydoc", "Pydoc: Search Results"), diff --git a/Misc/NEWS.d/next/Library/2025-11-13-02-37-20.bpo-18387.ppwPIb.rst b/Misc/NEWS.d/next/Library/2025-11-13-02-37-20.bpo-18387.ppwPIb.rst new file mode 100644 index 00000000000..35dcb332b91 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-11-13-02-37-20.bpo-18387.ppwPIb.rst @@ -0,0 +1,2 @@ +Add 'symbols' link to pydoc's html menu bar. Original patch by Ron Adam. +Enhanced by Sanyam Khurana.