Skip to content

GH-89812: Add pathlib._PurePathExt #104810

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 20 commits 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
55 changes: 30 additions & 25 deletions Doc/library/pathlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,9 @@ we also call *flavours*:
PurePosixPath('setup.py')

Each element of *pathsegments* can be either a string representing a
path segment, or an object implementing the :class:`os.PathLike` interface
path segment, an object implementing the :class:`os.PathLike` interface
where the :meth:`~os.PathLike.__fspath__` method returns a string,
such as another path object::
or another path object::

>>> PurePath('foo', 'some/path', 'bar')
PurePosixPath('foo/some/path/bar')
Expand Down Expand Up @@ -151,11 +151,6 @@ we also call *flavours*:
to ``PurePosixPath('bar')``, which is wrong if ``foo`` is a symbolic link
to another directory)

Pure path objects implement the :class:`os.PathLike` interface, allowing them
to be used anywhere the interface is accepted.

.. versionchanged:: 3.6
Added support for the :class:`os.PathLike` interface.

.. class:: PurePosixPath(*pathsegments)

Expand Down Expand Up @@ -232,14 +227,6 @@ relative path (e.g., ``r'\foo'``)::
>>> PureWindowsPath('c:/Windows', '/Program Files')
PureWindowsPath('c:/Program Files')

A path object can be used anywhere an object implementing :class:`os.PathLike`
is accepted::

>>> import os
>>> p = PurePath('/etc')
>>> os.fspath(p)
'/etc'

The string representation of a path is the raw filesystem path itself
(in native form, e.g. with backslashes under Windows), which you can
pass to any function taking a file path as a string::
Expand All @@ -251,16 +238,6 @@ pass to any function taking a file path as a string::
>>> str(p)
'c:\\Program Files'

Similarly, calling :class:`bytes` on a path gives the raw filesystem path as a
bytes object, as encoded by :func:`os.fsencode`::

>>> bytes(p)
b'/etc'

.. note::
Calling :class:`bytes` is only recommended under Unix. Under Windows,
the unicode form is the canonical representation of filesystem paths.


Accessing individual parts
^^^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down Expand Up @@ -781,6 +758,34 @@ bugs or failures in your application)::
NotImplementedError: cannot instantiate 'WindowsPath' on your system


Operators
^^^^^^^^^

Concrete path objects implement the :class:`os.PathLike` interface,
allowing them to be used anywhere the interface is accepted::

>>> import os
>>> p = Path('/etc')
>>> os.fspath(p)
'/etc'

.. versionchanged:: 3.6
Added support for the :class:`os.PathLike` interface.

Calling :class:`bytes` on a concrete path gives the raw filesystem path as a
bytes object, as encoded by :func:`os.fsencode`::

>>> bytes(p)
b'/etc'

.. note::
Calling :class:`bytes` is only recommended under Unix. Under Windows,
the unicode form is the canonical representation of filesystem paths.

For backwards compatibility, these operations are also supported by
instances of :class:`PurePosixPath` and :class:`PureWindowsPath`.


Methods
^^^^^^^

Expand Down
29 changes: 17 additions & 12 deletions Lib/pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,20 +391,12 @@ def __str__(self):
self._tail) or '.'
return self._str

def __fspath__(self):
return str(self)

def as_posix(self):
"""Return the string representation of the path with forward (/)
slashes."""
f = self._flavour
return str(self).replace(f.sep, '/')

def __bytes__(self):
"""Return the bytes representation of the path. This is only
recommended to use under Unix."""
return os.fsencode(self)

def __repr__(self):
return "{}({!r})".format(self.__class__.__name__, self.as_posix())

Expand Down Expand Up @@ -735,12 +727,25 @@ def match(self, path_pattern, *, case_sensitive=None):
raise ValueError("empty pattern")


class _PurePathExt(PurePath):
"""PurePath subclass that provides __fspath__ and __bytes__ methods."""
__slots__ = ()

def __fspath__(self):
return str(self)

def __bytes__(self):
"""Return the bytes representation of the path. This is only
recommended to use under Unix."""
return os.fsencode(self)


# Subclassing os.PathLike makes isinstance() checks slower,
# which in turn makes Path construction slower. Register instead!
os.PathLike.register(PurePath)
os.PathLike.register(_PurePathExt)


class PurePosixPath(PurePath):
class PurePosixPath(_PurePathExt):
"""PurePath subclass for non-Windows systems.

On a POSIX system, instantiating a PurePath should return this object.
Expand All @@ -750,7 +755,7 @@ class PurePosixPath(PurePath):
__slots__ = ()


class PureWindowsPath(PurePath):
class PureWindowsPath(_PurePathExt):
"""PurePath subclass for Windows systems.

On a Windows system, instantiating a PurePath should return this object.
Expand All @@ -763,7 +768,7 @@ class PureWindowsPath(PurePath):
# Filesystem-accessing classes


class Path(PurePath):
class Path(_PurePathExt):
"""PurePath subclass that can make system calls.

Path represents a filesystem path but unlike PurePath, also offers
Expand Down
41 changes: 28 additions & 13 deletions Lib/test/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,11 +266,6 @@ def test_as_posix_common(self):
self.assertEqual(P(pathstr).as_posix(), pathstr)
# Other tests for as_posix() are in test_equivalences().

def test_as_bytes_common(self):
sep = os.fsencode(self.sep)
P = self.cls
self.assertEqual(bytes(P('a/b')), b'a' + sep + b'b')

def test_as_uri_common(self):
P = self.cls
with self.assertRaises(ValueError):
Expand Down Expand Up @@ -419,12 +414,6 @@ def test_parts_common(self):
parts = p.parts
self.assertEqual(parts, (sep, 'a', 'b'))

def test_fspath_common(self):
P = self.cls
p = P('a/b')
self._check_str(p.__fspath__(), ('a/b',))
self._check_str(os.fspath(p), ('a/b',))

def test_equivalences(self):
for k, tuples in self.equivalences.items():
canon = k.replace('/', self.sep)
Expand Down Expand Up @@ -763,7 +752,22 @@ def test_pickling_common(self):
self.assertEqual(str(pp), str(p))


class PurePosixPathTest(PurePathTest):
class PurePathExtTest(PurePathTest):
cls = pathlib._PurePathExt

def test_fspath_common(self):
P = self.cls
p = P('a/b')
self._check_str(p.__fspath__(), ('a/b',))
self._check_str(os.fspath(p), ('a/b',))

def test_bytes_common(self):
sep = os.fsencode(self.sep)
P = self.cls
self.assertEqual(bytes(P('a/b')), b'a' + sep + b'b')


class PurePosixPathTest(PurePathExtTest):
cls = pathlib.PurePosixPath

def test_drive_root_parts(self):
Expand Down Expand Up @@ -857,7 +861,7 @@ def test_parse_windows_path(self):
self.assertEqual(p, pp)


class PureWindowsPathTest(PurePathTest):
class PureWindowsPathTest(PurePathExtTest):
cls = pathlib.PureWindowsPath

equivalences = PurePathTest.equivalences.copy()
Expand Down Expand Up @@ -1539,6 +1543,17 @@ class cls(pathlib.PurePath):
# repr() roundtripping is not supported in custom subclass.
test_repr_roundtrips = None

def test_not_path_like(self):
p = self.cls()
self.assertNotIsInstance(p, os.PathLike)
with self.assertRaises(TypeError):
os.fspath(p)

def test_not_bytes_like(self):
p = self.cls()
with self.assertRaises(TypeError):
bytes(p)


@only_posix
class PosixPathAsPureTest(PurePosixPathTest):
Expand Down