pathlib-next 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pathlib_next/path.py ADDED
@@ -0,0 +1,618 @@
1
+ """Object-oriented filesystem paths.
2
+
3
+ This module provides classes to represent abstract paths and concrete
4
+ paths with operations that have semantics appropriate for different
5
+ operating systems.
6
+ """
7
+
8
+ import os as _os
9
+ import re as _re
10
+ import stat as _stat
11
+ from pathlib import _ignore_error
12
+ import typing as _ty
13
+ import abc as _abc
14
+ import io as _io
15
+ import shutil as _shutil
16
+
17
+ from . import utils as _utils
18
+ from .utils import glob as _glob
19
+ from .utils.stat import FileStatLike
20
+
21
+ P = _ty.TypeVar("P", bound="Path")
22
+ PN = _ty.TypeVar("PN", bound="Pathname")
23
+
24
+ _P = _ty.TypeVar("_P")
25
+
26
+
27
+ class FsPathLike(_ty.Protocol):
28
+ __slots__ = ()
29
+
30
+ @_utils.notimplemented
31
+ def __fspath__(self) -> str: ...
32
+
33
+
34
+ _os.PathLike.register(FsPathLike)
35
+
36
+
37
+ _FsPathLike = str | FsPathLike
38
+
39
+ class _PathnameParents(_ty.Sequence[PN]):
40
+ """This object provides sequence-like access to the logical ancestors
41
+ of a path. Don't try to construct it yourself."""
42
+
43
+ __slots__ = ("_path", "_segments")
44
+
45
+ def __init__(self, path: PN):
46
+ self._path = path
47
+ segments = path.segments
48
+ while segments and not segments[-1]:
49
+ segments = segments[:-1]
50
+ self._segments = segments
51
+
52
+ def __len__(self):
53
+ return len(self._segments)
54
+
55
+ @_ty.overload
56
+ def __getitem__(self, idx: slice) -> tuple[PN]: ...
57
+ @_ty.overload
58
+ def __getitem__(self, idx: int) -> PN: ...
59
+ def __getitem__(self, idx: int | slice) -> tuple[PN] | PN:
60
+ if isinstance(idx, slice):
61
+ return tuple(self[i] for i in range(*idx.indices(len(self))))
62
+
63
+ if idx >= len(self) or idx < -len(self):
64
+ raise IndexError(idx)
65
+ if idx < 0:
66
+ idx += len(self)
67
+ return self._path.with_segments(*self._segments[: -idx - 1])
68
+
69
+ def __repr__(self):
70
+ return "<{}.parents>".format(type(self._path).__name__)
71
+
72
+ class Pathname(FsPathLike, _ty.Generic[_P]):
73
+ """Base class for manipulating paths without I/O."""
74
+
75
+ __slots__ = ()
76
+
77
+ @property
78
+ def _is_case_sensitive(self) -> bool:
79
+ return True
80
+
81
+ @_abc.abstractmethod
82
+ def as_uri(self) -> str: ...
83
+
84
+ @property
85
+ def name(self) -> str:
86
+ """The final path component, if any."""
87
+ segments = self.segments
88
+ return "" if not segments else segments[-1]
89
+
90
+ @property
91
+ def suffix(self) -> str:
92
+ """
93
+ The final component's last suffix, if any.
94
+
95
+ This includes the leading period. For example: '.txt'
96
+ """
97
+ name = self.name
98
+ i = name.rfind(".")
99
+ if 0 < i < len(name) - 1:
100
+ return name[i:]
101
+ else:
102
+ return ""
103
+
104
+ @property
105
+ def suffixes(self):
106
+ """
107
+ A list of the final component's suffixes, if any.
108
+
109
+ These include the leading periods. For example: ['.tar', '.gz']
110
+ """
111
+ name = self.name
112
+ if name.endswith("."):
113
+ return []
114
+ name = name.lstrip(".")
115
+ return ["." + suffix for suffix in name.split(".")[1:]]
116
+
117
+ @property
118
+ def stem(self):
119
+ """The final path component, minus its last suffix."""
120
+ name = self.name
121
+ i = name.rfind(".")
122
+ if 0 < i < len(name) - 1:
123
+ return name[:i]
124
+ else:
125
+ return name
126
+
127
+ @property
128
+ @_abc.abstractmethod
129
+ def segments(self) -> _ty.Iterable[str]: ...
130
+
131
+ @property
132
+ @_abc.abstractmethod
133
+ def parts(self) -> _P: ...
134
+
135
+ @_abc.abstractmethod
136
+ def with_segments(self, *segments: str) -> _ty.Self: ...
137
+
138
+ def with_name(self, name: str) -> _ty.Self:
139
+ if not self.name:
140
+ raise ValueError("%r has an empty name" % (self,))
141
+ segments = self.segments[:-1] + [name]
142
+ return self.with_segments(*segments)
143
+
144
+ def with_stem(self, stem: str) -> _ty.Self:
145
+ """Return a new path with the stem changed."""
146
+ return self.with_name(stem + self.suffix)
147
+
148
+ def with_suffix(self, suffix: str) -> _ty.Self:
149
+ name = self.name
150
+ if suffix and not suffix.startswith(".") or suffix == ".":
151
+ raise ValueError("Invalid suffix %r" % (suffix))
152
+ if not name:
153
+ raise ValueError("%r has an empty name" % (self,))
154
+ old_suffix = self.suffix
155
+ if not old_suffix:
156
+ name = name + suffix
157
+ else:
158
+ name = name[: -len(old_suffix)] + suffix
159
+ return self.with_name(name)
160
+
161
+ @_abc.abstractmethod
162
+ def relative_to(self, other: _ty.Self | str) -> _ty.Self:
163
+ """Return the relative path to another path identified by the passed
164
+ arguments. If the operation is not possible (because this is not
165
+ related to the other path), raise ValueError.
166
+ """
167
+ ...
168
+
169
+ def is_relative_to(self, other: _ty.Self | str):
170
+ """Return True if the path is relative to another path or False."""
171
+ cls = type(self)
172
+ other = other if isinstance(other, cls) else cls(self, other)
173
+ return other == self or other in self.parents
174
+
175
+ def __truediv__(self, key: _ty.Self | str) -> _ty.Self:
176
+ try:
177
+ return type(self)(self, key)
178
+ except (TypeError, NotImplementedError):
179
+ return NotImplemented
180
+
181
+ @property
182
+ @_abc.abstractmethod
183
+ def parent(self) -> _ty.Self:
184
+ """The logical parent of the path."""
185
+
186
+ @property
187
+ def parents(self)->_ty.Sequence[_ty.Self]:
188
+ return _PathnameParents(self)
189
+
190
+
191
+ @_utils.notimplemented
192
+ def is_absolute(self) -> bool:
193
+ """True if the path is absolute (has both a root and, if applicable,
194
+ a drive)."""
195
+ ...
196
+
197
+ def match(self, path_pattern: str | _re.Pattern, *, case_sensitive=None):
198
+ """
199
+ Return True if this path matches the given pattern.
200
+ """
201
+ if case_sensitive is None:
202
+ case_sensitive = self._is_case_sensitive
203
+ path = str(self)
204
+ if not isinstance(path_pattern, _re.Pattern):
205
+ if isinstance(str, path_pattern):
206
+ path_pattern = type(self)(path_pattern).__path__()
207
+ path_pattern = _glob.compile_pattern(path_pattern, case_sensitive)
208
+ return path_pattern.match(path) is not None
209
+
210
+ @_utils.notimplemented
211
+ def as_posix(self) -> str: ...
212
+
213
+
214
+ PurePathLike = str | Pathname
215
+
216
+
217
+ class Path(Pathname):
218
+ """Base class for manipulating paths with I/O."""
219
+
220
+ __slots__ = ()
221
+
222
+ def __new__(cls, *args, **kwargs):
223
+ if cls is Path:
224
+ from .fspath import LocalPath
225
+
226
+ cls = LocalPath
227
+ return Pathname.__new__(cls)
228
+
229
+ @_utils.notimplemented
230
+ def stat(self, *, follow_symlinks=True) -> FileStatLike: ...
231
+
232
+ def lstat(self) -> FileStatLike:
233
+ """
234
+ Like stat(), except if the path points to a symlink, the symlink's
235
+ status information is returned, rather than its target's.
236
+ """
237
+ return self.stat(follow_symlinks=False)
238
+
239
+ def _st_mode(self, *, follow_symlinks=True):
240
+ try:
241
+ return self.stat().st_mode
242
+ except FileNotFoundError:
243
+ return None
244
+ except OSError as e:
245
+ if not _ignore_error(e):
246
+ raise
247
+ return None
248
+ except ValueError:
249
+ return None
250
+
251
+ # Convenience functions for querying the stat results
252
+ def exists(self, *, follow_symlinks=True):
253
+ """
254
+ Whether this path exists.
255
+ """
256
+ return self._st_mode(follow_symlinks=follow_symlinks) != None
257
+
258
+ def is_hidden(self):
259
+ return self.name.startswith(".")
260
+
261
+ def is_dir(self):
262
+ """
263
+ Whether this path is a directory.
264
+ """
265
+ try:
266
+ return _stat.S_ISDIR(self._st_mode() or 0)
267
+ except NotImplementedError:
268
+ pass
269
+ try:
270
+ next(self.iterdir(), None)
271
+ return True
272
+ except NotADirectoryError:
273
+ return False
274
+
275
+ raise NotImplementedError("is_dir")
276
+
277
+ def is_file(self):
278
+ """
279
+ Whether this path is a regular file (also True for symlinks pointing
280
+ to regular files).
281
+ """
282
+ return _stat.S_ISREG(self._st_mode() or 0)
283
+
284
+ def is_symlink(self):
285
+ """
286
+ Whether this path is a symbolic link.
287
+ """
288
+ return _stat.S_ISLNK(self._st_mode(follow_symlinks=True) or 0)
289
+
290
+ def is_block_device(self):
291
+ """
292
+ Whether this path is a block device.
293
+ """
294
+ return _stat.S_ISBLK(self._st_mode() or 0)
295
+
296
+ def is_char_device(self):
297
+ """
298
+ Whether this path is a character device.
299
+ """
300
+ return _stat.S_ISCHR(self._st_mode() or 0)
301
+
302
+ def is_fifo(self):
303
+ """
304
+ Whether this path is a FIFO.
305
+ """
306
+ return _stat.S_ISFIFO(self._st_mode() or 0)
307
+
308
+ def is_socket(self):
309
+ """
310
+ Whether this path is a socket.
311
+ """
312
+ return _stat.S_ISSOCK(self._st_mode() or 0)
313
+
314
+ @_utils.notimplemented
315
+ def samefile(self, other_path: str | _ty.Self):
316
+ """Return whether other_path is the same or not as this file
317
+ (as returned by os.path.samefile()).
318
+ """
319
+ ...
320
+
321
+ @_utils.notimplemented
322
+ def _open(
323
+ self,
324
+ mode="r",
325
+ buffering=-1,
326
+ ) -> _io.IOBase:
327
+ """
328
+ All operations should be binary
329
+ To be used only by UriPath.open() to obtain binary stream
330
+ """
331
+ ...
332
+
333
+ def open(
334
+ self,
335
+ mode="r",
336
+ buffering=-1,
337
+ encoding: str = None,
338
+ errors: str = None,
339
+ newline: str = None,
340
+ ) -> _io.IOBase:
341
+ """
342
+ Open the file pointed by this path and return a file object, as
343
+ the built-in open() function does.
344
+ """
345
+ fh = self._open(mode.replace("b", ""), buffering)
346
+ if "b" not in mode:
347
+ encoding = _io.text_encoding(encoding)
348
+ fh = _io.TextIOWrapper(fh, encoding, errors, newline)
349
+ return fh
350
+
351
+ def read_bytes(self) -> bytes:
352
+ """
353
+ Open the file in bytes mode, read it, and close the file.
354
+ """
355
+ with self.open(mode="rb") as f:
356
+ return f.read()
357
+
358
+ def read_text(self, encoding: str = None, errors: str = None) -> str:
359
+ """
360
+ Open the file in text mode, read it, and close the file.
361
+ """
362
+ with self.open(mode="r", encoding=encoding, errors=errors) as f:
363
+ return f.read()
364
+
365
+ def write_bytes(self, data: bytes):
366
+ """
367
+ Open the file in bytes mode, write to it, and close the file.
368
+ """
369
+ # type-check for the buffer interface before truncating the file
370
+ view = memoryview(data)
371
+ with self.open(mode="wb") as f:
372
+ return f.write(view)
373
+
374
+ def write_text(
375
+ self, data: str, encoding: str = None, errors: str = None, newline: str = None
376
+ ):
377
+ """
378
+ Open the file in text mode, write to it, and close the file.
379
+ """
380
+ if not isinstance(data, str):
381
+ raise TypeError("data must be str, not %s" % data.__class__.__name__)
382
+ with self.open(
383
+ mode="w", encoding=encoding, errors=errors, newline=newline
384
+ ) as f:
385
+ return f.write(data)
386
+
387
+ def __iter__(self):
388
+ return self.iterdir()
389
+
390
+ @_utils.notimplemented
391
+ def iterdir(self) -> "_ty.Iterator[_ty.Self]":
392
+ """Yield path objects of the directory contents.
393
+
394
+ The children are yielded in arbitrary order, and the
395
+ special entries '.' and '..' are not included.
396
+ """
397
+ ...
398
+
399
+ def glob(
400
+ self,
401
+ pattern: str | _ty.Self,
402
+ *,
403
+ case_sensitive: bool = None,
404
+ include_hidden: bool = False,
405
+ recursive: bool = False,
406
+ dironly: bool = None,
407
+ ):
408
+ """Iterate over this subtree and yield all existing files (of any
409
+ kind, including directories) matching the given relative pattern.
410
+ """
411
+ yield from _glob.glob(
412
+ self / pattern,
413
+ case_sensitive=case_sensitive,
414
+ include_hidden=include_hidden,
415
+ recursive=recursive,
416
+ dironly=dironly,
417
+ )
418
+
419
+ def rglob(
420
+ self,
421
+ pattern: str | _ty.Self,
422
+ *,
423
+ case_sensitive: bool = None,
424
+ include_hidden: bool = False,
425
+ dironly: bool = None,
426
+ ):
427
+ """Recursively yield all existing files (of any kind, including
428
+ directories) matching the given relative pattern, anywhere in
429
+ this subtree.
430
+ """
431
+ yield from self.glob(
432
+ pattern,
433
+ case_sensitive=case_sensitive,
434
+ include_hidden=include_hidden,
435
+ recursive=True,
436
+ dironly=dironly,
437
+ )
438
+
439
+ def walk(
440
+ self,
441
+ top_down=True,
442
+ on_error: _ty.Callable[[OSError], None] = None,
443
+ follow_symlinks=False,
444
+ ):
445
+ """Walk the directory tree from this directory, similar to os.walk()."""
446
+ paths: "list[_ty.Self|tuple[_ty.Self, list[str], list[str]]]" = [self]
447
+
448
+ while paths:
449
+ path = paths.pop()
450
+ if isinstance(path, tuple):
451
+ yield path
452
+ continue
453
+ try:
454
+ scandir_it = path.iterdir()
455
+ except OSError as error:
456
+ if on_error is not None:
457
+ on_error(error)
458
+ continue
459
+
460
+ dirnames: "list[str]" = []
461
+ filenames: "list[str]" = []
462
+ for entry in scandir_it:
463
+ try:
464
+ is_symlink = entry.is_symlink()
465
+ is_dir = entry.is_dir()
466
+ except OSError:
467
+ # Carried over from os.path.isdir().
468
+ is_dir = False
469
+
470
+ if is_dir and (follow_symlinks or not is_symlink):
471
+ dirnames.append(entry.name)
472
+ else:
473
+ filenames.append(entry.name)
474
+
475
+ if top_down:
476
+ yield path, dirnames, filenames
477
+ else:
478
+ paths.append((path, dirnames, filenames))
479
+
480
+ paths += [path / d for d in reversed(dirnames)]
481
+
482
+ def touch(self, mode=0o666, exist_ok=True):
483
+ """
484
+ Create this file with the given access mode, if it doesn't exist.
485
+ """
486
+
487
+ if exist_ok:
488
+ if self.exists():
489
+ return
490
+
491
+ with self.open("w"):
492
+ ...
493
+ try:
494
+ self.chmod(mode)
495
+ except NotImplementedError:
496
+ pass
497
+
498
+ @_utils.notimplemented
499
+ def _mkdir(self, mode: int): ...
500
+
501
+ def mkdir(self, mode=0o777, parents=False, exist_ok=False):
502
+ """
503
+ Create a new directory at this given path.
504
+ """
505
+ try:
506
+ self._mkdir(mode)
507
+ except FileNotFoundError:
508
+ if not parents or self.parent == self:
509
+ raise
510
+ self.parent.mkdir(parents=True, exist_ok=True)
511
+ self.mkdir(mode, parents=False, exist_ok=exist_ok)
512
+ except OSError:
513
+ # Cannot rely on checking for EEXIST, since the operating system
514
+ # could give priority to other errors like EACCES or EROFS
515
+ if not exist_ok or not self.is_dir():
516
+ raise
517
+
518
+ @_utils.notimplemented
519
+ def chmod(self, mode: int, *, follow_symlinks=True):
520
+ """
521
+ Change the permissions of the path, like os.chmod().
522
+ """
523
+ ...
524
+
525
+ def lchmod(self, mode: int):
526
+ """
527
+ Like chmod(), except if the path points to a symlink, the symlink's
528
+ permissions are changed, rather than its target's.
529
+ """
530
+ self.chmod(mode, follow_symlinks=False)
531
+
532
+ @_utils.notimplemented
533
+ def unlink(self, missing_ok=False):
534
+ """
535
+ Remove this file or link.
536
+ If the path is a directory, use rmdir() instead.
537
+ """
538
+
539
+ @_utils.notimplemented
540
+ def rmdir(self):
541
+ """
542
+ Remove this directory. The directory must be empty.
543
+ """
544
+
545
+ def rm(
546
+ self,
547
+ /,
548
+ recursive=False,
549
+ missing_ok=False,
550
+ ignore_error: bool | _ty.Callable[[Exception, _ty.Self], bool] = False,
551
+ ):
552
+ _onerror = lambda _err, _path: (
553
+ ignore_error if not callable(ignore_error) else ignore_error
554
+ )
555
+ try:
556
+ if not self.exists():
557
+ if missing_ok:
558
+ return
559
+ raise FileNotFoundError(self)
560
+ if self.is_dir():
561
+ if recursive:
562
+ for child in self.iterdir():
563
+ child.rm(recursive=recursive, ignore_error=ignore_error)
564
+ self.rmdir()
565
+ else:
566
+ self.unlink()
567
+ except Exception as error:
568
+ if not _onerror(error, self):
569
+ raise
570
+
571
+ @_utils.notimplemented
572
+ def rename(self, target: "_ty.Self | str"): ...
573
+
574
+ def copy(self, target: "Path | str", *, overwrite=False):
575
+ if isinstance(target, str):
576
+ target = type(self)(target)
577
+ src = self
578
+ if src is None:
579
+ return
580
+
581
+ if target.exists():
582
+ if overwrite:
583
+ target.unlink()
584
+ else:
585
+ raise FileExistsError(target)
586
+
587
+ with target.open("wb") as output, src.open("rb") as input:
588
+ _shutil.copyfileobj(input, output)
589
+
590
+ try:
591
+ stat = src.stat()
592
+ target.chmod(stat.st_mode)
593
+ except NotImplementedError:
594
+ pass
595
+
596
+ def move(self, target: "Path|str", *, overwrite=False):
597
+ if isinstance(target, str):
598
+ target = type(self)(target)
599
+ src = self
600
+ if src is None:
601
+ return
602
+
603
+ if target.exists():
604
+ if overwrite:
605
+ target.unlink()
606
+ else:
607
+ raise FileExistsError(target)
608
+
609
+ try:
610
+ return src.rename(target)
611
+ except NotImplementedError:
612
+ pass
613
+
614
+ src.copy(target)
615
+ src.unlink()
616
+
617
+
618
+ PathLike = str | Path