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
pathapi/fspath.py
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextvars as _contextvars
|
|
4
|
+
import functools as _func
|
|
5
|
+
import ntpath as _ntpath
|
|
6
|
+
import os as _os
|
|
7
|
+
import pathlib as _path
|
|
8
|
+
import posixpath as _posixpath
|
|
9
|
+
import re as _re
|
|
10
|
+
import shutil as _shutil
|
|
11
|
+
import stat as _stat
|
|
12
|
+
import sys as _sys
|
|
13
|
+
import types as _types
|
|
14
|
+
import typing as _ty
|
|
15
|
+
|
|
16
|
+
from . import path as _proto
|
|
17
|
+
from . import _permissions
|
|
18
|
+
from . import glob as _glob
|
|
19
|
+
from ._filestat import FileStat as _FileStat
|
|
20
|
+
from .exceptions import NonRelativePatternError
|
|
21
|
+
|
|
22
|
+
__all__ = ["LocalPath", "PosixPathname", "WindowsPathname"]
|
|
23
|
+
|
|
24
|
+
# pathlib.Path.stat()/chmod() only accept follow_symlinks= on 3.10+; below
|
|
25
|
+
# that, LocalPath (which inherits them directly from pathlib.Path via MRO,
|
|
26
|
+
# see class LocalPath below) needs a shim.
|
|
27
|
+
_HAS_FOLLOW_SYMLINKS = _sys.version_info >= (3, 10)
|
|
28
|
+
|
|
29
|
+
#: True while `rglob()` calls `glob()` for itself: the audit event of that
|
|
30
|
+
#: inner call belongs to pathlib's `rglob()` only from 3.13 and is raised there.
|
|
31
|
+
_IN_RGLOB: "_contextvars.ContextVar[bool]" = _contextvars.ContextVar(
|
|
32
|
+
"pathapi_in_rglob", default=False
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@_func.cache
|
|
37
|
+
def _is_case_sensitive(flavour: _os.path) -> bool:
|
|
38
|
+
return flavour.normcase("Aa") == "Aa"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _translate_segment(part: str, not_sep: str) -> str:
|
|
42
|
+
"""One pattern segment to a regex in which `*`/`?` never match `sep`
|
|
43
|
+
(CPython 3.13's `fnmatch._translate(part, not_sep + "*", not_sep)`)."""
|
|
44
|
+
import fnmatch as _fnmatch
|
|
45
|
+
|
|
46
|
+
# fnmatch.translate() wraps its output; slice that off a bracket
|
|
47
|
+
# expression's translation to reuse its range/negation handling.
|
|
48
|
+
prefix, suffix = _fnmatch.translate("_").split("_")
|
|
49
|
+
out = []
|
|
50
|
+
i, n = 0, len(part)
|
|
51
|
+
while i < n:
|
|
52
|
+
c = part[i]
|
|
53
|
+
i += 1
|
|
54
|
+
if c == "*":
|
|
55
|
+
if not out or out[-1] != f"{not_sep}*":
|
|
56
|
+
out.append(f"{not_sep}*")
|
|
57
|
+
elif c == "?":
|
|
58
|
+
out.append(not_sep)
|
|
59
|
+
elif c == "[":
|
|
60
|
+
j = i
|
|
61
|
+
if j < n and part[j] == "!":
|
|
62
|
+
j += 1
|
|
63
|
+
if j < n and part[j] == "]":
|
|
64
|
+
j += 1
|
|
65
|
+
while j < n and part[j] != "]":
|
|
66
|
+
j += 1
|
|
67
|
+
if j >= n:
|
|
68
|
+
out.append("\\[")
|
|
69
|
+
else:
|
|
70
|
+
out.append(
|
|
71
|
+
_fnmatch.translate(part[i - 1 : j + 1])[len(prefix) : -len(suffix)]
|
|
72
|
+
)
|
|
73
|
+
i = j + 1
|
|
74
|
+
else:
|
|
75
|
+
out.append(_re.escape(c))
|
|
76
|
+
return "".join(out)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@_func.lru_cache(maxsize=512)
|
|
80
|
+
def _compile_full_match(pattern: str, sep: str, case_sensitive: bool):
|
|
81
|
+
"""Compile a `full_match()` pattern string the way CPython 3.13 does:
|
|
82
|
+
`glob.translate(pattern, recursive=True, include_hidden=True, seps=sep)`."""
|
|
83
|
+
esc = _re.escape(sep)
|
|
84
|
+
not_sep = f"[^{esc}]"
|
|
85
|
+
results = []
|
|
86
|
+
parts = pattern.split(sep)
|
|
87
|
+
last = len(parts) - 1
|
|
88
|
+
for idx, part in enumerate(parts):
|
|
89
|
+
if part == "*":
|
|
90
|
+
results.append(f"{not_sep}+{esc}" if idx < last else f"{not_sep}+")
|
|
91
|
+
elif part == "**":
|
|
92
|
+
if idx < last:
|
|
93
|
+
if parts[idx + 1] != "**":
|
|
94
|
+
results.append(f"(?:.+{esc})?")
|
|
95
|
+
else:
|
|
96
|
+
results.append(".*")
|
|
97
|
+
else:
|
|
98
|
+
if part:
|
|
99
|
+
results.append(_translate_segment(part, not_sep))
|
|
100
|
+
if idx < last:
|
|
101
|
+
results.append(esc)
|
|
102
|
+
flags = 0 if case_sensitive else _re.IGNORECASE
|
|
103
|
+
return _re.compile(f"(?s:{''.join(results)})\\Z", flags).match
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class _BaseFSPathname(_path.PurePath, _proto.Pathname):
|
|
107
|
+
__slots__ = ()
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def _parser(self) -> _os.path:
|
|
111
|
+
try:
|
|
112
|
+
# 3.13+: renamed to `parser`, already a module.
|
|
113
|
+
return self.parser
|
|
114
|
+
except AttributeError:
|
|
115
|
+
pass
|
|
116
|
+
flavour = self._flavour
|
|
117
|
+
if isinstance(flavour, _types.ModuleType):
|
|
118
|
+
# 3.12: `_flavour` is already a module (ntpath/posixpath).
|
|
119
|
+
return flavour
|
|
120
|
+
# 3.9-3.11: `_flavour` is a `_WindowsFlavour`/`_PosixFlavour` object
|
|
121
|
+
# with no `normcase`/`pathsep`/etc — bridge to the equivalent module.
|
|
122
|
+
return _ntpath if flavour.sep == "\\" else _posixpath
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def _path_separators(self) -> _ty.Sequence[str]:
|
|
126
|
+
parser = self._parser
|
|
127
|
+
return (parser.sep,) + ((parser.altsep,) if parser.altsep else ())
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def _is_case_sensitive(self) -> bool:
|
|
131
|
+
return _is_case_sensitive(self._parser)
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def segments(self):
|
|
135
|
+
return self.parts
|
|
136
|
+
|
|
137
|
+
def with_segments(self, *args: str | _proto.FsPathLike):
|
|
138
|
+
return type(self)(*args)
|
|
139
|
+
|
|
140
|
+
# match()/full_match(): stdlib's own wherever it has the promised signature.
|
|
141
|
+
# `PurePath` follows this class in the MRO, so an unconditional definition would
|
|
142
|
+
# displace stdlib's on versions that have it: hence the version gate.
|
|
143
|
+
|
|
144
|
+
if _sys.version_info < (3, 12):
|
|
145
|
+
|
|
146
|
+
def match(self, path_pattern, *, case_sensitive=None):
|
|
147
|
+
"""Return True if this path matches the given pattern (stdlib's
|
|
148
|
+
`PurePath.match`). `case_sensitive=` is 3.12+ in CPython; this
|
|
149
|
+
shim accepts it on older versions too."""
|
|
150
|
+
if case_sensitive is None:
|
|
151
|
+
return _path.PurePath.match(self, path_pattern)
|
|
152
|
+
return self._match_case(_os.fspath(path_pattern), case_sensitive)
|
|
153
|
+
|
|
154
|
+
def _match_case(self, pattern: str, case_sensitive: bool) -> bool:
|
|
155
|
+
# CPython 3.9-3.11's PurePath.match, with the flavour's casefold
|
|
156
|
+
# replaced by the requested sensitivity.
|
|
157
|
+
import fnmatch as _fnmatch
|
|
158
|
+
|
|
159
|
+
fold = (lambda s: s) if case_sensitive else str.lower
|
|
160
|
+
pat = self.with_segments(fold(pattern))
|
|
161
|
+
pat_parts = list(pat.parts)
|
|
162
|
+
if not pat_parts:
|
|
163
|
+
raise ValueError("empty pattern")
|
|
164
|
+
if pat.drive and pat.drive != fold(self.drive):
|
|
165
|
+
return False
|
|
166
|
+
if pat.root and pat.root != self.root:
|
|
167
|
+
return False
|
|
168
|
+
parts = [fold(part) for part in self.parts]
|
|
169
|
+
if pat.drive or pat.root:
|
|
170
|
+
if len(pat_parts) != len(parts):
|
|
171
|
+
return False
|
|
172
|
+
pat_parts = pat_parts[1:]
|
|
173
|
+
elif len(pat_parts) > len(parts):
|
|
174
|
+
return False
|
|
175
|
+
return all(
|
|
176
|
+
_fnmatch.fnmatchcase(part, pat)
|
|
177
|
+
for part, pat in zip(reversed(parts), reversed(pat_parts))
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
if _sys.version_info < (3, 13):
|
|
181
|
+
|
|
182
|
+
def full_match(self, pattern, *, case_sensitive=None):
|
|
183
|
+
"""Return True if this path matches the glob-style `pattern`
|
|
184
|
+
against the whole path, with "**" matching any number of
|
|
185
|
+
segments -- a port of CPython 3.13's `PurePath.full_match`. The
|
|
186
|
+
generic `Pathname.full_match` splits on "/" and never sees a
|
|
187
|
+
drive, a root or a backslash, so rooted and Windows patterns
|
|
188
|
+
failed before 3.13."""
|
|
189
|
+
if not isinstance(pattern, _path.PurePath):
|
|
190
|
+
pattern = self.with_segments(pattern)
|
|
191
|
+
if case_sensitive is None:
|
|
192
|
+
case_sensitive = self._is_case_sensitive
|
|
193
|
+
sep = "\\" if isinstance(pattern, _path.PureWindowsPath) else "/"
|
|
194
|
+
path_str = str(self)
|
|
195
|
+
pattern_str = str(pattern)
|
|
196
|
+
match = _compile_full_match(
|
|
197
|
+
"" if pattern_str == "." else pattern_str, sep, case_sensitive
|
|
198
|
+
)
|
|
199
|
+
return match("" if path_str == "." else path_str) is not None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class PosixPathname(_path.PurePosixPath, _BaseFSPathname):
|
|
203
|
+
"""Pure (no I/O) POSIX-flavour path, implementing the `Pathname`
|
|
204
|
+
protocol on top of `pathlib.PurePosixPath`."""
|
|
205
|
+
|
|
206
|
+
__slots__ = ()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class WindowsPathname(_path.PureWindowsPath, _BaseFSPathname):
|
|
210
|
+
"""Pure (no I/O) Windows-flavour path, implementing the `Pathname`
|
|
211
|
+
protocol on top of `pathlib.PureWindowsPath`."""
|
|
212
|
+
|
|
213
|
+
__slots__ = ()
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class LocalPath(
|
|
217
|
+
_path.WindowsPath if _os.name == "nt" else _path.PosixPath,
|
|
218
|
+
_proto.Path,
|
|
219
|
+
_BaseFSPathname,
|
|
220
|
+
):
|
|
221
|
+
"""The real local filesystem path: `pathlib.WindowsPath`/`PosixPath`
|
|
222
|
+
with this library's `Path` mixed in via MRO. Behaves exactly like
|
|
223
|
+
`pathlib.Path` for anything not explicitly overridden here (see
|
|
224
|
+
`docs/divergences.md`).
|
|
225
|
+
|
|
226
|
+
A class mixing a concrete stdlib path with `Path` but not this class is
|
|
227
|
+
given every function defined here (`Path.__init_subclass__`), so each one
|
|
228
|
+
names stdlib's implementation explicitly instead of calling `super()`."""
|
|
229
|
+
|
|
230
|
+
__slots__ = ()
|
|
231
|
+
|
|
232
|
+
def _scandir(self):
|
|
233
|
+
# On 3.11+ stdlib's `_scandir()` (ahead of ours in the MRO) returns raw
|
|
234
|
+
# `os.DirEntry`; walk()/glob() expect `(name, FileStat|None)`, so ours is
|
|
235
|
+
# asserted. The entry's cached lstat (matching walk()'s default) is reused.
|
|
236
|
+
for entry in _os.scandir(self):
|
|
237
|
+
try:
|
|
238
|
+
stat = _FileStat.from_stat(entry.stat(follow_symlinks=False))
|
|
239
|
+
except OSError:
|
|
240
|
+
stat = None
|
|
241
|
+
yield entry.name, stat
|
|
242
|
+
|
|
243
|
+
def _rename_compatible(self, target) -> bool:
|
|
244
|
+
if isinstance(target, _path.PurePath):
|
|
245
|
+
return True
|
|
246
|
+
# A class for a local file under another spelling (a `file:` URI): the
|
|
247
|
+
# local path is its `filepath`, and `os.rename()` takes it through
|
|
248
|
+
# `__fspath__`.
|
|
249
|
+
try:
|
|
250
|
+
return isinstance(target, _os.PathLike) and isinstance(
|
|
251
|
+
getattr(target, "filepath", None), _path.PurePath
|
|
252
|
+
)
|
|
253
|
+
except Exception:
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
def is_junction(self) -> bool:
|
|
257
|
+
"""Whether this is a Windows junction. `os.path.isjunction()` on
|
|
258
|
+
3.12+, the reparse tag directly before that.
|
|
259
|
+
|
|
260
|
+
lstat() reports a junction (IO_REPARSE_TAG_MOUNT_POINT) as a plain
|
|
261
|
+
directory -- CPython only rewrites the mode to S_IFLNK for real
|
|
262
|
+
symlinks -- so a recursive removal must test for it or it would walk
|
|
263
|
+
into one and delete the target's files, as `shutil.rmtree` guards.
|
|
264
|
+
"""
|
|
265
|
+
if _os.name != "nt":
|
|
266
|
+
return False
|
|
267
|
+
isjunction = getattr(_os.path, "isjunction", None)
|
|
268
|
+
if isjunction is not None:
|
|
269
|
+
try:
|
|
270
|
+
return bool(isjunction(self))
|
|
271
|
+
except (OSError, ValueError):
|
|
272
|
+
return False
|
|
273
|
+
try:
|
|
274
|
+
st = _os.lstat(self)
|
|
275
|
+
except OSError:
|
|
276
|
+
return False
|
|
277
|
+
return getattr(st, "st_reparse_tag", 0) == getattr(
|
|
278
|
+
_stat, "IO_REPARSE_TAG_MOUNT_POINT", 0xA0000003
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
def is_mount(self) -> bool:
|
|
282
|
+
"""Whether this is a mount point -- a bind mount included, which is
|
|
283
|
+
how POSIX spells what a junction does on Windows."""
|
|
284
|
+
try:
|
|
285
|
+
return bool(_os.path.ismount(self))
|
|
286
|
+
except (OSError, ValueError):
|
|
287
|
+
return False
|
|
288
|
+
|
|
289
|
+
def walk(self, top_down=True, on_error=None, follow_symlinks=False):
|
|
290
|
+
# 3.12+ stdlib `Path.walk()` would win the MRO, but it uses `self._scandir()` as a
|
|
291
|
+
# context manager (`with scandir_it:`) and ours is a plain generator: route
|
|
292
|
+
# explicitly to our own `walk()`.
|
|
293
|
+
return _proto.Path.walk(
|
|
294
|
+
self, top_down=top_down, on_error=on_error, follow_symlinks=follow_symlinks
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
def _symlink_target(self, target):
|
|
298
|
+
# pathlib stores the text it is given: "./t/" stays "./t/". Windows
|
|
299
|
+
# cannot resolve a relative target that spells its separators "/", so
|
|
300
|
+
# there the path class turns the text into its own form.
|
|
301
|
+
if isinstance(target, str) and _os.name == "nt":
|
|
302
|
+
return self.with_segments(target)
|
|
303
|
+
return target
|
|
304
|
+
|
|
305
|
+
def _symlink_to(
|
|
306
|
+
self, target: _proto.Path | str, target_is_directory: bool = False
|
|
307
|
+
) -> None:
|
|
308
|
+
# `symlink_to` is in _OPERATION_NAMES, so the generic `Path.symlink_to()` (which
|
|
309
|
+
# owns `force=`) resolves on LocalPath and delegates link creation here; stdlib's
|
|
310
|
+
# is reached explicitly. target_is_directory is meaningful here (Windows, via os.symlink).
|
|
311
|
+
return _path.Path.symlink_to(self, target, target_is_directory)
|
|
312
|
+
|
|
313
|
+
def _hardlink_to(self, target) -> None:
|
|
314
|
+
# pathlib.Path.hardlink_to() is 3.10+; `link_to()` before it takes the
|
|
315
|
+
# arguments the other way round.
|
|
316
|
+
return _os.link(target, self)
|
|
317
|
+
|
|
318
|
+
def stat(self, *, follow_symlinks=True):
|
|
319
|
+
# pathlib.Path.stat() (next in MRO via WindowsPath/PosixPath) only
|
|
320
|
+
# accepts follow_symlinks= on 3.10+; below that, lstat() is the
|
|
321
|
+
# equivalent for follow_symlinks=False.
|
|
322
|
+
if _HAS_FOLLOW_SYMLINKS:
|
|
323
|
+
return _path.Path.stat(self, follow_symlinks=follow_symlinks)
|
|
324
|
+
return _path.Path.stat(self) if follow_symlinks else _path.Path.lstat(self)
|
|
325
|
+
|
|
326
|
+
def chmod(self, mode: int | str, *, follow_symlinks: bool = True):
|
|
327
|
+
# Same follow_symlinks= 3.10+ gap as stat() above; lchmod() is the
|
|
328
|
+
# equivalent (NotImplementedError itself where os.lchmod is missing).
|
|
329
|
+
mode = _permissions.parse_mode(mode)
|
|
330
|
+
if _HAS_FOLLOW_SYMLINKS:
|
|
331
|
+
return _path.Path.chmod(self, mode, follow_symlinks=follow_symlinks)
|
|
332
|
+
return (
|
|
333
|
+
_path.Path.chmod(self, mode)
|
|
334
|
+
if follow_symlinks
|
|
335
|
+
else _path.Path.lchmod(self, mode)
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
if _sys.version_info < (3, 13):
|
|
339
|
+
# `is_dir()`/`is_file()` gained `follow_symlinks=` in 3.13 and the `Stat` protocol
|
|
340
|
+
# promises it. Following keeps stdlib's implementation; the non-following form
|
|
341
|
+
# is 3.13's (any OSError/ValueError is False).
|
|
342
|
+
|
|
343
|
+
def is_dir(self, *, follow_symlinks=True):
|
|
344
|
+
if follow_symlinks:
|
|
345
|
+
return _path.Path.is_dir(self)
|
|
346
|
+
try:
|
|
347
|
+
return _stat.S_ISDIR(self.stat(follow_symlinks=False).st_mode)
|
|
348
|
+
except (OSError, ValueError):
|
|
349
|
+
return False
|
|
350
|
+
|
|
351
|
+
def is_file(self, *, follow_symlinks=True):
|
|
352
|
+
if follow_symlinks:
|
|
353
|
+
return _path.Path.is_file(self)
|
|
354
|
+
try:
|
|
355
|
+
return _stat.S_ISREG(self.stat(follow_symlinks=False).st_mode)
|
|
356
|
+
except (OSError, ValueError):
|
|
357
|
+
return False
|
|
358
|
+
|
|
359
|
+
def _chown(
|
|
360
|
+
self,
|
|
361
|
+
uid: int | str | None,
|
|
362
|
+
gid: int | str | None,
|
|
363
|
+
*,
|
|
364
|
+
follow_symlinks: bool = True,
|
|
365
|
+
) -> None:
|
|
366
|
+
# shutil.chown() accepts names as well as ids and takes None for "leave unchanged",
|
|
367
|
+
# the canonical form Chmod.chown() normalizes to; os.chown's -1 never appears here.
|
|
368
|
+
if not hasattr(_os, "chown"):
|
|
369
|
+
# shutil.chown() exists on Windows while os.chown does not, and there is no `pwd`
|
|
370
|
+
# for it to consult, so an int would leak AttributeError and a name LookupError:
|
|
371
|
+
# docs/divergences.md promises NotImplementedError.
|
|
372
|
+
raise NotImplementedError("chown()")
|
|
373
|
+
if not follow_symlinks:
|
|
374
|
+
if not hasattr(_os, "lchown"):
|
|
375
|
+
raise NotImplementedError("chown(follow_symlinks=False)")
|
|
376
|
+
if isinstance(uid, str) or isinstance(gid, str):
|
|
377
|
+
# os.lchown takes numeric ids only; resolving a name would
|
|
378
|
+
# mean duplicating shutil's lookup, and guessing wrong here
|
|
379
|
+
# writes the wrong owner silently.
|
|
380
|
+
raise NotImplementedError(
|
|
381
|
+
"chown(follow_symlinks=False) requires numeric uid/gid"
|
|
382
|
+
)
|
|
383
|
+
return _os.lchown(
|
|
384
|
+
self, -1 if uid is None else uid, -1 if gid is None else gid
|
|
385
|
+
)
|
|
386
|
+
return _shutil.chown(self, uid, gid)
|
|
387
|
+
|
|
388
|
+
def _utime(
|
|
389
|
+
self, *, atime: "float | None" = None, mtime: "float | None" = None
|
|
390
|
+
) -> None:
|
|
391
|
+
# `os.utime` takes both times together: the one left alone is read
|
|
392
|
+
# from the file as it is.
|
|
393
|
+
if atime is None or mtime is None:
|
|
394
|
+
current = _os.stat(self)
|
|
395
|
+
atime = current.st_atime if atime is None else atime
|
|
396
|
+
mtime = current.st_mtime if mtime is None else mtime
|
|
397
|
+
_os.utime(self, times=(atime, mtime))
|
|
398
|
+
|
|
399
|
+
def glob(
|
|
400
|
+
self,
|
|
401
|
+
pattern: "str | _proto.FsPathLike | None",
|
|
402
|
+
*,
|
|
403
|
+
case_sensitive: bool = None,
|
|
404
|
+
include_hidden: bool = True,
|
|
405
|
+
recursive: bool = None,
|
|
406
|
+
dironly: bool = None,
|
|
407
|
+
recurse_symlinks: bool = False,
|
|
408
|
+
native: bool = True,
|
|
409
|
+
on_error: "_ty.Callable[[OSError], None]" = None,
|
|
410
|
+
bound_loops: bool = False,
|
|
411
|
+
):
|
|
412
|
+
"""Iterate over this subtree and yield all existing files (of any
|
|
413
|
+
kind, including directories) matching the given relative pattern.
|
|
414
|
+
|
|
415
|
+
Same semantics as Path.glob(), including `pattern=None` (expand the
|
|
416
|
+
pattern this path carries) and `native=` (follow the running
|
|
417
|
+
interpreter, or one rule on every version); every separator of this
|
|
418
|
+
flavour splits the pattern, and a pattern with a drive or root
|
|
419
|
+
raises `NonRelativePatternError` like pathlib.
|
|
420
|
+
|
|
421
|
+
Raises the audit event `pathlib.Path.glob` with `(self, pattern)`, as
|
|
422
|
+
pathlib does. `pattern=None` has no pathlib counterpart and raises none.
|
|
423
|
+
"""
|
|
424
|
+
if pattern is not None and not _IN_RGLOB.get():
|
|
425
|
+
_sys.audit("pathlib.Path.glob", self, pattern)
|
|
426
|
+
if pattern is None:
|
|
427
|
+
return _proto.Path.glob(
|
|
428
|
+
self,
|
|
429
|
+
None,
|
|
430
|
+
case_sensitive=case_sensitive,
|
|
431
|
+
include_hidden=include_hidden,
|
|
432
|
+
recursive=recursive,
|
|
433
|
+
dironly=dironly,
|
|
434
|
+
recurse_symlinks=recurse_symlinks,
|
|
435
|
+
native=native,
|
|
436
|
+
on_error=on_error,
|
|
437
|
+
bound_loops=bound_loops,
|
|
438
|
+
)
|
|
439
|
+
pattern = _os.fspath(pattern)
|
|
440
|
+
if pattern:
|
|
441
|
+
anchored = self.with_segments(pattern)
|
|
442
|
+
if anchored.drive or anchored.root:
|
|
443
|
+
raise NonRelativePatternError("Non-relative patterns are unsupported")
|
|
444
|
+
for sep in self._path_separators:
|
|
445
|
+
pattern = pattern.replace(sep, "/")
|
|
446
|
+
if any(self._parser.splitdrive(part)[0] for part in pattern.split("/")):
|
|
447
|
+
# "sub/C:/x": joining "C:" would re-anchor outside self, and
|
|
448
|
+
# no child can be named that, so nothing matches (pathlib).
|
|
449
|
+
return iter(())
|
|
450
|
+
return _proto.Path.glob(
|
|
451
|
+
self,
|
|
452
|
+
pattern,
|
|
453
|
+
case_sensitive=case_sensitive,
|
|
454
|
+
include_hidden=include_hidden,
|
|
455
|
+
recursive=recursive,
|
|
456
|
+
dironly=dironly,
|
|
457
|
+
recurse_symlinks=recurse_symlinks,
|
|
458
|
+
native=native,
|
|
459
|
+
on_error=on_error,
|
|
460
|
+
bound_loops=bound_loops,
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
def rglob(
|
|
464
|
+
self,
|
|
465
|
+
pattern: "str | _proto.FsPathLike | None",
|
|
466
|
+
*,
|
|
467
|
+
case_sensitive: bool = None,
|
|
468
|
+
include_hidden: bool = True,
|
|
469
|
+
recursive: bool = True,
|
|
470
|
+
dironly: bool = None,
|
|
471
|
+
recurse_symlinks: bool = False,
|
|
472
|
+
native: bool = True,
|
|
473
|
+
on_error: "_ty.Callable[[OSError], None]" = None,
|
|
474
|
+
bound_loops: bool = False,
|
|
475
|
+
):
|
|
476
|
+
"""Equivalent to `glob(f"**/{pattern}", recursive=True)`, as
|
|
477
|
+
`Path.rglob()`; raises the audit event `pathlib.Path.rglob` with
|
|
478
|
+
`(self, pattern)`, and from 3.13 the `pathlib.Path.glob` event of the
|
|
479
|
+
`glob()` it stands for, as pathlib does. `pattern=None` raises none."""
|
|
480
|
+
if pattern is None:
|
|
481
|
+
return _proto.Path.rglob(
|
|
482
|
+
self,
|
|
483
|
+
None,
|
|
484
|
+
case_sensitive=case_sensitive,
|
|
485
|
+
include_hidden=include_hidden,
|
|
486
|
+
recursive=recursive,
|
|
487
|
+
dironly=dironly,
|
|
488
|
+
recurse_symlinks=recurse_symlinks,
|
|
489
|
+
native=native,
|
|
490
|
+
on_error=on_error,
|
|
491
|
+
bound_loops=bound_loops,
|
|
492
|
+
)
|
|
493
|
+
_sys.audit("pathlib.Path.rglob", self, pattern)
|
|
494
|
+
if _sys.version_info >= (3, 13):
|
|
495
|
+
_sys.audit(
|
|
496
|
+
"pathlib.Path.glob", self, self._parser.join("**", _os.fspath(pattern))
|
|
497
|
+
)
|
|
498
|
+
token = _IN_RGLOB.set(True)
|
|
499
|
+
try:
|
|
500
|
+
return _proto.Path.rglob(
|
|
501
|
+
self,
|
|
502
|
+
pattern,
|
|
503
|
+
case_sensitive=case_sensitive,
|
|
504
|
+
include_hidden=include_hidden,
|
|
505
|
+
recursive=recursive,
|
|
506
|
+
dironly=dironly,
|
|
507
|
+
recurse_symlinks=recurse_symlinks,
|
|
508
|
+
native=native,
|
|
509
|
+
on_error=on_error,
|
|
510
|
+
bound_loops=bound_loops,
|
|
511
|
+
)
|
|
512
|
+
finally:
|
|
513
|
+
_IN_RGLOB.reset(token)
|
pathapi/ftp/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""FTP and FTPS paths on the standard library alone (`ftplib`).
|
|
2
|
+
|
|
3
|
+
`FTPPath` is a `Path` for a directory tree on an FTP server and needs no URI;
|
|
4
|
+
`FTPBackend` holds the connections to one server and account.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from ._backend import (
|
|
10
|
+
DEFAULT_PORT,
|
|
11
|
+
DEFAULT_TIMEOUT,
|
|
12
|
+
IDLE_PROBE_SECONDS,
|
|
13
|
+
BaseFTPBackend,
|
|
14
|
+
FTPBackend,
|
|
15
|
+
FTPEndpoint,
|
|
16
|
+
)
|
|
17
|
+
from ._path import FTPPath
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"BaseFTPBackend",
|
|
21
|
+
"DEFAULT_PORT",
|
|
22
|
+
"DEFAULT_TIMEOUT",
|
|
23
|
+
"FTPBackend",
|
|
24
|
+
"FTPEndpoint",
|
|
25
|
+
"FTPPath",
|
|
26
|
+
"IDLE_PROBE_SECONDS",
|
|
27
|
+
]
|