mirror of
https://github.com/python/cpython.git
synced 2025-10-03 05:35:59 +00:00
GH-107465: Add pathlib.Path.from_uri()
classmethod. (#107640)
This method supports file URIs (including variants) as described in RFC 8089, such as URIs generated by `pathlib.Path.as_uri()` and `urllib.request.pathname2url()`. The method is added to `Path` rather than `PurePath` because it uses `os.fsdecode()`, and so its results vary from system to system. I intend to deprecate `PurePath.as_uri()` and move it to `Path` for the same reason. Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com>
This commit is contained in:
parent
06faa9a39b
commit
15de493395
5 changed files with 120 additions and 5 deletions
|
@ -850,6 +850,42 @@ call fails (for example because the path doesn't exist).
|
||||||
.. versionadded:: 3.5
|
.. versionadded:: 3.5
|
||||||
|
|
||||||
|
|
||||||
|
.. classmethod:: Path.from_uri(uri)
|
||||||
|
|
||||||
|
Return a new path object from parsing a 'file' URI conforming to
|
||||||
|
:rfc:`8089`. For example::
|
||||||
|
|
||||||
|
>>> p = Path.from_uri('file:///etc/hosts')
|
||||||
|
PosixPath('/etc/hosts')
|
||||||
|
|
||||||
|
On Windows, DOS device and UNC paths may be parsed from URIs::
|
||||||
|
|
||||||
|
>>> p = Path.from_uri('file:///c:/windows')
|
||||||
|
WindowsPath('c:/windows')
|
||||||
|
>>> p = Path.from_uri('file://server/share')
|
||||||
|
WindowsPath('//server/share')
|
||||||
|
|
||||||
|
Several variant forms are supported::
|
||||||
|
|
||||||
|
>>> p = Path.from_uri('file:////server/share')
|
||||||
|
WindowsPath('//server/share')
|
||||||
|
>>> p = Path.from_uri('file://///server/share')
|
||||||
|
WindowsPath('//server/share')
|
||||||
|
>>> p = Path.from_uri('file:c:/windows')
|
||||||
|
WindowsPath('c:/windows')
|
||||||
|
>>> p = Path.from_uri('file:/c|/windows')
|
||||||
|
WindowsPath('c:/windows')
|
||||||
|
|
||||||
|
:exc:`ValueError` is raised if the URI does not start with ``file:``, or
|
||||||
|
the parsed path isn't absolute.
|
||||||
|
|
||||||
|
:func:`os.fsdecode` is used to decode percent-escaped byte sequences, and
|
||||||
|
so file URIs are not portable across machines with different
|
||||||
|
:ref:`filesystem encodings <filesystem-encoding>`.
|
||||||
|
|
||||||
|
.. versionadded:: 3.13
|
||||||
|
|
||||||
|
|
||||||
.. method:: Path.stat(*, follow_symlinks=True)
|
.. method:: Path.stat(*, follow_symlinks=True)
|
||||||
|
|
||||||
Return a :class:`os.stat_result` object containing information about this path, like :func:`os.stat`.
|
Return a :class:`os.stat_result` object containing information about this path, like :func:`os.stat`.
|
||||||
|
|
|
@ -184,6 +184,10 @@ pathlib
|
||||||
:exc:`NotImplementedError` when a path operation isn't supported.
|
:exc:`NotImplementedError` when a path operation isn't supported.
|
||||||
(Contributed by Barney Gale in :gh:`89812`.)
|
(Contributed by Barney Gale in :gh:`89812`.)
|
||||||
|
|
||||||
|
* Add :meth:`pathlib.Path.from_uri`, a new constructor to create a :class:`pathlib.Path`
|
||||||
|
object from a 'file' URI (``file:/``).
|
||||||
|
(Contributed by Barney Gale in :gh:`107465`.)
|
||||||
|
|
||||||
* Add support for recursive wildcards in :meth:`pathlib.PurePath.match`.
|
* Add support for recursive wildcards in :meth:`pathlib.PurePath.match`.
|
||||||
(Contributed by Barney Gale in :gh:`73435`.)
|
(Contributed by Barney Gale in :gh:`73435`.)
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,6 @@ import warnings
|
||||||
from _collections_abc import Sequence
|
from _collections_abc import Sequence
|
||||||
from errno import ENOENT, ENOTDIR, EBADF, ELOOP, EINVAL
|
from errno import ENOENT, ENOTDIR, EBADF, ELOOP, EINVAL
|
||||||
from stat import S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO
|
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
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import pwd
|
import pwd
|
||||||
|
@ -452,7 +451,8 @@ class PurePath:
|
||||||
# It's a posix path => 'file:///etc/hosts'
|
# It's a posix path => 'file:///etc/hosts'
|
||||||
prefix = 'file://'
|
prefix = 'file://'
|
||||||
path = str(self)
|
path = str(self)
|
||||||
return prefix + urlquote_from_bytes(os.fsencode(path))
|
from urllib.parse import quote_from_bytes
|
||||||
|
return prefix + quote_from_bytes(os.fsencode(path))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _str_normcase(self):
|
def _str_normcase(self):
|
||||||
|
@ -814,9 +814,10 @@ class _PathBase(PurePath):
|
||||||
__bytes__ = None
|
__bytes__ = None
|
||||||
__fspath__ = None # virtual paths have no local file system representation
|
__fspath__ = None # virtual paths have no local file system representation
|
||||||
|
|
||||||
def _unsupported(self, method_name):
|
@classmethod
|
||||||
msg = f"{type(self).__name__}.{method_name}() is unsupported"
|
def _unsupported(cls, method_name):
|
||||||
if isinstance(self, Path):
|
msg = f"{cls.__name__}.{method_name}() is unsupported"
|
||||||
|
if issubclass(cls, Path):
|
||||||
msg += " on this system"
|
msg += " on this system"
|
||||||
raise UnsupportedOperation(msg)
|
raise UnsupportedOperation(msg)
|
||||||
|
|
||||||
|
@ -1418,6 +1419,11 @@ class _PathBase(PurePath):
|
||||||
"""
|
"""
|
||||||
self._unsupported("group")
|
self._unsupported("group")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_uri(cls, uri):
|
||||||
|
"""Return a new path from the given 'file' URI."""
|
||||||
|
cls._unsupported("from_uri")
|
||||||
|
|
||||||
def as_uri(self):
|
def as_uri(self):
|
||||||
"""Return the path as a URI."""
|
"""Return the path as a URI."""
|
||||||
self._unsupported("as_uri")
|
self._unsupported("as_uri")
|
||||||
|
@ -1661,6 +1667,30 @@ class Path(_PathBase):
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@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}")
|
||||||
|
path = uri[5:]
|
||||||
|
if path[:3] == '///':
|
||||||
|
# Remove empty authority
|
||||||
|
path = path[2:]
|
||||||
|
elif path[:12] == '//localhost/':
|
||||||
|
# Remove 'localhost' authority
|
||||||
|
path = path[11:]
|
||||||
|
if path[:3] == '///' or (path[:1] == '/' and path[2:3] in ':|'):
|
||||||
|
# Remove slash before DOS device/UNC path
|
||||||
|
path = path[1:]
|
||||||
|
if path[1:2] == '|':
|
||||||
|
# Replace bar with colon in DOS drive
|
||||||
|
path = path[:1] + ':' + path[2:]
|
||||||
|
from urllib.parse import unquote_to_bytes
|
||||||
|
path = cls(os.fsdecode(unquote_to_bytes(path)))
|
||||||
|
if not path.is_absolute():
|
||||||
|
raise ValueError(f"URI is not absolute: {uri!r}")
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
class PosixPath(Path, PurePosixPath):
|
class PosixPath(Path, PurePosixPath):
|
||||||
"""Path subclass for non-Windows systems.
|
"""Path subclass for non-Windows systems.
|
||||||
|
|
|
@ -11,6 +11,7 @@ import stat
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
from urllib.request import pathname2url
|
||||||
|
|
||||||
from test.support import import_helper
|
from test.support import import_helper
|
||||||
from test.support import set_recursion_limit
|
from test.support import set_recursion_limit
|
||||||
|
@ -3602,6 +3603,24 @@ class PosixPathTest(PathTest):
|
||||||
self.fail("Bad file descriptor not handled.")
|
self.fail("Bad file descriptor not handled.")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def test_from_uri(self):
|
||||||
|
P = self.cls
|
||||||
|
self.assertEqual(P.from_uri('file:/foo/bar'), P('/foo/bar'))
|
||||||
|
self.assertEqual(P.from_uri('file://foo/bar'), P('//foo/bar'))
|
||||||
|
self.assertEqual(P.from_uri('file:///foo/bar'), P('/foo/bar'))
|
||||||
|
self.assertEqual(P.from_uri('file:////foo/bar'), P('//foo/bar'))
|
||||||
|
self.assertEqual(P.from_uri('file://localhost/foo/bar'), P('/foo/bar'))
|
||||||
|
self.assertRaises(ValueError, P.from_uri, 'foo/bar')
|
||||||
|
self.assertRaises(ValueError, P.from_uri, '/foo/bar')
|
||||||
|
self.assertRaises(ValueError, P.from_uri, '//foo/bar')
|
||||||
|
self.assertRaises(ValueError, P.from_uri, 'file:foo/bar')
|
||||||
|
self.assertRaises(ValueError, P.from_uri, 'http://foo/bar')
|
||||||
|
|
||||||
|
def test_from_uri_pathname2url(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'))
|
||||||
|
|
||||||
|
|
||||||
@only_nt
|
@only_nt
|
||||||
class WindowsPathTest(PathTest):
|
class WindowsPathTest(PathTest):
|
||||||
|
@ -3721,6 +3740,31 @@ class WindowsPathTest(PathTest):
|
||||||
env['HOME'] = 'C:\\Users\\eve'
|
env['HOME'] = 'C:\\Users\\eve'
|
||||||
check()
|
check()
|
||||||
|
|
||||||
|
def test_from_uri(self):
|
||||||
|
P = self.cls
|
||||||
|
# DOS drive paths
|
||||||
|
self.assertEqual(P.from_uri('file:c:/path/to/file'), P('c:/path/to/file'))
|
||||||
|
self.assertEqual(P.from_uri('file:c|/path/to/file'), P('c:/path/to/file'))
|
||||||
|
self.assertEqual(P.from_uri('file:/c|/path/to/file'), P('c:/path/to/file'))
|
||||||
|
self.assertEqual(P.from_uri('file:///c|/path/to/file'), P('c:/path/to/file'))
|
||||||
|
# UNC paths
|
||||||
|
self.assertEqual(P.from_uri('file://server/path/to/file'), P('//server/path/to/file'))
|
||||||
|
self.assertEqual(P.from_uri('file:////server/path/to/file'), P('//server/path/to/file'))
|
||||||
|
self.assertEqual(P.from_uri('file://///server/path/to/file'), P('//server/path/to/file'))
|
||||||
|
# Localhost paths
|
||||||
|
self.assertEqual(P.from_uri('file://localhost/c:/path/to/file'), P('c:/path/to/file'))
|
||||||
|
self.assertEqual(P.from_uri('file://localhost/c|/path/to/file'), P('c:/path/to/file'))
|
||||||
|
# Invalid paths
|
||||||
|
self.assertRaises(ValueError, P.from_uri, 'foo/bar')
|
||||||
|
self.assertRaises(ValueError, P.from_uri, 'c:/foo/bar')
|
||||||
|
self.assertRaises(ValueError, P.from_uri, '//foo/bar')
|
||||||
|
self.assertRaises(ValueError, P.from_uri, 'file:foo/bar')
|
||||||
|
self.assertRaises(ValueError, P.from_uri, 'http://foo/bar')
|
||||||
|
|
||||||
|
def test_from_uri_pathname2url(self):
|
||||||
|
P = self.cls
|
||||||
|
self.assertEqual(P.from_uri('file:' + pathname2url(r'c:\path\to\file')), P('c:/path/to/file'))
|
||||||
|
self.assertEqual(P.from_uri('file:' + pathname2url(r'\\server\path\to\file')), P('//server/path/to/file'))
|
||||||
|
|
||||||
|
|
||||||
class PathSubclassTest(PathTest):
|
class PathSubclassTest(PathTest):
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Add :meth:`pathlib.Path.from_uri` classmethod.
|
Loading…
Add table
Add a link
Reference in a new issue