Skip to content

Commit 9a7c9a2

Browse files
committed
GH-139174: Prepare pathlib.Path.info for new methods
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.
1 parent 3eec897 commit 9a7c9a2

File tree

3 files changed

+79
-114
lines changed

3 files changed

+79
-114
lines changed

Lib/pathlib/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
grp = None
2828

2929
from pathlib._os import (
30-
PathInfo, DirEntryInfo,
30+
StatResultInfo, DirEntryInfo,
3131
vfsopen, vfspath,
3232
ensure_different_files, ensure_distinct_paths,
3333
copyfile2, copyfileobj, copy_info,
@@ -637,7 +637,7 @@ def info(self):
637637
try:
638638
return self._info
639639
except AttributeError:
640-
self._info = PathInfo(self)
640+
self._info = StatResultInfo(str(self))
641641
return self._info
642642

643643
def stat(self, *, follow_symlinks=True):

Lib/pathlib/_os.py

Lines changed: 75 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -363,49 +363,19 @@ def copy_info(info, target, follow_symlinks=True):
363363
raise
364364

365365

366-
class _PathInfoBase:
367-
__slots__ = ('_path', '_stat_result', '_lstat_result')
366+
class _LocalPathInfo:
367+
__slots__ = ('_path',)
368368

369369
def __init__(self, path):
370-
self._path = str(path)
370+
self._path = path
371371

372372
def __repr__(self):
373373
path_type = "WindowsPath" if os.name == "nt" else "PosixPath"
374374
return f"<{path_type}.info>"
375375

376-
def _stat(self, *, follow_symlinks=True, ignore_errors=False):
377-
"""Return the status as an os.stat_result, or None if stat() fails and
378-
ignore_errors is true."""
379-
if follow_symlinks:
380-
try:
381-
result = self._stat_result
382-
except AttributeError:
383-
pass
384-
else:
385-
if ignore_errors or result is not None:
386-
return result
387-
try:
388-
self._stat_result = os.stat(self._path)
389-
except (OSError, ValueError):
390-
self._stat_result = None
391-
if not ignore_errors:
392-
raise
393-
return self._stat_result
394-
else:
395-
try:
396-
result = self._lstat_result
397-
except AttributeError:
398-
pass
399-
else:
400-
if ignore_errors or result is not None:
401-
return result
402-
try:
403-
self._lstat_result = os.lstat(self._path)
404-
except (OSError, ValueError):
405-
self._lstat_result = None
406-
if not ignore_errors:
407-
raise
408-
return self._lstat_result
376+
def _stat(self, *, follow_symlinks=True):
377+
"""Return the status as an os.stat_result."""
378+
raise NotImplementedError
409379

410380
def _posix_permissions(self, *, follow_symlinks=True):
411381
"""Return the POSIX file permissions."""
@@ -443,100 +413,93 @@ def _xattrs(self, *, follow_symlinks=True):
443413
return []
444414

445415

446-
class _WindowsPathInfo(_PathInfoBase):
447-
"""Implementation of pathlib.types.PathInfo that provides status
448-
information for Windows paths. Don't try to construct it yourself."""
449-
__slots__ = ('_exists', '_is_dir', '_is_file', '_is_symlink')
416+
_STAT_RESULT_ERROR = [] # falsy sentinel indicating stat() failed.
450417

451-
def exists(self, *, follow_symlinks=True):
452-
"""Whether this path exists."""
453-
if not follow_symlinks and self.is_symlink():
454-
return True
455-
try:
456-
return self._exists
457-
except AttributeError:
458-
if os.path.exists(self._path):
459-
self._exists = True
460-
return True
461-
else:
462-
self._exists = self._is_dir = self._is_file = False
463-
return False
464418

465-
def is_dir(self, *, follow_symlinks=True):
466-
"""Whether this path is a directory."""
467-
if not follow_symlinks and self.is_symlink():
468-
return False
469-
try:
470-
return self._is_dir
471-
except AttributeError:
472-
if os.path.isdir(self._path):
473-
self._is_dir = self._exists = True
474-
return True
475-
else:
476-
self._is_dir = False
477-
return False
478-
479-
def is_file(self, *, follow_symlinks=True):
480-
"""Whether this path is a regular file."""
481-
if not follow_symlinks and self.is_symlink():
482-
return False
483-
try:
484-
return self._is_file
485-
except AttributeError:
486-
if os.path.isfile(self._path):
487-
self._is_file = self._exists = True
488-
return True
489-
else:
490-
self._is_file = False
491-
return False
492-
493-
def is_symlink(self):
494-
"""Whether this path is a symbolic link."""
495-
try:
496-
return self._is_symlink
497-
except AttributeError:
498-
self._is_symlink = os.path.islink(self._path)
499-
return self._is_symlink
419+
class StatResultInfo(_LocalPathInfo):
420+
"""Implementation of pathlib.types.PathInfo that provides status
421+
information by querying a wrapped os.stat_result object. Don't try to
422+
construct it yourself."""
423+
__slots__ = ('_stat_result', '_lstat_result')
500424

425+
def __init__(self, path):
426+
super().__init__(path)
427+
self._stat_result = None
428+
self._lstat_result = None
501429

502-
class _PosixPathInfo(_PathInfoBase):
503-
"""Implementation of pathlib.types.PathInfo that provides status
504-
information for POSIX paths. Don't try to construct it yourself."""
505-
__slots__ = ()
430+
def _stat(self, *, follow_symlinks=True):
431+
"""Return the status as an os.stat_result."""
432+
if follow_symlinks:
433+
if not self._stat_result:
434+
try:
435+
self._stat_result = os.stat(self._path)
436+
except (OSError, ValueError):
437+
self._stat_result = _STAT_RESULT_ERROR
438+
raise
439+
return self._stat_result
440+
else:
441+
if not self._lstat_result:
442+
try:
443+
self._lstat_result = os.lstat(self._path)
444+
except (OSError, ValueError):
445+
self._lstat_result = _STAT_RESULT_ERROR
446+
raise
447+
return self._lstat_result
506448

507449
def exists(self, *, follow_symlinks=True):
508450
"""Whether this path exists."""
509-
st = self._stat(follow_symlinks=follow_symlinks, ignore_errors=True)
510-
if st is None:
451+
if follow_symlinks:
452+
if self._stat_result is _STAT_RESULT_ERROR:
453+
return False
454+
else:
455+
if self._lstat_result is _STAT_RESULT_ERROR:
456+
return False
457+
try:
458+
self._stat(follow_symlinks=follow_symlinks)
459+
except (OSError, ValueError):
511460
return False
512461
return True
513462

514463
def is_dir(self, *, follow_symlinks=True):
515464
"""Whether this path is a directory."""
516-
st = self._stat(follow_symlinks=follow_symlinks, ignore_errors=True)
517-
if st is None:
465+
if follow_symlinks:
466+
if self._stat_result is _STAT_RESULT_ERROR:
467+
return False
468+
else:
469+
if self._lstat_result is _STAT_RESULT_ERROR:
470+
return False
471+
try:
472+
st = self._stat(follow_symlinks=follow_symlinks)
473+
except (OSError, ValueError):
518474
return False
519475
return S_ISDIR(st.st_mode)
520476

521477
def is_file(self, *, follow_symlinks=True):
522478
"""Whether this path is a regular file."""
523-
st = self._stat(follow_symlinks=follow_symlinks, ignore_errors=True)
524-
if st is None:
479+
if follow_symlinks:
480+
if self._stat_result is _STAT_RESULT_ERROR:
481+
return False
482+
else:
483+
if self._lstat_result is _STAT_RESULT_ERROR:
484+
return False
485+
try:
486+
st = self._stat(follow_symlinks=follow_symlinks)
487+
except (OSError, ValueError):
525488
return False
526489
return S_ISREG(st.st_mode)
527490

528491
def is_symlink(self):
529492
"""Whether this path is a symbolic link."""
530-
st = self._stat(follow_symlinks=False, ignore_errors=True)
531-
if st is None:
493+
if self._lstat_result is _STAT_RESULT_ERROR:
494+
return False
495+
try:
496+
st = self._stat(follow_symlinks=False)
497+
except (OSError, ValueError):
532498
return False
533499
return S_ISLNK(st.st_mode)
534500

535501

536-
PathInfo = _WindowsPathInfo if os.name == 'nt' else _PosixPathInfo
537-
538-
539-
class DirEntryInfo(_PathInfoBase):
502+
class DirEntryInfo(_LocalPathInfo):
540503
"""Implementation of pathlib.types.PathInfo that provides status
541504
information by querying a wrapped os.DirEntry object. Don't try to
542505
construct it yourself."""
@@ -546,19 +509,19 @@ def __init__(self, entry):
546509
super().__init__(entry.path)
547510
self._entry = entry
548511

549-
def _stat(self, *, follow_symlinks=True, ignore_errors=False):
550-
try:
551-
return self._entry.stat(follow_symlinks=follow_symlinks)
552-
except OSError:
553-
if not ignore_errors:
554-
raise
555-
return None
512+
def _stat(self, *, follow_symlinks=True):
513+
"""Return the status as an os.stat_result."""
514+
return self._entry.stat(follow_symlinks=follow_symlinks)
556515

557516
def exists(self, *, follow_symlinks=True):
558517
"""Whether this path exists."""
559518
if not follow_symlinks:
560519
return True
561-
return self._stat(ignore_errors=True) is not None
520+
try:
521+
self._stat(follow_symlinks=follow_symlinks)
522+
except (OSError, ValueError):
523+
return False
524+
return True
562525

563526
def is_dir(self, *, follow_symlinks=True):
564527
"""Whether this path is a directory."""
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Simplify implementation of :attr:`pathlib.Path.info` in preparation for
2+
adding new methods.

0 commit comments

Comments
 (0)