Skip to content

Commit 07a943e

Browse files
gh-69371: Fix pydoc for modules whose path contains undecodable bytes
"python -m pydoc -w" and the pydoc HTTP server failed with UnicodeEncodeError if the path of the documented module contained bytes undecodable in the filesystem encoding. The file URL is now created with urllib.request.pathname2url(), which percent-encodes the path using the filesystem encoding, and characters unencodable in the generated HTML page (which is always UTF-8) are escaped with backslashes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 1d90627 commit 07a943e

3 files changed

Lines changed: 33 additions & 4 deletions

File tree

Lib/pydoc.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ class or function within a module or module in a package. If the
7171
import textwrap
7272
import time
7373
import tokenize
74-
import urllib.parse
7574
import warnings
7675
from annotationlib import Format
7776
from collections import deque
@@ -792,7 +791,8 @@ def docmodule(self, object, name=None, mod=None, *ignored):
792791
head = linkedname
793792
try:
794793
path = inspect.getabsfile(object)
795-
url = urllib.parse.quote(path)
794+
import urllib.request
795+
url = urllib.request.pathname2url(path)
796796
filelink = self.filelink(url, path)
797797
except TypeError:
798798
filelink = '(built-in)'
@@ -1787,7 +1787,8 @@ def writedoc(thing, forceload=0):
17871787
"""Write HTML documentation to a file in the current directory."""
17881788
object, name = resolve(thing, forceload)
17891789
page = html.page(describe(object), html.document(object, name))
1790-
with open(name + '.html', 'w', encoding='utf-8') as file:
1790+
with open(name + '.html', 'w', encoding='utf-8',
1791+
errors='backslashreplace') as file:
17911792
file.write(page)
17921793
print('wrote', name + '.html')
17931794

@@ -2384,7 +2385,7 @@ def do_GET(self):
23842385
self.send_header('Content-Type', '%s; charset=UTF-8' % content_type)
23852386
self.end_headers()
23862387
self.wfile.write(self.urlhandler(
2387-
self.path, content_type).encode('utf-8'))
2388+
self.path, content_type).encode('utf-8', 'backslashreplace'))
23882389

23892390
def log_message(self, *args):
23902391
# Don't log messages.

Lib/test/test_pydoc/test_pydoc.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import unittest
2020
import unittest.mock
2121
import urllib.parse
22+
import urllib.request
2223
import xml.etree
2324
import xml.etree.ElementTree
2425
import textwrap
@@ -1024,6 +1025,28 @@ def test_synopsis_sourceless_empty_doc(self):
10241025
synopsis_cached = pydoc.synopsis(cached_path, {})
10251026
self.assertIsNone(synopsis_cached)
10261027

1028+
@unittest.skipUnless(os_helper.TESTFN_UNDECODABLE,
1029+
'requires undecodable file names')
1030+
def test_html_doc_undecodable_path(self):
1031+
# gh-69371: the path of the module is not encodable in UTF-8.
1032+
with os_helper.temp_cwd() as test_dir:
1033+
subdir = os.path.join(os.fsencode(test_dir),
1034+
os_helper.TESTFN_UNDECODABLE)
1035+
os.mkdir(subdir)
1036+
with open(os.path.join(subdir, b'undecodable_mod.py'), 'w') as f:
1037+
f.write('"""Module docstring."""\n')
1038+
with import_helper.DirsOnSysPath(os.fsdecode(subdir)):
1039+
mod = import_helper.import_fresh_module('undecodable_mod')
1040+
doc = pydoc.HTMLDoc().docmodule(mod)
1041+
with captured_stdout():
1042+
pydoc.writedoc(mod)
1043+
# The link contains the percent-encoded path...
1044+
path = os.fsdecode(os.path.join(subdir, b'undecodable_mod.py'))
1045+
self.assertIn(urllib.request.pathname2url(path), doc)
1046+
# ...and the page can be written and served as UTF-8.
1047+
with open('undecodable_mod.html', encoding='utf-8') as f:
1048+
self.assertIn('undecodable_mod', f.read())
1049+
10271050
def test_splitdoc_with_description(self):
10281051
example_string = "I Am A Doc\n\n\nHere is my description"
10291052
self.assertEqual(pydoc.splitdoc(example_string),
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix :mod:`pydoc` for modules whose path contains undecodable bytes.
2+
:func:`!pydoc.writedoc` and the pydoc HTTP server no longer fail with
3+
:exc:`UnicodeEncodeError`: the file URL is now percent-encoded using the
4+
filesystem encoding, and characters unencodable in the generated HTML page
5+
are escaped with backslashes.

0 commit comments

Comments
 (0)