GH-125866: Support complete "file:" URLs in urllib (#132378)

Add optional *add_scheme* argument to `urllib.request.pathname2url()`; when
set to true, a complete URL is returned. Likewise add optional
*require_scheme* argument to `url2pathname()`; when set to true, a complete
URL is accepted.

Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
This commit is contained in:
Barney Gale 2025-04-14 01:49:02 +01:00 committed by GitHub
parent 4d3ad0467e
commit ccad61e35d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 121 additions and 33 deletions

View file

@ -1466,17 +1466,16 @@ class FileHandler(BaseHandler):
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, require_scheme=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, add_scheme=True)
return addinfourl(open(localfile, 'rb'), headers, origurl)
except OSError as exp:
raise URLError(exp, exp.filename)
@ -1635,9 +1634,16 @@ class DataHandler(BaseHandler):
# Code move from the old urllib module
def url2pathname(url):
"""OS-specific conversion from a relative URL of the 'file' scheme
to a file system path; not recommended for general use."""
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*
is set to true.
"""
if require_scheme:
scheme, url = _splittype(url)
if scheme != 'file':
raise URLError("URL is missing a 'file:' scheme")
authority, url = _splithost(url)
if os.name == 'nt':
if not _is_local_authority(authority):
@ -1661,13 +1667,17 @@ def url2pathname(url):
return unquote(url, encoding=encoding, errors=errors)
def pathname2url(pathname):
"""OS-specific conversion from a file system path to a relative URL
of the 'file' scheme; not recommended for general use."""
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.
"""
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 +1699,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