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