Skip to content

bpo-44412: add os.path.fileuri() function. #26708

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Doc/library/os.path.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Doc/library/pathlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 29 additions & 1 deletion Lib/ntpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@
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",
"getatime","getctime", "islink","exists","lexists","isdir","isfile",
"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):
Expand Down Expand Up @@ -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.
Expand Down
37 changes: 7 additions & 30 deletions Lib/nturl2path.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
23 changes: 1 addition & 22 deletions Lib/pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = [
Expand Down Expand Up @@ -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 = '/'
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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):
Expand Down
19 changes: 18 additions & 1 deletion Lib/posixpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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):
Expand Down Expand Up @@ -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")
26 changes: 26 additions & 0 deletions Lib/test/test_ntpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down Expand Up @@ -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()
15 changes: 15 additions & 0 deletions Lib/test/test_posixpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
15 changes: 6 additions & 9 deletions Lib/test/test_urllib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
]
Expand All @@ -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'),
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :func:`os.path.fileuri` function that represents a file path as a
``file://`` URI.