[Python-checkins] GH-89812: Churn `pathlib.Path` methods (GH-104243)
barneygale
webhook-mailer at python.org
Sun May 7 15:07:47 EDT 2023
https://github.com/python/cpython/commit/e8d77b03e08a4c7e7dde0830c5a12a0b41ff7c33
commit: e8d77b03e08a4c7e7dde0830c5a12a0b41ff7c33
branch: main
author: Barney Gale <barney.gale at gmail.com>
committer: barneygale <barney.gale at gmail.com>
date: 2023-05-07T20:07:07+01:00
summary:
GH-89812: Churn `pathlib.Path` methods (GH-104243)
Re-arrange `pathlib.Path` methods in source code. No other changes.
The methods are arranged as follows:
1. `stat()` and dependants (`exists()`, `is_dir()`, etc)
2. `open()` and dependants (`read_text()`, `write_bytes()`, etc)
3. `iterdir()` and dependants (`glob()`, `walk()`, etc)
4. All other `Path` methods
This patch prepares the ground for a new `_AbstractPath` class, which will
support the methods in groups 1, 2 and 3 above. By churning the methods
here, subsequent patches will be easier to review and less likely to break
things.
files:
M Lib/pathlib.py
diff --git a/Lib/pathlib.py b/Lib/pathlib.py
index 480c354ce8b6..68255aa3e511 100644
--- a/Lib/pathlib.py
+++ b/Lib/pathlib.py
@@ -740,68 +740,164 @@ class Path(PurePath):
"""
__slots__ = ()
- def __init__(self, *args, **kwargs):
- if kwargs:
- msg = ("support for supplying keyword arguments to pathlib.PurePath "
- "is deprecated and scheduled for removal in Python {remove}")
- warnings._deprecated("pathlib.PurePath(**kwargs)", msg, remove=(3, 14))
- super().__init__(*args)
+ def stat(self, *, follow_symlinks=True):
+ """
+ Return the result of the stat() system call on this path, like
+ os.stat() does.
+ """
+ return os.stat(self, follow_symlinks=follow_symlinks)
- def __new__(cls, *args, **kwargs):
- if cls is Path:
- cls = WindowsPath if os.name == 'nt' else PosixPath
- return object.__new__(cls)
+ def lstat(self):
+ """
+ Like stat(), except if the path points to a symlink, the symlink's
+ status information is returned, rather than its target's.
+ """
+ return self.stat(follow_symlinks=False)
- def _make_child_relpath(self, name):
- path_str = str(self)
- tail = self._tail
- if tail:
- path_str = f'{path_str}{self._flavour.sep}{name}'
- elif path_str != '.':
- path_str = f'{path_str}{name}'
- else:
- path_str = name
- path = self.with_segments(path_str)
- path._str = path_str
- path._drv = self.drive
- path._root = self.root
- path._tail_cached = tail + [name]
- return path
- def __enter__(self):
- # In previous versions of pathlib, __exit__() marked this path as
- # closed; subsequent attempts to perform I/O would raise an IOError.
- # This functionality was never documented, and had the effect of
- # making Path objects mutable, contrary to PEP 428.
- # In Python 3.9 __exit__() was made a no-op.
- # In Python 3.11 __enter__() began emitting DeprecationWarning.
- # In Python 3.13 __enter__() and __exit__() should be removed.
- warnings.warn("pathlib.Path.__enter__() is deprecated and scheduled "
- "for removal in Python 3.13; Path objects as a context "
- "manager is a no-op",
- DeprecationWarning, stacklevel=2)
- return self
+ # Convenience functions for querying the stat results
- def __exit__(self, t, v, tb):
- pass
+ def exists(self, *, follow_symlinks=True):
+ """
+ Whether this path exists.
- # Public API
+ This method normally follows symlinks; to check whether a symlink exists,
+ add the argument follow_symlinks=False.
+ """
+ try:
+ self.stat(follow_symlinks=follow_symlinks)
+ except OSError as e:
+ if not _ignore_error(e):
+ raise
+ return False
+ except ValueError:
+ # Non-encodable path
+ return False
+ return True
- @classmethod
- def cwd(cls):
- """Return a new path pointing to the current working directory."""
- # We call 'absolute()' rather than using 'os.getcwd()' directly to
- # enable users to replace the implementation of 'absolute()' in a
- # subclass and benefit from the new behaviour here. This works because
- # os.path.abspath('.') == os.getcwd().
- return cls().absolute()
+ def is_dir(self):
+ """
+ Whether this path is a directory.
+ """
+ try:
+ return S_ISDIR(self.stat().st_mode)
+ except OSError as e:
+ if not _ignore_error(e):
+ raise
+ # Path doesn't exist or is a broken symlink
+ # (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ )
+ return False
+ except ValueError:
+ # Non-encodable path
+ return False
- @classmethod
- def home(cls):
- """Return a new path pointing to the user's home directory (as
- returned by os.path.expanduser('~')).
+ def is_file(self):
"""
- return cls("~").expanduser()
+ Whether this path is a regular file (also True for symlinks pointing
+ to regular files).
+ """
+ try:
+ return S_ISREG(self.stat().st_mode)
+ except OSError as e:
+ if not _ignore_error(e):
+ raise
+ # Path doesn't exist or is a broken symlink
+ # (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ )
+ return False
+ except ValueError:
+ # Non-encodable path
+ return False
+
+ def is_mount(self):
+ """
+ Check if this path is a mount point
+ """
+ return self._flavour.ismount(self)
+
+ def is_symlink(self):
+ """
+ Whether this path is a symbolic link.
+ """
+ try:
+ return S_ISLNK(self.lstat().st_mode)
+ except OSError as e:
+ if not _ignore_error(e):
+ raise
+ # Path doesn't exist
+ return False
+ except ValueError:
+ # Non-encodable path
+ return False
+
+ def is_junction(self):
+ """
+ Whether this path is a junction.
+ """
+ return self._flavour.isjunction(self)
+
+ def is_block_device(self):
+ """
+ Whether this path is a block device.
+ """
+ try:
+ return S_ISBLK(self.stat().st_mode)
+ except OSError as e:
+ if not _ignore_error(e):
+ raise
+ # Path doesn't exist or is a broken symlink
+ # (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ )
+ return False
+ except ValueError:
+ # Non-encodable path
+ return False
+
+ def is_char_device(self):
+ """
+ Whether this path is a character device.
+ """
+ try:
+ return S_ISCHR(self.stat().st_mode)
+ except OSError as e:
+ if not _ignore_error(e):
+ raise
+ # Path doesn't exist or is a broken symlink
+ # (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ )
+ return False
+ except ValueError:
+ # Non-encodable path
+ return False
+
+ def is_fifo(self):
+ """
+ Whether this path is a FIFO.
+ """
+ try:
+ return S_ISFIFO(self.stat().st_mode)
+ except OSError as e:
+ if not _ignore_error(e):
+ raise
+ # Path doesn't exist or is a broken symlink
+ # (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ )
+ return False
+ except ValueError:
+ # Non-encodable path
+ return False
+
+ def is_socket(self):
+ """
+ Whether this path is a socket.
+ """
+ try:
+ return S_ISSOCK(self.stat().st_mode)
+ except OSError as e:
+ if not _ignore_error(e):
+ raise
+ # Path doesn't exist or is a broken symlink
+ # (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ )
+ return False
+ except ValueError:
+ # Non-encodable path
+ return False
def samefile(self, other_path):
"""Return whether other_path is the same or not as this file
@@ -814,6 +910,51 @@ def samefile(self, other_path):
other_st = self.with_segments(other_path).stat()
return self._flavour.samestat(st, other_st)
+ def open(self, mode='r', buffering=-1, encoding=None,
+ errors=None, newline=None):
+ """
+ Open the file pointed by this path and return a file object, as
+ the built-in open() function does.
+ """
+ if "b" not in mode:
+ encoding = io.text_encoding(encoding)
+ return io.open(self, mode, buffering, encoding, errors, newline)
+
+ def read_bytes(self):
+ """
+ Open the file in bytes mode, read it, and close the file.
+ """
+ with self.open(mode='rb') as f:
+ return f.read()
+
+ def read_text(self, encoding=None, errors=None):
+ """
+ Open the file in text mode, read it, and close the file.
+ """
+ encoding = io.text_encoding(encoding)
+ with self.open(mode='r', encoding=encoding, errors=errors) as f:
+ return f.read()
+
+ def write_bytes(self, data):
+ """
+ Open the file in bytes mode, write to it, and close the file.
+ """
+ # type-check for the buffer interface before truncating the file
+ view = memoryview(data)
+ with self.open(mode='wb') as f:
+ return f.write(view)
+
+ def write_text(self, data, encoding=None, errors=None, newline=None):
+ """
+ Open the file in text mode, write to it, and close the file.
+ """
+ if not isinstance(data, str):
+ raise TypeError('data must be str, not %s' %
+ data.__class__.__name__)
+ encoding = io.text_encoding(encoding)
+ with self.open(mode='w', encoding=encoding, errors=errors, newline=newline) as f:
+ return f.write(data)
+
def iterdir(self):
"""Yield path objects of the directory contents.
@@ -829,6 +970,22 @@ def _scandir(self):
# includes scandir(), which is used to implement glob().
return os.scandir(self)
+ def _make_child_relpath(self, name):
+ path_str = str(self)
+ tail = self._tail
+ if tail:
+ path_str = f'{path_str}{self._flavour.sep}{name}'
+ elif path_str != '.':
+ path_str = f'{path_str}{name}'
+ else:
+ path_str = name
+ path = self.with_segments(path_str)
+ path._str = path_str
+ path._drv = self.drive
+ path._root = self.root
+ path._tail_cached = tail + [name]
+ return path
+
def glob(self, pattern, *, case_sensitive=None):
"""Iterate over this subtree and yield all existing files (of any
kind, including directories) matching the given relative pattern.
@@ -860,6 +1017,98 @@ def rglob(self, pattern, *, case_sensitive=None):
for p in selector.select_from(self):
yield p
+ def walk(self, top_down=True, on_error=None, follow_symlinks=False):
+ """Walk the directory tree from this directory, similar to os.walk()."""
+ sys.audit("pathlib.Path.walk", self, on_error, follow_symlinks)
+ paths = [self]
+
+ while paths:
+ path = paths.pop()
+ if isinstance(path, tuple):
+ yield path
+ continue
+
+ # We may not have read permission for self, in which case we can't
+ # get a list of the files the directory contains. os.walk()
+ # always suppressed the exception in that instance, rather than
+ # blow up for a minor reason when (say) a thousand readable
+ # directories are still left to visit. That logic is copied here.
+ try:
+ scandir_it = path._scandir()
+ except OSError as error:
+ if on_error is not None:
+ on_error(error)
+ continue
+
+ with scandir_it:
+ dirnames = []
+ filenames = []
+ for entry in scandir_it:
+ try:
+ is_dir = entry.is_dir(follow_symlinks=follow_symlinks)
+ except OSError:
+ # Carried over from os.path.isdir().
+ is_dir = False
+
+ if is_dir:
+ dirnames.append(entry.name)
+ else:
+ filenames.append(entry.name)
+
+ if top_down:
+ yield path, dirnames, filenames
+ else:
+ paths.append((path, dirnames, filenames))
+
+ paths += [path._make_child_relpath(d) for d in reversed(dirnames)]
+
+ def __init__(self, *args, **kwargs):
+ if kwargs:
+ msg = ("support for supplying keyword arguments to pathlib.PurePath "
+ "is deprecated and scheduled for removal in Python {remove}")
+ warnings._deprecated("pathlib.PurePath(**kwargs)", msg, remove=(3, 14))
+ super().__init__(*args)
+
+ def __new__(cls, *args, **kwargs):
+ if cls is Path:
+ cls = WindowsPath if os.name == 'nt' else PosixPath
+ return object.__new__(cls)
+
+ def __enter__(self):
+ # In previous versions of pathlib, __exit__() marked this path as
+ # closed; subsequent attempts to perform I/O would raise an IOError.
+ # This functionality was never documented, and had the effect of
+ # making Path objects mutable, contrary to PEP 428.
+ # In Python 3.9 __exit__() was made a no-op.
+ # In Python 3.11 __enter__() began emitting DeprecationWarning.
+ # In Python 3.13 __enter__() and __exit__() should be removed.
+ warnings.warn("pathlib.Path.__enter__() is deprecated and scheduled "
+ "for removal in Python 3.13; Path objects as a context "
+ "manager is a no-op",
+ DeprecationWarning, stacklevel=2)
+ return self
+
+ def __exit__(self, t, v, tb):
+ pass
+
+ # Public API
+
+ @classmethod
+ def cwd(cls):
+ """Return a new path pointing to the current working directory."""
+ # We call 'absolute()' rather than using 'os.getcwd()' directly to
+ # enable users to replace the implementation of 'absolute()' in a
+ # subclass and benefit from the new behaviour here. This works because
+ # os.path.abspath('.') == os.getcwd().
+ return cls().absolute()
+
+ @classmethod
+ def home(cls):
+ """Return a new path pointing to the user's home directory (as
+ returned by os.path.expanduser('~')).
+ """
+ return cls("~").expanduser()
+
def absolute(self):
"""Return an absolute version of this path by prepending the current
working directory. No normalization or symlink resolution is performed.
@@ -911,13 +1160,6 @@ def check_eloop(e):
check_eloop(e)
return p
- def stat(self, *, follow_symlinks=True):
- """
- Return the result of the stat() system call on this path, like
- os.stat() does.
- """
- return os.stat(self, follow_symlinks=follow_symlinks)
-
def owner(self):
"""
Return the login name of the file owner.
@@ -939,51 +1181,6 @@ def group(self):
except ImportError:
raise NotImplementedError("Path.group() is unsupported on this system")
- def open(self, mode='r', buffering=-1, encoding=None,
- errors=None, newline=None):
- """
- Open the file pointed by this path and return a file object, as
- the built-in open() function does.
- """
- if "b" not in mode:
- encoding = io.text_encoding(encoding)
- return io.open(self, mode, buffering, encoding, errors, newline)
-
- def read_bytes(self):
- """
- Open the file in bytes mode, read it, and close the file.
- """
- with self.open(mode='rb') as f:
- return f.read()
-
- def read_text(self, encoding=None, errors=None):
- """
- Open the file in text mode, read it, and close the file.
- """
- encoding = io.text_encoding(encoding)
- with self.open(mode='r', encoding=encoding, errors=errors) as f:
- return f.read()
-
- def write_bytes(self, data):
- """
- Open the file in bytes mode, write to it, and close the file.
- """
- # type-check for the buffer interface before truncating the file
- view = memoryview(data)
- with self.open(mode='wb') as f:
- return f.write(view)
-
- def write_text(self, data, encoding=None, errors=None, newline=None):
- """
- Open the file in text mode, write to it, and close the file.
- """
- if not isinstance(data, str):
- raise TypeError('data must be str, not %s' %
- data.__class__.__name__)
- encoding = io.text_encoding(encoding)
- with self.open(mode='w', encoding=encoding, errors=errors, newline=newline) as f:
- return f.write(data)
-
def readlink(self):
"""
Return the path to which the symbolic link points.
@@ -1061,13 +1258,6 @@ def rmdir(self):
"""
os.rmdir(self)
- def lstat(self):
- """
- Like stat(), except if the path points to a symlink, the symlink's
- status information is returned, rather than its target's.
- """
- return self.stat(follow_symlinks=False)
-
def rename(self, target):
"""
Rename this path to the target path.
@@ -1113,151 +1303,6 @@ def hardlink_to(self, target):
raise NotImplementedError("os.link() not available on this system")
os.link(target, self)
-
- # Convenience functions for querying the stat results
-
- def exists(self, *, follow_symlinks=True):
- """
- Whether this path exists.
-
- This method normally follows symlinks; to check whether a symlink exists,
- add the argument follow_symlinks=False.
- """
- try:
- self.stat(follow_symlinks=follow_symlinks)
- except OSError as e:
- if not _ignore_error(e):
- raise
- return False
- except ValueError:
- # Non-encodable path
- return False
- return True
-
- def is_dir(self):
- """
- Whether this path is a directory.
- """
- try:
- return S_ISDIR(self.stat().st_mode)
- except OSError as e:
- if not _ignore_error(e):
- raise
- # Path doesn't exist or is a broken symlink
- # (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ )
- return False
- except ValueError:
- # Non-encodable path
- return False
-
- def is_file(self):
- """
- Whether this path is a regular file (also True for symlinks pointing
- to regular files).
- """
- try:
- return S_ISREG(self.stat().st_mode)
- except OSError as e:
- if not _ignore_error(e):
- raise
- # Path doesn't exist or is a broken symlink
- # (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ )
- return False
- except ValueError:
- # Non-encodable path
- return False
-
- def is_mount(self):
- """
- Check if this path is a mount point
- """
- return self._flavour.ismount(self)
-
- def is_symlink(self):
- """
- Whether this path is a symbolic link.
- """
- try:
- return S_ISLNK(self.lstat().st_mode)
- except OSError as e:
- if not _ignore_error(e):
- raise
- # Path doesn't exist
- return False
- except ValueError:
- # Non-encodable path
- return False
-
- def is_junction(self):
- """
- Whether this path is a junction.
- """
- return self._flavour.isjunction(self)
-
- def is_block_device(self):
- """
- Whether this path is a block device.
- """
- try:
- return S_ISBLK(self.stat().st_mode)
- except OSError as e:
- if not _ignore_error(e):
- raise
- # Path doesn't exist or is a broken symlink
- # (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ )
- return False
- except ValueError:
- # Non-encodable path
- return False
-
- def is_char_device(self):
- """
- Whether this path is a character device.
- """
- try:
- return S_ISCHR(self.stat().st_mode)
- except OSError as e:
- if not _ignore_error(e):
- raise
- # Path doesn't exist or is a broken symlink
- # (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ )
- return False
- except ValueError:
- # Non-encodable path
- return False
-
- def is_fifo(self):
- """
- Whether this path is a FIFO.
- """
- try:
- return S_ISFIFO(self.stat().st_mode)
- except OSError as e:
- if not _ignore_error(e):
- raise
- # Path doesn't exist or is a broken symlink
- # (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ )
- return False
- except ValueError:
- # Non-encodable path
- return False
-
- def is_socket(self):
- """
- Whether this path is a socket.
- """
- try:
- return S_ISSOCK(self.stat().st_mode)
- except OSError as e:
- if not _ignore_error(e):
- raise
- # Path doesn't exist or is a broken symlink
- # (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ )
- return False
- except ValueError:
- # Non-encodable path
- return False
-
def expanduser(self):
""" Return a new path with expanded ~ and ~user constructs
(as returned by os.path.expanduser)
@@ -1272,51 +1317,6 @@ def expanduser(self):
return self
- def walk(self, top_down=True, on_error=None, follow_symlinks=False):
- """Walk the directory tree from this directory, similar to os.walk()."""
- sys.audit("pathlib.Path.walk", self, on_error, follow_symlinks)
- paths = [self]
-
- while paths:
- path = paths.pop()
- if isinstance(path, tuple):
- yield path
- continue
-
- # We may not have read permission for self, in which case we can't
- # get a list of the files the directory contains. os.walk()
- # always suppressed the exception in that instance, rather than
- # blow up for a minor reason when (say) a thousand readable
- # directories are still left to visit. That logic is copied here.
- try:
- scandir_it = path._scandir()
- except OSError as error:
- if on_error is not None:
- on_error(error)
- continue
-
- with scandir_it:
- dirnames = []
- filenames = []
- for entry in scandir_it:
- try:
- is_dir = entry.is_dir(follow_symlinks=follow_symlinks)
- except OSError:
- # Carried over from os.path.isdir().
- is_dir = False
-
- if is_dir:
- dirnames.append(entry.name)
- else:
- filenames.append(entry.name)
-
- if top_down:
- yield path, dirnames, filenames
- else:
- paths.append((path, dirnames, filenames))
-
- paths += [path._make_child_relpath(d) for d in reversed(dirnames)]
-
class PosixPath(Path, PurePosixPath):
"""Path subclass for non-Windows systems.
More information about the Python-checkins
mailing list