bpo-43012: remove `pathlib._Accessor` (GH-25701)

Per Pitrou:

> The original intent for the “accessor” thing was to have a variant that did all accesses under a filesystem tree in a race condition-free way using openat and friends. It turned out to be much too hairy to actually implement, so was entirely abandoned, but the accessor abstraction was left there.

https://discuss.python.org/t/make-pathlib-extensible/3428/2

Accessors are:

- Lacking any internal purpose - '_NormalAccessor' is the only implementation
- Lacking any firm conceptual difference to `Path` objects themselves (inc. subclasses)
- Non-public, i.e. underscore prefixed - '_Accessor' and '_NormalAccessor' 
- Unofficially used to implement customized `Path` objects, but once once [bpo-24132]() is addressed there will be a supported route for that.

This patch preserves all existing behaviour.
This commit is contained in:
Barney Gale 2022-02-02 12:38:25 +00:00 committed by GitHub
parent 187930f74c
commit 08f8301b21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 77 additions and 129 deletions

View File

@ -275,93 +275,6 @@ def make_uri(self, path):
_posix_flavour = _PosixFlavour()
class _Accessor:
"""An accessor implements a particular (system-specific or not) way of
accessing paths on the filesystem."""
class _NormalAccessor(_Accessor):
stat = os.stat
open = io.open
listdir = os.listdir
scandir = os.scandir
chmod = os.chmod
mkdir = os.mkdir
unlink = os.unlink
if hasattr(os, "link"):
link = os.link
else:
def link(self, src, dst):
raise NotImplementedError("os.link() not available on this system")
rmdir = os.rmdir
rename = os.rename
replace = os.replace
if hasattr(os, "symlink"):
symlink = os.symlink
else:
def symlink(self, src, dst, target_is_directory=False):
raise NotImplementedError("os.symlink() not available on this system")
def touch(self, path, mode=0o666, exist_ok=True):
if exist_ok:
# First try to bump modification time
# Implementation note: GNU touch uses the UTIME_NOW option of
# the utimensat() / futimens() functions.
try:
os.utime(path, None)
except OSError:
# Avoid exception chaining
pass
else:
return
flags = os.O_CREAT | os.O_WRONLY
if not exist_ok:
flags |= os.O_EXCL
fd = os.open(path, flags, mode)
os.close(fd)
if hasattr(os, "readlink"):
readlink = os.readlink
else:
def readlink(self, path):
raise NotImplementedError("os.readlink() not available on this system")
def owner(self, path):
try:
import pwd
return pwd.getpwuid(self.stat(path).st_uid).pw_name
except ImportError:
raise NotImplementedError("Path.owner() is unsupported on this system")
def group(self, path):
try:
import grp
return grp.getgrgid(self.stat(path).st_gid).gr_name
except ImportError:
raise NotImplementedError("Path.group() is unsupported on this system")
getcwd = os.getcwd
expanduser = staticmethod(os.path.expanduser)
realpath = staticmethod(os.path.realpath)
_normal_accessor = _NormalAccessor()
#
# Globbing helpers
#
@ -402,7 +315,7 @@ def select_from(self, parent_path):
path_cls = type(parent_path)
is_dir = path_cls.is_dir
exists = path_cls.exists
scandir = parent_path._accessor.scandir
scandir = path_cls._scandir
if not is_dir(parent_path):
return iter([])
return self._select_from(parent_path, is_dir, exists, scandir)
@ -949,7 +862,6 @@ class Path(PurePath):
object. You can also instantiate a PosixPath or WindowsPath directly,
but cannot instantiate a WindowsPath on a POSIX system or vice versa.
"""
_accessor = _normal_accessor
__slots__ = ()
def __new__(cls, *args, **kwargs):
@ -988,7 +900,7 @@ def cwd(cls):
"""Return a new path pointing to the current working directory
(as returned by os.getcwd()).
"""
return cls(cls._accessor.getcwd())
return cls(os.getcwd())
@classmethod
def home(cls):
@ -1005,16 +917,22 @@ def samefile(self, other_path):
try:
other_st = other_path.stat()
except AttributeError:
other_st = self._accessor.stat(other_path)
other_st = self.__class__(other_path).stat()
return os.path.samestat(st, other_st)
def iterdir(self):
"""Iterate over the files in this directory. Does not yield any
result for the special paths '.' and '..'.
"""
for name in self._accessor.listdir(self):
for name in os.listdir(self):
yield self._make_child_relpath(name)
def _scandir(self):
# bpo-24132: a future version of pathlib will support subclassing of
# pathlib.Path to customize how the filesystem is accessed. This
# includes scandir(), which is used to implement glob().
return os.scandir(self)
def glob(self, pattern):
"""Iterate over this subtree and yield all existing files (of any
kind, including directories) matching the given relative pattern.
@ -1050,7 +968,7 @@ def absolute(self):
"""
if self.is_absolute():
return self
return self._from_parts([self._accessor.getcwd()] + self._parts)
return self._from_parts([self.cwd()] + self._parts)
def resolve(self, strict=False):
"""
@ -1064,7 +982,7 @@ def check_eloop(e):
raise RuntimeError("Symlink loop from %r" % e.filename)
try:
s = self._accessor.realpath(self, strict=strict)
s = os.path.realpath(self, strict=strict)
except OSError as e:
check_eloop(e)
raise
@ -1084,19 +1002,28 @@ def stat(self, *, follow_symlinks=True):
Return the result of the stat() system call on this path, like
os.stat() does.
"""
return self._accessor.stat(self, follow_symlinks=follow_symlinks)
return os.stat(self, follow_symlinks=follow_symlinks)
def owner(self):
"""
Return the login name of the file owner.
"""
return self._accessor.owner(self)
try:
import pwd
return pwd.getpwuid(self.stat().st_uid).pw_name
except ImportError:
raise NotImplementedError("Path.owner() is unsupported on this system")
def group(self):
"""
Return the group name of the file gid.
"""
return self._accessor.group(self)
try:
import grp
return grp.getgrgid(self.stat().st_gid).gr_name
except ImportError:
raise NotImplementedError("Path.group() is unsupported on this system")
def open(self, mode='r', buffering=-1, encoding=None,
errors=None, newline=None):
@ -1106,8 +1033,7 @@ def open(self, mode='r', buffering=-1, encoding=None,
"""
if "b" not in mode:
encoding = io.text_encoding(encoding)
return self._accessor.open(self, mode, buffering, encoding, errors,
newline)
return io.open(self, mode, buffering, encoding, errors, newline)
def read_bytes(self):
"""
@ -1148,21 +1074,38 @@ def readlink(self):
"""
Return the path to which the symbolic link points.
"""
path = self._accessor.readlink(self)
return self._from_parts((path,))
if not hasattr(os, "readlink"):
raise NotImplementedError("os.readlink() not available on this system")
return self._from_parts((os.readlink(self),))
def touch(self, mode=0o666, exist_ok=True):
"""
Create this file with the given access mode, if it doesn't exist.
"""
self._accessor.touch(self, mode, exist_ok)
if exist_ok:
# First try to bump modification time
# Implementation note: GNU touch uses the UTIME_NOW option of
# the utimensat() / futimens() functions.
try:
os.utime(self, None)
except OSError:
# Avoid exception chaining
pass
else:
return
flags = os.O_CREAT | os.O_WRONLY
if not exist_ok:
flags |= os.O_EXCL
fd = os.open(self, flags, mode)
os.close(fd)
def mkdir(self, mode=0o777, parents=False, exist_ok=False):
"""
Create a new directory at this given path.
"""
try:
self._accessor.mkdir(self, mode)
os.mkdir(self, mode)
except FileNotFoundError:
if not parents or self.parent == self:
raise
@ -1178,7 +1121,7 @@ def chmod(self, mode, *, follow_symlinks=True):
"""
Change the permissions of the path, like os.chmod().
"""
self._accessor.chmod(self, mode, follow_symlinks=follow_symlinks)
os.chmod(self, mode, follow_symlinks=follow_symlinks)
def lchmod(self, mode):
"""
@ -1193,7 +1136,7 @@ def unlink(self, missing_ok=False):
If the path is a directory, use rmdir() instead.
"""
try:
self._accessor.unlink(self)
os.unlink(self)
except FileNotFoundError:
if not missing_ok:
raise
@ -1202,7 +1145,7 @@ def rmdir(self):
"""
Remove this directory. The directory must be empty.
"""
self._accessor.rmdir(self)
os.rmdir(self)
def lstat(self):
"""
@ -1221,7 +1164,7 @@ def rename(self, target):
Returns the new Path instance pointing to the target path.
"""
self._accessor.rename(self, target)
os.rename(self, target)
return self.__class__(target)
def replace(self, target):
@ -1234,7 +1177,7 @@ def replace(self, target):
Returns the new Path instance pointing to the target path.
"""
self._accessor.replace(self, target)
os.replace(self, target)
return self.__class__(target)
def symlink_to(self, target, target_is_directory=False):
@ -1242,7 +1185,9 @@ def symlink_to(self, target, target_is_directory=False):
Make this path a symlink pointing to the target path.
Note the order of arguments (link, target) is the reverse of os.symlink.
"""
self._accessor.symlink(target, self, target_is_directory)
if not hasattr(os, "symlink"):
raise NotImplementedError("os.symlink() not available on this system")
os.symlink(target, self, target_is_directory)
def hardlink_to(self, target):
"""
@ -1250,7 +1195,9 @@ def hardlink_to(self, target):
Note the order of arguments (self, target) is the reverse of os.link's.
"""
self._accessor.link(target, self)
if not hasattr(os, "link"):
raise NotImplementedError("os.link() not available on this system")
os.link(target, self)
def link_to(self, target):
"""
@ -1268,7 +1215,7 @@ def link_to(self, target):
"for removal in Python 3.12. "
"Use pathlib.Path.hardlink_to() instead.",
DeprecationWarning, stacklevel=2)
self._accessor.link(self, target)
self.__class__(target).hardlink_to(self)
# Convenience functions for querying the stat results
@ -1425,7 +1372,7 @@ def expanduser(self):
"""
if (not (self._drv or self._root) and
self._parts and self._parts[0][:1] == '~'):
homedir = self._accessor.expanduser(self._parts[0])
homedir = os.path.expanduser(self._parts[0])
if homedir[:1] == "~":
raise RuntimeError("Could not determine home directory.")
return self._from_parts([homedir] + self._parts[1:])

View File

@ -1,3 +1,4 @@
import contextlib
import collections.abc
import io
import os
@ -1459,7 +1460,7 @@ def test_cwd(self):
def test_absolute_common(self):
P = self.cls
with mock.patch("pathlib._normal_accessor.getcwd") as getcwd:
with mock.patch("os.getcwd") as getcwd:
getcwd.return_value = BASE
# Simple relative paths.
@ -1738,21 +1739,18 @@ def test_glob_permissions(self):
# Patching is needed to avoid relying on the filesystem
# to return the order of the files as the error will not
# happen if the symlink is the last item.
real_scandir = os.scandir
def my_scandir(path):
with real_scandir(path) as scandir_it:
entries = list(scandir_it)
entries.sort(key=lambda entry: entry.name)
return contextlib.nullcontext(entries)
with mock.patch("os.scandir") as scandir:
scandir.return_value = sorted(os.scandir(base))
with mock.patch("os.scandir", my_scandir):
self.assertEqual(len(set(base.glob("*"))), 3)
subdir.mkdir()
with mock.patch("os.scandir") as scandir:
scandir.return_value = sorted(os.scandir(base))
subdir.mkdir()
self.assertEqual(len(set(base.glob("*"))), 4)
subdir.chmod(000)
with mock.patch("os.scandir") as scandir:
scandir.return_value = sorted(os.scandir(base))
subdir.chmod(000)
self.assertEqual(len(set(base.glob("*"))), 4)
def _check_resolve(self, p, expected, strict=True):
@ -2199,6 +2197,7 @@ def test_mkdir_concurrent_parent_creation(self):
p = self.cls(BASE, 'dirCPC%d' % pattern_num)
self.assertFalse(p.exists())
real_mkdir = os.mkdir
def my_mkdir(path, mode=0o777):
path = str(path)
# Emulate another process that would create the directory
@ -2207,15 +2206,15 @@ def my_mkdir(path, mode=0o777):
# function is called at most 5 times (dirCPC/dir1/dir2,
# dirCPC/dir1, dirCPC, dirCPC/dir1, dirCPC/dir1/dir2).
if pattern.pop():
os.mkdir(path, mode) # From another process.
real_mkdir(path, mode) # From another process.
concurrently_created.add(path)
os.mkdir(path, mode) # Our real call.
real_mkdir(path, mode) # Our real call.
pattern = [bool(pattern_num & (1 << n)) for n in range(5)]
concurrently_created = set()
p12 = p / 'dir1' / 'dir2'
try:
with mock.patch("pathlib._normal_accessor.mkdir", my_mkdir):
with mock.patch("os.mkdir", my_mkdir):
p12.mkdir(parents=True, exist_ok=False)
except FileExistsError:
self.assertIn(str(p12), concurrently_created)
@ -2676,7 +2675,7 @@ def test_absolute(self):
self.assertEqual(str(P(share + 'a\\b').absolute()), share + 'a\\b')
# UNC relative paths.
with mock.patch("pathlib._normal_accessor.getcwd") as getcwd:
with mock.patch("os.getcwd") as getcwd:
getcwd.return_value = share
self.assertEqual(str(P().absolute()), share)

View File

@ -0,0 +1,2 @@
The pathlib module's obsolete and internal ``_Accessor`` class has been
removed to prepare the terrain for upcoming enhancements to the module.