From dca72d38d6a8887534fba947afa527eb8c18eaa7 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 31 Aug 2026 17:00:44 +0300 Subject: [PATCH] gh-156713: Use the filesystem encoding in nturl2path urllib.request.pathname2url() and url2pathname() use the filesystem encoding and error handler since gh-85168, but nturl2path, which implements them on Windows before 3.14, was left unchanged. Paths containing surrogate characters raised UnicodeEncodeError. --- Lib/nturl2path.py | 14 ++++++++++--- Lib/test/test_nturl2path.py | 21 +++++++++++++++++++ ...-08-31-17-10-00.gh-issue-156713.Nt2URL.rst | 4 ++++ 3 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-31-17-10-00.gh-issue-156713.Nt2URL.rst diff --git a/Lib/nturl2path.py b/Lib/nturl2path.py index 57c7858dff0b818..47c49ca2020c5c8 100644 --- a/Lib/nturl2path.py +++ b/Lib/nturl2path.py @@ -22,7 +22,10 @@ def url2pathname(url): # ///C:/foo/bar/spam.foo # become # C:\foo\bar\spam.foo + import sys import urllib.parse + encoding = sys.getfilesystemencoding() + errors = sys.getfilesystemencodeerrors() if url[:3] == '///': # URL has an empty authority section, so the path begins on the third # character. @@ -40,7 +43,8 @@ def url2pathname(url): if url[1:2] == '|': # Older URLs use a pipe after a drive letter url = url[:1] + ':' + url[2:] - return urllib.parse.unquote(url.replace('/', '\\')) + return urllib.parse.unquote(url.replace('/', '\\'), + encoding=encoding, errors=errors) def pathname2url(p): """OS-specific conversion from a file system path to a relative URL @@ -50,7 +54,10 @@ def pathname2url(p): # becomes # ///C:/foo/bar/spam.foo import ntpath + import sys import urllib.parse + encoding = sys.getfilesystemencoding() + errors = sys.getfilesystemencodeerrors() # First, clean up some special forms. We are going to sacrifice # the additional information anyway p = p.replace('\\', '/') @@ -65,10 +72,11 @@ def pathname2url(p): # an authority section with a zero-length authority, and a path # section starting with a single slash. drive = f'///{drive}' - drive = urllib.parse.quote(drive, safe='/:') + drive = urllib.parse.quote(drive, encoding=encoding, errors=errors, + safe='/:') elif root: # Add explicitly empty authority to path beginning with one slash. root = f'//{root}' - tail = urllib.parse.quote(tail) + tail = urllib.parse.quote(tail, encoding=encoding, errors=errors) return drive + root + tail diff --git a/Lib/test/test_nturl2path.py b/Lib/test/test_nturl2path.py index a6a3422a0f75b2e..b4532137968d6e3 100644 --- a/Lib/test/test_nturl2path.py +++ b/Lib/test/test_nturl2path.py @@ -1,4 +1,6 @@ +import sys import unittest +import urllib.parse from test.support import warnings_helper @@ -58,6 +60,15 @@ def test_pathname2url(self): for url in urls: self.assertEqual(fn(nturl2path.url2pathname(url)), url) + def test_pathname2url_surrogates(self): + # gh-156713: the filesystem encoding and error handler are used, + # so that paths containing surrogate characters can be converted. + encoding = sys.getfilesystemencoding() + errors = sys.getfilesystemencodeerrors() + tail = urllib.parse.quote('a\udcff', encoding=encoding, errors=errors) + self.assertEqual(nturl2path.pathname2url('C:\\a\udcff'), + '///C:/' + tail) + def test_url2pathname(self): fn = nturl2path.url2pathname self.assertEqual(fn('/'), '\\') @@ -103,5 +114,15 @@ def test_url2pathname(self): self.assertEqual(fn(nturl2path.pathname2url(path)), path) + def test_url2pathname_surrogates(self): + # gh-156713: the filesystem encoding and error handler are used, so + # that URLs containing percent-encoded surrogates can be converted. + encoding = sys.getfilesystemencoding() + errors = sys.getfilesystemencodeerrors() + url = urllib.parse.quote('a\udcff', encoding=encoding, errors=errors) + self.assertEqual(nturl2path.url2pathname('///C:/' + url), + 'C:\\a\udcff') + + if __name__ == '__main__': unittest.main() diff --git a/Misc/NEWS.d/next/Library/2026-08-31-17-10-00.gh-issue-156713.Nt2URL.rst b/Misc/NEWS.d/next/Library/2026-08-31-17-10-00.gh-issue-156713.Nt2URL.rst new file mode 100644 index 000000000000000..1d21fc70fd9256f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-31-17-10-00.gh-issue-156713.Nt2URL.rst @@ -0,0 +1,4 @@ +Fix :func:`!nturl2path.pathname2url` and :func:`!nturl2path.url2pathname`: +the filesystem encoding and error handler are now used for percent-encoding +and decoding, as in :mod:`urllib.request`. Previously paths containing +surrogate characters raised :exc:`UnicodeEncodeError`.