https://github.com/python/cpython/commit/b871e39ff57fd769b91fd6d717d080cc4623fd03
commit: b871e39ff57fd769b91fd6d717d080cc4623fd03
branch: 3.13
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-09-13T13:12:43Z
summary:

[3.13] gh-156713: Use the filesystem encoding in nturl2path (GH-156717) 
(GH-157425)

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.
(cherry picked from commit 01599e203e25610421bb5df413f32fa0848f3ade)

files:
A Misc/NEWS.d/next/Library/2026-08-31-17-10-00.gh-issue-156713.Nt2URL.rst
M Lib/nturl2path.py
M Lib/test/test_urllib.py

diff --git a/Lib/nturl2path.py b/Lib/nturl2path.py
index 757fd01bec8223..0761fbf3474975 100644
--- a/Lib/nturl2path.py
+++ b/Lib/nturl2path.py
@@ -14,7 +14,9 @@ def url2pathname(url):
     #   ///C:/foo/bar/spam.foo
     # become
     #   C:\foo\bar\spam.foo
-    import string, urllib.parse
+    import string, sys, 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.
@@ -30,13 +32,15 @@ def url2pathname(url):
     if not '|' in url:
         # No drive specifier, just convert slashes
         # make sure not to convert quoted slashes :-)
-        return urllib.parse.unquote(url.replace('/', '\\'))
+        return urllib.parse.unquote(url.replace('/', '\\'),
+                                    encoding=encoding, errors=errors)
     comp = url.split('|')
     if len(comp) != 2 or comp[0][-1] not in string.ascii_letters:
         error = 'Bad URL: ' + url
         raise OSError(error)
     drive = comp[0][-1].upper()
-    tail = urllib.parse.unquote(comp[1].replace('/', '\\'))
+    tail = urllib.parse.unquote(comp[1].replace('/', '\\'),
+                                encoding=encoding, errors=errors)
     return drive + ':' + tail
 
 def pathname2url(p):
@@ -46,7 +50,9 @@ def pathname2url(p):
     #   C:\foo\bar\spam.foo
     # becomes
     #   ///C:/foo/bar/spam.foo
-    import urllib.parse
+    import sys, 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('\\', '/')
@@ -58,12 +64,12 @@ def pathname2url(p):
             raise OSError('Bad path: ' + p)
     if not ':' in p:
         # No DOS drive specified, just quote the pathname
-        return urllib.parse.quote(p)
+        return urllib.parse.quote(p, encoding=encoding, errors=errors)
     comp = p.split(':', maxsplit=2)
     if len(comp) != 2 or len(comp[0]) > 1:
         error = 'Bad path: ' + p
         raise OSError(error)
 
     drive = urllib.parse.quote(comp[0].upper())
-    tail = urllib.parse.quote(comp[1])
+    tail = urllib.parse.quote(comp[1], encoding=encoding, errors=errors)
     return '///' + drive + ':' + tail
diff --git a/Lib/test/test_urllib.py b/Lib/test/test_urllib.py
index c6a3157698d3ad..d0f743178f7819 100644
--- a/Lib/test/test_urllib.py
+++ b/Lib/test/test_urllib.py
@@ -4,6 +4,7 @@
 import urllib.request
 import urllib.error
 import http.client
+import nturl2path
 import email.message
 import io
 import unittest
@@ -1659,6 +1660,15 @@ def test_pathname2url_nonascii(self):
         url = urllib.parse.quote(os_helper.FS_NONASCII, encoding=encoding, 
errors=errors)
         self.assertEqual(urllib.request.pathname2url(os_helper.FS_NONASCII), 
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)
+
     @unittest.skipUnless(sys.platform == 'win32',
                          'test specific to Windows pathnames.')
     def test_url2pathname_win(self):
@@ -1720,6 +1730,15 @@ def test_url2pathname_nonascii(self):
         url = urllib.parse.quote(url, encoding=encoding, errors=errors)
         self.assertEqual(urllib.request.url2pathname(url), 
os_helper.FS_NONASCII)
 
+    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')
+
 class Utility_Tests(unittest.TestCase):
     """Testcase to test the various utility functions in the urllib."""
 
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 00000000000000..1d21fc70fd9256
--- /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`.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to