pathapi 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.
Files changed (104) hide show
  1. pathapi/AGENTS.md +2355 -0
  2. pathapi/README.md +205 -0
  3. pathapi/__init__.py +62 -0
  4. pathapi/_commit.py +116 -0
  5. pathapi/_discard.py +92 -0
  6. pathapi/_errorpolicy.py +106 -0
  7. pathapi/_filestat.py +194 -0
  8. pathapi/_lru.py +138 -0
  9. pathapi/_names.py +70 -0
  10. pathapi/_permissions.py +102 -0
  11. pathapi/_stubs.py +76 -0
  12. pathapi/_tree.py +136 -0
  13. pathapi/archive/__init__.py +44 -0
  14. pathapi/archive/_backend.py +393 -0
  15. pathapi/archive/_names.py +63 -0
  16. pathapi/archive/_pack.py +394 -0
  17. pathapi/archive/_path.py +352 -0
  18. pathapi/archive/_snapshot.py +269 -0
  19. pathapi/archive/_tar.py +126 -0
  20. pathapi/archive/_write.py +359 -0
  21. pathapi/archive/_zip.py +563 -0
  22. pathapi/archive/_zipentries.py +272 -0
  23. pathapi/checksum.py +61 -0
  24. pathapi/exceptions.py +22 -0
  25. pathapi/fspath.py +513 -0
  26. pathapi/ftp/__init__.py +27 -0
  27. pathapi/ftp/_backend.py +418 -0
  28. pathapi/ftp/_facts.py +117 -0
  29. pathapi/ftp/_path.py +582 -0
  30. pathapi/glob/__init__.py +27 -0
  31. pathapi/glob/_entries.py +103 -0
  32. pathapi/glob/_patterns.py +164 -0
  33. pathapi/glob/_recursion.py +180 -0
  34. pathapi/glob/_selection.py +303 -0
  35. pathapi/http/__init__.py +84 -0
  36. pathapi/http/_body.py +156 -0
  37. pathapi/http/_builtin.py +553 -0
  38. pathapi/http/_client.py +211 -0
  39. pathapi/http/_errors.py +79 -0
  40. pathapi/http/_follow.py +230 -0
  41. pathapi/http/_headers.py +233 -0
  42. pathapi/http/_pool.py +169 -0
  43. pathapi/http/_proxies.py +142 -0
  44. pathapi/http/_response.py +255 -0
  45. pathapi/http/_settings.py +12 -0
  46. pathapi/http/_status.py +68 -0
  47. pathapi/http/_transports.py +173 -0
  48. pathapi/http/testing/__init__.py +80 -0
  49. pathapi/http/testing/__main__.py +10 -0
  50. pathapi/http/testing/suite/conftest.py +98 -0
  51. pathapi/http/testing/suite/test_client_default.py +285 -0
  52. pathapi/http/testing/suite/test_client_errors.py +181 -0
  53. pathapi/http/testing/suite/test_client_peer_text.py +86 -0
  54. pathapi/http/testing/suite/test_client_redirect_origin.py +134 -0
  55. pathapi/http/testing/suite/test_client_redirects.py +231 -0
  56. pathapi/http/testing/suite/test_client_reply_limits.py +201 -0
  57. pathapi/http/testing/suite/test_client_write_redirects.py +239 -0
  58. pathapi/http/testing/suite/test_transport_body.py +302 -0
  59. pathapi/http/testing/suite/test_transport_send.py +662 -0
  60. pathapi/http/testing/suite/transport_fakes.py +133 -0
  61. pathapi/http/testing/suite/transport_guard.py +174 -0
  62. pathapi/http/testing/suite/transport_wire.py +511 -0
  63. pathapi/mempath.py +438 -0
  64. pathapi/path/__init__.py +19 -0
  65. pathapi/path/_info.py +10 -0
  66. pathapi/path/_path.py +198 -0
  67. pathapi/path/_pure.py +522 -0
  68. pathapi/path/_relation.py +10 -0
  69. pathapi/path/_transfer.py +10 -0
  70. pathapi/protocols/__init__.py +35 -0
  71. pathapi/protocols/_bundles.py +22 -0
  72. pathapi/protocols/_errors.py +12 -0
  73. pathapi/protocols/_info.py +65 -0
  74. pathapi/protocols/_relation.py +191 -0
  75. pathapi/protocols/_removal.py +69 -0
  76. pathapi/protocols/_support.py +122 -0
  77. pathapi/protocols/_transfer.py +100 -0
  78. pathapi/protocols/checksum.py +80 -0
  79. pathapi/protocols/fs.py +429 -0
  80. pathapi/protocols/io.py +224 -0
  81. pathapi/protocols/links.py +135 -0
  82. pathapi/protocols/pure.py +31 -0
  83. pathapi/protocols/resolve.py +97 -0
  84. pathapi/protocols/transfer.py +656 -0
  85. pathapi/protocols/tree.py +492 -0
  86. pathapi/providers.py +48 -0
  87. pathapi/py.typed +1 -0
  88. pathapi/sync/__init__.py +8 -0
  89. pathapi/sync/_compare.py +157 -0
  90. pathapi/sync/_events.py +97 -0
  91. pathapi/sync/_links.py +93 -0
  92. pathapi/sync/_offer.py +33 -0
  93. pathapi/sync/_paths.py +50 -0
  94. pathapi/sync/_replace.py +98 -0
  95. pathapi/sync/_syncer.py +989 -0
  96. pathapi/testing/__init__.py +96 -0
  97. pathapi/testing/_contract.py +522 -0
  98. pathapi/testing/_fixtures.py +83 -0
  99. pathapi/testing/_read.py +279 -0
  100. pathapi-0.1.0.dist-info/METADATA +242 -0
  101. pathapi-0.1.0.dist-info/RECORD +104 -0
  102. pathapi-0.1.0.dist-info/WHEEL +4 -0
  103. pathapi-0.1.0.dist-info/entry_points.txt +2 -0
  104. pathapi-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,492 @@
