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/path/_pure.py ADDED
@@ -0,0 +1,522 @@
1
+ """The pure pathname: `Pathname`, its parents and the name rules it follows."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import abc as _abc
6
+ import operator as _operator
7
+ import os as _os
8
+ import re as _re
9
+ import sys as _sys
10
+ import typing as _ty
11
+
12
+ from .. import _stubs
13
+ from .. import glob as _glob
14
+
15
+ if _ty.TYPE_CHECKING:
16
+ from ._path import Path
17
+
18
+
19
+ P = _ty.TypeVar("P", bound="Path")
20
+ PN = _ty.TypeVar("PN", bound="Pathname")
21
+
22
+ _P = _ty.TypeVar("_P")
23
+ _F = _ty.TypeVar("_F", bound=_ty.Callable[..., _ty.Any])
24
+
25
+ if _ty.TYPE_CHECKING:
26
+
27
+ def _abstract(method: _F) -> _F:
28
+ # `Path(...)` builds a LocalPath, so type checkers must read the bare
29
+ # class as instantiable; at run time ABCMeta still refuses a subclass
30
+ # that leaves one of these out.
31
+ return method
32
+
33
+ else:
34
+ _abstract = _abc.abstractmethod
35
+
36
+ # 3.13 refuses an empty stem on a name that has a suffix (`with_stem("")`).
37
+ _STEM_KEEPS_SUFFIX = _sys.version_info >= (3, 13)
38
+
39
+ # A final component's suffix split, following the running interpreter's
40
+ # pathlib: 3.14 treats a trailing "." as a suffix ("a." -> ".") and ignores
41
+ # leading dots; earlier versions do neither.
42
+ if _sys.version_info >= (3, 14):
43
+
44
+ def _name_suffix(name: str) -> str:
45
+ name = name.lstrip(".")
46
+ i = name.rfind(".")
47
+ return name[i:] if i != -1 else ""
48
+
49
+ def _name_stem(name: str) -> str:
50
+ i = name.rfind(".")
51
+ if i != -1:
52
+ stem = name[:i]
53
+ # The stem must contain at least one non-dot character.
54
+ if stem.lstrip("."):
55
+ return stem
56
+ return name
57
+
58
+ def _name_suffixes(name: str) -> "list[str]":
59
+ return ["." + ext for ext in name.lstrip(".").split(".")[1:]]
60
+
61
+ else:
62
+
63
+ def _name_suffix(name: str) -> str:
64
+ i = name.rfind(".")
65
+ return name[i:] if 0 < i < len(name) - 1 else ""
66
+
67
+ def _name_stem(name: str) -> str:
68
+ i = name.rfind(".")
69
+ return name[:i] if 0 < i < len(name) - 1 else name
70
+
71
+ def _name_suffixes(name: str) -> "list[str]":
72
+ if name.endswith("."):
73
+ return []
74
+ return ["." + suffix for suffix in name.lstrip(".").split(".")[1:]]
75
+
76
+
77
+ class FsPathLike(_ty.Protocol):
78
+ """Anything implementing `__fspath__`, for annotations. A class qualifies
79
+ by defining `__fspath__` itself, and `os.PathLike` recognises it the same
80
+ way; this protocol is not registered with `os.PathLike`, so a class that
81
+ does not define one is rejected by `os.fspath()` with `TypeError`."""
82
+
83
+ __slots__ = ()
84
+
85
+ def __fspath__(self) -> str:
86
+ return _stubs._unimplemented("__fspath__")
87
+
88
+
89
+ class _PathnameParents(_ty.Sequence[PN]):
90
+ """This object provides sequence-like access to the logical ancestors
91
+ of a path. Don't try to construct it yourself."""
92
+
93
+ __slots__ = ("_path", "_anchor", "_segments")
94
+
95
+ def __init__(self, path: PN):
96
+ self._path = path
97
+ segments = tuple(path.segments)
98
+ # The anchor (a leading "" for the root) is no ancestor of its own: counting
99
+ # it adds an entry and spells the root as the relative empty path.
100
+ self._anchor = path._anchor_segments()
101
+ segments = segments[len(self._anchor) :]
102
+ while segments and not segments[-1]:
103
+ segments = segments[:-1]
104
+ self._segments = segments
105
+
106
+ def __len__(self):
107
+ return len(self._segments)
108
+
109
+ @_ty.overload
110
+ def __getitem__(self, idx: slice) -> tuple[PN]: ...
111
+ @_ty.overload
112
+ def __getitem__(self, idx: int) -> PN: ...
113
+ def __getitem__(self, idx: int | slice) -> tuple[PN] | PN:
114
+ if isinstance(idx, slice):
115
+ return tuple(self[i] for i in range(*idx.indices(len(self))))
116
+
117
+ if idx >= len(self) or idx < -len(self):
118
+ raise IndexError(idx)
119
+ if idx < 0:
120
+ idx += len(self)
121
+ kept = self._segments[: -idx - 1]
122
+ if not self._anchor:
123
+ return self._path.with_segments(*kept)
124
+ # ("", "") is the root in with_segments' "/"-joined spelling.
125
+ return self._path.with_segments(*self._anchor, *(kept or ("",)))
126
+
127
+ def __repr__(self):
128
+ return "<{}.parents>".format(type(self._path).__name__)
129
+
130
+
131
+ class Pathname(_ty.Generic[_P]):
132
+ """Base class for manipulating paths without I/O."""
133
+
134
+ __slots__ = ()
135
+
136
+ @property
137
+ def _is_case_sensitive(self) -> bool:
138
+ return True
139
+
140
+ @_abstract
141
+ def as_uri(self) -> str:
142
+ """Return the path as a URI string."""
143
+ ...
144
+
145
+ @property
146
+ def name(self) -> str:
147
+ """The final path component, if any."""
148
+ segments = self.segments
149
+ return "" if not segments else segments[-1]
150
+
151
+ @property
152
+ def suffix(self) -> str:
153
+ """
154
+ The final component's last suffix, if any.
155
+
156
+ This includes the leading period. For example: '.txt'
157
+ """
158
+ return _name_suffix(self.name)
159
+
160
+ @property
161
+ def suffixes(self):
162
+ """
163
+ A list of the final component's suffixes, if any.
164
+
165
+ These include the leading periods. For example: ['.tar', '.gz']
166
+ """
167
+ return _name_suffixes(self.name)
168
+
169
+ @property
170
+ def stem(self):
171
+ """The final path component, minus its last suffix."""
172
+ return _name_stem(self.name)
173
+
174
+ @property
175
+ @_abstract
176
+ def segments(self) -> _ty.Sequence[str]:
177
+ """The sequence of path component strings."""
178
+ ...
179
+
180
+ @property
181
+ @_abstract
182
+ def parts(self) -> _P:
183
+ """The individual components/parts of the path."""
184
+ ...
185
+
186
+ @_abstract
187
+ def with_segments(self, *segments: str) -> _ty.Self:
188
+ """Construct a same-type path instance from new segments."""
189
+ ...
190
+
191
+ def with_name(self, name: str) -> _ty.Self:
192
+ """Return a new path with the name changed.
193
+
194
+ Raises ValueError for an empty name, ".", or a name containing "/",
195
+ as pathlib does. Without the check a name such as "../../etc/passwd"
196
+ or "x/y" was spliced in verbatim, so a caller relying on pathlib's
197
+ validation of an untrusted file name got a path outside the
198
+ directory. A bare ".." is accepted, also as pathlib does; use
199
+ `providers.is_safe_child_name()` to reject it. `with_stem()`/
200
+ `with_suffix()` validate their result through here.
201
+ """
202
+ if not name or "/" in name or name == ".":
203
+ raise ValueError("Invalid name %r" % (name,))
204
+ if not self.name:
205
+ raise ValueError("%r has an empty name" % (self,))
206
+ return self.with_segments(*self.segments[:-1], name)
207
+
208
+ def with_stem(self, stem: str) -> _ty.Self:
209
+ """Return a new path with the stem changed (validated like
210
+ `with_name()`). From 3.13 on an empty stem is refused when the name
211
+ has a suffix, as `pathlib` does: it would leave a name made of the
212
+ suffix alone."""
213
+ suffix = self.suffix
214
+ if _STEM_KEEPS_SUFFIX and suffix and not stem:
215
+ raise ValueError("%r has a non-empty suffix" % (self,))
216
+ return self.with_name(stem + suffix)
217
+
218
+ def with_suffix(self, suffix: str) -> _ty.Self:
219
+ """Return a new path with the suffix changed or added."""
220
+ name = self.name
221
+ if (
222
+ suffix
223
+ and not suffix.startswith(".")
224
+ or (suffix == "." and _sys.version_info < (3, 14))
225
+ ):
226
+ raise ValueError("Invalid suffix %r" % (suffix))
227
+ if not name:
228
+ raise ValueError("%r has an empty name" % (self,))
229
+ old_suffix = self.suffix
230
+ if not old_suffix:
231
+ name = name + suffix
232
+ else:
233
+ name = name[: -len(old_suffix)] + suffix
234
+ return self.with_name(name)
235
+
236
+ @_abstract
237
+ def relative_to(self, other: _ty.Self | str) -> _ty.Self:
238
+ """Return the relative path to another path identified by the passed
239
+ arguments. If the operation is not possible (because this is not
240
+ related to the other path), raise ValueError.
241
+ """
242
+ ...
243
+
244
+ def is_relative_to(self, other: _ty.Self | str):
245
+ """Return True if the path is relative to another path or False."""
246
+ cls = type(self)
247
+ # `other` is parsed standalone, as CPython does, not joined under `self`;
248
+ # `with_segments()` rather than the constructor keeps per-instance state
249
+ # such as MemPath's backend.
250
+ other = other if isinstance(other, cls) else self.with_segments(other)
251
+ return other == self or other in self.parents
252
+
253
+ def __eq__(self, other: object) -> bool:
254
+ """Compare by (exact type, segments).
255
+
256
+ Identity equality would make `is_relative_to()` (which decides via
257
+ `==`) return False for every subclass and break paths as dict keys
258
+ or set members, so a pure subclass that defines none gets this one.
259
+ `_BaseFSPathname`/`LocalPath` are unaffected: `pathlib.PurePath`
260
+ precedes `Pathname` in their MRO and keeps its own `__eq__`, as
261
+ does any class that defines one.
262
+ """
263
+ if type(self) is not type(other):
264
+ return NotImplemented
265
+ return tuple(self.segments) == tuple(other.segments)
266
+
267
+ def __hash__(self) -> int:
268
+ return hash((type(self), tuple(self.segments)))
269
+
270
+ def _order_key(self) -> _ty.Any:
271
+ """What `<`, `<=`, `>` and `>=` compare, between two paths of one
272
+ exact type: the segments, as `__eq__` does. A type whose `__eq__`
273
+ compares something else overrides it with that."""
274
+ return tuple(self.segments)
275
+
276
+ def _compare(
277
+ self, other: Pathname, compare: _ty.Callable[[_ty.Any, _ty.Any], bool]
278
+ ):
279
+ if type(self) is not type(other):
280
+ return NotImplemented
281
+ return compare(self._order_key(), other._order_key())
282
+
283
+ def __lt__(self, other: Pathname) -> bool:
284
+ return self._compare(other, _operator.lt)
285
+
286
+ def __le__(self, other: Pathname) -> bool:
287
+ return self._compare(other, _operator.le)
288
+
289
+ def __gt__(self, other: Pathname) -> bool:
290
+ return self._compare(other, _operator.gt)
291
+
292
+ def __ge__(self, other: Pathname) -> bool:
293
+ return self._compare(other, _operator.ge)
294
+
295
+ def __truediv__(self, key: _ty.Self | str) -> _ty.Self:
296
+ try:
297
+ return type(self)(self, key)
298
+ except (TypeError, NotImplementedError):
299
+ return NotImplemented
300
+
301
+ def __rtruediv__(self, key: str) -> _ty.Self:
302
+ """`"prefix" / path`, as pathlib supports. `self` is passed as an
303
+ object, so per-instance state it carries (a `MemPath` backend)
304
+ survives, and an absolute `self` restarts the join."""
305
+ if not isinstance(key, str):
306
+ return NotImplemented
307
+ try:
308
+ return type(self)(key, self)
309
+ except (TypeError, NotImplementedError):
310
+ return NotImplemented
311
+
312
+ def joinpath(self, *args: str | _ty.Self) -> _ty.Self:
313
+ """Combine this path with one or more paths/segments."""
314
+ return type(self)(self, *args)
315
+
316
+ @property
317
+ def root(self) -> str:
318
+ """The root of the path, if any (e.g. "/"). See `docs/divergences.md`
319
+ for how this is derived generically vs. LocalPath's real one."""
320
+ segments = self.segments
321
+ return "/" if segments and not segments[0] else ""
322
+
323
+ @property
324
+ def drive(self) -> str:
325
+ """No generic concept of a drive; always "". LocalPath gets a real
326
+ one from pathlib on Windows."""
327
+ return ""
328
+
329
+ @property
330
+ def anchor(self) -> str:
331
+ """`drive + root`."""
332
+ return self.drive + self.root
333
+
334
+ @property
335
+ @_abstract
336
+ def parent(self) -> _ty.Self:
337
+ """The logical parent of the path."""
338
+
339
+ @property
340
+ def parents(self) -> _ty.Sequence[_ty.Self]:
341
+ """An immutable sequence providing access to the logical ancestors of the path."""
342
+ return _PathnameParents(self)
343
+
344
+ def _anchor_segments(self) -> tuple[str, ...]:
345
+ """The leading entries of `segments` that anchor the path rather than
346
+ name a directory, for `parents`: `("",)` for a rooted path, else
347
+ `()`. A subclass that spells its root differently overrides it."""
348
+ segments = self.segments
349
+ return ("",) if segments and segments[0] == "" else ()
350
+
351
+ def is_absolute(self) -> bool:
352
+ """True if the path is absolute: it has a root. A class whose paths
353
+ can be rooted without being absolute overrides it."""
354
+ return bool(self.root)
355
+
356
+ def _match_parts(self) -> tuple[bool, list[str]]:
357
+ """`(anchored, names)`: the path as `match()` sees it -- whether it
358
+ is rooted, and its names with empty and "." segments dropped, the
359
+ way `PurePosixPath` parses a string."""
360
+ segments = self.segments
361
+ anchored = bool(segments) and segments[0] == ""
362
+ return anchored, [s for s in segments if s and s != "."]
363
+
364
+ def match(self, path_pattern: str | _re.Pattern, *, case_sensitive=None):
365
+ """
366
+ Return True if this path matches the given glob-style pattern.
367
+
368
+ pathlib's semantics on the running interpreter: a relative pattern
369
+ matches the last N segments (from the right), an absolute pattern
370
+ must match the whole path, and each segment is matched on its own,
371
+ so `*` never crosses a "/". "**" acts like "*" (use `full_match()`
372
+ for recursive matching). An empty pattern raises ValueError.
373
+
374
+ A compiled `re.Pattern` cannot be split into segments; it is matched
375
+ against the whole path string instead ("/"-joined segments, so a
376
+ URI's scheme and host are never part of it).
377
+ """
378
+ import fnmatch as _fnmatch
379
+
380
+ if case_sensitive is None:
381
+ case_sensitive = self._is_case_sensitive
382
+ anchored, names = self._match_parts()
383
+ if isinstance(path_pattern, _re.Pattern):
384
+ path = ("/" if anchored else "") + "/".join(names)
385
+ return path_pattern.match(path) is not None
386
+ if isinstance(path_pattern, Pathname):
387
+ path_pattern = "/".join(path_pattern.segments)
388
+ elif not isinstance(path_pattern, str):
389
+ path_pattern = _os.fspath(path_pattern)
390
+ pattern_anchored = path_pattern.startswith("/")
391
+ pattern_names = [s for s in path_pattern.split("/") if s and s != "."]
392
+ if not pattern_names and not pattern_anchored:
393
+ raise ValueError("empty pattern")
394
+ if _sys.version_info[:2] == (3, 12):
395
+ # 3.12 matches the whole path as one string with its separators swapped for
396
+ # newlines, which differs at the edges: see `_match_lines_312`.
397
+ return _match_lines_312(
398
+ anchored, names, pattern_anchored, pattern_names, case_sensitive
399
+ )
400
+ flags = 0 if case_sensitive else _re.IGNORECASE
401
+ if pattern_anchored and not (anchored and len(names) == len(pattern_names)):
402
+ return False
403
+ # The root counts as one more part a relative pattern can reach.
404
+ path_parts = len(names) + anchored
405
+ if len(pattern_names) > path_parts:
406
+ return False
407
+ for index, pattern in enumerate(reversed(pattern_names)):
408
+ if index == len(names):
409
+ # A relative pattern as long as the path reaches its root. 3.13+ compiles the
410
+ # root like any part (a bracket expression matches "/", "*" and "?" do not);
411
+ # 3.9-3.11 fnmatch the root string, so "*" and "?" match it too.
412
+ if _sys.version_info >= (3, 13):
413
+ from ..fspath import _translate_segment
414
+
415
+ regex = f"(?s:{_translate_segment(pattern, '[^/]')})\\Z"
416
+ return _re.match(regex, "/", flags) is not None
417
+ return _re.match(_fnmatch.translate(pattern), "/", flags) is not None
418
+ name = names[len(names) - 1 - index]
419
+ if _re.match(_fnmatch.translate(pattern), name, flags) is None:
420
+ return False
421
+ return True
422
+
423
+ def full_match(
424
+ self, pattern: str | _os.PathLike, *, case_sensitive: bool | None = None
425
+ ) -> bool:
426
+ """Return True if this path matches the glob-style `pattern`
427
+ against the whole path (3.13 parity). Unlike match(), this isn't a
428
+ right-anchored partial match, and "**" matches any number of path
429
+ segments (including zero). The root is a component of its own: a
430
+ rooted path needs a rooted pattern, or a leading "**" that reaches
431
+ past it (see `glob.full_match`). Empty and "." components of
432
+ the pattern are ignored, as `pathlib` parses its pattern as a path.
433
+ """
434
+ if case_sensitive is None:
435
+ case_sensitive = self._is_case_sensitive
436
+ if isinstance(pattern, Pathname):
437
+ pattern = "/".join(pattern.segments)
438
+ elif not isinstance(pattern, str):
439
+ pattern = _os.fspath(pattern)
440
+ return _glob.full_match(self.segments, pattern, case_sensitive)
441
+
442
+ def as_posix(self) -> str:
443
+ """Return the string representation of the path with forward slashes."""
444
+ return "/".join(self.segments)
445
+
446
+ def has_glob_pattern(self):
447
+ """Return True if any of the path segments contain glob wildcards.
448
+
449
+ The anchor is not one of them: a Windows extended-length path
450
+ (`\\\\?\\C:\\data`, and the `\\\\?\\UNC\\server\\share` form) carries a
451
+ literal `?` in its drive, which is a prefix, not a wildcard -- so a
452
+ caller asking "is this a pattern or a plain path?" got `True` for
453
+ every such path.
454
+ """
455
+ segments = list(self.segments)
456
+ anchor = getattr(self, "anchor", "")
457
+ if anchor and segments and segments[0] == anchor:
458
+ segments = segments[1:]
459
+ for segment in segments:
460
+ if _glob.WILDCARD_PATTERN.search(segment) is not None:
461
+ return True
462
+ return False
463
+
464
+
465
+ PurePathLike = _ty.Union[str, Pathname]
466
+
467
+ #: `fnmatch.translate()` wraps its output in "(?s:...)\\Z"; 3.12 strips that
468
+ #: to splice parts together, which is what drops the DOTALL flag and lets a
469
+ #: bracket expression -- but not "." -- match the newline separator.
470
+ _FNMATCH_SLICE = slice(len("(?s:"), -len(")\\Z"))
471
+
472
+
473
+ #: 3.12 swaps in both directions, so a newline inside a name becomes a "/" and
474
+ #: can never act as a separator.
475
+ _SWAP_SEP_AND_NEWLINE = str.maketrans({"/": "\n", "\n": "/"})
476
+
477
+
478
+ def _match_lines_312(
479
+ anchored: bool,
480
+ names: "_ty.Sequence[str]",
481
+ pattern_anchored: bool,
482
+ pattern_names: "_ty.Sequence[str]",
483
+ case_sensitive: bool,
484
+ ) -> bool:
485
+ """`match()` as Python 3.12 implements it, for 3.12 only.
486
+
487
+ 3.12 swaps separators for newlines and matches the resulting strings
488
+ with one regex (`_compile_pattern_lines` in its `pathlib`), instead of
489
+ comparing parts pairwise the way every other version does. The
490
+ difference shows at the edges: the root is "\n", a single line that a
491
+ part matching the empty string ("**", but not "*", which 3.12 compiles
492
+ as ".+") reaches from either side, and a bracket expression such as
493
+ "[!a]" can consume the newline itself. Porting the algorithm keeps those
494
+ edges right without enumerating them.
495
+ """
496
+ import fnmatch as _fnmatch
497
+
498
+ # pathlib spells an empty path "." and gives it no lines at all.
499
+ path_lines = (("/" if anchored else "") + "/".join(names)).translate(
500
+ _SWAP_SEP_AND_NEWLINE
501
+ )
502
+ pattern_lines = (
503
+ ("/" if pattern_anchored else "") + "/".join(pattern_names)
504
+ ).translate(_SWAP_SEP_AND_NEWLINE)
505
+ parts = ["^"]
506
+ for part in pattern_lines.splitlines(keepends=True):
507
+ if part == "*\n":
508
+ part = r".+\n"
509
+ elif part == "*":
510
+ part = r".+"
511
+ else:
512
+ part = _fnmatch.translate(part)[_FNMATCH_SLICE]
513
+ parts.append(part)
514
+ parts.append(r"\Z")
515
+ flags = _re.MULTILINE
516
+ if not case_sensitive:
517
+ flags |= _re.IGNORECASE
518
+ regex = _re.compile("".join(parts), flags=flags)
519
+ # An anchored pattern must match the whole path, a relative one its tail.
520
+ if pattern_anchored:
521
+ return regex.match(path_lines) is not None
522
+ return regex.search(path_lines) is not None
@@ -0,0 +1,10 @@
1
+ """The relation helpers live with the protocols that call them; this name
2
+ stays importable and is the same module."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import sys as _sys
7
+
8
+ from ..protocols import _relation
9
+
10
+ _sys.modules[__name__] = _relation
@@ -0,0 +1,10 @@
1
+ """The transfer helpers live with the protocols that call them; this name
2
+ stays importable and is the same module."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import sys as _sys
7
+
8
+ from ..protocols import _transfer
9
+
10
+ _sys.modules[__name__] = _transfer
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from .fs import Chmod, Chown, Create, FileStatLike, Owned, Stat, Utime
4
+ from .io import BinaryOpen
5
+ from .checksum import NativeChecksum
6
+ from .pure import Named
7
+ from .tree import Listing, Remove
8
+ from .links import Hardlink, Readlink, Symlink
9
+ from .resolve import Resolve
10
+ from .transfer import Copy, Identity, Move
11
+ from ._support import capabilities, supports
12
+
13
+ __all__ = [
14
+ "BinaryOpen",
15
+ "Chmod",
16
+ "Chown",
17
+ "Copy",
18
+ "Create",
19
+ "FileStatLike",
20
+ "Hardlink",
21
+ "Identity",
22
+ "Listing",
23
+ "Move",
24
+ "Named",
25
+ "NativeChecksum",
26
+ "Owned",
27
+ "Readlink",
28
+ "Remove",
29
+ "Resolve",
30
+ "Stat",
31
+ "Symlink",
32
+ "Utime",
33
+ "capabilities",
34
+ "supports",
35
+ ]
@@ -0,0 +1,22 @@
1
+ """What a helper reads from a path, as one type: a pure path plus the
2
+ protocols the helper calls. Used in annotations."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import typing as _ty
7
+
8
+ from .io import BinaryOpen
9
+ from .pure import Named
10
+ from .tree import Listing
11
+
12
+
13
+ class Walkable(Named, Listing, _ty.Protocol):
14
+ """A path that can be listed and stat'ed: what `glob` and `walk` read."""
15
+
16
+ __slots__ = ()
17
+
18
+
19
+ class Readable(Walkable, BinaryOpen, _ty.Protocol):
20
+ """A path whose files can be listed and read: a `PathSyncer` source."""
21
+
22
+ __slots__ = ()
@@ -0,0 +1,12 @@
1
+ """The error shape the operations of the protocols raise."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os as _os
6
+
7
+
8
+ def _os_error(exc_type: type, code: int, path: object) -> OSError:
9
+ """`exc_type(code, strerror, str(path))` -- an OSError shaped like the
10
+ ones `os` raises. `FileNotFoundError(path)` left `errno` and `filename`
11
+ as None, so `e.errno == errno.ENOENT` never matched."""
12
+ return exc_type(code, _os.strerror(code), str(path))
@@ -0,0 +1,65 @@
1
+ """`Path.info`: the file type of one path from at most two stats."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import stat as _stat
6
+ import typing as _ty
7
+
8
+ if _ty.TYPE_CHECKING:
9
+ from ..path import Path
10
+
11
+
12
+ class _PathInfo:
13
+ """`Path.info`: the file type of one path, from at most one stat that
14
+ follows links and one that does not, both kept on this object only."""
15
+
16
+ __slots__ = ("_path", "_followed", "_unfollowed")
17
+
18
+ def __init__(self, path: Path) -> None:
19
+ self._path = path
20
+ self._followed = self._unfollowed = _UNSET
21
+
22
+ def _stat(self, follow_symlinks: bool):
23
+ """The stat, or None where there is no such entry."""
24
+ if follow_symlinks:
25
+ if self._followed is _UNSET:
26
+ # An entry that is missing or is not a link is the same stat
27
+ # either way.
28
+ plain = self._unfollowed
29
+ if plain is _UNSET or (
30
+ plain is not None and _stat.S_ISLNK(plain.st_mode)
31
+ ):
32
+ self._followed = self._ask(True)
33
+ else:
34
+ self._followed = plain
35
+ return self._followed
36
+ if self._unfollowed is _UNSET:
37
+ self._unfollowed = self._ask(False)
38
+ return self._unfollowed
39
+
40
+ def _ask(self, follow_symlinks: bool):
41
+ try:
42
+ return self._path.stat(follow_symlinks=follow_symlinks)
43
+ except (OSError, ValueError):
44
+ return None
45
+
46
+ def exists(self, *, follow_symlinks: bool = True) -> bool:
47
+ return self._stat(follow_symlinks) is not None
48
+
49
+ def is_dir(self, *, follow_symlinks: bool = True) -> bool:
50
+ stat = self._stat(follow_symlinks)
51
+ return stat is not None and _stat.S_ISDIR(stat.st_mode)
52
+
53
+ def is_file(self, *, follow_symlinks: bool = True) -> bool:
54
+ stat = self._stat(follow_symlinks)
55
+ return stat is not None and _stat.S_ISREG(stat.st_mode)
56
+
57
+ def is_symlink(self) -> bool:
58
+ stat = self._stat(False)
59
+ return stat is not None and _stat.S_ISLNK(stat.st_mode)
60
+
61
+ def __repr__(self) -> str:
62
+ return f"<{type(self._path).__name__}.info>"
63
+
64
+
65
+ _UNSET = object()