1414from errno import *
1515from glob import _StringGlobber , _no_recurse_symlinks
1616from 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+ )
1820from _collections_abc import Sequence
1921
2022try :
2729 grp = None
2830
2931from pathlib ._os import (
30- StatResultInfo , 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+
615857class 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 = StatResultInfo (str (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