16
16
import warnings
17
17
from _collections_abc import Sequence
18
18
from errno import ENOENT , ENOTDIR , EBADF , ELOOP
19
- from operator import attrgetter
20
19
from stat import S_ISDIR , S_ISLNK , S_ISREG , S_ISSOCK , S_ISBLK , S_ISCHR , S_ISFIFO
21
20
from urllib .parse import quote_from_bytes as urlquote_from_bytes
22
21
@@ -216,8 +215,8 @@ class _PathParents(Sequence):
216
215
def __init__ (self , path ):
217
216
# We don't store the instance to avoid reference cycles
218
217
self ._pathcls = type (path )
219
- self ._drv = path ._drv
220
- self ._root = path ._root
218
+ self ._drv = path .drive
219
+ self ._root = path .root
221
220
self ._parts = path ._parts
222
221
223
222
def __len__ (self ):
@@ -251,36 +250,33 @@ class PurePath(object):
251
250
directly, regardless of your system.
252
251
"""
253
252
__slots__ = (
254
- '_drv' , '_root' , '_parts ' ,
253
+ '_raw_path' , ' _drv' , '_root' , '_parts_cached ' ,
255
254
'_str' , '_hash' , '_parts_tuple' , '_parts_normcase_cached' ,
256
255
)
257
256
_flavour = os .path
258
257
259
- def __new__ (cls , * args ):
258
+ def __new__ (cls , * args , ** kwargs ):
260
259
"""Construct a PurePath from one or several strings and or existing
261
260
PurePath objects. The strings and path objects are combined so as
262
261
to yield a canonicalized path, which is incorporated into the
263
262
new PurePath object.
264
263
"""
265
264
if cls is PurePath :
266
265
cls = PureWindowsPath if os .name == 'nt' else PurePosixPath
267
- return cls . _from_parts ( args )
266
+ return object . __new__ ( cls )
268
267
269
268
def __reduce__ (self ):
270
269
# Using the parts tuple helps share interned path parts
271
270
# when pickling related paths.
272
- return (self .__class__ , tuple ( self ._parts ) )
271
+ return (self .__class__ , self .parts )
273
272
274
- @classmethod
275
- def _parse_parts (cls , parts ):
276
- if not parts :
277
- return '' , '' , []
278
- elif len (parts ) == 1 :
279
- path = os .fspath (parts [0 ])
273
+ def __init__ (self , * args ):
274
+ if not args :
275
+ path = ''
276
+ elif len (args ) == 1 :
277
+ path = os .fspath (args [0 ])
280
278
else :
281
- path = cls ._flavour .join (* parts )
282
- sep = cls ._flavour .sep
283
- altsep = cls ._flavour .altsep
279
+ path = self ._flavour .join (* args )
284
280
if isinstance (path , str ):
285
281
# Force-cast str subclasses to str (issue #21127)
286
282
path = str (path )
@@ -289,6 +285,14 @@ def _parse_parts(cls, parts):
289
285
"argument should be a str or an os.PathLike "
290
286
"object where __fspath__ returns a str, "
291
287
f"not { type (path ).__name__ !r} " )
288
+ self ._raw_path = path
289
+
290
+ @classmethod
291
+ def _parse_path (cls , path ):
292
+ if not path :
293
+ return '' , '' , []
294
+ sep = cls ._flavour .sep
295
+ altsep = cls ._flavour .altsep
292
296
if altsep :
293
297
path = path .replace (altsep , sep )
294
298
drv , root , rel = cls ._flavour .splitroot (path )
@@ -299,21 +303,20 @@ def _parse_parts(cls, parts):
299
303
parsed = [sys .intern (x ) for x in unfiltered_parsed if x and x != '.' ]
300
304
return drv , root , parsed
301
305
302
- @classmethod
303
- def _from_parts (cls , args ):
304
- self = object .__new__ (cls )
305
- drv , root , parts = self ._parse_parts (args )
306
+ def _load_parts (self ):
307
+ drv , root , parts = self ._parse_path (self ._raw_path )
306
308
self ._drv = drv
307
309
self ._root = root
308
- self ._parts = parts
309
- return self
310
+ self ._parts_cached = parts
310
311
311
312
@classmethod
312
313
def _from_parsed_parts (cls , drv , root , parts ):
313
- self = object .__new__ (cls )
314
+ path = cls ._format_parsed_parts (drv , root , parts )
315
+ self = cls (path )
316
+ self ._str = path or '.'
314
317
self ._drv = drv
315
318
self ._root = root
316
- self ._parts = parts
319
+ self ._parts_cached = parts
317
320
return self
318
321
319
322
@classmethod
@@ -330,7 +333,7 @@ def __str__(self):
330
333
try :
331
334
return self ._str
332
335
except AttributeError :
333
- self ._str = self ._format_parsed_parts (self ._drv , self ._root ,
336
+ self ._str = self ._format_parsed_parts (self .drive , self .root ,
334
337
self ._parts ) or '.'
335
338
return self ._str
336
339
@@ -356,7 +359,7 @@ def as_uri(self):
356
359
if not self .is_absolute ():
357
360
raise ValueError ("relative path can't be expressed as a file URI" )
358
361
359
- drive = self ._drv
362
+ drive = self .drive
360
363
if len (drive ) == 2 and drive [1 ] == ':' :
361
364
# It's a path on a local drive => 'file:///c:/a/b'
362
365
prefix = 'file:///' + drive
@@ -412,23 +415,43 @@ def __ge__(self, other):
412
415
return NotImplemented
413
416
return self ._parts_normcase >= other ._parts_normcase
414
417
415
- drive = property (attrgetter ('_drv' ),
416
- doc = """The drive prefix (letter or UNC path), if any.""" )
418
+ @property
419
+ def drive (self ):
420
+ """The drive prefix (letter or UNC path), if any."""
421
+ try :
422
+ return self ._drv
423
+ except AttributeError :
424
+ self ._load_parts ()
425
+ return self ._drv
426
+
427
+ @property
428
+ def root (self ):
429
+ """The root of the path, if any."""
430
+ try :
431
+ return self ._root
432
+ except AttributeError :
433
+ self ._load_parts ()
434
+ return self ._root
417
435
418
- root = property (attrgetter ('_root' ),
419
- doc = """The root of the path, if any.""" )
436
+ @property
437
+ def _parts (self ):
438
+ try :
439
+ return self ._parts_cached
440
+ except AttributeError :
441
+ self ._load_parts ()
442
+ return self ._parts_cached
420
443
421
444
@property
422
445
def anchor (self ):
423
446
"""The concatenation of the drive and root, or ''."""
424
- anchor = self ._drv + self ._root
447
+ anchor = self .drive + self .root
425
448
return anchor
426
449
427
450
@property
428
451
def name (self ):
429
452
"""The final path component, if any."""
430
453
parts = self ._parts
431
- if len (parts ) == (1 if (self ._drv or self ._root ) else 0 ):
454
+ if len (parts ) == (1 if (self .drive or self .root ) else 0 ):
432
455
return ''
433
456
return parts [- 1 ]
434
457
@@ -477,7 +500,7 @@ def with_name(self, name):
477
500
drv , root , tail = f .splitroot (name )
478
501
if drv or root or not tail or f .sep in tail or (f .altsep and f .altsep in tail ):
479
502
raise ValueError ("Invalid name %r" % (name ))
480
- return self ._from_parsed_parts (self ._drv , self ._root ,
503
+ return self ._from_parsed_parts (self .drive , self .root ,
481
504
self ._parts [:- 1 ] + [name ])
482
505
483
506
def with_stem (self , stem ):
@@ -502,7 +525,7 @@ def with_suffix(self, suffix):
502
525
name = name + suffix
503
526
else :
504
527
name = name [:- len (old_suffix )] + suffix
505
- return self ._from_parsed_parts (self ._drv , self ._root ,
528
+ return self ._from_parsed_parts (self .drive , self .root ,
506
529
self ._parts [:- 1 ] + [name ])
507
530
508
531
def relative_to (self , other , / , * _deprecated , walk_up = False ):
@@ -561,22 +584,7 @@ def joinpath(self, *args):
561
584
paths) or a totally different path (if one of the arguments is
562
585
anchored).
563
586
"""
564
- drv1 , root1 , parts1 = self ._drv , self ._root , self ._parts
565
- drv2 , root2 , parts2 = self ._parse_parts (args )
566
- if root2 :
567
- if not drv2 and drv1 :
568
- return self ._from_parsed_parts (drv1 , root2 , [drv1 + root2 ] + parts2 [1 :])
569
- else :
570
- return self ._from_parsed_parts (drv2 , root2 , parts2 )
571
- elif drv2 :
572
- if drv2 == drv1 or self ._flavour .normcase (drv2 ) == self ._flavour .normcase (drv1 ):
573
- # Same drive => second path is relative to the first.
574
- return self ._from_parsed_parts (drv1 , root1 , parts1 + parts2 [1 :])
575
- else :
576
- return self ._from_parsed_parts (drv2 , root2 , parts2 )
577
- else :
578
- # Second path is non-anchored (common case).
579
- return self ._from_parsed_parts (drv1 , root1 , parts1 + parts2 )
587
+ return self .__class__ (self ._raw_path , * args )
580
588
581
589
def __truediv__ (self , key ):
582
590
try :
@@ -586,15 +594,15 @@ def __truediv__(self, key):
586
594
587
595
def __rtruediv__ (self , key ):
588
596
try :
589
- return self . _from_parts ([ key ] + self ._parts )
597
+ return type ( self )( key , self ._raw_path )
590
598
except TypeError :
591
599
return NotImplemented
592
600
593
601
@property
594
602
def parent (self ):
595
603
"""The logical parent of the path."""
596
- drv = self ._drv
597
- root = self ._root
604
+ drv = self .drive
605
+ root = self .root
598
606
parts = self ._parts
599
607
if len (parts ) == 1 and (drv or root ):
600
608
return self
@@ -610,7 +618,7 @@ def is_absolute(self):
610
618
a drive)."""
611
619
# ntpath.isabs() is defective - see GH-44626 .
612
620
if self ._flavour is ntpath :
613
- return bool (self ._drv and self ._root )
621
+ return bool (self .drive and self .root )
614
622
return self ._flavour .isabs (self )
615
623
616
624
def is_reserved (self ):
@@ -634,7 +642,7 @@ def match(self, path_pattern):
634
642
Return True if this path matches the given pattern.
635
643
"""
636
644
path_pattern = self ._flavour .normcase (path_pattern )
637
- drv , root , pat_parts = self ._parse_parts (( path_pattern ,) )
645
+ drv , root , pat_parts = self ._parse_path ( path_pattern )
638
646
if not pat_parts :
639
647
raise ValueError ("empty pattern" )
640
648
parts = self ._parts_normcase
@@ -687,20 +695,23 @@ class Path(PurePath):
687
695
"""
688
696
__slots__ = ()
689
697
690
- def __new__ ( cls , * args , ** kwargs ):
698
+ def __init__ ( self , * args , ** kwargs ):
691
699
if kwargs :
692
700
msg = ("support for supplying keyword arguments to pathlib.PurePath "
693
701
"is deprecated and scheduled for removal in Python {remove}" )
694
702
warnings ._deprecated ("pathlib.PurePath(**kwargs)" , msg , remove = (3 , 14 ))
703
+ super ().__init__ (* args )
704
+
705
+ def __new__ (cls , * args , ** kwargs ):
695
706
if cls is Path :
696
707
cls = WindowsPath if os .name == 'nt' else PosixPath
697
- return cls . _from_parts ( args )
708
+ return object . __new__ ( cls )
698
709
699
710
def _make_child_relpath (self , part ):
700
711
# This is an optimization used for dir walking. `part` must be
701
712
# a single part relative to this path.
702
713
parts = self ._parts + [part ]
703
- return self ._from_parsed_parts (self ._drv , self ._root , parts )
714
+ return self ._from_parsed_parts (self .drive , self .root , parts )
704
715
705
716
def __enter__ (self ):
706
717
# In previous versions of pathlib, __exit__() marked this path as
@@ -770,7 +781,7 @@ def glob(self, pattern):
770
781
sys .audit ("pathlib.Path.glob" , self , pattern )
771
782
if not pattern :
772
783
raise ValueError ("Unacceptable pattern: {!r}" .format (pattern ))
773
- drv , root , pattern_parts = self ._parse_parts (( pattern ,) )
784
+ drv , root , pattern_parts = self ._parse_path ( pattern )
774
785
if drv or root :
775
786
raise NotImplementedError ("Non-relative patterns are unsupported" )
776
787
if pattern [- 1 ] in (self ._flavour .sep , self ._flavour .altsep ):
@@ -785,7 +796,7 @@ def rglob(self, pattern):
785
796
this subtree.
786
797
"""
787
798
sys .audit ("pathlib.Path.rglob" , self , pattern )
788
- drv , root , pattern_parts = self ._parse_parts (( pattern ,) )
799
+ drv , root , pattern_parts = self ._parse_path ( pattern )
789
800
if drv or root :
790
801
raise NotImplementedError ("Non-relative patterns are unsupported" )
791
802
if pattern and pattern [- 1 ] in (self ._flavour .sep , self ._flavour .altsep ):
@@ -802,12 +813,12 @@ def absolute(self):
802
813
"""
803
814
if self .is_absolute ():
804
815
return self
805
- elif self ._drv :
816
+ elif self .drive :
806
817
# There is a CWD on each drive-letter drive.
807
- cwd = self ._flavour .abspath (self ._drv )
818
+ cwd = self ._flavour .abspath (self .drive )
808
819
else :
809
820
cwd = os .getcwd ()
810
- return self . _from_parts ([ cwd ] + self ._parts )
821
+ return type ( self )( cwd , self ._raw_path )
811
822
812
823
def resolve (self , strict = False ):
813
824
"""
@@ -825,7 +836,7 @@ def check_eloop(e):
825
836
except OSError as e :
826
837
check_eloop (e )
827
838
raise
828
- p = self . _from_parts (( s ,) )
839
+ p = type ( self )( s )
829
840
830
841
# In non-strict mode, realpath() doesn't raise on symlink loops.
831
842
# Ensure we get an exception by calling stat()
@@ -915,7 +926,7 @@ def readlink(self):
915
926
"""
916
927
if not hasattr (os , "readlink" ):
917
928
raise NotImplementedError ("os.readlink() not available on this system" )
918
- return self . _from_parts (( os .readlink (self ), ))
929
+ return type ( self )( os .readlink (self ))
919
930
920
931
def touch (self , mode = 0o666 , exist_ok = True ):
921
932
"""
@@ -1184,12 +1195,12 @@ def expanduser(self):
1184
1195
""" Return a new path with expanded ~ and ~user constructs
1185
1196
(as returned by os.path.expanduser)
1186
1197
"""
1187
- if (not (self ._drv or self ._root ) and
1198
+ if (not (self .drive or self .root ) and
1188
1199
self ._parts and self ._parts [0 ][:1 ] == '~' ):
1189
1200
homedir = self ._flavour .expanduser (self ._parts [0 ])
1190
1201
if homedir [:1 ] == "~" :
1191
1202
raise RuntimeError ("Could not determine home directory." )
1192
- drv , root , parts = self ._parse_parts (( homedir ,) )
1203
+ drv , root , parts = self ._parse_path ( homedir )
1193
1204
return self ._from_parsed_parts (drv , root , parts + self ._parts [1 :])
1194
1205
1195
1206
return self
0 commit comments