diff --git a/Doc/library/os.path.rst b/Doc/library/os.path.rst index d06d9ce8c9e3d7..517b6e6645d52a 100644 --- a/Doc/library/os.path.rst +++ b/Doc/library/os.path.rst @@ -205,6 +205,14 @@ the :mod:`glob` module.) Accepts a :term:`path-like object`. +.. function:: fileuri(path): + + Represent the given path as a ``file://`` URI. :exc:`ValueError` is raised + if the path isn't absolute. + + .. versionadded:: 3.11 + + .. function:: getatime(path) Return the time of last access of *path*. The return value is a floating point number giving diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index b6507eb4d6fa2c..9e2bd77965f111 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1274,6 +1274,7 @@ Below is a table mapping various :mod:`os` functions to their corresponding :func:`os.path.dirname` :data:`PurePath.parent` :func:`os.path.samefile` :meth:`Path.samefile` :func:`os.path.splitext` :data:`PurePath.suffix` +:func:`os.path.fileuri` :meth:`PurePath.as_uri` ==================================== ============================== .. rubric:: Footnotes diff --git a/Lib/ntpath.py b/Lib/ntpath.py index 527c7ae1938fbb..69daa262aceeed 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -22,6 +22,7 @@ import stat import genericpath from genericpath import * +from urllib.parse import quote_from_bytes as urlquote __all__ = ["normcase","isabs","join","splitdrive","split","splitext", "basename","dirname","commonprefix","getsize","getmtime", @@ -29,7 +30,7 @@ "ismount", "expanduser","expandvars","normpath","abspath", "curdir","pardir","sep","pathsep","defpath","altsep", "extsep","devnull","realpath","supports_unicode_filenames","relpath", - "samefile", "sameopenfile", "samestat", "commonpath"] + "samefile", "sameopenfile", "samestat", "commonpath", "fileuri"] def _get_bothseps(path): if isinstance(path, bytes): @@ -798,6 +799,33 @@ def commonpath(paths): raise +def fileuri(path): + """ + Return the given path expressed as a ``file://`` URI. + """ + + # File URIs use the UTF-8 encoding on Windows. + path = os.fspath(path) + if not isinstance(path, bytes): + path = path.encode('utf8') + + # Strip UNC prefixes + path = path.replace(b'\\', b'/') + if path.startswith(b'//?/UNC/'): + path = b'//' + path[8:] + elif path.startswith(b'//?/'): + path = path[4:] + + if path[1:3] == b':/': + # It's a path on a local drive => 'file:///c:/a/b' + return 'file:///' + urlquote(path[:1]) + ':' + urlquote(path[2:]) + elif path[0:2] == b'//': + # It's a path on a network drive => 'file://host/share/a/b' + return 'file:' + urlquote(path) + else: + raise ValueError("relative path can't be expressed as a file URI") + + try: # The genericpath.isdir implementation uses os.stat and checks the mode # attribute to tell whether or not the path is a directory. diff --git a/Lib/nturl2path.py b/Lib/nturl2path.py index 61852aff58912d..d3381806a8cd4f 100644 --- a/Lib/nturl2path.py +++ b/Lib/nturl2path.py @@ -3,6 +3,9 @@ This module only exists to provide OS-specific code for urllib.requests, thus do not use directly. """ + +import ntpath + # Testing is done through test_urllib. def url2pathname(url): @@ -49,33 +52,7 @@ def pathname2url(p): # C:\foo\bar\spam.foo # becomes # ///C:/foo/bar/spam.foo - import urllib.parse - # First, clean up some special forms. We are going to sacrifice - # the additional information anyway - if p[:4] == '\\\\?\\': - p = p[4:] - if p[:4].upper() == 'UNC\\': - p = '\\' + p[4:] - elif p[1:2] != ':': - raise OSError('Bad path: ' + p) - if not ':' in p: - # No drive specifier, just convert slashes and quote the name - if p[:2] == '\\\\': - # path is something like \\host\path\on\remote\host - # convert this to ////host/path/on/remote/host - # (notice doubling of slashes at the start of the path) - p = '\\\\' + p - components = p.split('\\') - return urllib.parse.quote('/'.join(components)) - 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()) - components = comp[1].split('\\') - path = '///' + drive + ':' - for comp in components: - if comp: - path = path + '/' + urllib.parse.quote(comp) - return path + try: + return ntpath.fileuri(p).removeprefix('file:') + except ValueError: + raise OSError('Bad path: ' + p) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 8e6eb48b9767ca..92f21a6b12cabc 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -11,7 +11,6 @@ from errno import EINVAL, ENOENT, ENOTDIR, EBADF, ELOOP from operator import attrgetter from stat import S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO -from urllib.parse import quote_from_bytes as urlquote_from_bytes __all__ = [ @@ -205,18 +204,6 @@ def is_reserved(self, parts): return False return parts[-1].partition('.')[0].upper() in self.reserved_names - def make_uri(self, path): - # Under Windows, file URIs use the UTF-8 encoding. - drive = path.drive - if len(drive) == 2 and drive[1] == ':': - # It's a path on a local drive => 'file:///c:/a/b' - rest = path.as_posix()[2:].lstrip('/') - return 'file:///%s/%s' % ( - drive, urlquote_from_bytes(rest.encode('utf-8'))) - else: - # It's a path on a network drive => 'file://host/share/a/b' - return 'file:' + urlquote_from_bytes(path.as_posix().encode('utf-8')) - class _PosixFlavour(_Flavour): sep = '/' @@ -253,12 +240,6 @@ def compile_pattern(self, pattern): def is_reserved(self, parts): return False - def make_uri(self, path): - # We represent the path using the local filesystem encoding, - # for portability to other applications. - bpath = bytes(path) - return 'file://' + urlquote_from_bytes(bpath) - _windows_flavour = _WindowsFlavour() _posix_flavour = _PosixFlavour() @@ -635,9 +616,7 @@ def __repr__(self): def as_uri(self): """Return the path as a 'file' URI.""" - if not self.is_absolute(): - raise ValueError("relative path can't be expressed as a file URI") - return self._flavour.make_uri(self) + return self._flavour.pathmod.fileuri(str(self)) @property def _cparts(self): diff --git a/Lib/posixpath.py b/Lib/posixpath.py index 259baa64b193b8..e9e176303e9174 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -27,6 +27,7 @@ import stat import genericpath from genericpath import * +from urllib.parse import quote_from_bytes as urlquote __all__ = ["normcase","isabs","join","splitdrive","split","splitext", "basename","dirname","commonprefix","getsize","getmtime", @@ -35,7 +36,7 @@ "samefile","sameopenfile","samestat", "curdir","pardir","sep","pathsep","defpath","altsep","extsep", "devnull","realpath","supports_unicode_filenames","relpath", - "commonpath"] + "commonpath", "fileuri"] def _get_sep(path): @@ -538,3 +539,19 @@ def commonpath(paths): except (TypeError, AttributeError): genericpath._check_arg_types('commonpath', *paths) raise + + +def fileuri(path): + """ + Return the given path expressed as a ``file://`` URI. + """ + + # We represent the path using the local filesystem encoding, + # for portability to other applications. + path = os.fspath(path) + if not isinstance(path, bytes): + path = os.fsencode(path) + if path[:1] == b'/': + return 'file://' + urlquote(path) + else: + raise ValueError("relative path can't be expressed as a file URI") diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py index 661c59d6171635..60155ff1ff7b95 100644 --- a/Lib/test/test_ntpath.py +++ b/Lib/test/test_ntpath.py @@ -786,6 +786,26 @@ def test_nt_helpers(self): self.assertIsInstance(b_final_path, bytes) self.assertGreater(len(b_final_path), 0) + def test_fileuri(self): + fn = ntpath.fileuri + self.assertEqual(fn('c:/'), 'file:///c:/') + self.assertEqual(fn('c:/a/b.c'), 'file:///c:/a/b.c') + self.assertEqual(fn('c:/a/b%#c'), 'file:///c:/a/b%25%23c') + self.assertEqual(fn('c:/a/b\xe9'), 'file:///c:/a/b%C3%A9') + self.assertEqual(fn('//some/share/'), 'file://some/share/') + self.assertEqual(fn('//some/share/a/b.c'), 'file://some/share/a/b.c') + self.assertEqual(fn('//some/share/a/b%#c\xe9'), + 'file://some/share/a/b%25%23c%C3%A9') + + self.assertEqual(fn(b'c:/'), 'file:///c:/') + self.assertEqual(fn(b'c:/a/b.c'), 'file:///c:/a/b.c') + self.assertEqual(fn(b'c:/a/b%#c'), 'file:///c:/a/b%25%23c') + self.assertEqual(fn(b'c:/a/b\xc3\xa9'), 'file:///c:/a/b%C3%A9') + self.assertEqual(fn(b'//some/share/'), 'file://some/share/') + self.assertEqual(fn(b'//some/share/a/b.c'), 'file://some/share/a/b.c') + self.assertEqual(fn(b'//some/share/a/b%#c\xc3\xa9'), + 'file://some/share/a/b%25%23c%C3%A9') + class NtCommonTest(test_genericpath.CommonTest, unittest.TestCase): pathmodule = ntpath attributes = ['relpath'] @@ -864,6 +884,12 @@ def test_path_commonpath(self): def test_path_isdir(self): self._check_function(self.path.isdir) + def test_path_fileuri(self): + file_name = 'C:\\foo\\bar' + file_path = FakePath(file_name) + self.assertPathEqual( + self.path.fileuri(file_name), + self.path.fileuri(file_path)) if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py index 8d398ec0103544..a934b90e6b967d 100644 --- a/Lib/test/test_posixpath.py +++ b/Lib/test/test_posixpath.py @@ -676,6 +676,15 @@ def check_error(exc, paths): self.assertRaises(TypeError, posixpath.commonpath, ['usr/lib/', b'/usr/lib/python3']) + def test_fileuri(self): + self.assertEqual(posixpath.fileuri('/'), 'file:///') + self.assertEqual(posixpath.fileuri('/a/b.c'), 'file:///a/b.c') + self.assertEqual(posixpath.fileuri('/a/b%#c'), 'file:///a/b%25%23c') + + self.assertEqual(posixpath.fileuri(b'/'), 'file:///') + self.assertEqual(posixpath.fileuri(b'/a/b.c'), 'file:///a/b.c') + self.assertEqual(posixpath.fileuri(b'/a/b%#c'), 'file:///a/b%25%23c') + class PosixCommonTest(test_genericpath.CommonTest, unittest.TestCase): pathmodule = posixpath @@ -752,6 +761,12 @@ def test_path_commonpath(self): common_path = self.path.commonpath([self.file_path, self.file_name]) self.assertEqual(common_path, self.file_name) + def test_path_fileuri(self): + file_name = '/foo/bar' + file_path = FakePath(file_name) + self.assertEqual( + self.path.fileuri(file_name), + self.path.fileuri(file_path)) if __name__=="__main__": unittest.main() diff --git a/Lib/test/test_urllib.py b/Lib/test/test_urllib.py index 82f1d9dc2e7bb3..9914f6fb909c83 100644 --- a/Lib/test/test_urllib.py +++ b/Lib/test/test_urllib.py @@ -1663,7 +1663,7 @@ def test_non_ascii_drive_letter(self): self.assertRaises(IOError, url2pathname, "///\u00e8|/") def test_roundtrip_url2pathname(self): - list_of_paths = ['C:', + list_of_paths = ['C:\\', r'\\\C\test\\', r'C:\foo\bar\spam.foo' ] @@ -1673,16 +1673,13 @@ def test_roundtrip_url2pathname(self): class PathName2URLTests(unittest.TestCase): def test_converting_drive_letter(self): - self.assertEqual(pathname2url("C:"), '///C:') - self.assertEqual(pathname2url("C:\\"), '///C:') + self.assertEqual(pathname2url("C:\\"), '///C:/') def test_converting_when_no_drive_letter(self): self.assertEqual(pathname2url(r"\\\folder\test" "\\"), - '/////folder/test/') + '///folder/test/') self.assertEqual(pathname2url(r"\\folder\test" "\\"), - '////folder/test/') - self.assertEqual(pathname2url(r"\folder\test" "\\"), - '/folder/test/') + '//folder/test/') def test_simple_compare(self): self.assertEqual(pathname2url(r'C:\foo\bar\spam.foo'), @@ -1692,8 +1689,8 @@ def test_long_drive_letter(self): self.assertRaises(IOError, pathname2url, "XX:\\") def test_roundtrip_pathname2url(self): - list_of_paths = ['///C:', - '/////folder/test/', + list_of_paths = ['///C:/', + '//server/folder/test/', '///C:/foo/bar/spam.foo'] for path in list_of_paths: self.assertEqual(pathname2url(url2pathname(path)), path) diff --git a/Misc/NEWS.d/next/Library/2021-06-13-17-35-24.bpo-44412.qFjsi6.rst b/Misc/NEWS.d/next/Library/2021-06-13-17-35-24.bpo-44412.qFjsi6.rst new file mode 100644 index 00000000000000..adedbf3cc3791d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-06-13-17-35-24.bpo-44412.qFjsi6.rst @@ -0,0 +1,2 @@ +Add :func:`os.path.fileuri` function that represents a file path as a +``file://`` URI.