@@ -57,6 +57,132 @@ def concat_path(path, text):
57
57
return path .with_segments (str (path ) + text )
58
58
59
59
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
+
60
186
class PurePathBase :
61
187
"""Base class for pure path objects.
62
188
@@ -374,31 +500,6 @@ def is_symlink(self):
374
500
except (OSError , ValueError ):
375
501
return False
376
502
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
-
402
503
def open (self , mode = 'r' , buffering = - 1 , encoding = None ,
403
504
errors = None , newline = None ):
404
505
"""
@@ -537,88 +638,13 @@ def symlink_to(self, target, target_is_directory=False):
537
638
"""
538
639
raise NotImplementedError
539
640
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
-
547
641
def mkdir (self , mode = 0o777 , parents = False , exist_ok = False ):
548
642
"""
549
643
Create a new directory at this given path.
550
644
"""
551
645
raise NotImplementedError
552
646
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__ )
622
648
623
649
def copy_into (self , target_dir , * , follow_symlinks = True ,
624
650
dirs_exist_ok = False , preserve_metadata = False ):
0 commit comments