77it's developed alongside pathlib. If it finds success and maturity as a PyPI
88package, it could become a public part of the standard library.
99
10- Two base classes are defined here -- PurePathBase and PathBase -- that
11- resemble pathlib's PurePath and Path respectively .
10+ Three base classes are defined here -- JoinablePath, ReadablePath and
11+ WritablePath .
1212"""
1313
1414import functools
@@ -56,13 +56,13 @@ def concat_path(path, text):
5656 return path .with_segments (str (path ) + text )
5757
5858
59- class CopyWorker :
59+ class CopyReader :
6060 """
6161 Class that implements copying between path objects. An instance of this
62- class is available from the PathBase .copy property; it's made callable so
63- that PathBase .copy() can be treated as a method.
62+ class is available from the ReadablePath .copy property; it's made callable
63+ so that ReadablePath .copy() can be treated as a method.
6464
65- The target path's CopyWorker drives the process from its _create() method.
65+ The target path's CopyWriter drives the process from its _create() method.
6666 Files and directories are exchanged by calling methods on the source and
6767 target paths, and metadata is exchanged by calling
6868 source.copy._read_metadata() and target.copy._write_metadata().
@@ -77,11 +77,15 @@ def __call__(self, target, follow_symlinks=True, dirs_exist_ok=False,
7777 """
7878 Recursively copy this file or directory tree to the given destination.
7979 """
80- if not isinstance (target , PathBase ):
80+ if not isinstance (target , ReadablePath ):
8181 target = self ._path .with_segments (target )
8282
83- # Delegate to the target path's CopyWorker object.
84- return target .copy ._create (self ._path , follow_symlinks , dirs_exist_ok , preserve_metadata )
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 )
8589
8690 _readable_metakeys = frozenset ()
8791
@@ -91,6 +95,10 @@ def _read_metadata(self, metakeys, *, follow_symlinks=True):
9195 """
9296 raise NotImplementedError
9397
98+
99+ class CopyWriter (CopyReader ):
100+ __slots__ = ()
101+
94102 _writable_metakeys = frozenset ()
95103
96104 def _write_metadata (self , metadata , * , follow_symlinks = True ):
@@ -182,7 +190,7 @@ def _ensure_distinct_path(self, source):
182190 raise err
183191
184192
185- class PurePathBase :
193+ class JoinablePath :
186194 """Base class for pure path objects.
187195
188196 This class *does not* provide several magic methods that are defined in
@@ -334,7 +342,7 @@ def match(self, path_pattern, *, case_sensitive=None):
334342 is matched. The recursive wildcard '**' is *not* supported by this
335343 method.
336344 """
337- if not isinstance (path_pattern , PurePathBase ):
345+ if not isinstance (path_pattern , JoinablePath ):
338346 path_pattern = self .with_segments (path_pattern )
339347 if case_sensitive is None :
340348 case_sensitive = _is_case_sensitive (self .parser )
@@ -359,7 +367,7 @@ def full_match(self, pattern, *, case_sensitive=None):
359367 Return True if this path matches the given glob-style pattern. The
360368 pattern is matched against the entire path.
361369 """
362- if not isinstance (pattern , PurePathBase ):
370+ if not isinstance (pattern , JoinablePath ):
363371 pattern = self .with_segments (pattern )
364372 if case_sensitive is None :
365373 case_sensitive = _is_case_sensitive (self .parser )
@@ -369,7 +377,7 @@ def full_match(self, pattern, *, case_sensitive=None):
369377
370378
371379
372- class PathBase ( PurePathBase ):
380+ class ReadablePath ( JoinablePath ):
373381 """Base class for concrete path objects.
374382
375383 This class provides dummy implementations for many methods that derived
@@ -434,25 +442,6 @@ def read_text(self, encoding=None, errors=None, newline=None):
434442 with self .open (mode = 'r' , encoding = encoding , errors = errors , newline = newline ) as f :
435443 return f .read ()
436444
437- def write_bytes (self , data ):
438- """
439- Open the file in bytes mode, write to it, and close the file.
440- """
441- # type-check for the buffer interface before truncating the file
442- view = memoryview (data )
443- with self .open (mode = 'wb' ) as f :
444- return f .write (view )
445-
446- def write_text (self , data , encoding = None , errors = None , newline = None ):
447- """
448- Open the file in text mode, write to it, and close the file.
449- """
450- if not isinstance (data , str ):
451- raise TypeError ('data must be str, not %s' %
452- data .__class__ .__name__ )
453- with self .open (mode = 'w' , encoding = encoding , errors = errors , newline = newline ) as f :
454- return f .write (data )
455-
456445 def _scandir (self ):
457446 """Yield os.DirEntry-like objects of the directory contents.
458447
@@ -474,7 +463,7 @@ def glob(self, pattern, *, case_sensitive=None, recurse_symlinks=True):
474463 """Iterate over this subtree and yield all existing files (of any
475464 kind, including directories) matching the given relative pattern.
476465 """
477- if not isinstance (pattern , PurePathBase ):
466+ if not isinstance (pattern , JoinablePath ):
478467 pattern = self .with_segments (pattern )
479468 anchor , parts = _explode_path (pattern )
480469 if anchor :
@@ -496,7 +485,7 @@ def rglob(self, pattern, *, case_sensitive=None, recurse_symlinks=True):
496485 directories) matching the given relative pattern, anywhere in
497486 this subtree.
498487 """
499- if not isinstance (pattern , PurePathBase ):
488+ if not isinstance (pattern , JoinablePath ):
500489 pattern = self .with_segments (pattern )
501490 pattern = '**' / pattern
502491 return self .glob (pattern , case_sensitive = case_sensitive , recurse_symlinks = recurse_symlinks )
@@ -543,6 +532,28 @@ def readlink(self):
543532 """
544533 raise NotImplementedError
545534
535+ copy = property (CopyReader , doc = CopyReader .__call__ .__doc__ )
536+
537+ def copy_into (self , target_dir , * , follow_symlinks = True ,
538+ dirs_exist_ok = False , preserve_metadata = False ):
539+ """
540+ Copy this file or directory tree into the given existing directory.
541+ """
542+ name = self .name
543+ if not name :
544+ raise ValueError (f"{ self !r} has an empty name" )
545+ elif isinstance (target_dir , ReadablePath ):
546+ target = target_dir / name
547+ else :
548+ target = self .with_segments (target_dir , name )
549+ return self .copy (target , follow_symlinks = follow_symlinks ,
550+ dirs_exist_ok = dirs_exist_ok ,
551+ preserve_metadata = preserve_metadata )
552+
553+
554+ class WritablePath (ReadablePath ):
555+ __slots__ = ()
556+
546557 def symlink_to (self , target , target_is_directory = False ):
547558 """
548559 Make this path a symlink pointing to the target path.
@@ -556,20 +567,23 @@ def mkdir(self, mode=0o777, parents=False, exist_ok=False):
556567 """
557568 raise NotImplementedError
558569
559- copy = property (CopyWorker , doc = CopyWorker .__call__ .__doc__ )
570+ def write_bytes (self , data ):
571+ """
572+ Open the file in bytes mode, write to it, and close the file.
573+ """
574+ # type-check for the buffer interface before truncating the file
575+ view = memoryview (data )
576+ with self .open (mode = 'wb' ) as f :
577+ return f .write (view )
560578
561- def copy_into (self , target_dir , * , follow_symlinks = True ,
562- dirs_exist_ok = False , preserve_metadata = False ):
579+ def write_text (self , data , encoding = None , errors = None , newline = None ):
563580 """
564- Copy this file or directory tree into the given existing directory .
581+ Open the file in text mode, write to it, and close the file .
565582 """
566- name = self .name
567- if not name :
568- raise ValueError (f"{ self !r} has an empty name" )
569- elif isinstance (target_dir , PathBase ):
570- target = target_dir / name
571- else :
572- target = self .with_segments (target_dir , name )
573- return self .copy (target , follow_symlinks = follow_symlinks ,
574- dirs_exist_ok = dirs_exist_ok ,
575- preserve_metadata = preserve_metadata )
583+ if not isinstance (data , str ):
584+ raise TypeError ('data must be str, not %s' %
585+ data .__class__ .__name__ )
586+ with self .open (mode = 'w' , encoding = encoding , errors = errors , newline = newline ) as f :
587+ return f .write (data )
588+
589+ copy = property (CopyWriter , doc = CopyWriter .__call__ .__doc__ )
0 commit comments