pathapi 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. pathapi/AGENTS.md +2355 -0
  2. pathapi/README.md +205 -0
  3. pathapi/__init__.py +62 -0
  4. pathapi/_commit.py +116 -0
  5. pathapi/_discard.py +92 -0
  6. pathapi/_errorpolicy.py +106 -0
  7. pathapi/_filestat.py +194 -0
  8. pathapi/_lru.py +138 -0
  9. pathapi/_names.py +70 -0
  10. pathapi/_permissions.py +102 -0
  11. pathapi/_stubs.py +76 -0
  12. pathapi/_tree.py +136 -0
  13. pathapi/archive/__init__.py +44 -0
  14. pathapi/archive/_backend.py +393 -0
  15. pathapi/archive/_names.py +63 -0
  16. pathapi/archive/_pack.py +394 -0
  17. pathapi/archive/_path.py +352 -0
  18. pathapi/archive/_snapshot.py +269 -0
  19. pathapi/archive/_tar.py +126 -0
  20. pathapi/archive/_write.py +359 -0
  21. pathapi/archive/_zip.py +563 -0
  22. pathapi/archive/_zipentries.py +272 -0
  23. pathapi/checksum.py +61 -0
  24. pathapi/exceptions.py +22 -0
  25. pathapi/fspath.py +513 -0
  26. pathapi/ftp/__init__.py +27 -0
  27. pathapi/ftp/_backend.py +418 -0
  28. pathapi/ftp/_facts.py +117 -0
  29. pathapi/ftp/_path.py +582 -0
  30. pathapi/glob/__init__.py +27 -0
  31. pathapi/glob/_entries.py +103 -0
  32. pathapi/glob/_patterns.py +164 -0
  33. pathapi/glob/_recursion.py +180 -0
  34. pathapi/glob/_selection.py +303 -0
  35. pathapi/http/__init__.py +84 -0
  36. pathapi/http/_body.py +156 -0
  37. pathapi/http/_builtin.py +553 -0
  38. pathapi/http/_client.py +211 -0
  39. pathapi/http/_errors.py +79 -0
  40. pathapi/http/_follow.py +230 -0
  41. pathapi/http/_headers.py +233 -0
  42. pathapi/http/_pool.py +169 -0
  43. pathapi/http/_proxies.py +142 -0
  44. pathapi/http/_response.py +255 -0
  45. pathapi/http/_settings.py +12 -0
  46. pathapi/http/_status.py +68 -0
  47. pathapi/http/_transports.py +173 -0
  48. pathapi/http/testing/__init__.py +80 -0
  49. pathapi/http/testing/__main__.py +10 -0
  50. pathapi/http/testing/suite/conftest.py +98 -0
  51. pathapi/http/testing/suite/test_client_default.py +285 -0
  52. pathapi/http/testing/suite/test_client_errors.py +181 -0
  53. pathapi/http/testing/suite/test_client_peer_text.py +86 -0
  54. pathapi/http/testing/suite/test_client_redirect_origin.py +134 -0
  55. pathapi/http/testing/suite/test_client_redirects.py +231 -0
  56. pathapi/http/testing/suite/test_client_reply_limits.py +201 -0
  57. pathapi/http/testing/suite/test_client_write_redirects.py +239 -0
  58. pathapi/http/testing/suite/test_transport_body.py +302 -0
  59. pathapi/http/testing/suite/test_transport_send.py +662 -0
  60. pathapi/http/testing/suite/transport_fakes.py +133 -0
  61. pathapi/http/testing/suite/transport_guard.py +174 -0
  62. pathapi/http/testing/suite/transport_wire.py +511 -0
  63. pathapi/mempath.py +438 -0
  64. pathapi/path/__init__.py +19 -0
  65. pathapi/path/_info.py +10 -0
  66. pathapi/path/_path.py +198 -0
  67. pathapi/path/_pure.py +522 -0
  68. pathapi/path/_relation.py +10 -0
  69. pathapi/path/_transfer.py +10 -0
  70. pathapi/protocols/__init__.py +35 -0
  71. pathapi/protocols/_bundles.py +22 -0
  72. pathapi/protocols/_errors.py +12 -0
  73. pathapi/protocols/_info.py +65 -0
  74. pathapi/protocols/_relation.py +191 -0
  75. pathapi/protocols/_removal.py +69 -0
  76. pathapi/protocols/_support.py +122 -0
  77. pathapi/protocols/_transfer.py +100 -0
  78. pathapi/protocols/checksum.py +80 -0
  79. pathapi/protocols/fs.py +429 -0
  80. pathapi/protocols/io.py +224 -0
  81. pathapi/protocols/links.py +135 -0
  82. pathapi/protocols/pure.py +31 -0
  83. pathapi/protocols/resolve.py +97 -0
  84. pathapi/protocols/transfer.py +656 -0
  85. pathapi/protocols/tree.py +492 -0
  86. pathapi/providers.py +48 -0
  87. pathapi/py.typed +1 -0
  88. pathapi/sync/__init__.py +8 -0
  89. pathapi/sync/_compare.py +157 -0
  90. pathapi/sync/_events.py +97 -0
  91. pathapi/sync/_links.py +93 -0
  92. pathapi/sync/_offer.py +33 -0
  93. pathapi/sync/_paths.py +50 -0
  94. pathapi/sync/_replace.py +98 -0
  95. pathapi/sync/_syncer.py +989 -0
  96. pathapi/testing/__init__.py +96 -0
  97. pathapi/testing/_contract.py +522 -0
  98. pathapi/testing/_fixtures.py +83 -0
  99. pathapi/testing/_read.py +279 -0
  100. pathapi-0.1.0.dist-info/METADATA +242 -0
  101. pathapi-0.1.0.dist-info/RECORD +104 -0
  102. pathapi-0.1.0.dist-info/WHEEL +4 -0
  103. pathapi-0.1.0.dist-info/entry_points.txt +2 -0
  104. pathapi-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,135 @@