1
+ """Listing a directory and removing what is in it."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import errno as _errno
6
+ import typing as _ty
7
+
8
+ from .. import _errorpolicy, _names, _stubs
9
+ from .. import glob as _glob
10
+ from .._errorpolicy import check_follow
11
+ from .._filestat import FileStat
12
+ from ..exceptions import NonRelativePatternError
13
+ from ..glob import _entries
14
+ from ._errors import _os_error
15
+ from ._removal import _InvalidPolicy, _follow_policy, _is_junction, _listing
16
+ from .fs import Stat
17
+
18
+ __all__ = ["Listing", "Remove"]
19
+
20
+
21
+ class Listing(Stat, _ty.Protocol):
22
+ """Listing the entries of a directory: `iterdir()` is the primitive;
23
+ `_scandir()`, `glob()`, `rglob()`, `walk()` and iteration derive from it."""
24
+
25
+ __slots__ = ()
26
+
27
+ __primitives__: _ty.ClassVar[tuple[object, ...]] = ("iterdir",)
28
+
29
+ def __iter__(self):
30
+ return self.iterdir()
31
+
32
+ @_stubs.primitive
33
+ def iterdir(self) -> "_ty.Iterator[_ty.Self]":
34
+ """Yield path objects of the directory contents.
35
+
36
+ The children are yielded in arbitrary order, and the
37
+ special entries '.' and '..' are not included.
38
+ """
39
+ return _stubs._unimplemented("iterdir")
40
+
41
+ def _scandir(self) -> "_ty.Iterator[_ty.Tuple[str, _ty.Optional[FileStat]]]":
42
+ """Yield (name, stat_or_None) for each directory entry. Used by
43
+ `walk()`/`glob()` instead of `iterdir()` so schemes whose listing
44
+ call already returns metadata (an HTTP index, SFTP, FTP, an object
45
+ store) can answer `is_dir()` on the results without a further
46
+ round trip per entry. Default: falls back to `iterdir()` + one
47
+ `stat()` per child (no round-trip savings, but no subclass is
48
+ required to implement this) -- see `docs/guides/extending.md`.
49
+ """
50
+ for entry in self.iterdir():
51
+ try:
52
+ # follow_symlink=False to match walk()'s own default -- an
53
+ # explicit walk(follow_symlinks=True) always re-stats each
54
+ # entry itself regardless of what's yielded here.
55
+ stat = FileStat.from_path(entry, follow_symlink=False)
56
+ except OSError:
57
+ stat = None
58
+ yield entry.name, stat
59
+
60
+ def glob(
61
+ self,
62
+ pattern: str | _ty.Self | None,
63
+ *,
64
+ case_sensitive: bool = None,
65
+ include_hidden: bool = True,
66
+ recursive: bool = None,
67
+ dironly: bool = None,
68
+ recurse_symlinks: bool = False,
69
+ native: bool = True,
70
+ on_error: "_ty.Callable[[OSError], None]" = None,
71
+ bound_loops: bool = False,
72
+ ):
73
+ """Iterate over this subtree and yield all existing files (of any
74
+ kind, including directories) matching the given relative pattern.
75
+
76
+ Pathlib semantics: hidden entries are matched (`include_hidden=False`
77
+ filters them out and skips hidden directories), a trailing separator
78
+ selects directories only, "**" does not descend into a directory
79
+ symlink by default, and a trailing "**" also selects files on Python 3.13+
80
+ (directories only before). A missing or non-directory parent selects
81
+ nothing. An empty pattern raises `ValueError`, an absolute one
82
+ `NonRelativePatternError` (a `NotImplementedError` and a
83
+ `ValueError`).
84
+
85
+ `recurse_symlinks=True` lets "**" descend through symlinks to
86
+ directories, as pathlib 3.13 does. Where that would loop it stops: a
87
+ link to a directory the descent is already inside (compared by
88
+ `st_dev`/`st_ino`) is an entry but is not entered, and where the stat
89
+ carries no identity no more than 40 links are followed in one
90
+ descent. A backend without symlinks is not affected.
91
+
92
+ `pattern=None` expands the pattern THIS PATH CARRIES
93
+ (`LocalPath("/etc/*.conf").glob(None)`) instead of applying one to a
94
+ directory: the path is split at its first wildcard and globbed from
95
+ there. `""` still raises, as pathlib does. Every keyword means what
96
+ it does for a pattern argument -- `recursive=None` auto-enables
97
+ recursion for a carried "**", and `native=` applies. `include_hidden`
98
+ keeps THIS method's default (True); the module-level
99
+ `glob.glob()` this delegates to defaults it to False,
100
+ stdlib-`glob`-style, so the two spellings differ on hidden names
101
+ unless the keyword is passed.
102
+
103
+ `native=True` (the default) follows the running interpreter on the
104
+ two rules pathlib changed mid-series: a trailing "/" is ignored
105
+ before 3.11, and a component that merely contains "**" ("a**")
106
+ raises `ValueError` before 3.13. `native=False` applies one rule on
107
+ every version -- trailing "/" always selects directories only, "a**"
108
+ is always a plain wildcard -- so a pattern answers the same
109
+ everywhere, which is what a cross-backend or cross-version caller
110
+ usually wants.
111
+
112
+ `on_error(error)` is called when a directory cannot be listed, the
113
+ same contract as `walk()` and `os.walk`: raising from it propagates
114
+ (an unreadable directory becomes an error), returning treats that
115
+ directory as empty. Without it such a listing is skipped silently --
116
+ pathlib's behaviour, and the reason a config loader could not tell a
117
+ missing layer from an unreadable one. `error.filename` names the
118
+ directory even when the backend left it empty.
119
+
120
+ `bound_loops=True` skips a directory whose `(st_dev, st_ino)` is
121
+ already on the current descent path, which is what a loop is: that
122
+ bounds a Windows junction loop, which no symlink check can see (a
123
+ junction reports `is_symlink() == False`, so `recurse_symlinks=False`
124
+ does not help and pathlib itself loops until the recursion limit). A
125
+ directory reachable under two sibling names is entered under both. A
126
+ backend whose stat carries no identity is walked unbounded.
127
+
128
+ A "**" component auto-enables recursion. Pass `recursive=False`
129
+ explicitly to treat "**" as a plain "*" instead.
130
+ Note for remote schemes (http/sftp): a recursive glob walks the
131
+ whole remote subtree, one request/roundtrip per directory.
132
+ """
133
+ if pattern is None:
134
+ # The pattern is the path itself; `glob()` splits at the first
135
+ # wildcard, so an absolute one is fine here (unlike a pattern
136
+ # argument, which must stay relative to self).
137
+ return _glob.glob(
138
+ self,
139
+ recursive=recursive,
140
+ include_hidden=include_hidden,
141
+ case_sensitive=case_sensitive,
142
+ dironly=bool(dironly),
143
+ native=native,
144
+ on_error=on_error,
145
+ bound_loops=bound_loops,
146
+ recurse_symlinks=recurse_symlinks,
147
+ )
148
+ # Validates eagerly (like pathlib 3.13+); the returned selection is
149
+ # lazy. The pattern is never joined onto self: `self / pattern` let an
150
+ # absolute pattern escape self and re-parsed "?" as a URI query.
151
+ parts, trailing_sep = _glob.parse_pattern(pattern, native=native)
152
+ if recursive is None:
153
+ recursive = _glob.RECURSIVE in parts
154
+ return _glob.select(
155
+ self,
156
+ parts,
157
+ dironly=trailing_sep if dironly is None else dironly,
158
+ recursive=recursive,
159
+ include_hidden=include_hidden,
160
+ case_sensitive=case_sensitive,
161
+ native=native,
162
+ on_error=on_error,
163
+ bound_loops=bound_loops,
164
+ recurse_symlinks=recurse_symlinks,
165
+ )
166
+
167
+ def rglob(
168
+ self,
169
+ pattern: str | None,
170
+ *,
171
+ case_sensitive: bool = None,
172
+ include_hidden: bool = True,
173
+ recursive: bool = True,
174
+ dironly: bool = None,
175
+ recurse_symlinks: bool = False,
176
+ native: bool = True,
177
+ on_error: "_ty.Callable[[OSError], None]" = None,
178
+ bound_loops: bool = False,
179
+ ):
180
+ """Equivalent to `glob(f"**/{pattern}", recursive=True)`.
181
+
182
+ `pattern=None` and `native=` mean what they do on `glob()`; with
183
+ `None` this is `glob(None, recursive=True)`, since a carried pattern
184
+ brings its own anchor and nothing can be prefixed to it."""
185
+ if pattern is None:
186
+ return self.glob(
187
+ None,
188
+ case_sensitive=case_sensitive,
189
+ include_hidden=include_hidden,
190
+ recursive=recursive,
191
+ dironly=dironly,
192
+ recurse_symlinks=recurse_symlinks,
193
+ native=native,
194
+ on_error=on_error,
195
+ bound_loops=bound_loops,
196
+ )
197
+ if not (isinstance(pattern, str) and not pattern):
198
+ # Reject an absolute pattern before "**/" hides its anchor; glob() validates
199
+ # without listing. A pattern with no component ("." or "./") is allowed.
200
+ try:
201
+ self.glob(pattern, recursive=False, native=native)
202
+ except NonRelativePatternError:
203
+ raise
204
+ except ValueError:
205
+ pass
206
+ return self.glob(
207
+ f"**/{pattern}",
208
+ case_sensitive=case_sensitive,
209
+ include_hidden=include_hidden,
210
+ recursive=recursive,
211
+ dironly=dironly,
212
+ recurse_symlinks=recurse_symlinks,
213
+ native=native,
214
+ on_error=on_error,
215
+ bound_loops=bound_loops,
216
+ )
217
+
218
+ def walk(
219
+ self,
220
+ top_down=True,
221
+ on_error: _ty.Callable[[OSError | ValueError], None] = None,
222
+ follow_symlinks=False,
223
+ ):
224
+ """Walk the directory tree from this directory, similar to os.walk().
225
+
226
+ Uses `_scandir()` rather than `iterdir()` + a `stat()` per entry --
227
+ for schemes whose listing already carries type metadata (HTTP/DAV
228
+ indexes, SFTP `listdir_attr`, FTP MLSD, S3 list pages), this turns a
229
+ remote-tree walk from O(entries) round trips into O(dirs). The
230
+ pre-seeded stat is trusted only when `follow_symlinks` is False
231
+ (matching `walk()`'s own default and the lstat-like semantics of
232
+ those listing calls); an explicit `follow_symlinks=True` always
233
+ re-`stat()`s each entry so a symlink is still resolved.
234
+
235
+ Without `follow_symlinks` a symlink to a directory is listed in
236
+ `filenames` and not entered, and so is a Windows junction
237
+ (`is_junction()`): like a symlink it is a second NAME for another
238
+ tree, which `pathlib.Path.walk()` does not enter either (a junction
239
+ that points at its own parent would otherwise be walked until the
240
+ path is too long). Nothing protects a walk with `follow_symlinks=True`
241
+ from a loop.
242
+
243
+ A listed name that is not one path component (`""`, `.`, `..`, or one
244
+ containing `/` or NUL; see `providers.is_safe_child_name()`) is left out
245
+ of `dirnames` and `filenames`. `on_error`, when given, is called with
246
+ a `ValueError` for it, `error.filename` naming the directory.
247
+ """
248
+ paths: "list[_ty.Self|tuple[_ty.Self, list[str], list[str]]]" = [self]
249
+
250
+ while paths:
251
+ path = paths.pop()
252
+ if isinstance(path, tuple):
253
+ yield path
254
+ continue
255
+ try:
256
+ # Materialized inside the try: `_scandir()` is a generator, so a failing
257
+ # listing may raise at the first `next()`, and `on_error` must see it.
258
+ entries = list(path._scandir())
259
+ except OSError as error:
260
+ if on_error is not None:
261
+ on_error(error)
262
+ continue
263
+
264
+ dirnames: "list[str]" = []
265
+ filenames: "list[str]" = []
266
+ for name, stat in entries:
267
+ if not _names.is_safe_child_name(name):
268
+ # A listing is untrusted input: "..", "a/b" or "" would
269
+ # build a child outside `path` and send the walk there.
270
+ if on_error is not None:
271
+ error = ValueError(
272
+ f"refusing unsafe child name {name!r} under {path}"
273
+ )
274
+ error.filename = str(path)
275
+ on_error(error)
276
+ continue
277
+ try:
278
+ if stat is None or follow_symlinks:
279
+ stat = FileStat.from_path(
280
+ _entries._child(path, name), follow_symlink=follow_symlinks
281
+ )
282
+ is_dir = stat.is_dir() if stat is not None else False
283
+ if is_dir and not follow_symlinks:
284
+ is_dir = not _is_junction(_entries._child(path, name))
285
+ except OSError:
286
+ # Carried over from os.path.isdir().
287
+ is_dir = False
288
+
289
+ if is_dir:
290
+ dirnames.append(name)
291
+ else:
292
+ filenames.append(name)
293
+
294
+ if top_down:
295
+ yield path, dirnames, filenames
296
+ else:
297
+ paths.append((path, dirnames, filenames))
298
+
299
+ paths += [_entries._child(path, d) for d in reversed(dirnames)]
300
+
301
+
302
+ class Remove(Listing, _ty.Protocol):
303
+ """Removing a file, an empty directory or a whole tree: `unlink()` and
304
+ `rmdir()` are the primitives; `rm()` derives from them and from
305
+ `_scandir()`."""
306
+
307
+ __slots__ = ()
308
+
309
+ __primitives__: _ty.ClassVar[tuple[object, ...]] = ("unlink", "rmdir")
310
+
311
+ @_stubs.primitive
312
+ def unlink(self, missing_ok=False):
313
+ """
314
+ Remove this file or link.
315
+ If the path is a directory, use rmdir() instead.
316
+ """
317
+ return _stubs._unimplemented("unlink")
318
+
319
+ @_stubs.primitive
320
+ def rmdir(self):
321
+ """
322
+ Remove this directory. The directory must be empty.
323
+ """
324
+ return _stubs._unimplemented("rmdir")
325
+
326
+ def rm(
327
+ self,
328
+ /,
329
+ recursive=False,
330
+ missing_ok=False,
331
+ ignore_error: bool | _ty.Callable[[Exception, _ty.Self], bool] = False,
332
+ *,
333
+ follow_symlinks: "bool | None | _ty.Callable[[_ty.Self], bool | None]" = False,
334
+ follow_binds: "bool | None | _ty.Callable[[_ty.Self], bool | None]" = False,
335
+ ):
336
+ """Remove this file or directory, optionally recursively and ignoring errors.
337
+
338
+ `follow_symlinks` applies to a symlink met during a recursive
339
+ removal, `follow_binds` to a binding -- a directory that is another
340
+ tree's second NAME rather than part of this one (`is_dir_binding()`:
341
+ a Windows junction, a mount point, a bind mount). Both take:
342
+
343
+ - `False` (default) -- remove the entry itself, never what is behind
344
+ it: `unlink()` for a symlink, `rmdir()` for a binding. This is what
345
+ `rm -r` does, and on a live mount the `rmdir()` fails rather than
346
+ emptying someone else's filesystem.
347
+ - `True` -- remove the contents behind it, then the entry. What a
348
+ walker that cannot tell the difference does by accident.
349
+ - `None` -- leave it in place. The enclosing directory is then not
350
+ empty, so its own removal reports that.
351
+ - a callable `policy(path) -> bool | None` -- asked per entry with
352
+ the same three answers, so one tree can keep one mount and follow
353
+ another.
354
+
355
+ Path components BEFORE the final one are followed as usual; these
356
+ decide what happens to the entry itself, so they apply to the path
357
+ `rm()` is called on as to one met on the way down: a symlink named
358
+ directly is removed, emptied first (`True`) or left (`None`), and
359
+ without `recursive` it is only ever unlinked. The name matches
360
+ `stat()`/`walk()`/`copy()`'s `follow_symlinks=` rather than
361
+ inventing a second vocabulary for the same idea.
362
+
363
+ A policy that is not `True`, `False`, `None` or a callable raises
364
+ `ValueError` before anything is removed; so does a callable's answer
365
+ that is not one of the three, whatever `ignore_error` says. A listed
366
+ name that is not one path component (`""`, `.`, `..`, or one
367
+ containing `/` or NUL; see `providers.is_safe_child_name()`) is never
368
+ joined onto the directory: it raises `ValueError`, offered to
369
+ `ignore_error` as `(error, directory)` once per entry, and nothing is
370
+ removed for it.
371
+ """
372
+ # Same bool-or-callable normalization as copy()/PathSyncer, via the
373
+ # shared helper. A supplied callable keeps rm()'s own `(error, path)`
374
+ # arity -- arities differ per call site by design, see the helper.
375
+ _onerror = _errorpolicy.as_error_handler(ignore_error)
376
+ check_follow("follow_symlinks", follow_symlinks)
377
+ check_follow("follow_binds", follow_binds)
378
+
379
+ # An error the handler declined, or raised itself, on its way out:
380
+ # each enclosing directory's `except` catches it again, and consulting
381
+ # the handler there reported one failure once per ancestor.
382
+ declined = []
383
+
384
+ def _handle(error, path):
385
+ if isinstance(error, _InvalidPolicy):
386
+ # A mistake of the caller, not a removal that failed.
387
+ raise error
388
+ if any(error is seen for seen in declined):
389
+ raise error
390
+ try:
391
+ swallowed = _onerror(error, path)
392
+ except BaseException as raised:
393
+ declined.append(raised)
394
+ raise
395
+ if not swallowed:
396
+ declined.append(error)
397
+ raise error
398
+
399
+ def _remove_tree(path, *, link=False):
400
+ try:
401
+ entries = list(_listing(path))
402
+ except Exception as error:
403
+ _handle(error, path)
404
+ return
405
+
406
+ for name, child_stat in entries:
407
+ child = None
408
+ try:
409
+ # Checked before any child is built from the name; only the one-component
410
+ # rule, as a listed name is not rewritten by the platform that listed it.
411
+ if not _names.is_safe_child_name(name):
412
+ raise ValueError(
413
+ f"refusing unsafe child name {name!r} under {path}"
414
+ )
415
+ child = _entries._child(path, name)
416
+ if child_stat is None:
417
+ child_stat = FileStat.from_path(child, follow_symlink=False)
418
+ if child_stat is not None and child_stat.is_symlink():
419
+ # A link, whatever it points at. The non-following
420
+ # stat says "link", so this branch is reached before
421
+ # the directory one.
422
+ policy = _follow_policy(follow_symlinks, child)
423
+ if policy == "ignore":
424
+ continue
425
+ if policy == "follow" and child.is_dir():
426
+ _remove_tree(child, link=True)
427
+ else:
428
+ child.unlink()
429
+ elif child_stat is not None and child_stat.is_dir():
430
+ if child.is_dir_binding():
431
+ # A binding: what is inside belongs to the tree
432
+ # it names, not to this one.
433
+ policy = _follow_policy(follow_binds, child)
434
+ if policy == "ignore":
435
+ continue
436
+ if policy == "follow":
437
+ _remove_tree(child)
438
+ else:
439
+ # `rmdir()` on a live mount fails (EBUSY) --
440
+ # the right answer, and far better than
441
+ # emptying someone else's filesystem.
442
+ child.rmdir()
443
+ else:
444
+ _remove_tree(child)
445
+ else:
446
+ child.unlink()
447
+ except Exception as error:
448
+ _handle(error, path if child is None else child)
449
+
450
+ try:
451
+ # `rmdir()` on a symlink is ENOTDIR on POSIX: the link is
452
+ # unlinked once what is behind it has been removed.
453
+ path.unlink() if link else path.rmdir()
454
+ except Exception as error:
455
+ _handle(error, path)
456
+
457
+ try:
458
+ stat = FileStat.from_path(self, follow_symlink=False)
459
+ except Exception as error:
460
+ _handle(error, self)
461
+ return
462
+ if stat is None:
463
+ if not missing_ok:
464
+ _handle(_os_error(FileNotFoundError, _errno.ENOENT, self), self)
465
+ elif stat.is_symlink():
466
+ policy = _follow_policy(follow_symlinks, self)
467
+ if policy == "ignore":
468
+ return
469
+ try:
470
+ if policy == "follow" and recursive and self.is_dir():
471
+ _remove_tree(self, link=True)
472
+ else:
473
+ self.unlink()
474
+ except Exception as error:
475
+ _handle(error, self)
476
+ elif stat.is_dir():
477
+ binding = self.is_dir_binding()
478
+ policy = _follow_policy(follow_binds, self) if binding else "follow"
479
+ if policy == "ignore":
480
+ return
481
+ if recursive and (not binding or policy == "follow"):
482
+ _remove_tree(self)
483
+ else:
484
+ try:
485
+ self.rmdir()
486
+ except Exception as error:
487
+ _handle(error, self)
488
+ else:
489
+ try:
490
+ self.unlink()
491
+ except Exception as error:
492
+ _handle(error, self)
pathapi/providers.py ADDED
@@ -0,0 +1,48 @@
1
+ """What a class built on `Path` calls: error and follow policies, name and
2
+ mode checks, the split of a name into stem and suffix, the commit-on-close
3
+ buffer, the LRU cache and the `contains` test.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from ._commit import CommitOnClose, DiscardOnError
9
+ from ._errorpolicy import (
10
+ OfferedOnce,
11
+ as_error_handler,
12
+ as_error_predicate,
13
+ check_follow,
14
+ copy_error_predicate,
15
+ )
16
+ from ._lru import LRU
17
+ from ._names import is_safe_child_name, is_windows_flavoured
18
+ from ._permissions import UNCHANGED, parse_mode, parse_owner
19
+ from ._stubs import derived_from, notimplemented, primitive
20
+ from ._tree import TreePath
21
+ from .path._pure import _name_stem as name_stem
22
+ from .path._pure import _name_suffix as name_suffix
23
+ from .path._pure import _name_suffixes as name_suffixes
24
+ from .protocols._relation import contains
25
+
26
+ __all__ = [
27
+ "CommitOnClose",
28
+ "DiscardOnError",
29
+ "LRU",
30
+ "OfferedOnce",
31
+ "TreePath",
32
+ "UNCHANGED",
33
+ "as_error_handler",
34
+ "as_error_predicate",
35
+ "check_follow",
36
+ "contains",
37
+ "copy_error_predicate",
38
+ "is_safe_child_name",
39
+ "is_windows_flavoured",
40
+ "name_stem",
41
+ "name_suffix",
42
+ "name_suffixes",
43
+ "derived_from",
44
+ "notimplemented",
45
+ "primitive",
46
+ "parse_mode",
47
+ "parse_owner",
48
+ ]
pathapi/py.typed ADDED
@@ -0,0 +1 @@
1
+ # Marker file for PEP 561.
@@ -0,0 +1,8 @@
1
+ """Synchronizing a tree of paths onto another, across any two `Path` classes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ._events import PathAndStat, SyncEvent
6
+ from ._syncer import PathSyncer
7
+
8
+ __all__ = ["PathAndStat", "PathSyncer", "SyncEvent"]