12
12
"""
13
13
14
14
import functools
15
+ import io
15
16
import operator
16
17
import posixpath
17
18
from errno import EINVAL
@@ -41,6 +42,40 @@ def _explode_path(path):
41
42
return path ,names
42
43
43
44
45
+ def magic_open (path ,mode = 'r' ,buffering = - 1 ,encoding = None ,errors = None ,
46
+ newline = None ):
47
+ """
48
+ Open the file pointed to by this path and return a file object, as
49
+ the built-in open() function does.
50
+ """
51
+ try :
52
+ return io .open (path ,mode ,buffering ,encoding ,errors ,newline )
53
+ except TypeError :
54
+ pass
55
+ cls = type (path )
56
+ text = 'b' not in mode
57
+ mode = '' .join (sorted (c for c in mode if c not in 'bt' ))
58
+ if text :
59
+ try :
60
+ attr = getattr (cls ,f'__open_{ mode } __' )
61
+ except AttributeError :
62
+ pass
63
+ else :
64
+ return attr (path ,buffering ,encoding ,errors ,newline )
65
+
66
+ try :
67
+ attr = getattr (cls ,f'__open_{ mode } b__' )
68
+ except AttributeError :
69
+ pass
70
+ else :
71
+ stream = attr (path ,buffering )
72
+ if text :
73
+ stream = io .TextIOWrapper (stream ,encoding ,errors ,newline )
74
+ return stream
75
+
76
+ raise TypeError (f"{ cls .__name__ } can't be opened with mode{ mode !r} " )
77
+
78
+
44
79
class PathGlobber (_GlobberBase ):
45
80
"""
46
81
Class providing shell-style globbing for path objects.
@@ -58,35 +93,15 @@ def concat_path(path, text):
58
93
59
94
class CopyReader :
60
95
"""
61
- Class that implements copying between path objects. An instance of this
62
- class is available from the ReadablePath.copy property; it's made callable
63
- so that ReadablePath.copy() can be treated as a method.
64
-
65
- The target path's CopyWriter drives the process from its _create() method.
66
- Files and directories are exchanged by calling methods on the source and
67
- target paths, and metadata is exchanged by calling
68
- source.copy._read_metadata() and target.copy._write_metadata().
96
+ Class that implements the "read" part of copying between path objects.
97
+ An instance of this class is available from the ReadablePath._copy_reader
98
+ property.
69
99
"""
70
100
__slots__ = ('_path' ,)
71
101
72
102
def __init__ (self ,path ):
73
103
self ._path = path
74
104
75
- def __call__ (self ,target ,follow_symlinks = True ,dirs_exist_ok = False ,
76
- preserve_metadata = False ):
77
- """
78
- Recursively copy this file or directory tree to the given destination.
79
- """
80
- if not isinstance (target ,ReadablePath ):
81
- target = self ._path .with_segments (target )
82
-
83
- # Delegate to the target path's CopyWriter object.
84
- try :
85
- create = target .copy ._create
86
- except AttributeError :
87
- raise TypeError (f"Target is not writable:{ target } " )from None
88
- return create (self ._path ,follow_symlinks ,dirs_exist_ok ,preserve_metadata )
89
-
90
105
_readable_metakeys = frozenset ()
91
106
92
107
def _read_metadata (self ,metakeys ,* ,follow_symlinks = True ):
@@ -96,8 +111,16 @@ def _read_metadata(self, metakeys, *, follow_symlinks=True):
96
111
raise NotImplementedError
97
112
98
113
99
- class CopyWriter (CopyReader ):
100
- __slots__ = ()
114
+ class CopyWriter :
115
+ """
116
+ Class that implements the "write" part of copying between path objects. An
117
+ instance of this class is available from the WritablePath._copy_writer
118
+ property.
119
+ """
120
+ __slots__ = ('_path' ,)
121
+
122
+ def __init__ (self ,path ):
123
+ self ._path = path
101
124
102
125
_writable_metakeys = frozenset ()
103
126
@@ -110,7 +133,7 @@ def _write_metadata(self, metadata, *, follow_symlinks=True):
110
133
def _create (self ,source ,follow_symlinks ,dirs_exist_ok ,preserve_metadata ):
111
134
self ._ensure_distinct_path (source )
112
135
if preserve_metadata :
113
- metakeys = self ._writable_metakeys & source .copy ._readable_metakeys
136
+ metakeys = self ._writable_metakeys & source ._copy_reader ._readable_metakeys
114
137
else :
115
138
metakeys = None
116
139
if not follow_symlinks and source .is_symlink ():
@@ -128,22 +151,22 @@ def _create_dir(self, source, metakeys, follow_symlinks, dirs_exist_ok):
128
151
for src in children :
129
152
dst = self ._path .joinpath (src .name )
130
153
if not follow_symlinks and src .is_symlink ():
131
- dst .copy ._create_symlink (src ,metakeys )
154
+ dst ._copy_writer ._create_symlink (src ,metakeys )
132
155
elif src .is_dir ():
133
- dst .copy ._create_dir (src ,metakeys ,follow_symlinks ,dirs_exist_ok )
156
+ dst ._copy_writer ._create_dir (src ,metakeys ,follow_symlinks ,dirs_exist_ok )
134
157
else :
135
- dst .copy ._create_file (src ,metakeys )
158
+ dst ._copy_writer ._create_file (src ,metakeys )
136
159
if metakeys :
137
- metadata = source .copy ._read_metadata (metakeys )
160
+ metadata = source ._copy_reader ._read_metadata (metakeys )
138
161
if metadata :
139
162
self ._write_metadata (metadata )
140
163
141
164
def _create_file (self ,source ,metakeys ):
142
165
"""Copy the given file to our path."""
143
166
self ._ensure_different_file (source )
144
- with source . open ( 'rb' )as source_f :
167
+ with magic_open ( source , 'rb' )as source_f :
145
168
try :
146
- with self ._path . open ( 'wb' )as target_f :
169
+ with magic_open ( self ._path , 'wb' )as target_f :
147
170
copyfileobj (source_f ,target_f )
148
171
except IsADirectoryError as e :
149
172
if not self ._path .exists ():
@@ -152,15 +175,15 @@ def _create_file(self, source, metakeys):
152
175
f'Directory does not exist:{ self ._path } ' )from e
153
176
raise
154
177
if metakeys :
155
- metadata = source .copy ._read_metadata (metakeys )
178
+ metadata = source ._copy_reader ._read_metadata (metakeys )
156
179
if metadata :
157
180
self ._write_metadata (metadata )
158
181
159
182
def _create_symlink (self ,source ,metakeys ):
160
183
"""Copy the given symbolic link to our path."""
161
184
self ._path .symlink_to (source .readlink ())
162
185
if metakeys :
163
- metadata = source .copy ._read_metadata (metakeys ,follow_symlinks = False )
186
+ metadata = source ._copy_reader ._read_metadata (metakeys ,follow_symlinks = False )
164
187
if metadata :
165
188
self ._write_metadata (metadata ,follow_symlinks = False )
166
189
@@ -420,26 +443,25 @@ def is_symlink(self):
420
443
"""
421
444
raise NotImplementedError
422
445
423
- def open (self ,mode = 'r' ,buffering = - 1 ,encoding = None ,
424
- errors = None ,newline = None ):
446
+ def __open_rb__ (self ,buffering = - 1 ):
425
447
"""
426
- Open the file pointed to by this pathand return a file object, as
427
- the built-in open() function does .
448
+ Open the file pointed to by this pathfor reading in binary mode and
449
+ return a file object, like open(mode='rb') .
428
450
"""
429
451
raise NotImplementedError
430
452
431
453
def read_bytes (self ):
432
454
"""
433
455
Open the file in bytes mode, read it, and close the file.
434
456
"""
435
- with self . open ( mode = 'rb' ,buffering = 0 )as f :
457
+ with magic_open ( self , mode = 'rb' ,buffering = 0 )as f :
436
458
return f .read ()
437
459
438
460
def read_text (self ,encoding = None ,errors = None ,newline = None ):
439
461
"""
440
462
Open the file in text mode, read it, and close the file.
441
463
"""
442
- with self . open ( mode = 'r' ,encoding = encoding ,errors = errors ,newline = newline )as f :
464
+ with magic_open ( self , mode = 'r' ,encoding = encoding ,errors = errors ,newline = newline )as f :
443
465
return f .read ()
444
466
445
467
def _scandir (self ):
@@ -532,7 +554,22 @@ def readlink(self):
532
554
"""
533
555
raise NotImplementedError
534
556
535
- copy = property (CopyReader ,doc = CopyReader .__call__ .__doc__ )
557
+ _copy_reader = property (CopyReader )
558
+
559
+ def copy (self ,target ,follow_symlinks = True ,dirs_exist_ok = False ,
560
+ preserve_metadata = False ):
561
+ """
562
+ Recursively copy this file or directory tree to the given destination.
563
+ """
564
+ if not hasattr (target ,'_copy_writer' ):
565
+ target = self .with_segments (target )
566
+
567
+ # Delegate to the target path's CopyWriter object.
568
+ try :
569
+ create = target ._copy_writer ._create
570
+ except AttributeError :
571
+ raise TypeError (f"Target is not writable:{ target } " )from None
572
+ return create (self ,follow_symlinks ,dirs_exist_ok ,preserve_metadata )
536
573
537
574
def copy_into (self ,target_dir ,* ,follow_symlinks = True ,
538
575
dirs_exist_ok = False ,preserve_metadata = False ):
@@ -542,7 +579,7 @@ def copy_into(self, target_dir, *, follow_symlinks=True,
542
579
name = self .name
543
580
if not name :
544
581
raise ValueError (f"{ self !r} has an empty name" )
545
- elif isinstance (target_dir ,ReadablePath ):
582
+ elif hasattr (target_dir ,'_copy_writer' ):
546
583
target = target_dir / name
547
584
else :
548
585
target = self .with_segments (target_dir ,name )
@@ -551,7 +588,7 @@ def copy_into(self, target_dir, *, follow_symlinks=True,
551
588
preserve_metadata = preserve_metadata )
552
589
553
590
554
- class WritablePath (ReadablePath ):
591
+ class WritablePath (JoinablePath ):
555
592
__slots__ = ()
556
593
557
594
def symlink_to (self ,target ,target_is_directory = False ):
@@ -567,13 +604,20 @@ def mkdir(self, mode=0o777, parents=False, exist_ok=False):
567
604
"""
568
605
raise NotImplementedError
569
606
607
+ def __open_wb__ (self ,buffering = - 1 ):
608
+ """
609
+ Open the file pointed to by this path for writing in binary mode and
610
+ return a file object, like open(mode='wb').
611
+ """
612
+ raise NotImplementedError
613
+
570
614
def write_bytes (self ,data ):
571
615
"""
572
616
Open the file in bytes mode, write to it, and close the file.
573
617
"""
574
618
# type-check for the buffer interface before truncating the file
575
619
view = memoryview (data )
576
- with self . open ( mode = 'wb' )as f :
620
+ with magic_open ( self , mode = 'wb' )as f :
577
621
return f .write (view )
578
622
579
623
def write_text (self ,data ,encoding = None ,errors = None ,newline = None ):
@@ -583,7 +627,7 @@ def write_text(self, data, encoding=None, errors=None, newline=None):
583
627
if not isinstance (data ,str ):
584
628
raise TypeError ('data must be str, not %s' %
585
629
data .__class__ .__name__ )
586
- with self . open ( mode = 'w' ,encoding = encoding ,errors = errors ,newline = newline )as f :
630
+ with magic_open ( self , mode = 'w' ,encoding = encoding ,errors = errors ,newline = newline )as f :
587
631
return f .write (data )
588
632
589
- copy = property (CopyWriter , doc = CopyWriter . __call__ . __doc__ )
633
+ _copy_writer = property (CopyWriter )