From 5bc8b5513188de2227d6f593234a2fcb27b1c5d1 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Sat, 31 Oct 2026 12:50:01 +0000 Subject: [PATCH] Render interactive examples as code blocks in `pydoc` HTML output --- Lib/pydoc.py | 21 +++++++++++++++-- Lib/pydoc_data/_pydoc.css | 12 +++++++++- Lib/test/test_pydoc/test_pydoc.py | 23 +++++++++++++++++++ ...-10-31-12-47-48.gh-issue-160998.5EpsBx.rst | 2 ++ 4 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-31-12-47-48.gh-issue-160998.5EpsBx.rst diff --git a/Lib/pydoc.py b/Lib/pydoc.py index e8a3fe1c4908f78..c60fdcb56c42e6e 100644 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -723,12 +723,15 @@ def markup(self, text, escape=None, funcs={}, classes={}, methods={}): pattern = re.compile(r'\b((http|https|ftp)://\S+[\w/]|' r'RFC[- ]?(\d+)|' r'PEP[- ]?(\d+)|' - r'(self\.)?(\w+))') + r'(self\.)?(\w+))|' + r'\n*^(?P[ ]*)>>>(?:[ ].*)?$' + r'(?:\n(?P=indent)(?=.*\S).*)*\n*', + re.MULTILINE) while match := pattern.search(text, here): start, end = match.span() results.append(escape(text[here:start])) - all, scheme, rfc, pep, selfdot, name = match.groups() + all, scheme, rfc, pep, selfdot, name, indent = match.groups() if scheme: url = escape(all).replace('"', '"') results.append('%s' % (url, url)) @@ -738,6 +741,20 @@ def markup(self, text, escape=None, funcs={}, classes={}, methods={}): elif pep: url = 'https://peps.python.org/pep-%04d/' % int(pep) results.append('%s' % (url, escape(all))) + elif indent is not None: + lines = [] + for line in match.group().strip('\n').split('\n'): + line = line.removeprefix(indent) + if prompt := re.match(r'(>>>|\.\.\.)(?: |$)', line): + line = ('%s' + % escape(prompt.group()) + + self.markup(line[prompt.end():], escape, + funcs, classes, methods)) + else: + line = self.markup(line, escape, + funcs, classes, methods) + lines.append(line) + results.append('
%s
' % '\n'.join(lines)) elif selfdot: # Create a link for methods like 'self.method(...)' # and use for attributes like 'self.attr' diff --git a/Lib/pydoc_data/_pydoc.css b/Lib/pydoc_data/_pydoc.css index 9e5c5c2f6f320dc..7066cf732bb6e72 100644 --- a/Lib/pydoc_data/_pydoc.css +++ b/Lib/pydoc_data/_pydoc.css @@ -125,9 +125,10 @@ section section { .docstring { white-space: pre-wrap; margin: 0.3em 0; + max-width: 50em; } -pre.topic, pre.keyword { +pre.topic, pre.keyword, pre.repl { background-color: var(--block-background-color); border: 1px solid var(--block-border-color); border-radius: 3px; @@ -135,6 +136,15 @@ pre.topic, pre.keyword { overflow-x: auto; } +pre.repl { + font-size: inherit; + margin: 0.5em 0; +} + +.prompt { + color: var(--muted-color); +} + /* Documented names (functions, methods, data) */ dl.doc { diff --git a/Lib/test/test_pydoc/test_pydoc.py b/Lib/test/test_pydoc/test_pydoc.py index f57e596d67cf7de..9f4f0f3679b6f90 100644 --- a/Lib/test/test_pydoc/test_pydoc.py +++ b/Lib/test/test_pydoc/test_pydoc.py @@ -1966,6 +1966,29 @@ def a_fn_with_https_link(): html ) + @requires_docstrings + def test_html_for_repl_examples(self): + def a_fn_with_repl_example(): + """Do something. + + Example: + + >>> a_fn_with_repl_example() + ... # comment + result + Done. + """ + pass + + html = pydoc.HTMLDoc().document(a_fn_with_repl_example) + self.assertIn( + 'Example:
'
+            '>>> a_fn_with_repl_example()\n'
+            '... # comment\n'
+            'result
Done.', + html + ) + def test_module_none(self): # Issue #128772 from test.test_pydoc import module_none diff --git a/Misc/NEWS.d/next/Library/2026-10-31-12-47-48.gh-issue-160998.5EpsBx.rst b/Misc/NEWS.d/next/Library/2026-10-31-12-47-48.gh-issue-160998.5EpsBx.rst new file mode 100644 index 000000000000000..df4008562a74050 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-31-12-47-48.gh-issue-160998.5EpsBx.rst @@ -0,0 +1,2 @@ +Render interactive examples in docstrings as separate blocks in the HTML output +of :mod:`pydoc`.