1
+ """Symbolic links."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typing as _ty
6
+
7
+ from .. import _stubs
8
+ from .fs import Stat
9
+ from .tree import Remove
10
+
11
+ __all__ = ["Hardlink", "Readlink", "Symlink"]
12
+
13
+
14
+ class Readlink(Stat, _ty.Protocol):
15
+ """Reading a symbolic link: `readlink()` is the primitive. A read-only
16
+ backend (an archive) has this and not `Symlink`."""
17
+
18
+ __slots__ = ()
19
+
20
+ __primitives__: _ty.ClassVar[tuple[object, ...]] = ("readlink",)
21
+
22
+ @_stubs.primitive
23
+ def readlink(self) -> _ty.Self:
24
+ """The path this symbolic link points to, as a path of this class whose
25
+ `str()` and `as_posix()` are the link text, unresolved (a stub:
26
+ `NotImplementedError`). `OSError(EINVAL)` where the path is not a
27
+ link."""
28
+ return _stubs._unimplemented("readlink")
29
+
30
+
31
+ class Symlink(Remove, _ty.Protocol):
32
+ """Making a symbolic link: `_symlink_to()` is the primitive;
33
+ `symlink_to()` normalizes its target and honours `force=`."""
34
+
35
+ __slots__ = ()
36
+
37
+ __primitives__: _ty.ClassVar[tuple[object, ...]] = (("_symlink_to", "symlink_to"),)
38
+
39
+ @_stubs.primitive
40
+ def _symlink_to(
41
+ self, target: "_ty.Self", target_is_directory: bool = False
42
+ ) -> None:
43
+ """Create a symlink at this path pointing at `target`.
44
+
45
+ The backend primitive behind `symlink_to()`, in the same shape as
46
+ `_mkdir`/`_open`: implement only this, and the generic conveniences
47
+ (`force=`) come for free. `target` has already been normalized to a
48
+ path object of this class by `symlink_to()` -- accepting `str` and
49
+ turning it into a path is the wrapper's job, not the primitive's.
50
+
51
+ Implementations take the raw target string from the path object the
52
+ way their own backend needs it (the path of the request for wire
53
+ protocols, `os.fspath()`/`as_posix()` locally); it never carries a scheme or
54
+ host prefix, and a relative target stays relative.
55
+ """
56
+ return _stubs._unimplemented("_symlink_to")
57
+
58
+ def _symlink_target(self, target: "_ty.Self | str") -> "_ty.Self":
59
+ """Normalize a `symlink_to()` target argument to a path object.
60
+
61
+ A `str` target is the **literal link target** -- whatever it says
62
+ is what gets stored, verbatim and unresolved, exactly as
63
+ `pathlib.Path.symlink_to()` does. The default builds it with
64
+ `with_segments()`, which keeps per-instance state (a `MemPath`'s
65
+ backend) but normalizes the text as this class parses a path;
66
+ override it wherever that would reinterpret the string instead of
67
+ taking it literally (`LocalPath` hands it on untouched, and a class
68
+ for URI references must not parse it as one).
69
+ """
70
+ return self.with_segments(target) if isinstance(target, str) else target
71
+
72
+ def symlink_to(
73
+ self,
74
+ target: "_ty.Self | str",
75
+ target_is_directory: bool = False,
76
+ *,
77
+ force: bool = False,
78
+ ) -> None:
79
+ """Make this path a symlink pointing to `target`.
80
+
81
+ Signature-compatible with `pathlib.Path.symlink_to()`; `force` is a
82
+ pathapi extension (see `docs/divergences.md`).
83
+
84
+ With `force=True`, an existing entry at *this* path is removed
85
+ first. No filesystem or transport offers an atomic "replace a
86
+ symlink" operation, so this is an unlink-then-symlink sequence and
87
+ is therefore **not** atomic: between the two steps the path does not
88
+ exist, and a concurrent writer can win the race. It is a
89
+ convenience, not a locking primitive.
90
+
91
+ Only a non-directory entry is removed -- an existing *directory* at
92
+ the link path is left alone and the underlying `FileExistsError`
93
+ (or the backend's equivalent) propagates. Silently deleting a
94
+ directory tree is never what `force=` on a symlink call is asking
95
+ for.
96
+ """
97
+ # Through `_symlink_target()`, not `type(self)(target)`: for a URI-backed path
98
+ # the constructor re-parses the string as URI syntax, cutting the target at "?"
99
+ # or "#" and percent-decoding it.
100
+ target = self._symlink_target(target)
101
+ if force:
102
+ try:
103
+ self.unlink(missing_ok=True)
104
+ except (IsADirectoryError, PermissionError, OSError) as error:
105
+ # A directory at the link path (IsADirectoryError on POSIX; PermissionError or
106
+ # an OSError on Windows and remote backends) is not removed by force=: the
107
+ # symlink attempt below raises the error that describes the conflict.
108
+ if not self.is_dir():
109
+ raise error
110
+ return self._symlink_to(target, target_is_directory)
111
+
112
+
113
+ class Hardlink(Stat, _ty.Protocol):
114
+ """Making a path a second name for an existing file: `_hardlink_to()` is
115
+ the primitive; `hardlink_to()` turns a `str` target into a path of this
116
+ backend."""
117
+
118
+ __slots__ = ()
119
+
120
+ __primitives__: _ty.ClassVar[tuple[object, ...]] = (
121
+ ("_hardlink_to", "hardlink_to"),
122
+ )
123
+
124
+ @_stubs.primitive
125
+ def _hardlink_to(self, target: _ty.Self) -> None:
126
+ """Backend primitive: make this path a hard link to `target`, a path
127
+ of this class (a stub: `NotImplementedError`)."""
128
+ return _stubs._unimplemented("_hardlink_to")
129
+
130
+ def hardlink_to(self, target: "_ty.Self | str") -> None:
131
+ """Make this path a hard link to `target` (pathlib 3.10 parity). A
132
+ `str` is a path on this backend."""
133
+ if isinstance(target, str):
134
+ target = self.with_segments(target)
135
+ return self._hardlink_to(target)
@@ -0,0 +1,31 @@
1
+ """What the helpers that walk, match and sync need from a pure path."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typing as _ty
6
+
7
+ __all__ = ["Named"]
8
+
9
+
10
+ class Named(_ty.Protocol):
11
+ """The pure half of a path as `glob`, `walk` and `sync` read it: a name,
12
+ the segments it is made of, a parent, and the means of building and
13
+ joining paths of the same kind. Structural only: `Pathname` and
14
+ `pathlib.PurePath` satisfy it without naming it, and nothing subclasses it."""
15
+
16
+ @property
17
+ def name(self) -> str: ...
18
+
19
+ @property
20
+ def segments(self) -> _ty.Sequence[str]: ...
21
+
22
+ @property
23
+ def parent(self) -> _ty.Self: ...
24
+
25
+ def with_segments(self, *segments: str) -> _ty.Self: ...
26
+
27
+ def joinpath(self, *args: "str | _ty.Self") -> _ty.Self: ...
28
+
29
+ def __truediv__(self, key: "str | _ty.Self") -> _ty.Self: ...
30
+
31
+ def as_posix(self) -> str: ...
@@ -0,0 +1,97 @@
1
+ """Making a path absolute and following the links on its way."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import errno as _errno
6
+ import stat as _stat
7
+ import sys as _sys
8
+ import typing as _ty
9
+
10
+ from .. import _stubs
11
+ from ._support import supports
12
+ from .links import Readlink
13
+
14
+ __all__ = ["Resolve"]
15
+
16
+ #: Links followed in one `resolve()` before it reports a loop.
17
+ _MAX_LINKS = 40
18
+
19
+
20
+ class Resolve(Readlink, _ty.Protocol):
21
+ """`absolute()` and `resolve()`. `_cwd()` is the primitive: the directory
22
+ a relative path is relative to. `resolve()` follows links through
23
+ `readlink()` where the class has it and is lexical otherwise."""
24
+
25
+ __slots__ = ()
26
+
27
+ __primitives__: _ty.ClassVar[tuple[object, ...]] = (("_cwd", "absolute"),)
28
+
29
+ @_stubs.primitive
30
+ def _cwd(self) -> _ty.Self:
31
+ """The directory a relative path is relative to, as a path of this
32
+ class (a stub: `NotImplementedError`)."""
33
+ return _stubs._unimplemented("_cwd")
34
+
35
+ def absolute(self) -> _ty.Self:
36
+ """This path when it is absolute, else `_cwd()` joined with it."""
37
+ if self.is_absolute():
38
+ return self
39
+ return self._cwd().joinpath(self)
40
+
41
+ def resolve(self, strict: bool = False) -> _ty.Self:
42
+ """The absolute path with `..` applied and every link replaced by what
43
+ `readlink()` says, at most 40 of them. `strict=True` raises
44
+ `FileNotFoundError` for a component that is missing; otherwise the
45
+ rest of the names are kept as they are. A link loop raises what
46
+ `pathlib` raises on this interpreter: `RuntimeError` before 3.13,
47
+ `OSError(ELOOP)` from 3.13 when `strict` (otherwise the looping link
48
+ is kept)."""
49
+ path = self.absolute()
50
+ links = supports(self, Readlink)
51
+ root = path.with_segments(path.anchor or "/")
52
+ resolved = root
53
+ pending = [name for name in path.segments if name][::-1]
54
+ followed = 0
55
+ while pending:
56
+ name = pending.pop()
57
+ if name == ".":
58
+ continue
59
+ if name == "..":
60
+ resolved = resolved.parent
61
+ continue
62
+ candidate = resolved.joinpath(name)
63
+ if not links:
64
+ if strict:
65
+ candidate.stat()
66
+ resolved = candidate
67
+ continue
68
+ try:
69
+ mode = candidate.stat(follow_symlinks=False).st_mode
70
+ except OSError:
71
+ if strict:
72
+ raise
73
+ resolved = candidate
74
+ continue
75
+ if not _stat.S_ISLNK(mode):
76
+ resolved = candidate
77
+ continue
78
+ followed += 1
79
+ if followed > _MAX_LINKS:
80
+ if _sys.version_info < (3, 13):
81
+ raise RuntimeError(f"Symlink loop from {candidate!r}")
82
+ if strict:
83
+ raise OSError(
84
+ _errno.ELOOP,
85
+ "Too many levels of symbolic links",
86
+ str(candidate),
87
+ )
88
+ resolved = candidate
89
+ continue
90
+ target = candidate.readlink()
91
+ text = getattr(target, "path", None)
92
+ if not isinstance(text, str):
93
+ text = target.as_posix()
94
+ if text.startswith("/"):
95
+ resolved = root
96
+ pending.extend(part for part in reversed(text.split("/")) if part)
97
+ return resolved