1212"""
1313
1414import functools
15+ import io
1516import operator
1617import posixpath
1718from errno import EINVAL
@@ -41,6 +42,40 @@ def _explode_path(path):
4142 return path , names
4243
4344
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+
4479class PathGlobber (_GlobberBase ):
4580 """
4681 Class providing shell-style globbing for path objects.
@@ -58,35 +93,15 @@ def concat_path(path, text):
5893
5994class CopyReader :
6095 """
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.
6999 """
70100 __slots__ = ('_path' ,)
71101
72102 def __init__ (self , path ):
73103 self ._path = path
74104
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-
90105 _readable_metakeys = frozenset ()
91106
92107 def _read_metadata (self , metakeys , * , follow_symlinks = True ):
@@ -96,8 +111,16 @@ def _read_metadata(self, metakeys, *, follow_symlinks=True):
96111 raise NotImplementedError
97112
98113
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
101124
102125 _writable_metakeys = frozenset ()
103126
@@ -110,7 +133,7 @@ def _write_metadata(self, metadata, *, follow_symlinks=True):
110133 def _create (self , source , follow_symlinks , dirs_exist_ok , preserve_metadata ):
111134 self ._ensure_distinct_path (source )
112135 if preserve_metadata :
113- metakeys = self ._writable_metakeys & source .copy ._readable_metakeys
136+ metakeys = self ._writable_metakeys & source ._copy_reader ._readable_metakeys
114137 else :
115138 metakeys = None
116139 if not follow_symlinks and source .is_symlink ():
@@ -128,22 +151,22 @@ def _create_dir(self, source, metakeys, follow_symlinks, dirs_exist_ok):
128151 for src in children :
129152 dst = self ._path .joinpath (src .name )
130153 if not follow_symlinks and src .is_symlink ():
131- dst .copy ._create_symlink (src , metakeys )
154+ dst ._copy_writer ._create_symlink (src , metakeys )
132155 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 )
134157 else :
135- dst .copy ._create_file (src , metakeys )
158+ dst ._copy_writer ._create_file (src , metakeys )
136159 if metakeys :
137- metadata = source .copy ._read_metadata (metakeys )
160+ metadata = source ._copy_reader ._read_metadata (metakeys )
138161 if metadata :
139162 self ._write_metadata (metadata )
140163
141164 def _create_file (self , source , metakeys ):
142165 """Copy the given file to our path."""
143166 self ._ensure_different_file (source )
144- with source . open ( 'rb' ) as source_f :
167+ with magic_open ( source , 'rb' ) as source_f :
145168 try :
146- with self ._path . open ( 'wb' ) as target_f :
169+ with magic_open ( self ._path , 'wb' ) as target_f :
147170 copyfileobj (source_f , target_f )
148171 except IsADirectoryError as e :
149172 if not self ._path .exists ():
@@ -152,15 +175,15 @@ def _create_file(self, source, metakeys):
152175 f'Directory does not exist: { self ._path } ' ) from e
153176 raise
154177 if metakeys :
155- metadata = source .copy ._read_metadata (metakeys )
178+ metadata = source ._copy_reader ._read_metadata (metakeys )
156179 if metadata :
157180 self ._write_metadata (metadata )
158181
159182 def _create_symlink (self , source , metakeys ):
160183 """Copy the given symbolic link to our path."""
161184 self ._path .symlink_to (source .readlink ())
162185 if metakeys :
163- metadata = source .copy ._read_metadata (metakeys , follow_symlinks = False )
186+ metadata = source ._copy_reader ._read_metadata (metakeys , follow_symlinks = False )
164187 if metadata :
165188 self ._write_metadata (metadata , follow_symlinks = False )
166189
@@ -420,26 +443,25 @@ def is_symlink(self):
420443 """
421444 raise NotImplementedError
422445
423- def open (self , mode = 'r' , buffering = - 1 , encoding = None ,
424- errors = None , newline = None ):
446+ def __open_rb__ (self , buffering = - 1 ):
425447 """
426- Open the file pointed to by this path and return a file object, as
427- the built-in open() function does .
448+ Open the file pointed to by this path for reading in binary mode and
449+ return a file object, like open(mode='rb') .
428450 """
429451 raise NotImplementedError
430452
431453 def read_bytes (self ):
432454 """
433455 Open the file in bytes mode, read it, and close the file.
434456 """
435- with self . open ( mode = 'rb' , buffering = 0 ) as f :
457+ with magic_open ( self , mode = 'rb' , buffering = 0 ) as f :
436458 return f .read ()
437459
438460 def read_text (self , encoding = None , errors = None , newline = None ):
439461 """
440462 Open the file in text mode, read it, and close the file.
441463 """
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 :
443465 return f .read ()
444466
445467 def _scandir (self ):
@@ -532,7 +554,22 @@ def readlink(self):
532554 """
533555 raise NotImplementedError
534556
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 )
536573
537574 def copy_into (self , target_dir , * , follow_symlinks = True ,
538575 dirs_exist_ok = False , preserve_metadata = False ):
@@ -542,7 +579,7 @@ def copy_into(self, target_dir, *, follow_symlinks=True,
542579 name = self .name
543580 if not name :
544581 raise ValueError (f"{ self !r} has an empty name" )
545- elif isinstance (target_dir , ReadablePath ):
582+ elif hasattr (target_dir , '_copy_writer' ):
546583 target = target_dir / name
547584 else :
548585 target = self .with_segments (target_dir , name )
@@ -551,7 +588,7 @@ def copy_into(self, target_dir, *, follow_symlinks=True,
551588 preserve_metadata = preserve_metadata )
552589
553590
554- class WritablePath (ReadablePath ):
591+ class WritablePath (JoinablePath ):
555592 __slots__ = ()
556593
557594 def symlink_to (self , target , target_is_directory = False ):
@@ -567,13 +604,20 @@ def mkdir(self, mode=0o777, parents=False, exist_ok=False):
567604 """
568605 raise NotImplementedError
569606
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+
570614 def write_bytes (self , data ):
571615 """
572616 Open the file in bytes mode, write to it, and close the file.
573617 """
574618 # type-check for the buffer interface before truncating the file
575619 view = memoryview (data )
576- with self . open ( mode = 'wb' ) as f :
620+ with magic_open ( self , mode = 'wb' ) as f :
577621 return f .write (view )
578622
579623 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):
583627 if not isinstance (data , str ):
584628 raise TypeError ('data must be str, not %s' %
585629 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 :
587631 return f .write (data )
588632
589- copy = property (CopyWriter , doc = CopyWriter . __call__ . __doc__ )
633+ _copy_writer = property (CopyWriter )
0 commit comments