12
12
"""
13
13
14
14
import functools
15
- import posixpath
16
15
from errno import ENOENT , ENOTDIR , EBADF , ELOOP , EINVAL
17
16
from stat import S_ISDIR , S_ISLNK , S_ISREG , S_ISSOCK , S_ISBLK , S_ISCHR , S_ISFIFO
18
17
19
18
#
20
19
# Internals
21
20
#
22
21
23
- # Reference for Windows paths can be found at
24
- # https://learn.microsoft.com/en-gb/windows/win32/fileio/naming-a-file .
25
- _WIN_RESERVED_NAMES = frozenset (
26
- {'CON' , 'PRN' , 'AUX' , 'NUL' , 'CONIN$' , 'CONOUT$' } |
27
- {f'COM{ c } ' for c in '123456789\xb9 \xb2 \xb3 ' } |
28
- {f'LPT{ c } ' for c in '123456789\xb9 \xb2 \xb3 ' }
29
- )
30
-
31
22
_WINERROR_NOT_READY = 21 # drive exists but is not accessible
32
23
_WINERROR_INVALID_NAME = 123 # fix for bpo-35306
33
24
_WINERROR_CANT_RESOLVE_FILENAME = 1921 # broken symlink pointing to itself
@@ -144,6 +135,53 @@ class UnsupportedOperation(NotImplementedError):
144
135
pass
145
136
146
137
138
+ class PathModuleBase :
139
+ """Base class for path modules, which do low-level path manipulation.
140
+
141
+ Path modules provide a subset of the os.path API, specifically those
142
+ functions needed to provide PurePathBase functionality. Each PurePathBase
143
+ subclass references its path module via a 'pathmod' class attribute.
144
+
145
+ Every method in this base class raises an UnsupportedOperation exception.
146
+ """
147
+
148
+ @classmethod
149
+ def _unsupported (cls , attr ):
150
+ raise UnsupportedOperation (f"{ cls .__name__ } .{ attr } is unsupported" )
151
+
152
+ @property
153
+ def sep (self ):
154
+ """The character used to separate path components."""
155
+ self ._unsupported ('sep' )
156
+
157
+ def join (self , path , * paths ):
158
+ """Join path segments."""
159
+ self ._unsupported ('join()' )
160
+
161
+ def split (self , path ):
162
+ """Split the path into a pair (head, tail), where *head* is everything
163
+ before the final path separator, and *tail* is everything after.
164
+ Either part may be empty.
165
+ """
166
+ self ._unsupported ('split()' )
167
+
168
+ def splitroot (self , path ):
169
+ """Split the pathname path into a 3-item tuple (drive, root, tail),
170
+ where *drive* is a device name or mount point, *root* is a string of
171
+ separators after the drive, and *tail* is everything after the root.
172
+ Any part may be empty."""
173
+ self ._unsupported ('splitroot()' )
174
+
175
+ def normcase (self , path ):
176
+ """Normalize the case of the path."""
177
+ self ._unsupported ('normcase()' )
178
+
179
+ def isabs (self , path ):
180
+ """Returns whether the path is absolute, i.e. unaffected by the
181
+ current directory or drive."""
182
+ self ._unsupported ('isabs()' )
183
+
184
+
147
185
class PurePathBase :
148
186
"""Base class for pure path objects.
149
187
@@ -154,19 +192,19 @@ class PurePathBase:
154
192
"""
155
193
156
194
__slots__ = (
157
- # The `_raw_paths ` slot stores unnormalized string paths . This is set
158
- # in the `__init__()` method.
159
- '_raw_paths ' ,
195
+ # The `_raw_path ` slot store a joined string path . This is set in the
196
+ # `__init__()` method.
197
+ '_raw_path ' ,
160
198
161
199
# The '_resolving' slot stores a boolean indicating whether the path
162
200
# is being processed by `PathBase.resolve()`. This prevents duplicate
163
201
# work from occurring when `resolve()` calls `stat()` or `readlink()`.
164
202
'_resolving' ,
165
203
)
166
- pathmod = posixpath
204
+ pathmod = PathModuleBase ()
167
205
168
- def __init__ (self , * paths ):
169
- self ._raw_paths = paths
206
+ def __init__ (self , path , * paths ):
207
+ self ._raw_path = self . pathmod . join ( path , * paths ) if paths else path
170
208
self ._resolving = False
171
209
172
210
def with_segments (self , * pathsegments ):
@@ -176,11 +214,6 @@ def with_segments(self, *pathsegments):
176
214
"""
177
215
return type (self )(* pathsegments )
178
216
179
- @property
180
- def _raw_path (self ):
181
- """The joined but unnormalized path."""
182
- return self .pathmod .join (* self ._raw_paths )
183
-
184
217
def __str__ (self ):
185
218
"""Return the string representation of the path, suitable for
186
219
passing to system calls."""
@@ -194,7 +227,7 @@ def as_posix(self):
194
227
@property
195
228
def drive (self ):
196
229
"""The drive prefix (letter or UNC path), if any."""
197
- return self .pathmod .splitdrive (self ._raw_path )[0 ]
230
+ return self .pathmod .splitroot (self ._raw_path )[0 ]
198
231
199
232
@property
200
233
def root (self ):
@@ -210,7 +243,7 @@ def anchor(self):
210
243
@property
211
244
def name (self ):
212
245
"""The final path component, if any."""
213
- return self .pathmod .basename (self ._raw_path )
246
+ return self .pathmod .split (self ._raw_path )[ 1 ]
214
247
215
248
@property
216
249
def suffix (self ):
@@ -251,10 +284,10 @@ def stem(self):
251
284
252
285
def with_name (self , name ):
253
286
"""Return a new path with the file name changed."""
254
- dirname = self .pathmod .dirname
255
- if dirname (name ):
287
+ split = self .pathmod .split
288
+ if split (name )[ 0 ] :
256
289
raise ValueError (f"Invalid name { name !r} " )
257
- return self .with_segments (dirname (self ._raw_path ), name )
290
+ return self .with_segments (split (self ._raw_path )[ 0 ] , name )
258
291
259
292
def with_stem (self , stem ):
260
293
"""Return a new path with the stem changed."""
@@ -336,17 +369,17 @@ def joinpath(self, *pathsegments):
336
369
paths) or a totally different path (if one of the arguments is
337
370
anchored).
338
371
"""
339
- return self .with_segments (* self ._raw_paths , * pathsegments )
372
+ return self .with_segments (self ._raw_path , * pathsegments )
340
373
341
374
def __truediv__ (self , key ):
342
375
try :
343
- return self .joinpath ( key )
376
+ return self .with_segments ( self . _raw_path , key )
344
377
except TypeError :
345
378
return NotImplemented
346
379
347
380
def __rtruediv__ (self , key ):
348
381
try :
349
- return self .with_segments (key , * self ._raw_paths )
382
+ return self .with_segments (key , self ._raw_path )
350
383
except TypeError :
351
384
return NotImplemented
352
385
@@ -371,7 +404,7 @@ def _stack(self):
371
404
def parent (self ):
372
405
"""The logical parent of the path."""
373
406
path = self ._raw_path
374
- parent = self .pathmod .dirname (path )
407
+ parent = self .pathmod .split (path )[ 0 ]
375
408
if path != parent :
376
409
parent = self .with_segments (parent )
377
410
parent ._resolving = self ._resolving
@@ -381,43 +414,20 @@ def parent(self):
381
414
@property
382
415
def parents (self ):
383
416
"""A sequence of this path's logical parents."""
384
- dirname = self .pathmod .dirname
417
+ split = self .pathmod .split
385
418
path = self ._raw_path
386
- parent = dirname (path )
419
+ parent = split (path )[ 0 ]
387
420
parents = []
388
421
while path != parent :
389
422
parents .append (self .with_segments (parent ))
390
423
path = parent
391
- parent = dirname (path )
424
+ parent = split (path )[ 0 ]
392
425
return tuple (parents )
393
426
394
427
def is_absolute (self ):
395
428
"""True if the path is absolute (has both a root and, if applicable,
396
429
a drive)."""
397
- if self .pathmod is posixpath :
398
- # Optimization: work with raw paths on POSIX.
399
- for path in self ._raw_paths :
400
- if path .startswith ('/' ):
401
- return True
402
- return False
403
- else :
404
- return self .pathmod .isabs (self ._raw_path )
405
-
406
- def is_reserved (self ):
407
- """Return True if the path contains one of the special names reserved
408
- by the system, if any."""
409
- if self .pathmod is posixpath or not self .name :
410
- return False
411
-
412
- # NOTE: the rules for reserved names seem somewhat complicated
413
- # (e.g. r"..\NUL" is reserved but not r"foo\NUL" if "foo" does not
414
- # exist). We err on the side of caution and return True for paths
415
- # which are not considered reserved by Windows.
416
- if self .drive .startswith ('\\ \\ ' ):
417
- # UNC paths are never reserved.
418
- return False
419
- name = self .name .partition ('.' )[0 ].partition (':' )[0 ].rstrip (' ' )
420
- return name .upper () in _WIN_RESERVED_NAMES
430
+ return self .pathmod .isabs (self ._raw_path )
421
431
422
432
def match (self , path_pattern , * , case_sensitive = None ):
423
433
"""
@@ -726,7 +736,7 @@ def glob(self, pattern, *, case_sensitive=None, follow_symlinks=None):
726
736
raise ValueError ("Unacceptable pattern: {!r}" .format (pattern ))
727
737
728
738
pattern_parts = list (path_pattern .parts )
729
- if not self .pathmod .basename (pattern ):
739
+ if not self .pathmod .split (pattern )[ 1 ] :
730
740
# GH-65238: pathlib doesn't preserve trailing slash. Add it back.
731
741
pattern_parts .append ('' )
732
742
0 commit comments