Skip to content

Commit 9e64938

Browse files
authored
GH-139174: Prepare pathlib.Path.info for new methods (#139175)
Merge `_WindowsPathInfo` and `_PosixPathInfo` classes into a new `_StatResultInfo` class. On Windows, this means relying on `os.stat()` rather than `os.path.isfile()` and friends, which is a little slower. But there's value in making the code easier to maintain, and we're going to need the stat result for implementing `size()`, `mode()` etc. Also move the classes from `pathlib._os` to `pathlib` proper.
1 parent 1a2e00c commit 9e64938

File tree

2 files changed

+251
-288
lines changed

2 files changed

+251
-288
lines changed

Lib/pathlib/__init__.py

Lines changed: 251 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
from errno import *
1515
from glob import _StringGlobber, _no_recurse_symlinks
1616
from itertools import chain
17-
from stat import S_ISDIR, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO
17+
from stat import (
18+
S_IMODE, S_ISDIR, S_ISREG, S_ISLNK, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO,
19+
)
1820
from _collections_abc import Sequence
1921

2022
try:
@@ -27,10 +29,9 @@
2729
grp = None
2830

2931
from pathlib._os import (
30-
PathInfo, DirEntryInfo,
3132
vfsopen, vfspath,
3233
ensure_different_files, ensure_distinct_paths,
33-
copyfile2, copyfileobj, copy_info,
34+
copyfile2, copyfileobj,
3435
)
3536

3637

@@ -612,6 +613,247 @@ class PureWindowsPath(PurePath):
612613
__slots__ = ()
613614

614615

616+
class _Info:
617+
__slots__ = ('_path',)
618+
619+
def __init__(self, path):
620+
self._path = path
621+
622+
def __repr__(self):
623+
path_type = "WindowsPath" if os.name == "nt" else "PosixPath"
624+
return f"<{path_type}.info>"
625+
626+
def _stat(self, *, follow_symlinks=True):
627+
"""Return the status as an os.stat_result."""
628+
raise NotImplementedError
629+
630+
def _posix_permissions(self, *, follow_symlinks=True):
631+
"""Return the POSIX file permissions."""
632+
return S_IMODE(self._stat(follow_symlinks=follow_symlinks).st_mode)
633+
634+
def _file_id(self, *, follow_symlinks=True):
635+
"""Returns the identifier of the file."""
636+
st = self._stat(follow_symlinks=follow_symlinks)
637+
return st.st_dev, st.st_ino
638+
639+
def _access_time_ns(self, *, follow_symlinks=True):
640+
"""Return the access time in nanoseconds."""
641+
return self._stat(follow_symlinks=follow_symlinks).st_atime_ns
642+
643+
def _mod_time_ns(self, *, follow_symlinks=True):
644+
"""Return the modify time in nanoseconds."""
645+
return self._stat(follow_symlinks=follow_symlinks).st_mtime_ns
646+
647+
if hasattr(os.stat_result, 'st_flags'):
648+
def _bsd_flags(self, *, follow_symlinks=True):
649+
"""Return the flags."""
650+
return self._stat(follow_symlinks=follow_symlinks).st_flags
651+
652+
if hasattr(os, 'listxattr'):
653+
def _xattrs(self, *, follow_symlinks=True):
654+
"""Return the xattrs as a list of (attr, value) pairs, or an empty
655+
list if extended attributes aren't supported."""
656+
try:
657+
return [
658+
(attr, os.getxattr(self._path, attr, follow_symlinks=follow_symlinks))
659+
for attr in os.listxattr(self._path, follow_symlinks=follow_symlinks)]
660+
except OSError as err:
661+
if err.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES):
662+
raise
663+
return []
664+
665+
666+
_STAT_RESULT_ERROR = [] # falsy sentinel indicating stat() failed.
667+
668+
669+
class _StatResultInfo(_Info):
670+
"""Implementation of pathlib.types.PathInfo that provides status
671+
information by querying a wrapped os.stat_result object. Don't try to
672+
construct it yourself."""
673+
__slots__ = ('_stat_result', '_lstat_result')
674+
675+
def __init__(self, path):
676+
super().__init__(path)
677+
self._stat_result = None
678+
self._lstat_result = None
679+
680+
def _stat(self, *, follow_symlinks=True):
681+
"""Return the status as an os.stat_result."""
682+
if follow_symlinks:
683+
if not self._stat_result:
684+
try:
685+
self._stat_result = os.stat(self._path)
686+
except (OSError, ValueError):
687+
self._stat_result = _STAT_RESULT_ERROR
688+
raise
689+
return self._stat_result
690+
else:
691+
if not self._lstat_result:
692+
try:
693+
self._lstat_result = os.lstat(self._path)
694+
except (OSError, ValueError):
695+
self._lstat_result = _STAT_RESULT_ERROR
696+
raise
697+
return self._lstat_result
698+
699+
def exists(self, *, follow_symlinks=True):
700+
"""Whether this path exists."""
701+
if follow_symlinks:
702+
if self._stat_result is _STAT_RESULT_ERROR:
703+
return False
704+
else:
705+
if self._lstat_result is _STAT_RESULT_ERROR:
706+
return False
707+
try:
708+
self._stat(follow_symlinks=follow_symlinks)
709+
except (OSError, ValueError):
710+
return False
711+
return True
712+
713+
def is_dir(self, *, follow_symlinks=True):
714+
"""Whether this path is a directory."""
715+
if follow_symlinks:
716+
if self._stat_result is _STAT_RESULT_ERROR:
717+
return False
718+
else:
719+
if self._lstat_result is _STAT_RESULT_ERROR:
720+
return False
721+
try:
722+
st = self._stat(follow_symlinks=follow_symlinks)
723+
except (OSError, ValueError):
724+
return False
725+
return S_ISDIR(st.st_mode)
726+
727+
def is_file(self, *, follow_symlinks=True):
728+
"""Whether this path is a regular file."""
729+
if follow_symlinks:
730+
if self._stat_result is _STAT_RESULT_ERROR:
731+
return False
732+
else:
733+
if self._lstat_result is _STAT_RESULT_ERROR:
734+
return False
735+
try:
736+
st = self._stat(follow_symlinks=follow_symlinks)
737+
except (OSError, ValueError):
738+
return False
739+
return S_ISREG(st.st_mode)
740+
741+
def is_symlink(self):
742+
"""Whether this path is a symbolic link."""
743+
if self._lstat_result is _STAT_RESULT_ERROR:
744+
return False
745+
try:
746+
st = self._stat(follow_symlinks=False)
747+
except (OSError, ValueError):
748+
return False
749+
return S_ISLNK(st.st_mode)
750+
751+
752+
class _DirEntryInfo(_Info):
753+
"""Implementation of pathlib.types.PathInfo that provides status
754+
information by querying a wrapped os.DirEntry object. Don't try to
755+
construct it yourself."""
756+
__slots__ = ('_entry',)
757+
758+
def __init__(self, entry):
759+
super().__init__(entry.path)
760+
self._entry = entry
761+
762+
def _stat(self, *, follow_symlinks=True):
763+
"""Return the status as an os.stat_result."""
764+
return self._entry.stat(follow_symlinks=follow_symlinks)
765+
766+
def exists(self, *, follow_symlinks=True):
767+
"""Whether this path exists."""
768+
if not follow_symlinks:
769+
return True
770+
try:
771+
self._stat(follow_symlinks=follow_symlinks)
772+
except OSError:
773+
return False
774+
return True
775+
776+
def is_dir(self, *, follow_symlinks=True):
777+
"""Whether this path is a directory."""
778+
try:
779+
return self._entry.is_dir(follow_symlinks=follow_symlinks)
780+
except OSError:
781+
return False
782+
783+
def is_file(self, *, follow_symlinks=True):
784+
"""Whether this path is a regular file."""
785+
try:
786+
return self._entry.is_file(follow_symlinks=follow_symlinks)
787+
except OSError:
788+
return False
789+
790+
def is_symlink(self):
791+
"""Whether this path is a symbolic link."""
792+
try:
793+
return self._entry.is_symlink()
794+
except OSError:
795+
return False
796+
797+
798+
def _copy_info(info, target, follow_symlinks=True):
799+
"""Copy metadata from the given PathInfo to the given local path."""
800+
copy_times_ns = (
801+
hasattr(info, '_access_time_ns') and
802+
hasattr(info, '_mod_time_ns') and
803+
(follow_symlinks or os.utime in os.supports_follow_symlinks))
804+
if copy_times_ns:
805+
t0 = info._access_time_ns(follow_symlinks=follow_symlinks)
806+
t1 = info._mod_time_ns(follow_symlinks=follow_symlinks)
807+
os.utime(target, ns=(t0, t1), follow_symlinks=follow_symlinks)
808+
809+
# We must copy extended attributes before the file is (potentially)
810+
# chmod()'ed read-only, otherwise setxattr() will error with -EACCES.
811+
copy_xattrs = (
812+
hasattr(info, '_xattrs') and
813+
hasattr(os, 'setxattr') and
814+
(follow_symlinks or os.setxattr in os.supports_follow_symlinks))
815+
if copy_xattrs:
816+
xattrs = info._xattrs(follow_symlinks=follow_symlinks)
817+
for attr, value in xattrs:
818+
try:
819+
os.setxattr(target, attr, value, follow_symlinks=follow_symlinks)
820+
except OSError as e:
821+
if e.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES):
822+
raise
823+
824+
copy_posix_permissions = (
825+
hasattr(info, '_posix_permissions') and
826+
(follow_symlinks or os.chmod in os.supports_follow_symlinks))
827+
if copy_posix_permissions:
828+
posix_permissions = info._posix_permissions(follow_symlinks=follow_symlinks)
829+
try:
830+
os.chmod(target, posix_permissions, follow_symlinks=follow_symlinks)
831+
except NotImplementedError:
832+
# if we got a NotImplementedError, it's because
833+
# * follow_symlinks=False,
834+
# * lchown() is unavailable, and
835+
# * either
836+
# * fchownat() is unavailable or
837+
# * fchownat() doesn't implement AT_SYMLINK_NOFOLLOW.
838+
# (it returned ENOSUP.)
839+
# therefore we're out of options--we simply cannot chown the
840+
# symlink. give up, suppress the error.
841+
# (which is what shutil always did in this circumstance.)
842+
pass
843+
844+
copy_bsd_flags = (
845+
hasattr(info, '_bsd_flags') and
846+
hasattr(os, 'chflags') and
847+
(follow_symlinks or os.chflags in os.supports_follow_symlinks))
848+
if copy_bsd_flags:
849+
bsd_flags = info._bsd_flags(follow_symlinks=follow_symlinks)
850+
try:
851+
os.chflags(target, bsd_flags, follow_symlinks=follow_symlinks)
852+
except OSError as why:
853+
if why.errno not in (EOPNOTSUPP, ENOTSUP):
854+
raise
855+
856+
615857
class Path(PurePath):
616858
"""PurePath subclass that can make system calls.
617859
@@ -637,7 +879,7 @@ def info(self):
637879
try:
638880
return self._info
639881
except AttributeError:
640-
self._info = PathInfo(self)
882+
self._info = _StatResultInfo(str(self))
641883
return self._info
642884

643885
def stat(self, *, follow_symlinks=True):
@@ -817,7 +1059,7 @@ def _filter_trailing_slash(self, paths):
8171059
def _from_dir_entry(self, dir_entry, path_str):
8181060
path = self.with_segments(path_str)
8191061
path._str = path_str
820-
path._info = DirEntryInfo(dir_entry)
1062+
path._info = _DirEntryInfo(dir_entry)
8211063
return path
8221064

8231065
def iterdir(self):
@@ -1123,7 +1365,7 @@ def _copy_from(self, source, follow_symlinks=True, preserve_metadata=False):
11231365
self.joinpath(child.name)._copy_from(
11241366
child, follow_symlinks, preserve_metadata)
11251367
if preserve_metadata:
1126-
copy_info(source.info, self)
1368+
_copy_info(source.info, self)
11271369
else:
11281370
self._copy_from_file(source, preserve_metadata)
11291371

@@ -1133,7 +1375,7 @@ def _copy_from_file(self, source, preserve_metadata=False):
11331375
with open(self, 'wb') as target_f:
11341376
copyfileobj(source_f, target_f)
11351377
if preserve_metadata:
1136-
copy_info(source.info, self)
1378+
_copy_info(source.info, self)
11371379

11381380
if copyfile2:
11391381
# Use fast OS routine for local file copying where available.
@@ -1155,12 +1397,12 @@ def _copy_from_file(self, source, preserve_metadata=False):
11551397
def _copy_from_symlink(self, source, preserve_metadata=False):
11561398
os.symlink(vfspath(source.readlink()), self, source.info.is_dir())
11571399
if preserve_metadata:
1158-
copy_info(source.info, self, follow_symlinks=False)
1400+
_copy_info(source.info, self, follow_symlinks=False)
11591401
else:
11601402
def _copy_from_symlink(self, source, preserve_metadata=False):
11611403
os.symlink(vfspath(source.readlink()), self)
11621404
if preserve_metadata:
1163-
copy_info(source.info, self, follow_symlinks=False)
1405+
_copy_info(source.info, self, follow_symlinks=False)
11641406

11651407
def move(self, target):
11661408
"""

0 commit comments

Comments
 (0)