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.
- pathapi/AGENTS.md +2355 -0
- pathapi/README.md +205 -0
- pathapi/__init__.py +62 -0
- pathapi/_commit.py +116 -0
- pathapi/_discard.py +92 -0
- pathapi/_errorpolicy.py +106 -0
- pathapi/_filestat.py +194 -0
- pathapi/_lru.py +138 -0
- pathapi/_names.py +70 -0
- pathapi/_permissions.py +102 -0
- pathapi/_stubs.py +76 -0
- pathapi/_tree.py +136 -0
- pathapi/archive/__init__.py +44 -0
- pathapi/archive/_backend.py +393 -0
- pathapi/archive/_names.py +63 -0
- pathapi/archive/_pack.py +394 -0
- pathapi/archive/_path.py +352 -0
- pathapi/archive/_snapshot.py +269 -0
- pathapi/archive/_tar.py +126 -0
- pathapi/archive/_write.py +359 -0
- pathapi/archive/_zip.py +563 -0
- pathapi/archive/_zipentries.py +272 -0
- pathapi/checksum.py +61 -0
- pathapi/exceptions.py +22 -0
- pathapi/fspath.py +513 -0
- pathapi/ftp/__init__.py +27 -0
- pathapi/ftp/_backend.py +418 -0
- pathapi/ftp/_facts.py +117 -0
- pathapi/ftp/_path.py +582 -0
- pathapi/glob/__init__.py +27 -0
- pathapi/glob/_entries.py +103 -0
- pathapi/glob/_patterns.py +164 -0
- pathapi/glob/_recursion.py +180 -0
- pathapi/glob/_selection.py +303 -0
- pathapi/http/__init__.py +84 -0
- pathapi/http/_body.py +156 -0
- pathapi/http/_builtin.py +553 -0
- pathapi/http/_client.py +211 -0
- pathapi/http/_errors.py +79 -0
- pathapi/http/_follow.py +230 -0
- pathapi/http/_headers.py +233 -0
- pathapi/http/_pool.py +169 -0
- pathapi/http/_proxies.py +142 -0
- pathapi/http/_response.py +255 -0
- pathapi/http/_settings.py +12 -0
- pathapi/http/_status.py +68 -0
- pathapi/http/_transports.py +173 -0
- pathapi/http/testing/__init__.py +80 -0
- pathapi/http/testing/__main__.py +10 -0
- pathapi/http/testing/suite/conftest.py +98 -0
- pathapi/http/testing/suite/test_client_default.py +285 -0
- pathapi/http/testing/suite/test_client_errors.py +181 -0
- pathapi/http/testing/suite/test_client_peer_text.py +86 -0
- pathapi/http/testing/suite/test_client_redirect_origin.py +134 -0
- pathapi/http/testing/suite/test_client_redirects.py +231 -0
- pathapi/http/testing/suite/test_client_reply_limits.py +201 -0
- pathapi/http/testing/suite/test_client_write_redirects.py +239 -0
- pathapi/http/testing/suite/test_transport_body.py +302 -0
- pathapi/http/testing/suite/test_transport_send.py +662 -0
- pathapi/http/testing/suite/transport_fakes.py +133 -0
- pathapi/http/testing/suite/transport_guard.py +174 -0
- pathapi/http/testing/suite/transport_wire.py +511 -0
- pathapi/mempath.py +438 -0
- pathapi/path/__init__.py +19 -0
- pathapi/path/_info.py +10 -0
- pathapi/path/_path.py +198 -0
- pathapi/path/_pure.py +522 -0
- pathapi/path/_relation.py +10 -0
- pathapi/path/_transfer.py +10 -0
- pathapi/protocols/__init__.py +35 -0
- pathapi/protocols/_bundles.py +22 -0
- pathapi/protocols/_errors.py +12 -0
- pathapi/protocols/_info.py +65 -0
- pathapi/protocols/_relation.py +191 -0
- pathapi/protocols/_removal.py +69 -0
- pathapi/protocols/_support.py +122 -0
- pathapi/protocols/_transfer.py +100 -0
- pathapi/protocols/checksum.py +80 -0
- pathapi/protocols/fs.py +429 -0
- pathapi/protocols/io.py +224 -0
- pathapi/protocols/links.py +135 -0
- pathapi/protocols/pure.py +31 -0
- pathapi/protocols/resolve.py +97 -0
- pathapi/protocols/transfer.py +656 -0
- pathapi/protocols/tree.py +492 -0
- pathapi/providers.py +48 -0
- pathapi/py.typed +1 -0
- pathapi/sync/__init__.py +8 -0
- pathapi/sync/_compare.py +157 -0
- pathapi/sync/_events.py +97 -0
- pathapi/sync/_links.py +93 -0
- pathapi/sync/_offer.py +33 -0
- pathapi/sync/_paths.py +50 -0
- pathapi/sync/_replace.py +98 -0
- pathapi/sync/_syncer.py +989 -0
- pathapi/testing/__init__.py +96 -0
- pathapi/testing/_contract.py +522 -0
- pathapi/testing/_fixtures.py +83 -0
- pathapi/testing/_read.py +279 -0
- pathapi-0.1.0.dist-info/METADATA +242 -0
- pathapi-0.1.0.dist-info/RECORD +104 -0
- pathapi-0.1.0.dist-info/WHEEL +4 -0
- pathapi-0.1.0.dist-info/entry_points.txt +2 -0
- 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.
|
pathapi/sync/__init__.py
ADDED