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
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)
@@ -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
+ ]