@@ -57,6 +57,132 @@ def concat_path(path, text):
5757 return path .with_segments (str (path ) + text )
5858
5959
60+ class CopyWorker :
61+ """
62+ Class that implements copying between path objects. An instance of this
63+ class is available from the PathBase.copy property; it's made callable so
64+ that PathBase.copy() can be treated as a method.
65+
66+ The target path's CopyWorker drives the process from its _create() method.
67+ Files and directories are exchanged by calling methods on the source and
68+ target paths, and metadata is exchanged by calling
69+ source.copy._read_metadata() and target.copy._write_metadata().
70+ """
71+ __slots__ = ('_path' ,)
72+
73+ def __init__ (self , path ):
74+ self ._path = path
75+
76+ def __call__ (self , target , follow_symlinks = True , dirs_exist_ok = False ,
77+ preserve_metadata = False ):
78+ """
79+ Recursively copy this file or directory tree to the given destination.
80+ """
81+ if not isinstance (target , PathBase ):
82+ target = self ._path .with_segments (target )
83+
84+ # Delegate to the target path's CopyWorker object.
85+ return target .copy ._create (self ._path , follow_symlinks , dirs_exist_ok , preserve_metadata )
86+
87+ _readable_metakeys = frozenset ()
88+
89+ def _read_metadata (self , metakeys , * , follow_symlinks = True ):
90+ """
91+ Returns path metadata as a dict with string keys.
92+ """
93+ raise NotImplementedError
94+
95+ _writable_metakeys = frozenset ()
96+
97+ def _write_metadata (self , metadata , * , follow_symlinks = True ):
98+ """
99+ Sets path metadata from the given dict with string keys.
100+ """
101+ raise NotImplementedError
102+
103+ def _create (self , source , follow_symlinks , dirs_exist_ok , preserve_metadata ):
104+ self ._ensure_distinct_path (source )
105+ if preserve_metadata :
106+ metakeys = self ._writable_metakeys & source .copy ._readable_metakeys
107+ else :
108+ metakeys = None
109+ if not follow_symlinks and source .is_symlink ():
110+ self ._create_symlink (source , metakeys )
111+ elif source .is_dir ():
112+ self ._create_dir (source , metakeys , follow_symlinks , dirs_exist_ok )
113+ else :
114+ self ._create_file (source , metakeys )
115+ return self ._path
116+
117+ def _create_dir (self , source , metakeys , follow_symlinks , dirs_exist_ok ):
118+ """Copy the given directory to our path."""
119+ children = list (source .iterdir ())
120+ self ._path .mkdir (exist_ok = dirs_exist_ok )
121+ for src in children :
122+ dst = self ._path .joinpath (src .name )
123+ if not follow_symlinks and src .is_symlink ():
124+ dst .copy ._create_symlink (src , metakeys )
125+ elif src .is_dir ():
126+ dst .copy ._create_dir (src , metakeys , follow_symlinks , dirs_exist_ok )
127+ else :
128+ dst .copy ._create_file (src , metakeys )
129+ if metakeys :
130+ metadata = source .copy ._read_metadata (metakeys )
131+ if metadata :
132+ self ._write_metadata (metadata )
133+
134+ def _create_file (self , source , metakeys ):
135+ """Copy the given file to our path."""
136+ self ._ensure_different_file (source )
137+ with source .open ('rb' ) as source_f :
138+ try :
139+ with self ._path .open ('wb' ) as target_f :
140+ copyfileobj (source_f , target_f )
141+ except IsADirectoryError as e :
142+ if not self ._path .exists ():
143+ # Raise a less confusing exception.
144+ raise FileNotFoundError (
145+ f'Directory does not exist: { self ._path } ' ) from e
146+ raise
147+ if metakeys :
148+ metadata = source .copy ._read_metadata (metakeys )
149+ if metadata :
150+ self ._write_metadata (metadata )
151+
152+ def _create_symlink (self , source , metakeys ):
153+ """Copy the given symbolic link to our path."""
154+ self ._path .symlink_to (source .readlink ())
155+ if metakeys :
156+ metadata = source .copy ._read_metadata (metakeys , follow_symlinks = False )
157+ if metadata :
158+ self ._write_metadata (metadata , follow_symlinks = False )
159+
160+ def _ensure_different_file (self , source ):
161+ """
162+ Raise OSError(EINVAL) if both paths refer to the same file.
163+ """
164+ pass
165+
166+ def _ensure_distinct_path (self , source ):
167+ """
168+ Raise OSError(EINVAL) if the other path is within this path.
169+ """
170+ # Note: there is no straightforward, foolproof algorithm to determine
171+ # if one directory is within another (a particularly perverse example
172+ # would be a single network share mounted in one location via NFS, and
173+ # in another location via CIFS), so we simply checks whether the
174+ # other path is lexically equal to, or within, this path.
175+ if source == self ._path :
176+ err = OSError (EINVAL , "Source and target are the same path" )
177+ elif source in self ._path .parents :
178+ err = OSError (EINVAL , "Source path is a parent of target path" )
179+ else :
180+ return
181+ err .filename = str (source )
182+ err .filename2 = str (self ._path )
183+ raise err
184+
185+
60186class PurePathBase :
61187 """Base class for pure path objects.
62188
@@ -374,31 +500,6 @@ def is_symlink(self):
374500 except (OSError , ValueError ):
375501 return False
376502
377- def _ensure_different_file (self , other_path ):
378- """
379- Raise OSError(EINVAL) if both paths refer to the same file.
380- """
381- pass
382-
383- def _ensure_distinct_path (self , other_path ):
384- """
385- Raise OSError(EINVAL) if the other path is within this path.
386- """
387- # Note: there is no straightforward, foolproof algorithm to determine
388- # if one directory is within another (a particularly perverse example
389- # would be a single network share mounted in one location via NFS, and
390- # in another location via CIFS), so we simply checks whether the
391- # other path is lexically equal to, or within, this path.
392- if self == other_path :
393- err = OSError (EINVAL , "Source and target are the same path" )
394- elif self in other_path .parents :
395- err = OSError (EINVAL , "Source path is a parent of target path" )
396- else :
397- return
398- err .filename = str (self )
399- err .filename2 = str (other_path )
400- raise err
401-
402503 def open (self , mode = 'r' , buffering = - 1 , encoding = None ,
403504 errors = None , newline = None ):
404505 """
@@ -537,88 +638,13 @@ def symlink_to(self, target, target_is_directory=False):
537638 """
538639 raise NotImplementedError
539640
540- def _symlink_to_target_of (self , link ):
541- """
542- Make this path a symlink with the same target as the given link. This
543- is used by copy().
544- """
545- self .symlink_to (link .readlink ())
546-
547641 def mkdir (self , mode = 0o777 , parents = False , exist_ok = False ):
548642 """
549643 Create a new directory at this given path.
550644 """
551645 raise NotImplementedError
552646
553- # Metadata keys supported by this path type.
554- _readable_metadata = _writable_metadata = frozenset ()
555-
556- def _read_metadata (self , keys = None , * , follow_symlinks = True ):
557- """
558- Returns path metadata as a dict with string keys.
559- """
560- raise NotImplementedError
561-
562- def _write_metadata (self , metadata , * , follow_symlinks = True ):
563- """
564- Sets path metadata from the given dict with string keys.
565- """
566- raise NotImplementedError
567-
568- def _copy_metadata (self , target , * , follow_symlinks = True ):
569- """
570- Copies metadata (permissions, timestamps, etc) from this path to target.
571- """
572- # Metadata types supported by both source and target.
573- keys = self ._readable_metadata & target ._writable_metadata
574- if keys :
575- metadata = self ._read_metadata (keys , follow_symlinks = follow_symlinks )
576- target ._write_metadata (metadata , follow_symlinks = follow_symlinks )
577-
578- def _copy_file (self , target ):
579- """
580- Copy the contents of this file to the given target.
581- """
582- self ._ensure_different_file (target )
583- with self .open ('rb' ) as source_f :
584- try :
585- with target .open ('wb' ) as target_f :
586- copyfileobj (source_f , target_f )
587- except IsADirectoryError as e :
588- if not target .exists ():
589- # Raise a less confusing exception.
590- raise FileNotFoundError (
591- f'Directory does not exist: { target } ' ) from e
592- else :
593- raise
594-
595- def copy (self , target , * , follow_symlinks = True , dirs_exist_ok = False ,
596- preserve_metadata = False ):
597- """
598- Recursively copy this file or directory tree to the given destination.
599- """
600- if not isinstance (target , PathBase ):
601- target = self .with_segments (target )
602- self ._ensure_distinct_path (target )
603- stack = [(self , target )]
604- while stack :
605- src , dst = stack .pop ()
606- if not follow_symlinks and src .is_symlink ():
607- dst ._symlink_to_target_of (src )
608- if preserve_metadata :
609- src ._copy_metadata (dst , follow_symlinks = False )
610- elif src .is_dir ():
611- children = src .iterdir ()
612- dst .mkdir (exist_ok = dirs_exist_ok )
613- stack .extend ((child , dst .joinpath (child .name ))
614- for child in children )
615- if preserve_metadata :
616- src ._copy_metadata (dst )
617- else :
618- src ._copy_file (dst )
619- if preserve_metadata :
620- src ._copy_metadata (dst )
621- return target
647+ copy = property (CopyWorker , doc = CopyWorker .__call__ .__doc__ )
622648
623649 def copy_into (self , target_dir , * , follow_symlinks = True ,
624650 dirs_exist_ok = False , preserve_metadata = False ):
0 commit comments