From 8c9fe623ffbfa9929887067d0064a567e91dec12 Mon Sep 17 00:00:00 2001 From: barneygale Date: Thu, 10 Apr 2025 21:42:26 +0100 Subject: [PATCH 01/13] GH-125866: Support complete "file:" URLs in urllib Add optional *add_scheme* argument to `urllib.request.pathname2url()`; when set to true, a complete URL is returned. Likewise add optional *has_scheme* argument to `urllib.request.url2pathname()`; when set to true, a complete URL is accepted. --- Doc/library/urllib.request.rst | 32 +++++++++++++------ Doc/whatsnew/3.14.rst | 5 ++- Lib/pathlib/__init__.py | 6 ++-- Lib/test/test_pathlib/test_pathlib.py | 4 +-- Lib/test/test_urllib.py | 27 ++++++++++++++-- Lib/test/test_urllib2.py | 2 +- Lib/test/test_urllib2net.py | 2 +- Lib/urllib/request.py | 18 +++++++---- ...-04-10-21-43-04.gh-issue-125866.EZ9X8D.rst | 4 +++ 9 files changed, 72 insertions(+), 28 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-04-10-21-43-04.gh-issue-125866.EZ9X8D.rst diff --git a/Doc/library/urllib.request.rst b/Doc/library/urllib.request.rst index edfc249eb43c78..5f68b7af07aaa5 100644 --- a/Doc/library/urllib.request.rst +++ b/Doc/library/urllib.request.rst @@ -146,16 +146,19 @@ The :mod:`urllib.request` module defines the following functions: attribute to modify its position in the handlers list. -.. function:: pathname2url(path) +.. function:: pathname2url(path, add_scheme=False) Convert the given local path to a ``file:`` URL. This function uses - :func:`~urllib.parse.quote` function to encode the path. For historical - reasons, the return value omits the ``file:`` scheme prefix. This example - shows the function being used on Windows:: + :func:`~urllib.parse.quote` function to encode the path. + + If *add_scheme* is false (the default), the return value omits the + ``file:`` scheme prefix. Set *add_scheme* to true to return a complete URL. + + This example shows the function being used on Windows:: >>> from urllib.request import pathname2url >>> path = 'C:\\Program Files' - >>> 'file:' + pathname2url(path) + >>> pathname2url(path, add_scheme=True) 'file:///C:/Program%20Files' .. versionchanged:: 3.14 @@ -168,17 +171,23 @@ The :mod:`urllib.request` module defines the following functions: sections. For example, the path ``/etc/hosts`` is converted to the URL ``///etc/hosts``. + .. versionchanged:: next + The *add_scheme* argument was added. + -.. function:: url2pathname(url) +.. function:: url2pathname(url, has_scheme=False) Convert the given ``file:`` URL to a local path. This function uses - :func:`~urllib.parse.unquote` to decode the URL. For historical reasons, - the given value *must* omit the ``file:`` scheme prefix. This example shows - the function being used on Windows:: + :func:`~urllib.parse.unquote` to decode the URL. + + If *has_scheme* is false (the default), the given value should omit the + ``file:`` scheme prefix. Set *has_scheme* to true to supply a complete URL. + + This example shows the function being used on Windows:: >>> from urllib.request import url2pathname >>> url = 'file:///C:/Program%20Files' - >>> url2pathname(url.removeprefix('file:')) + >>> url2pathname(url, has_scheme=True) 'C:\\Program Files' .. versionchanged:: 3.14 @@ -193,6 +202,9 @@ The :mod:`urllib.request` module defines the following functions: returned (as before), and on other platforms a :exc:`~urllib.error.URLError` is raised. + .. versionchanged:: next + The *has_scheme* argument was added. + .. function:: getproxies() diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index f8cae78b909a00..2f719c7386555c 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -1197,16 +1197,19 @@ urllib supporting SHA-256 digest authentication as specified in :rfc:`7616`. (Contributed by Calvin Bui in :gh:`128193`.) -* Improve standards compliance when parsing and emitting ``file:`` URLs. +* Improve ergonomics and standards compliance when parsing and emitting + ``file:`` URLs. In :func:`urllib.request.url2pathname`: + - Accept a complete URL when the new *has_scheme* argument is set to true. - Discard URL authorities that resolve to a local IP address. - Raise :exc:`~urllib.error.URLError` if a URL authority doesn't resolve to ``localhost``, except on Windows where we return a UNC path. In :func:`urllib.request.pathname2url`: + - Return a complete URL when the new *add_scheme* argument is set to true. - Include an empty URL authority when a path begins with a slash. For example, the path ``/etc/hosts`` is converted to the URL ``///etc/hosts``. diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py index 43a5440e0132ff..464a23c8e65eaf 100644 --- a/Lib/pathlib/__init__.py +++ b/Lib/pathlib/__init__.py @@ -1271,17 +1271,15 @@ def as_uri(self): if not self.is_absolute(): raise ValueError("relative paths can't be expressed as file URIs") from urllib.request import pathname2url - return f'file:{pathname2url(str(self))}' + return pathname2url(str(self), True) @classmethod def from_uri(cls, uri): """Return a new path from the given 'file' URI.""" - if not uri.startswith('file:'): - raise ValueError(f"URI does not start with 'file:': {uri!r}") from urllib.error import URLError from urllib.request import url2pathname try: - path = cls(url2pathname(uri.removeprefix('file:'))) + path = cls(url2pathname(uri, True)) except URLError as exc: raise ValueError(exc.reason) from None if not path.is_absolute(): diff --git a/Lib/test/test_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py index 00ec17e21e235f..07cbd2033dd8f3 100644 --- a/Lib/test/test_pathlib/test_pathlib.py +++ b/Lib/test/test_pathlib/test_pathlib.py @@ -3302,8 +3302,8 @@ def test_from_uri_posix(self): @needs_posix def test_from_uri_pathname2url_posix(self): P = self.cls - self.assertEqual(P.from_uri('file:' + pathname2url('/foo/bar')), P('/foo/bar')) - self.assertEqual(P.from_uri('file:' + pathname2url('//foo/bar')), P('//foo/bar')) + self.assertEqual(P.from_uri(pathname2url('/foo/bar', True)), P('/foo/bar')) + self.assertEqual(P.from_uri(pathname2url('//foo/bar', True)), P('//foo/bar')) @needs_windows def test_absolute_windows(self): diff --git a/Lib/test/test_urllib.py b/Lib/test/test_urllib.py index ecf429e17811a4..3e1c27658dd136 100644 --- a/Lib/test/test_urllib.py +++ b/Lib/test/test_urllib.py @@ -476,7 +476,7 @@ def test_missing_localfile(self): def test_file_notexists(self): fd, tmp_file = tempfile.mkstemp() - tmp_file_canon_url = 'file:' + urllib.request.pathname2url(tmp_file) + tmp_file_canon_url = urllib.request.pathname2url(tmp_file, True) parsed = urllib.parse.urlsplit(tmp_file_canon_url) tmp_fileurl = parsed._replace(netloc='localhost').geturl() try: @@ -620,7 +620,7 @@ def tearDown(self): def constructLocalFileUrl(self, filePath): filePath = os.path.abspath(filePath) - return "file:" + urllib.request.pathname2url(filePath) + return urllib.request.pathname2url(filePath, True) def createNewTempFile(self, data=b""): """Creates a new temporary file containing the specified data, @@ -1435,6 +1435,12 @@ def test_pathname2url(self): self.assertEqual(fn(f'a{sep}b.c'), 'a/b.c') self.assertEqual(fn(f'{sep}a{sep}b.c'), '///a/b.c') self.assertEqual(fn(f'{sep}a{sep}b%#c'), '///a/b%25%23c') + self.assertEqual(fn('', add_scheme=True), 'file:') + self.assertEqual(fn(sep, add_scheme=True), 'file:///') + self.assertEqual(fn('a', add_scheme=True), 'file:a') + self.assertEqual(fn(f'a{sep}b.c', add_scheme=True), 'file:a/b.c') + self.assertEqual(fn(f'{sep}a{sep}b.c', add_scheme=True), 'file:///a/b.c') + self.assertEqual(fn(f'{sep}a{sep}b%#c', add_scheme=True), 'file:///a/b%25%23c') @unittest.skipUnless(sys.platform == 'win32', 'test specific to Windows pathnames.') @@ -1503,6 +1509,23 @@ def test_url2pathname(self): self.assertEqual(fn('//localhost/foo/bar'), f'{sep}foo{sep}bar') self.assertEqual(fn('///foo/bar'), f'{sep}foo{sep}bar') self.assertEqual(fn('////foo/bar'), f'{sep}{sep}foo{sep}bar') + self.assertEqual(fn('file:', has_scheme=True), '') + self.assertEqual(fn('FILE:', has_scheme=True), '') + self.assertEqual(fn('FiLe:', has_scheme=True), '') + self.assertEqual(fn('file:/', has_scheme=True), f'{sep}') + self.assertEqual(fn('file:///', has_scheme=True), f'{sep}') + self.assertEqual(fn('file:////', has_scheme=True), f'{sep}{sep}') + self.assertEqual(fn('file:foo', has_scheme=True), 'foo') + self.assertEqual(fn('file:foo/bar', has_scheme=True), f'foo{sep}bar') + self.assertEqual(fn('file:/foo/bar', has_scheme=True), f'{sep}foo{sep}bar') + self.assertEqual(fn('file://localhost/foo/bar', has_scheme=True), f'{sep}foo{sep}bar') + self.assertEqual(fn('file:///foo/bar', has_scheme=True), f'{sep}foo{sep}bar') + self.assertEqual(fn('file:////foo/bar', has_scheme=True), f'{sep}{sep}foo{sep}bar') + self.assertRaises(urllib.error.URLError, fn, '', has_scheme=True) + self.assertRaises(urllib.error.URLError, fn, ':', has_scheme=True) + self.assertRaises(urllib.error.URLError, fn, 'foo', has_scheme=True) + self.assertRaises(urllib.error.URLError, fn, 'http:foo', has_scheme=True) + self.assertRaises(urllib.error.URLError, fn, 'localfile:foo', has_scheme=True) @unittest.skipUnless(sys.platform == 'win32', 'test specific to Windows pathnames.') diff --git a/Lib/test/test_urllib2.py b/Lib/test/test_urllib2.py index 088ee4c4f90803..8a491823c4ec80 100644 --- a/Lib/test/test_urllib2.py +++ b/Lib/test/test_urllib2.py @@ -809,7 +809,7 @@ def test_file(self): TESTFN = os_helper.TESTFN towrite = b"hello, world\n" - canonurl = 'file:' + urllib.request.pathname2url(os.path.abspath(TESTFN)) + canonurl = urllib.request.pathname2url(os.path.abspath(TESTFN), True) parsed = urlsplit(canonurl) if parsed.netloc: raise unittest.SkipTest("non-local working directory") diff --git a/Lib/test/test_urllib2net.py b/Lib/test/test_urllib2net.py index b84290a7368c29..27d7516ef5816f 100644 --- a/Lib/test/test_urllib2net.py +++ b/Lib/test/test_urllib2net.py @@ -150,7 +150,7 @@ def test_file(self): f.write('hi there\n') f.close() urls = [ - 'file:' + urllib.request.pathname2url(os.path.abspath(TESTFN)), + urllib.request.pathname2url(os.path.abspath(TESTFN), True), ('file:///nonsensename/etc/passwd', None, urllib.error.URLError), ] diff --git a/Lib/urllib/request.py b/Lib/urllib/request.py index 84c075ec8b359f..63ae10141328c8 100644 --- a/Lib/urllib/request.py +++ b/Lib/urllib/request.py @@ -1466,17 +1466,16 @@ def get_names(self): def open_local_file(self, req): import email.utils import mimetypes - filename = _splittype(req.full_url)[1] - localfile = url2pathname(filename) + localfile = url2pathname(req.full_url, True) try: stats = os.stat(localfile) size = stats.st_size modified = email.utils.formatdate(stats.st_mtime, usegmt=True) - mtype = mimetypes.guess_type(filename)[0] + mtype = mimetypes.guess_file_type(localfile)[0] headers = email.message_from_string( 'Content-type: %s\nContent-length: %d\nLast-modified: %s\n' % (mtype or 'text/plain', size, modified)) - origurl = f'file:{pathname2url(localfile)}' + origurl = pathname2url(localfile, True) return addinfourl(open(localfile, 'rb'), headers, origurl) except OSError as exp: raise URLError(exp, exp.filename) @@ -1635,9 +1634,13 @@ def data_open(self, req): # Code move from the old urllib module -def url2pathname(url): +def url2pathname(url, has_scheme=False): """OS-specific conversion from a relative URL of the 'file' scheme to a file system path; not recommended for general use.""" + if has_scheme: + scheme, url = _splittype(url) + if scheme != 'file': + raise URLError("URL does not use file: scheme") authority, url = _splithost(url) if os.name == 'nt': if not _is_local_authority(authority): @@ -1661,13 +1664,14 @@ def url2pathname(url): return unquote(url, encoding=encoding, errors=errors) -def pathname2url(pathname): +def pathname2url(pathname, add_scheme=False): """OS-specific conversion from a file system path to a relative URL of the 'file' scheme; not recommended for general use.""" if os.name == 'nt': pathname = pathname.replace('\\', '/') encoding = sys.getfilesystemencoding() errors = sys.getfilesystemencodeerrors() + scheme = 'file:' if add_scheme else '' drive, root, tail = os.path.splitroot(pathname) if drive: # First, clean up some special forms. We are going to sacrifice the @@ -1689,7 +1693,7 @@ def pathname2url(pathname): # avoids interpreting the path as a URL authority. root = '//' + root tail = quote(tail, encoding=encoding, errors=errors) - return drive + root + tail + return scheme + drive + root + tail # Utility functions diff --git a/Misc/NEWS.d/next/Library/2025-04-10-21-43-04.gh-issue-125866.EZ9X8D.rst b/Misc/NEWS.d/next/Library/2025-04-10-21-43-04.gh-issue-125866.EZ9X8D.rst new file mode 100644 index 00000000000000..7f83957a5ca179 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-04-10-21-43-04.gh-issue-125866.EZ9X8D.rst @@ -0,0 +1,4 @@ +Add optional *add_scheme* argument to `urllib.request.pathname2url()`; when +set to true, a complete URL is returned. Likewise add optional *has_scheme* +argument to `urllib.request.url2pathname()`; when set to true, a complete +URL is accepted. From e5a94ced19287298168428b6e2a3aae5f33d1579 Mon Sep 17 00:00:00 2001 From: barneygale Date: Thu, 10 Apr 2025 21:49:33 +0100 Subject: [PATCH 02/13] Fix lint --- .../Library/2025-04-10-21-43-04.gh-issue-125866.EZ9X8D.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2025-04-10-21-43-04.gh-issue-125866.EZ9X8D.rst b/Misc/NEWS.d/next/Library/2025-04-10-21-43-04.gh-issue-125866.EZ9X8D.rst index 7f83957a5ca179..03049aab8d5f88 100644 --- a/Misc/NEWS.d/next/Library/2025-04-10-21-43-04.gh-issue-125866.EZ9X8D.rst +++ b/Misc/NEWS.d/next/Library/2025-04-10-21-43-04.gh-issue-125866.EZ9X8D.rst @@ -1,4 +1,4 @@ -Add optional *add_scheme* argument to `urllib.request.pathname2url()`; when +Add optional *add_scheme* argument to :func:`urllib.request.pathname2url`; when set to true, a complete URL is returned. Likewise add optional *has_scheme* -argument to `urllib.request.url2pathname()`; when set to true, a complete +argument to :func:`~urllib.request.url2pathname`; when set to true, a complete URL is accepted. From 3dff942926b754a0f5e92f2e4d93971a4a40d202 Mon Sep 17 00:00:00 2001 From: barneygale Date: Thu, 10 Apr 2025 22:00:34 +0100 Subject: [PATCH 03/13] Document that URLError is raised --- Doc/library/urllib.request.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Doc/library/urllib.request.rst b/Doc/library/urllib.request.rst index 5f68b7af07aaa5..acf6501c52a9db 100644 --- a/Doc/library/urllib.request.rst +++ b/Doc/library/urllib.request.rst @@ -180,8 +180,10 @@ The :mod:`urllib.request` module defines the following functions: Convert the given ``file:`` URL to a local path. This function uses :func:`~urllib.parse.unquote` to decode the URL. - If *has_scheme* is false (the default), the given value should omit the - ``file:`` scheme prefix. Set *has_scheme* to true to supply a complete URL. + If *has_scheme* is false (the default), the given value should omit a + ``file:`` scheme prefix. If *has_scheme* is set to true, the given value + should include the prefix; a :exc:`~urllib.error.URLError` is raised if it + doesn't. This example shows the function being used on Windows:: From 4be48ab38dc44f7ea38df0687c6f785622c62a98 Mon Sep 17 00:00:00 2001 From: barneygale Date: Thu, 10 Apr 2025 23:03:37 +0100 Subject: [PATCH 04/13] Doc[string] tweaks --- Doc/whatsnew/3.14.rst | 2 +- Lib/urllib/request.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 2f719c7386555c..dd5aa83ededa1b 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -1205,7 +1205,7 @@ urllib - Accept a complete URL when the new *has_scheme* argument is set to true. - Discard URL authorities that resolve to a local IP address. - Raise :exc:`~urllib.error.URLError` if a URL authority doesn't resolve - to ``localhost``, except on Windows where we return a UNC path. + to a local IP address, except on Windows where we return a UNC path. In :func:`urllib.request.pathname2url`: diff --git a/Lib/urllib/request.py b/Lib/urllib/request.py index 63ae10141328c8..20fdf7929fbe00 100644 --- a/Lib/urllib/request.py +++ b/Lib/urllib/request.py @@ -1635,8 +1635,8 @@ def data_open(self, req): # Code move from the old urllib module def url2pathname(url, has_scheme=False): - """OS-specific conversion from a relative URL of the 'file' scheme - to a file system path; not recommended for general use.""" + """Convert the given file URL to a local file system path. The 'file:' + scheme prefix must be omitted unless *has_scheme* is set to true.""" if has_scheme: scheme, url = _splittype(url) if scheme != 'file': @@ -1665,8 +1665,8 @@ def url2pathname(url, has_scheme=False): def pathname2url(pathname, add_scheme=False): - """OS-specific conversion from a file system path to a relative URL - of the 'file' scheme; not recommended for general use.""" + """Convert the given local file system path to a file URL. The 'file:' + scheme prefix is omitted unless *add_scheme* is set to true.""" if os.name == 'nt': pathname = pathname.replace('\\', '/') encoding = sys.getfilesystemencoding() From 19d3a7006e556de6ea956f4b51922743c30ed2db Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Sat, 12 Apr 2025 14:36:50 +0100 Subject: [PATCH 05/13] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/urllib/request.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Lib/urllib/request.py b/Lib/urllib/request.py index 20fdf7929fbe00..fe2c5620082235 100644 --- a/Lib/urllib/request.py +++ b/Lib/urllib/request.py @@ -1635,12 +1635,15 @@ def data_open(self, req): # Code move from the old urllib module def url2pathname(url, has_scheme=False): - """Convert the given file URL to a local file system path. The 'file:' - scheme prefix must be omitted unless *has_scheme* is set to true.""" + """Convert the given file URL to a local file system path. + + The 'file:' scheme prefix must be omitted unless *has_scheme* + is set to true. + """ if has_scheme: scheme, url = _splittype(url) if scheme != 'file': - raise URLError("URL does not use file: scheme") + raise URLError("URL is missing a 'file:' scheme") authority, url = _splithost(url) if os.name == 'nt': if not _is_local_authority(authority): @@ -1665,8 +1668,11 @@ def url2pathname(url, has_scheme=False): def pathname2url(pathname, add_scheme=False): - """Convert the given local file system path to a file URL. The 'file:' - scheme prefix is omitted unless *add_scheme* is set to true.""" + """Convert the given local file system path to a file URL. + + The 'file:' scheme prefix is omitted unless *add_scheme* + is set to true. + """ if os.name == 'nt': pathname = pathname.replace('\\', '/') encoding = sys.getfilesystemencoding() From 2a744f86a606361ab14e94fc049619d2b94d3f37 Mon Sep 17 00:00:00 2001 From: barneygale Date: Sat, 12 Apr 2025 15:12:28 +0100 Subject: [PATCH 06/13] Make keyword-only --- Doc/library/urllib.request.rst | 4 ++-- Lib/pathlib/__init__.py | 4 ++-- Lib/test/test_pathlib/test_pathlib.py | 4 ++-- Lib/test/test_urllib.py | 4 ++-- Lib/test/test_urllib2.py | 2 +- Lib/test/test_urllib2net.py | 2 +- Lib/urllib/request.py | 4 ++-- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Doc/library/urllib.request.rst b/Doc/library/urllib.request.rst index acf6501c52a9db..026299d3ca32b4 100644 --- a/Doc/library/urllib.request.rst +++ b/Doc/library/urllib.request.rst @@ -146,7 +146,7 @@ The :mod:`urllib.request` module defines the following functions: attribute to modify its position in the handlers list. -.. function:: pathname2url(path, add_scheme=False) +.. function:: pathname2url(path, *, add_scheme=False) Convert the given local path to a ``file:`` URL. This function uses :func:`~urllib.parse.quote` function to encode the path. @@ -175,7 +175,7 @@ The :mod:`urllib.request` module defines the following functions: The *add_scheme* argument was added. -.. function:: url2pathname(url, has_scheme=False) +.. function:: url2pathname(url, *, has_scheme=False) Convert the given ``file:`` URL to a local path. This function uses :func:`~urllib.parse.unquote` to decode the URL. diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py index 464a23c8e65eaf..7ce9bce8ba98a0 100644 --- a/Lib/pathlib/__init__.py +++ b/Lib/pathlib/__init__.py @@ -1271,7 +1271,7 @@ def as_uri(self): if not self.is_absolute(): raise ValueError("relative paths can't be expressed as file URIs") from urllib.request import pathname2url - return pathname2url(str(self), True) + return pathname2url(str(self), add_scheme=True) @classmethod def from_uri(cls, uri): @@ -1279,7 +1279,7 @@ def from_uri(cls, uri): from urllib.error import URLError from urllib.request import url2pathname try: - path = cls(url2pathname(uri, True)) + path = cls(url2pathname(uri, has_scheme=True)) except URLError as exc: raise ValueError(exc.reason) from None if not path.is_absolute(): diff --git a/Lib/test/test_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py index 07cbd2033dd8f3..a2dac372459e33 100644 --- a/Lib/test/test_pathlib/test_pathlib.py +++ b/Lib/test/test_pathlib/test_pathlib.py @@ -3302,8 +3302,8 @@ def test_from_uri_posix(self): @needs_posix def test_from_uri_pathname2url_posix(self): P = self.cls - self.assertEqual(P.from_uri(pathname2url('/foo/bar', True)), P('/foo/bar')) - self.assertEqual(P.from_uri(pathname2url('//foo/bar', True)), P('//foo/bar')) + self.assertEqual(P.from_uri(pathname2url('/foo/bar', add_scheme=True)), P('/foo/bar')) + self.assertEqual(P.from_uri(pathname2url('//foo/bar', add_scheme=True)), P('//foo/bar')) @needs_windows def test_absolute_windows(self): diff --git a/Lib/test/test_urllib.py b/Lib/test/test_urllib.py index 3e1c27658dd136..f8935ce5bd0228 100644 --- a/Lib/test/test_urllib.py +++ b/Lib/test/test_urllib.py @@ -476,7 +476,7 @@ def test_missing_localfile(self): def test_file_notexists(self): fd, tmp_file = tempfile.mkstemp() - tmp_file_canon_url = urllib.request.pathname2url(tmp_file, True) + tmp_file_canon_url = urllib.request.pathname2url(tmp_file, add_scheme=True) parsed = urllib.parse.urlsplit(tmp_file_canon_url) tmp_fileurl = parsed._replace(netloc='localhost').geturl() try: @@ -620,7 +620,7 @@ def tearDown(self): def constructLocalFileUrl(self, filePath): filePath = os.path.abspath(filePath) - return urllib.request.pathname2url(filePath, True) + return urllib.request.pathname2url(filePath, add_scheme=True) def createNewTempFile(self, data=b""): """Creates a new temporary file containing the specified data, diff --git a/Lib/test/test_urllib2.py b/Lib/test/test_urllib2.py index 8a491823c4ec80..f44d324b3ab763 100644 --- a/Lib/test/test_urllib2.py +++ b/Lib/test/test_urllib2.py @@ -809,7 +809,7 @@ def test_file(self): TESTFN = os_helper.TESTFN towrite = b"hello, world\n" - canonurl = urllib.request.pathname2url(os.path.abspath(TESTFN), True) + canonurl = urllib.request.pathname2url(os.path.abspath(TESTFN), add_scheme=True) parsed = urlsplit(canonurl) if parsed.netloc: raise unittest.SkipTest("non-local working directory") diff --git a/Lib/test/test_urllib2net.py b/Lib/test/test_urllib2net.py index 27d7516ef5816f..e6a18476908495 100644 --- a/Lib/test/test_urllib2net.py +++ b/Lib/test/test_urllib2net.py @@ -150,7 +150,7 @@ def test_file(self): f.write('hi there\n') f.close() urls = [ - urllib.request.pathname2url(os.path.abspath(TESTFN), True), + urllib.request.pathname2url(os.path.abspath(TESTFN), add_scheme=True), ('file:///nonsensename/etc/passwd', None, urllib.error.URLError), ] diff --git a/Lib/urllib/request.py b/Lib/urllib/request.py index fe2c5620082235..6e245d3e3dabf3 100644 --- a/Lib/urllib/request.py +++ b/Lib/urllib/request.py @@ -1466,7 +1466,7 @@ def get_names(self): def open_local_file(self, req): import email.utils import mimetypes - localfile = url2pathname(req.full_url, True) + localfile = url2pathname(req.full_url, has_scheme=True) try: stats = os.stat(localfile) size = stats.st_size @@ -1475,7 +1475,7 @@ def open_local_file(self, req): headers = email.message_from_string( 'Content-type: %s\nContent-length: %d\nLast-modified: %s\n' % (mtype or 'text/plain', size, modified)) - origurl = pathname2url(localfile, True) + origurl = pathname2url(localfile, add_scheme=True) return addinfourl(open(localfile, 'rb'), headers, origurl) except OSError as exp: raise URLError(exp, exp.filename) From 873b13cb5300d5944b20a690f0ddab88e0f15111 Mon Sep 17 00:00:00 2001 From: barneygale Date: Sat, 12 Apr 2025 15:37:49 +0100 Subject: [PATCH 07/13] Add a few more test cases --- Lib/test/test_urllib.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Lib/test/test_urllib.py b/Lib/test/test_urllib.py index f8935ce5bd0228..3e2e636677a926 100644 --- a/Lib/test/test_urllib.py +++ b/Lib/test/test_urllib.py @@ -1509,6 +1509,8 @@ def test_url2pathname(self): self.assertEqual(fn('//localhost/foo/bar'), f'{sep}foo{sep}bar') self.assertEqual(fn('///foo/bar'), f'{sep}foo{sep}bar') self.assertEqual(fn('////foo/bar'), f'{sep}{sep}foo{sep}bar') + self.assertEqual(fn('data:blah'), 'data:blah') + self.assertEqual(fn('data://blah'), 'data://blah') self.assertEqual(fn('file:', has_scheme=True), '') self.assertEqual(fn('FILE:', has_scheme=True), '') self.assertEqual(fn('FiLe:', has_scheme=True), '') @@ -1521,11 +1523,16 @@ def test_url2pathname(self): self.assertEqual(fn('file://localhost/foo/bar', has_scheme=True), f'{sep}foo{sep}bar') self.assertEqual(fn('file:///foo/bar', has_scheme=True), f'{sep}foo{sep}bar') self.assertEqual(fn('file:////foo/bar', has_scheme=True), f'{sep}{sep}foo{sep}bar') + self.assertEqual(fn('file:data:blah', has_scheme=True), 'data:blah') + self.assertEqual(fn('file:data://blah', has_scheme=True), 'data://blah') self.assertRaises(urllib.error.URLError, fn, '', has_scheme=True) self.assertRaises(urllib.error.URLError, fn, ':', has_scheme=True) self.assertRaises(urllib.error.URLError, fn, 'foo', has_scheme=True) self.assertRaises(urllib.error.URLError, fn, 'http:foo', has_scheme=True) self.assertRaises(urllib.error.URLError, fn, 'localfile:foo', has_scheme=True) + self.assertRaises(urllib.error.URLError, fn, 'data:foo', has_scheme=True) + self.assertRaises(urllib.error.URLError, fn, 'data:file:foo', has_scheme=True) + self.assertRaises(urllib.error.URLError, fn, 'data:file://foo', has_scheme=True) @unittest.skipUnless(sys.platform == 'win32', 'test specific to Windows pathnames.') From c9ba69c5729e994f422bd5ca24805a79b2bfcd1c Mon Sep 17 00:00:00 2001 From: barneygale Date: Sat, 12 Apr 2025 16:03:20 +0100 Subject: [PATCH 08/13] Fix windows tests --- Lib/test/test_urllib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_urllib.py b/Lib/test/test_urllib.py index 3e2e636677a926..9119cdd79e895f 100644 --- a/Lib/test/test_urllib.py +++ b/Lib/test/test_urllib.py @@ -1510,7 +1510,7 @@ def test_url2pathname(self): self.assertEqual(fn('///foo/bar'), f'{sep}foo{sep}bar') self.assertEqual(fn('////foo/bar'), f'{sep}{sep}foo{sep}bar') self.assertEqual(fn('data:blah'), 'data:blah') - self.assertEqual(fn('data://blah'), 'data://blah') + self.assertEqual(fn('data://blah'), f'data:{sep}{sep}') self.assertEqual(fn('file:', has_scheme=True), '') self.assertEqual(fn('FILE:', has_scheme=True), '') self.assertEqual(fn('FiLe:', has_scheme=True), '') @@ -1524,7 +1524,7 @@ def test_url2pathname(self): self.assertEqual(fn('file:///foo/bar', has_scheme=True), f'{sep}foo{sep}bar') self.assertEqual(fn('file:////foo/bar', has_scheme=True), f'{sep}{sep}foo{sep}bar') self.assertEqual(fn('file:data:blah', has_scheme=True), 'data:blah') - self.assertEqual(fn('file:data://blah', has_scheme=True), 'data://blah') + self.assertEqual(fn('file:data://blah', has_scheme=True), f'data:{sep}{sep}blah') self.assertRaises(urllib.error.URLError, fn, '', has_scheme=True) self.assertRaises(urllib.error.URLError, fn, ':', has_scheme=True) self.assertRaises(urllib.error.URLError, fn, 'foo', has_scheme=True) From 5e7b71ed8506d261e2d026a7041ba6c66da41c64 Mon Sep 17 00:00:00 2001 From: barneygale Date: Sun, 13 Apr 2025 13:21:39 +0100 Subject: [PATCH 09/13] Fix tests --- Lib/test/test_urllib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_urllib.py b/Lib/test/test_urllib.py index 9119cdd79e895f..bb434d395f7a48 100644 --- a/Lib/test/test_urllib.py +++ b/Lib/test/test_urllib.py @@ -1510,7 +1510,7 @@ def test_url2pathname(self): self.assertEqual(fn('///foo/bar'), f'{sep}foo{sep}bar') self.assertEqual(fn('////foo/bar'), f'{sep}{sep}foo{sep}bar') self.assertEqual(fn('data:blah'), 'data:blah') - self.assertEqual(fn('data://blah'), f'data:{sep}{sep}') + self.assertEqual(fn('data://blah'), f'data:{sep}{sep}blah') self.assertEqual(fn('file:', has_scheme=True), '') self.assertEqual(fn('FILE:', has_scheme=True), '') self.assertEqual(fn('FiLe:', has_scheme=True), '') From 7a4140c09deeb530801df8d0c814f2b5f4f92239 Mon Sep 17 00:00:00 2001 From: barneygale Date: Sun, 13 Apr 2025 17:49:08 +0100 Subject: [PATCH 10/13] has_scheme --> require_scheme --- Doc/library/urllib.request.rst | 14 +++--- Doc/whatsnew/3.14.rst | 3 +- Lib/pathlib/__init__.py | 2 +- Lib/test/test_urllib.py | 44 +++++++++---------- Lib/urllib/request.py | 8 ++-- ...-04-10-21-43-04.gh-issue-125866.EZ9X8D.rst | 2 +- 6 files changed, 37 insertions(+), 36 deletions(-) diff --git a/Doc/library/urllib.request.rst b/Doc/library/urllib.request.rst index 026299d3ca32b4..a5f1b9b292a85a 100644 --- a/Doc/library/urllib.request.rst +++ b/Doc/library/urllib.request.rst @@ -175,21 +175,21 @@ The :mod:`urllib.request` module defines the following functions: The *add_scheme* argument was added. -.. function:: url2pathname(url, *, has_scheme=False) +.. function:: url2pathname(url, *, require_scheme=False) Convert the given ``file:`` URL to a local path. This function uses :func:`~urllib.parse.unquote` to decode the URL. - If *has_scheme* is false (the default), the given value should omit a - ``file:`` scheme prefix. If *has_scheme* is set to true, the given value - should include the prefix; a :exc:`~urllib.error.URLError` is raised if it - doesn't. + If *require_scheme* is false (the default), the given value should omit a + ``file:`` scheme prefix. If *require_scheme* is set to true, the given + value should include the prefix; a :exc:`~urllib.error.URLError` is raised + if it doesn't. This example shows the function being used on Windows:: >>> from urllib.request import url2pathname >>> url = 'file:///C:/Program%20Files' - >>> url2pathname(url, has_scheme=True) + >>> url2pathname(url, require_scheme=True) 'C:\\Program Files' .. versionchanged:: 3.14 @@ -205,7 +205,7 @@ The :mod:`urllib.request` module defines the following functions: :exc:`~urllib.error.URLError` is raised. .. versionchanged:: next - The *has_scheme* argument was added. + The *require_scheme* argument was added. .. function:: getproxies() diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index dd5aa83ededa1b..2d26f467057d94 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -1202,7 +1202,8 @@ urllib In :func:`urllib.request.url2pathname`: - - Accept a complete URL when the new *has_scheme* argument is set to true. + - Accept a complete URL when the new *require_scheme* argument is set to + true. - Discard URL authorities that resolve to a local IP address. - Raise :exc:`~urllib.error.URLError` if a URL authority doesn't resolve to a local IP address, except on Windows where we return a UNC path. diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py index 7ce9bce8ba98a0..12cf9f579cb32d 100644 --- a/Lib/pathlib/__init__.py +++ b/Lib/pathlib/__init__.py @@ -1279,7 +1279,7 @@ def from_uri(cls, uri): from urllib.error import URLError from urllib.request import url2pathname try: - path = cls(url2pathname(uri, has_scheme=True)) + path = cls(url2pathname(uri, require_scheme=True)) except URLError as exc: raise ValueError(exc.reason) from None if not path.is_absolute(): diff --git a/Lib/test/test_urllib.py b/Lib/test/test_urllib.py index bb434d395f7a48..b158aba13ae72d 100644 --- a/Lib/test/test_urllib.py +++ b/Lib/test/test_urllib.py @@ -1511,28 +1511,28 @@ def test_url2pathname(self): self.assertEqual(fn('////foo/bar'), f'{sep}{sep}foo{sep}bar') self.assertEqual(fn('data:blah'), 'data:blah') self.assertEqual(fn('data://blah'), f'data:{sep}{sep}blah') - self.assertEqual(fn('file:', has_scheme=True), '') - self.assertEqual(fn('FILE:', has_scheme=True), '') - self.assertEqual(fn('FiLe:', has_scheme=True), '') - self.assertEqual(fn('file:/', has_scheme=True), f'{sep}') - self.assertEqual(fn('file:///', has_scheme=True), f'{sep}') - self.assertEqual(fn('file:////', has_scheme=True), f'{sep}{sep}') - self.assertEqual(fn('file:foo', has_scheme=True), 'foo') - self.assertEqual(fn('file:foo/bar', has_scheme=True), f'foo{sep}bar') - self.assertEqual(fn('file:/foo/bar', has_scheme=True), f'{sep}foo{sep}bar') - self.assertEqual(fn('file://localhost/foo/bar', has_scheme=True), f'{sep}foo{sep}bar') - self.assertEqual(fn('file:///foo/bar', has_scheme=True), f'{sep}foo{sep}bar') - self.assertEqual(fn('file:////foo/bar', has_scheme=True), f'{sep}{sep}foo{sep}bar') - self.assertEqual(fn('file:data:blah', has_scheme=True), 'data:blah') - self.assertEqual(fn('file:data://blah', has_scheme=True), f'data:{sep}{sep}blah') - self.assertRaises(urllib.error.URLError, fn, '', has_scheme=True) - self.assertRaises(urllib.error.URLError, fn, ':', has_scheme=True) - self.assertRaises(urllib.error.URLError, fn, 'foo', has_scheme=True) - self.assertRaises(urllib.error.URLError, fn, 'http:foo', has_scheme=True) - self.assertRaises(urllib.error.URLError, fn, 'localfile:foo', has_scheme=True) - self.assertRaises(urllib.error.URLError, fn, 'data:foo', has_scheme=True) - self.assertRaises(urllib.error.URLError, fn, 'data:file:foo', has_scheme=True) - self.assertRaises(urllib.error.URLError, fn, 'data:file://foo', has_scheme=True) + self.assertEqual(fn('file:', require_scheme=True), '') + self.assertEqual(fn('FILE:', require_scheme=True), '') + self.assertEqual(fn('FiLe:', require_scheme=True), '') + self.assertEqual(fn('file:/', require_scheme=True), f'{sep}') + self.assertEqual(fn('file:///', require_scheme=True), f'{sep}') + self.assertEqual(fn('file:////', require_scheme=True), f'{sep}{sep}') + self.assertEqual(fn('file:foo', require_scheme=True), 'foo') + self.assertEqual(fn('file:foo/bar', require_scheme=True), f'foo{sep}bar') + self.assertEqual(fn('file:/foo/bar', require_scheme=True), f'{sep}foo{sep}bar') + self.assertEqual(fn('file://localhost/foo/bar', require_scheme=True), f'{sep}foo{sep}bar') + self.assertEqual(fn('file:///foo/bar', require_scheme=True), f'{sep}foo{sep}bar') + self.assertEqual(fn('file:////foo/bar', require_scheme=True), f'{sep}{sep}foo{sep}bar') + self.assertEqual(fn('file:data:blah', require_scheme=True), 'data:blah') + self.assertEqual(fn('file:data://blah', require_scheme=True), f'data:{sep}{sep}blah') + self.assertRaises(urllib.error.URLError, fn, '', require_scheme=True) + self.assertRaises(urllib.error.URLError, fn, ':', require_scheme=True) + self.assertRaises(urllib.error.URLError, fn, 'foo', require_scheme=True) + self.assertRaises(urllib.error.URLError, fn, 'http:foo', require_scheme=True) + self.assertRaises(urllib.error.URLError, fn, 'localfile:foo', require_scheme=True) + self.assertRaises(urllib.error.URLError, fn, 'data:foo', require_scheme=True) + self.assertRaises(urllib.error.URLError, fn, 'data:file:foo', require_scheme=True) + self.assertRaises(urllib.error.URLError, fn, 'data:file://foo', require_scheme=True) @unittest.skipUnless(sys.platform == 'win32', 'test specific to Windows pathnames.') diff --git a/Lib/urllib/request.py b/Lib/urllib/request.py index 6e245d3e3dabf3..0bd58a635a079d 100644 --- a/Lib/urllib/request.py +++ b/Lib/urllib/request.py @@ -1466,7 +1466,7 @@ def get_names(self): def open_local_file(self, req): import email.utils import mimetypes - localfile = url2pathname(req.full_url, has_scheme=True) + localfile = url2pathname(req.full_url, require_scheme=True) try: stats = os.stat(localfile) size = stats.st_size @@ -1634,13 +1634,13 @@ def data_open(self, req): # Code move from the old urllib module -def url2pathname(url, has_scheme=False): +def url2pathname(url, require_scheme=False): """Convert the given file URL to a local file system path. - The 'file:' scheme prefix must be omitted unless *has_scheme* + The 'file:' scheme prefix must be omitted unless *require_scheme* is set to true. """ - if has_scheme: + if require_scheme: scheme, url = _splittype(url) if scheme != 'file': raise URLError("URL is missing a 'file:' scheme") diff --git a/Misc/NEWS.d/next/Library/2025-04-10-21-43-04.gh-issue-125866.EZ9X8D.rst b/Misc/NEWS.d/next/Library/2025-04-10-21-43-04.gh-issue-125866.EZ9X8D.rst index 03049aab8d5f88..0d60a16a17753a 100644 --- a/Misc/NEWS.d/next/Library/2025-04-10-21-43-04.gh-issue-125866.EZ9X8D.rst +++ b/Misc/NEWS.d/next/Library/2025-04-10-21-43-04.gh-issue-125866.EZ9X8D.rst @@ -1,4 +1,4 @@ Add optional *add_scheme* argument to :func:`urllib.request.pathname2url`; when -set to true, a complete URL is returned. Likewise add optional *has_scheme* +set to true, a complete URL is returned. Likewise add optional *require_scheme* argument to :func:`~urllib.request.url2pathname`; when set to true, a complete URL is accepted. From ec272ef506a13164af1158dce5cdcae792652fad Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Sun, 13 Apr 2025 18:08:24 +0100 Subject: [PATCH 11/13] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/urllib/request.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/urllib/request.py b/Lib/urllib/request.py index 0bd58a635a079d..2c9c7b6ca5394d 100644 --- a/Lib/urllib/request.py +++ b/Lib/urllib/request.py @@ -1634,7 +1634,7 @@ def data_open(self, req): # Code move from the old urllib module -def url2pathname(url, require_scheme=False): +def url2pathname(url, *, require_scheme=False): """Convert the given file URL to a local file system path. The 'file:' scheme prefix must be omitted unless *require_scheme* @@ -1667,7 +1667,7 @@ def url2pathname(url, require_scheme=False): return unquote(url, encoding=encoding, errors=errors) -def pathname2url(pathname, add_scheme=False): +def pathname2url(pathname, *, add_scheme=False): """Convert the given local file system path to a file URL. The 'file:' scheme prefix is omitted unless *add_scheme* From c570e2a3d16a139438b0f49fab12fbd1c0c9fe00 Mon Sep 17 00:00:00 2001 From: barneygale Date: Sun, 13 Apr 2025 18:26:47 +0100 Subject: [PATCH 12/13] Use subtests --- Lib/test/test_urllib.py | 83 +++++++++++++++++++++++++++-------------- 1 file changed, 55 insertions(+), 28 deletions(-) diff --git a/Lib/test/test_urllib.py b/Lib/test/test_urllib.py index b158aba13ae72d..dd708b5249e45f 100644 --- a/Lib/test/test_urllib.py +++ b/Lib/test/test_urllib.py @@ -1435,12 +1435,21 @@ def test_pathname2url(self): self.assertEqual(fn(f'a{sep}b.c'), 'a/b.c') self.assertEqual(fn(f'{sep}a{sep}b.c'), '///a/b.c') self.assertEqual(fn(f'{sep}a{sep}b%#c'), '///a/b%25%23c') - self.assertEqual(fn('', add_scheme=True), 'file:') - self.assertEqual(fn(sep, add_scheme=True), 'file:///') - self.assertEqual(fn('a', add_scheme=True), 'file:a') - self.assertEqual(fn(f'a{sep}b.c', add_scheme=True), 'file:a/b.c') - self.assertEqual(fn(f'{sep}a{sep}b.c', add_scheme=True), 'file:///a/b.c') - self.assertEqual(fn(f'{sep}a{sep}b%#c', add_scheme=True), 'file:///a/b%25%23c') + + def test_pathname2url_add_scheme(self): + sep = os.path.sep + subtests = [ + ('', 'file:'), + (sep, 'file:///'), + ('a', 'file:a'), + (f'a{sep}b.c', 'file:a/b.c'), + (f'{sep}a{sep}b.c', 'file:///a/b.c'), + (f'{sep}a{sep}b%#c', 'file:///a/b%25%23c'), + ] + for path, expected_url in subtests: + with self.subTest(path=path): + self.assertEqual( + urllib.request.pathname2url(path, add_scheme=True), expected_url) @unittest.skipUnless(sys.platform == 'win32', 'test specific to Windows pathnames.') @@ -1511,28 +1520,46 @@ def test_url2pathname(self): self.assertEqual(fn('////foo/bar'), f'{sep}{sep}foo{sep}bar') self.assertEqual(fn('data:blah'), 'data:blah') self.assertEqual(fn('data://blah'), f'data:{sep}{sep}blah') - self.assertEqual(fn('file:', require_scheme=True), '') - self.assertEqual(fn('FILE:', require_scheme=True), '') - self.assertEqual(fn('FiLe:', require_scheme=True), '') - self.assertEqual(fn('file:/', require_scheme=True), f'{sep}') - self.assertEqual(fn('file:///', require_scheme=True), f'{sep}') - self.assertEqual(fn('file:////', require_scheme=True), f'{sep}{sep}') - self.assertEqual(fn('file:foo', require_scheme=True), 'foo') - self.assertEqual(fn('file:foo/bar', require_scheme=True), f'foo{sep}bar') - self.assertEqual(fn('file:/foo/bar', require_scheme=True), f'{sep}foo{sep}bar') - self.assertEqual(fn('file://localhost/foo/bar', require_scheme=True), f'{sep}foo{sep}bar') - self.assertEqual(fn('file:///foo/bar', require_scheme=True), f'{sep}foo{sep}bar') - self.assertEqual(fn('file:////foo/bar', require_scheme=True), f'{sep}{sep}foo{sep}bar') - self.assertEqual(fn('file:data:blah', require_scheme=True), 'data:blah') - self.assertEqual(fn('file:data://blah', require_scheme=True), f'data:{sep}{sep}blah') - self.assertRaises(urllib.error.URLError, fn, '', require_scheme=True) - self.assertRaises(urllib.error.URLError, fn, ':', require_scheme=True) - self.assertRaises(urllib.error.URLError, fn, 'foo', require_scheme=True) - self.assertRaises(urllib.error.URLError, fn, 'http:foo', require_scheme=True) - self.assertRaises(urllib.error.URLError, fn, 'localfile:foo', require_scheme=True) - self.assertRaises(urllib.error.URLError, fn, 'data:foo', require_scheme=True) - self.assertRaises(urllib.error.URLError, fn, 'data:file:foo', require_scheme=True) - self.assertRaises(urllib.error.URLError, fn, 'data:file://foo', require_scheme=True) + + def test_url2pathname_require_scheme(self): + sep = os.path.sep + subtests = [ + ('file:', ''), + ('FILE:', ''), + ('FiLe:', ''), + ('file:/', f'{sep}'), + ('file:///', f'{sep}'), + ('file:////', f'{sep}{sep}'), + ('file:foo', 'foo'), + ('file:foo/bar', f'foo{sep}bar'), + ('file:/foo/bar', f'{sep}foo{sep}bar'), + ('file://localhost/foo/bar', f'{sep}foo{sep}bar'), + ('file:///foo/bar', f'{sep}foo{sep}bar'), + ('file:////foo/bar', f'{sep}{sep}foo{sep}bar'), + ('file:data:blah', 'data:blah'), + ('file:data://blah', f'data:{sep}{sep}blah'), + ] + for url, expected_path in subtests: + with self.subTest(url=url): + self.assertEqual( + urllib.request.url2pathname(url, require_scheme=True), + expected_path) + error_subtests = [ + '', + ':', + 'foo', + 'http:foo', + 'localfile:foo', + 'data:foo', + 'data:file:foo', + 'data:file://foo', + ] + for url in error_subtests: + with self.subTest(url=url): + self.assertRaises( + urllib.error.URLError, + urllib.request.url2pathname, + url, require_scheme=True) @unittest.skipUnless(sys.platform == 'win32', 'test specific to Windows pathnames.') From b5e2fb78863cad053bcfb8e4bac3580755f8ea14 Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Mon, 14 Apr 2025 01:20:20 +0100 Subject: [PATCH 13/13] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/test/test_urllib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_urllib.py b/Lib/test/test_urllib.py index dd708b5249e45f..abfbed8840ca03 100644 --- a/Lib/test/test_urllib.py +++ b/Lib/test/test_urllib.py @@ -1544,6 +1544,7 @@ def test_url2pathname_require_scheme(self): self.assertEqual( urllib.request.url2pathname(url, require_scheme=True), expected_path) + error_subtests = [ '', ':',