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/AGENTS.md
ADDED
|
@@ -0,0 +1,2355 @@
|
|
|
1
|
+
# `pathapi` — public API header
|
|
2
|
+
|
|
3
|
+
Header-file-style reference for the `pathapi` package: every public export
|
|
4
|
+
with its signature, arguments, contract and gotchas, so the package can be used
|
|
5
|
+
without reading its source. It ships inside the package and is self-contained.
|
|
6
|
+
Development documentation lives with the source at
|
|
7
|
+
<https://github.com/jose-pr/pathapi>; the rendered documentation is at
|
|
8
|
+
<https://jose-pr.github.io/pathapi/> and the project overview is the
|
|
9
|
+
`README.md` shipped next to this file (`pathapi/README.md`). Every
|
|
10
|
+
deliberate behavioral difference from `pathlib.Path` is listed, with its
|
|
11
|
+
rationale, at <https://jose-pr.github.io/pathapi/divergences/>; this file
|
|
12
|
+
states the resulting contract.
|
|
13
|
+
|
|
14
|
+
Names are imported from the module named in the heading of the section that
|
|
15
|
+
documents them; the root `pathapi` re-exports the core ones ("Package
|
|
16
|
+
root"). A module or name that starts with an underscore is private. The file
|
|
17
|
+
documents the core alone: path classes for URIs, and for SFTP, object stores
|
|
18
|
+
and repository hosts, are built on it by other distributions that ship their
|
|
19
|
+
own header.
|
|
20
|
+
|
|
21
|
+
`pip install pathapi` (published as `pathlib-next`, import name `pathlib_next`,
|
|
22
|
+
up to 0.10.0). No dependencies: everything in this file runs on
|
|
23
|
+
the standard library, and `pytest` is needed only to import
|
|
24
|
+
`pathapi.testing` and `pathapi.http.testing`.
|
|
25
|
+
|
|
26
|
+
`pathapi.__version__` is the installed distribution's version
|
|
27
|
+
(`importlib.metadata`), `"0+unknown"` for a source tree that is not installed.
|
|
28
|
+
|
|
29
|
+
## Package root (`pathapi`)
|
|
30
|
+
|
|
31
|
+
`import pathapi` exposes `Path`, `Pathname`, `LocalPath`,
|
|
32
|
+
`PosixPathname`, `WindowsPathname`, `MemPath`, `FileStat`, the protocols `Stat`,
|
|
33
|
+
`Chmod`, `BinaryOpen`, `FsPathLike`, the aliases `PathLike`/`PurePathLike`, the
|
|
34
|
+
exceptions `PathError` and `NonRelativePatternError`, and the modules `glob`
|
|
35
|
+
(`pathapi.glob`) and `sync` (`pathapi.sync`). `FTPPath`, `ZipPath`,
|
|
36
|
+
`TarPath` and `ArchivePath` are exported too, imported on first use so that
|
|
37
|
+
`import pathapi` loads neither `ftplib` nor `ssl` nor any archive code.
|
|
38
|
+
`pathapi.http` (HTTP requests) is not exported there: import it when
|
|
39
|
+
it is needed, since it loads `http.client` and `ssl`.
|
|
40
|
+
`MemPath` is defined in `pathapi.mempath`, which also holds
|
|
41
|
+
`MemPathBackend` and `MemFile`; `pathapi.testing` is never imported
|
|
42
|
+
implicitly.
|
|
43
|
+
|
|
44
|
+
Every public module declares `__all__` with exactly the names this file
|
|
45
|
+
documents for it: the root, `path`, `fspath`, `mempath`, `protocols` (and its
|
|
46
|
+
`fs`, `io`, `checksum`), `testing`, `sync`, `glob`, `checksum`, `archive`,
|
|
47
|
+
`ftp`, `http`, `providers` and `exceptions`. So `from pathapi import *` publishes
|
|
48
|
+
the root's names and nothing else (`P` and `PN` are not among them; they stay
|
|
49
|
+
importable from `pathapi.path`). Every other module of the core starts
|
|
50
|
+
with an underscore and is private.
|
|
51
|
+
|
|
52
|
+
The package ships `py.typed`: `Path(...)`, `LocalPath(...)`, `MemPath(...)` and
|
|
53
|
+
`FileStat(...)` pass mypy and pyright at their defaults. A protocol stub (`stat`,
|
|
54
|
+
`chmod`, `_chown`, `_open`, `checksum`) is an ordinary method that
|
|
55
|
+
raises `NotImplementedError`, and a type checker reads `Pathname`'s abstract
|
|
56
|
+
members as ordinary methods too, because the bare `Path(...)` builds a
|
|
57
|
+
`LocalPath`: a subclass that leaves one out is refused when it is instantiated,
|
|
58
|
+
not by a checker.
|
|
59
|
+
|
|
60
|
+
### `FileStat` (`pathapi`)
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
class FileStat:
|
|
64
|
+
st_mode: int
|
|
65
|
+
st_nlink: int
|
|
66
|
+
st_uid: int
|
|
67
|
+
st_gid: int
|
|
68
|
+
st_size: int
|
|
69
|
+
st_atime: float
|
|
70
|
+
st_mtime: float
|
|
71
|
+
st_ctime: float
|
|
72
|
+
mode_known
|
|
73
|
+
size_known
|
|
74
|
+
def __init__(
|
|
75
|
+
self, st_mode=None, st_size=0, st_mtime=0, is_dir=False, size_known=True,
|
|
76
|
+
): ...
|
|
77
|
+
@classmethod
|
|
78
|
+
def from_stat(cls, stat) -> FileStat: ...
|
|
79
|
+
@classmethod
|
|
80
|
+
def from_path(cls, path, *, follow_symlink=True): ...
|
|
81
|
+
def settime(self, value): ...
|
|
82
|
+
def setmode(self, value, isdir=None): ...
|
|
83
|
+
def items(self): ...
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Slotted stat for non-`os` backends, with the fields of `os.stat_result`
|
|
87
|
+
(`st_mode`, `st_nlink`, `st_uid`, `st_gid`, `st_size`, `st_atime`, `st_mtime`,
|
|
88
|
+
`st_ctime`), `mode_known` and `size_known`. Without `st_mode`, a placeholder
|
|
89
|
+
(`S_IFREG|0o444` / `S_IFDIR|0o555`) with `mode_known=False`. A bare
|
|
90
|
+
permission `st_mode` (`0o644`) gets the type bits of `is_dir` ORed in and
|
|
91
|
+
counts as reported; one that carries a type keeps it. `size_known=False`
|
|
92
|
+
(default `True`) marks a listing's **type-only** stat: the entry is known to
|
|
93
|
+
be a file (or a directory) and `st_size` and `st_mtime` are placeholders (0).
|
|
94
|
+
It answers the type (`is_dir()`, `is_file()`, `exists()`, `walk()`, `glob()`,
|
|
95
|
+
`rm(recursive=True)`) and nothing else: a path class hands such a stat on
|
|
96
|
+
once, for a type question, and `stat()` asks the server; `PathSyncer`, `copy()`'s progress total and
|
|
97
|
+
`quick_check` ask the path for a size or a time instead of reading the
|
|
98
|
+
placeholder (`from_stat()` copies the flag from a foreign stat that has one).
|
|
99
|
+
`from_stat(stat)` (a `FileStat` passes through; `None` fields become 0),
|
|
100
|
+
`from_path(path, *, follow_symlink=True) -> FileStat | None` (`None` only
|
|
101
|
+
on `FileNotFoundError`), `settime(value)`, `setmode(value, isdir=None)`,
|
|
102
|
+
`items()`. `is_dir()`/`is_file()`/... are **methods**.
|
|
103
|
+
|
|
104
|
+
## Pure paths and I/O paths (`pathapi.path`)
|
|
105
|
+
|
|
106
|
+
### `Pathname`
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
class Pathname:
|
|
110
|
+
# abstract
|
|
111
|
+
segments: Sequence[str]
|
|
112
|
+
parts
|
|
113
|
+
parent: Self
|
|
114
|
+
def with_segments(self, *segments) -> Self: ...
|
|
115
|
+
def as_uri(self) -> str: ...
|
|
116
|
+
def relative_to(self, other) -> Self: ...
|
|
117
|
+
# derived
|
|
118
|
+
name: str
|
|
119
|
+
suffix: str
|
|
120
|
+
suffixes
|
|
121
|
+
stem
|
|
122
|
+
def with_name(self, name) -> Self: ...
|
|
123
|
+
def with_stem(self, stem) -> Self: ...
|
|
124
|
+
def with_suffix(self, suffix) -> Self: ...
|
|
125
|
+
parents: Sequence[Self]
|
|
126
|
+
def is_relative_to(self, other): ...
|
|
127
|
+
def joinpath(self, *args) -> Self: ...
|
|
128
|
+
root: str
|
|
129
|
+
drive: str
|
|
130
|
+
anchor: str
|
|
131
|
+
def match(self, path_pattern, *, case_sensitive=None): ...
|
|
132
|
+
def full_match(self, pattern, *, case_sensitive=None) -> bool: ...
|
|
133
|
+
def as_posix(self) -> str: ...
|
|
134
|
+
def has_glob_pattern(self): ...
|
|
135
|
+
def is_absolute(self) -> bool: ...
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
ABC for a pure (no I/O) path. Abstract: `segments`,
|
|
139
|
+
`parts`, `parent`, `with_segments()`, `as_uri()`,
|
|
140
|
+
`relative_to()`. Derived: `name`, `suffix`, `suffixes`, `stem`
|
|
141
|
+
(suffix rules of the running interpreter), `with_name`/`with_stem`/
|
|
142
|
+
`with_suffix` (`with_name()` and `with_stem()` raise `ValueError` for `""`,
|
|
143
|
+
`.` or a separator; the edge cases of `with_stem("")` and `with_suffix(".")`
|
|
144
|
+
are those of the running `pathlib`: from 3.13 `with_stem("")` raises when the
|
|
145
|
+
name has a suffix), `parents`,
|
|
146
|
+
`is_relative_to()`, `joinpath()`, `/` and `"prefix" / path`,
|
|
147
|
+
`root`/`drive`/`anchor` (`root` is `"/"` when the first segment is empty;
|
|
148
|
+
`drive` is `""`), `match()` (pathlib's
|
|
149
|
+
right-anchored per-segment match; empty pattern → `ValueError`),
|
|
150
|
+
`full_match()` (3.13 semantics: the root is
|
|
151
|
+
a component of its own, so a rooted path needs a rooted pattern or a leading
|
|
152
|
+
`**` that reaches past it, and a lone `*` never matches it; `.` and empty
|
|
153
|
+
components of the pattern are ignored; a bracket expression never matches a
|
|
154
|
+
separator), `as_posix()`, `has_glob_pattern()`, `is_absolute()` (the path
|
|
155
|
+
has a root; a class whose rooted paths are not all absolute overrides it).
|
|
156
|
+
|
|
157
|
+
- `__eq__`/`__hash__` default to `(type(self), tuple(self.segments))`: exact
|
|
158
|
+
type, so a subclass never equals its base. `LocalPath`/`PosixPathname`/
|
|
159
|
+
`WindowsPathname` keep `pathlib.PurePath` equality; a class that compares
|
|
160
|
+
something else (a URI's text) overrides both together.
|
|
161
|
+
- `<`, `<=`, `>`, `>=` compare `_order_key()` (default: the segments)
|
|
162
|
+
between two paths of the same exact type, so `sorted()` works over
|
|
163
|
+
`MemPath`; any other operand gives `NotImplemented`
|
|
164
|
+
(`TypeError`), as `pathlib` does across flavours. A type with its own
|
|
165
|
+
`__eq__` overrides `_order_key()` with the value that `__eq__` compares.
|
|
166
|
+
`LocalPath`/`PosixPathname`/`WindowsPathname` keep `pathlib`'s ordering.
|
|
167
|
+
### `Path`
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
class Path(
|
|
171
|
+
Pathname, Move, Copy, Identity, Create, Symlink, Remove, Listing, Resolve,
|
|
172
|
+
Readlink, Hardlink, Owned, Stat, Chmod, Chown, Utime, BinaryOpen,
|
|
173
|
+
):
|
|
174
|
+
info
|
|
175
|
+
def is_hidden(self): ...
|
|
176
|
+
def is_junction(self) -> bool: ...
|
|
177
|
+
def is_mount(self) -> bool: ...
|
|
178
|
+
def is_dir_binding(self) -> bool: ...
|
|
179
|
+
def samefile(self, other_path): ...
|
|
180
|
+
def iterdir(self) -> Iterator[Self]: ...
|
|
181
|
+
def glob(
|
|
182
|
+
self, pattern, *, case_sensitive=None, include_hidden=True,
|
|
183
|
+
recursive=None, dironly=None, recurse_symlinks=False, native=True,
|
|
184
|
+
on_error=None, bound_loops=False,
|
|
185
|
+
): ...
|
|
186
|
+
def rglob(
|
|
187
|
+
self, pattern, *, case_sensitive=None, include_hidden=True,
|
|
188
|
+
recursive=True, dironly=None, recurse_symlinks=False, native=True,
|
|
189
|
+
on_error=None, bound_loops=False,
|
|
190
|
+
): ...
|
|
191
|
+
def walk(self, top_down=True, on_error=None, follow_symlinks=False): ...
|
|
192
|
+
def touch(self, mode=None, exist_ok=True): ...
|
|
193
|
+
def mkdir(self, mode=0o777, parents=False, exist_ok=False): ...
|
|
194
|
+
def unlink(self, missing_ok=False): ...
|
|
195
|
+
def rmdir(self): ...
|
|
196
|
+
def rm(
|
|
197
|
+
self, recursive=False, missing_ok=False, ignore_error=False, *,
|
|
198
|
+
follow_symlinks=False, follow_binds=False,
|
|
199
|
+
): ...
|
|
200
|
+
def rename(self, target): ...
|
|
201
|
+
def symlink_to(
|
|
202
|
+
self, target, target_is_directory=False, *, force=False,
|
|
203
|
+
) -> None: ...
|
|
204
|
+
def readlink(self) -> Self: ...
|
|
205
|
+
def hardlink_to(self, target) -> None: ...
|
|
206
|
+
def absolute(self) -> Self: ...
|
|
207
|
+
def resolve(self, strict=False) -> Self: ...
|
|
208
|
+
def owner(self, *, follow_symlinks=True) -> str: ...
|
|
209
|
+
def group(self, *, follow_symlinks=True) -> str: ...
|
|
210
|
+
def copy(
|
|
211
|
+
self, target, *, overwrite=False, follow_symlinks=True,
|
|
212
|
+
preserve_metadata=True, recursive=False, ignore_error=None,
|
|
213
|
+
progress=None, on_error=None,
|
|
214
|
+
): ...
|
|
215
|
+
def copy_into(
|
|
216
|
+
self, target_dir, *, overwrite=False, follow_symlinks=True,
|
|
217
|
+
preserve_metadata=True, recursive=False, ignore_error=None,
|
|
218
|
+
progress=None, on_error=None,
|
|
219
|
+
) -> Path: ...
|
|
220
|
+
def move_into(self, target_dir, *, overwrite=False) -> Path: ...
|
|
221
|
+
def replace(self, target) -> Path: ...
|
|
222
|
+
def move(self, target, *, overwrite=False): ...
|
|
223
|
+
# override hooks
|
|
224
|
+
def _same_filesystem(self, other) -> bool: ...
|
|
225
|
+
def _node_key(self) -> tuple[object, tuple[str, ...]] | None: ...
|
|
226
|
+
def _scandir(self) -> Iterator[Tuple[str, Optional[FileStat]]]: ...
|
|
227
|
+
def _mkdir(self, mode): ...
|
|
228
|
+
def _symlink_to(self, target, target_is_directory=False) -> None: ...
|
|
229
|
+
def _coerce_target(self, target) -> Path: ...
|
|
230
|
+
def _rename_compatible(self, target) -> bool: ...
|
|
231
|
+
def _symlink_target(self, target) -> Self: ...
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Base class for I/O paths.
|
|
235
|
+
`Path(*args)` on the bare class constructs a `LocalPath`.
|
|
236
|
+
|
|
237
|
+
- Operation precedence: `Path.__init_subclass__` re-asserts pathapi's
|
|
238
|
+
`copy`, `move`, `copy_into`, `move_into`, `exists`, `rglob`, `read_text`,
|
|
239
|
+
`write_text` and `symlink_to` on any subclass that would otherwise inherit stdlib
|
|
240
|
+
`pathlib`'s. A class mixing a concrete stdlib path with `Path` but not
|
|
241
|
+
`LocalPath` is a local class too: it gets every function `LocalPath`
|
|
242
|
+
defines (`stat`, `chmod`, `is_junction`, `is_mount`, `glob`, `walk`,
|
|
243
|
+
`_scandir`, `_symlink_to`, `_chown`, `is_dir`/`is_file` before 3.13, ...)
|
|
244
|
+
wherever stdlib or a generic default would answer, so `rm(recursive=True)`
|
|
245
|
+
stays out of a junction there as on `LocalPath`. A method defined in the
|
|
246
|
+
subclass itself, or in a mixin of your own, always wins.
|
|
247
|
+
- `is_hidden()` — name starts with `"."`. `__iter__()` is `iterdir()`.
|
|
248
|
+
- Every operation above is defined by a protocol (see "Protocols"): `Path` adds
|
|
249
|
+
only `is_hidden()`, `__new__` and `__init_subclass__`. `readlink()`,
|
|
250
|
+
`hardlink_to()`, `absolute()`, `resolve()`, `owner()` and `group()` are
|
|
251
|
+
defined on every `Path`, and raise `NotImplementedError` until a class supplies
|
|
252
|
+
`readlink()`, `_hardlink_to()`, `_cwd()`, `_user_name()` and `_group_name()`;
|
|
253
|
+
`supports(path, Readlink)` and the like say which it did. `LocalPath` keeps
|
|
254
|
+
pathlib's own.
|
|
255
|
+
- `samefile()` — compares `(st_dev, st_ino)`; `NotImplementedError`
|
|
256
|
+
when `stat()` lacks them (`LocalPath` uses pathlib's).
|
|
257
|
+
- `_same_filesystem()` — override hook: whether `other`'s
|
|
258
|
+
segments are resolved in the same namespace (host, store, tree) as this
|
|
259
|
+
path's. `==` ignores that, so `copy()`, `move()` and `PathSyncer` ask it
|
|
260
|
+
before treating two paths as the same file, nested, or overlapping.
|
|
261
|
+
Called only with `type(other) is type(self)`; must be symmetric and do no
|
|
262
|
+
I/O. Default: both paths share one `_backend` object, or neither has
|
|
263
|
+
one, so equal paths of a type that says nothing ARE the same file. **A
|
|
264
|
+
type whose instances can front different hosts or stores must override
|
|
265
|
+
it**, or a transfer between two of them spelled alike is refused
|
|
266
|
+
(`OSError(EINVAL)` "same file" / `ValueError` "overlap"). `MemPath`:
|
|
267
|
+
same `MemPathBackend` instance.
|
|
268
|
+
- `_node_key()` — override hook for a type
|
|
269
|
+
whose segments spell one node several ways (`MemPath`: `a.txt`,
|
|
270
|
+
`/a.txt`, `/d/../a.txt`; an archive member: the spellings of its
|
|
271
|
+
archive). `namespace` is an object two paths share exactly
|
|
272
|
+
when they resolve names in one tree (compared with `is`); `names` is the
|
|
273
|
+
node's normalized position in it (`()` for the root). `copy()` and
|
|
274
|
+
`move()` compare two paths that both answer it, whatever their classes.
|
|
275
|
+
Default `None`: two paths of one type that `_same_filesystem()` places
|
|
276
|
+
together are compared with `==` and `is_relative_to()`. No I/O.
|
|
277
|
+
- `iterdir()` — **stub** (`NotImplementedError`); a
|
|
278
|
+
listable `Path` must implement it (or `_scandir()`).
|
|
279
|
+
- `_scandir()` — non-following stat
|
|
280
|
+
per entry; default: `iterdir()` + one `stat(follow_symlinks=False)` per
|
|
281
|
+
child. Consumed by `walk()`, `glob()`, `rm(recursive=True)` and
|
|
282
|
+
`PathSyncer`; override it when the listing call already returns metadata.
|
|
283
|
+
`None` means "unknown", never "missing". Never yield a name that is not
|
|
284
|
+
one path component (`providers.is_safe_child_name()`: `""`, `.`, `..`, or one
|
|
285
|
+
containing `/` or NUL): every listing of the package skips such a name, and
|
|
286
|
+
an override must too.
|
|
287
|
+
- `glob()` —
|
|
288
|
+
pathlib semantics: hidden entries included, `**` does not descend into
|
|
289
|
+
directory symlinks unless `recurse_symlinks=True`, a
|
|
290
|
+
trailing `**` selects files too on 3.13+, a missing or non-directory base
|
|
291
|
+
yields nothing, `""` → `ValueError`, an absolute pattern →
|
|
292
|
+
`NonRelativePatternError`. `recursive=None` enables recursion when a
|
|
293
|
+
component is `**`; an explicit value wins. Validates eagerly, selects
|
|
294
|
+
lazily. On a remote scheme a recursive glob lists every directory of the
|
|
295
|
+
subtree, once. A path two `**` reach along several splits is yielded once
|
|
296
|
+
(3.13+ `pathlib` yields it once per split). A literal component is checked
|
|
297
|
+
as the running `pathlib` checks it: before 3.13 one that is not the last
|
|
298
|
+
must be a directory (`a.txt/..` selects nothing), and from 3.12 the last
|
|
299
|
+
is tested without following a link, so a dangling symlink is selected.
|
|
300
|
+
An explicit `case_sensitive` compares literals against the listing too
|
|
301
|
+
and yields the name as stored, and so does 3.12 without it (3.12 lists
|
|
302
|
+
every literal; later versions join it as typed); `.` and `..` stay
|
|
303
|
+
literal. From 3.13 a final literal with a trailing separator is
|
|
304
|
+
tested as `os.path.lexists(name + os.sep)`, so the platform decides: Windows
|
|
305
|
+
selects a dangling directory link, macOS a link to a file (`LocalPath` and
|
|
306
|
+
stdlib paths; `native=False` tests it by following the link). A pattern with
|
|
307
|
+
no component (`.`) is a `ValueError`; `rglob(".")` selects every entry
|
|
308
|
+
below.
|
|
309
|
+
- **`pattern=None`** expands the pattern THIS PATH CARRIES
|
|
310
|
+
(`LocalPath("/etc/*.conf").glob(None)`), splitting at the first
|
|
311
|
+
wildcard — the supported form for a path that is itself a pattern.
|
|
312
|
+
`""` still raises.
|
|
313
|
+
- **`on_error(error)`** is called when a directory cannot be listed,
|
|
314
|
+
the same contract as `walk()`/`os.walk`: raising from it propagates,
|
|
315
|
+
returning treats that directory as empty. Without it the listing is
|
|
316
|
+
skipped in silence (pathlib's behaviour), so a caller could not tell
|
|
317
|
+
an unreadable directory from an absent one. A base that is missing or
|
|
318
|
+
not a directory is reported (once) whatever the first component is.
|
|
319
|
+
`error.filename` names the directory even when the backend left it
|
|
320
|
+
unset.
|
|
321
|
+
- **`bound_loops=True`** skips a directory whose `(st_dev, st_ino)` is
|
|
322
|
+
already on the CURRENT DESCENT PATH — a directory reachable below
|
|
323
|
+
itself, which is what a loop is. It bounds a Windows junction loop,
|
|
324
|
+
which `recurse_symlinks=False` cannot (a junction reports
|
|
325
|
+
`is_symlink() == False`) and which `pathlib` itself walks until the
|
|
326
|
+
recursion limit. One directory deliberately reachable under two
|
|
327
|
+
SIBLING names (a shared layer junctioned in twice) is not a loop and
|
|
328
|
+
both names expand — the rule is the ancestor chain, not everything
|
|
329
|
+
seen, the same line `find -L` draws. A backend whose stat carries no
|
|
330
|
+
identity (`MemPath`, most remote backends, a `st_ino` of 0 or `None`) is
|
|
331
|
+
walked unbounded.
|
|
332
|
+
- **`recurse_symlinks=True`** lets `**` descend through symlinks to
|
|
333
|
+
directories, as pathlib 3.13 does (the keyword is accepted on every
|
|
334
|
+
version). A link to a directory the descent is already inside
|
|
335
|
+
(`(st_dev, st_ino)` equal) is an entry and is not entered, and where the
|
|
336
|
+
stat carries no identity no more than 40 links are followed in one
|
|
337
|
+
descent, so a loop ends where pathlib follows it until the system refuses
|
|
338
|
+
it; those repeated entries are the only difference. A backend without
|
|
339
|
+
symlinks answers as before.
|
|
340
|
+
- **`native=True`** (default) follows the running interpreter on the three
|
|
341
|
+
rules pathlib changed mid-series: a trailing `/` is ignored before 3.11
|
|
342
|
+
and selects directories only from 3.11; `a**` raises `ValueError`
|
|
343
|
+
before 3.13 and is a plain wildcard from 3.13; a trailing `**` selects
|
|
344
|
+
directories only before 3.13 and files too from 3.13. `native=False`
|
|
345
|
+
applies one rule on every version (trailing `/` → directories only,
|
|
346
|
+
`a**` → a plain wildcard, trailing `**` → files too), so a pattern
|
|
347
|
+
answers the same on every interpreter and backend;
|
|
348
|
+
`pathapi.testing`'s contract suite uses it.
|
|
349
|
+
- `rglob()` — `glob(f"**/{pattern}", recursive=True)`.
|
|
350
|
+
`pattern=None` is `glob(None)`. `LocalPath` raises the audit events
|
|
351
|
+
`pathlib.Path.glob` and `pathlib.Path.rglob` with the arguments `pathlib`
|
|
352
|
+
passes on the running version (3.13+ also raises `glob` for the call
|
|
353
|
+
`rglob` stands for); `pattern=None` raises none.
|
|
354
|
+
- `walk()` — drives
|
|
355
|
+
`_scandir()`; its stats are trusted only with `follow_symlinks=False`.
|
|
356
|
+
Without `follow_symlinks` a symlink to a directory and a Windows junction
|
|
357
|
+
(`is_junction()`) are listed in `filenames` and not entered, as
|
|
358
|
+
`pathlib.Path.walk()` does; with it they are entered, and nothing protects
|
|
359
|
+
the walk from a loop (`LocalPath` included: its walk is this one).
|
|
360
|
+
A listed name that is not one path component (`providers.is_safe_child_name()`)
|
|
361
|
+
is left out of `dirnames`/`filenames`; `on_error`, when given, is called
|
|
362
|
+
with a `ValueError` for it (`error.filename` is the directory).
|
|
363
|
+
- `touch()` — `FileExistsError` when `exist_ok=False`
|
|
364
|
+
and the path exists; never truncates; creates with `open("x")` (falls
|
|
365
|
+
back to `"w"` when `x` is unsupported); `chmod(mode)` only when `mode` is
|
|
366
|
+
passed (unmasked), and a class that cannot chmod (`NotImplementedError`)
|
|
367
|
+
raises it and removes the file it created, so a mode asked for is never
|
|
368
|
+
dropped; an existing file is left as it is, mode included, as `pathlib`
|
|
369
|
+
does; sets an existing file's access and modification times
|
|
370
|
+
to now through `_utime()`, and leaves them as they are where the class
|
|
371
|
+
cannot set one (`NotImplementedError`). `LocalPath` uses pathlib's
|
|
372
|
+
`touch()`.
|
|
373
|
+
- `_mkdir()` (stub) / `mkdir()`.
|
|
374
|
+
- `unlink()`, `rmdir()` — stubs.
|
|
375
|
+
- `rm()` — extension.
|
|
376
|
+
`ignore_error` is a bool or `callable(error, path) -> bool` (True
|
|
377
|
+
swallows); each error is offered once, and a predicate that raises ends
|
|
378
|
+
the call with its own exception, which is not offered to it again. Recursive removal is bottom-up and
|
|
379
|
+
never descends through a directory symlink or a binding (a Windows
|
|
380
|
+
junction, a mount point — `is_dir_binding()`): the entry itself is
|
|
381
|
+
removed, never what is behind it, which is what `rm -r` does.
|
|
382
|
+
`follow_symlinks=` (symlinks) and `follow_binds=` (bindings) choose per
|
|
383
|
+
call: `False` (default, remove the entry), `True` (remove the contents
|
|
384
|
+
behind it too, then the entry: a followed symlink is unlinked), `None`
|
|
385
|
+
(leave it in place — the enclosing directory is then not empty and says
|
|
386
|
+
so), or a callable `policy(path) -> bool | None` asked per entry, so one
|
|
387
|
+
tree can keep one mount and follow another. Path components before the
|
|
388
|
+
final one are followed as usual; the path `rm()` is called on is decided by the same
|
|
389
|
+
policy as one met on the way down (without `recursive` a symlink is only
|
|
390
|
+
unlinked). A policy that is not `True`/`False`/`None`/a callable, or a
|
|
391
|
+
callable's answer that is not one of the three, raises `ValueError`
|
|
392
|
+
before anything is removed and is never offered to `ignore_error`. A class
|
|
393
|
+
that overrides `rm()` takes and checks both keywords; one whose backend holds
|
|
394
|
+
no symlinks or bindings decides nothing with them, and one with a native
|
|
395
|
+
recursive removal uses it only for the default `follow_symlinks=False`
|
|
396
|
+
and runs this walk otherwise. A listed name that is not one path component
|
|
397
|
+
(`providers.is_safe_child_name()` with `windows=False`: the names come from
|
|
398
|
+
the directory's own listing) is never joined onto it: it raises
|
|
399
|
+
`ValueError`, offered to `ignore_error` as `(error, directory)` once per
|
|
400
|
+
entry, and nothing is removed for it.
|
|
401
|
+
- `rename()` — stub. Implementations return the new path.
|
|
402
|
+
- `_symlink_to()` (stub; receives a path
|
|
403
|
+
object) / `symlink_to()`
|
|
404
|
+
— `force=True` unlinks an existing non-directory entry first (not atomic;
|
|
405
|
+
never removes a directory). A `str` target goes through the overridable
|
|
406
|
+
`_symlink_target()` and is stored verbatim; relative stays relative:
|
|
407
|
+
`LocalPath` hands the text on untouched (`./t/` stays `./t/`, as
|
|
408
|
+
`pathlib` stores it) except on Windows, which cannot resolve a relative
|
|
409
|
+
target spelled with `/`, so the path class rewrites it there; a class for
|
|
410
|
+
URI references must not parse it as one, and any other class normalizes it
|
|
411
|
+
with `with_segments()`, which keeps per-instance state. Implemented by
|
|
412
|
+
`LocalPath`, and by classes of other distributions whose backend has links.
|
|
413
|
+
- `copy()`
|
|
414
|
+
- A file copy calls `stat()` once on the source (type, mode, size and
|
|
415
|
+
identity) and once on the target.
|
|
416
|
+
- Existing target: `FileExistsError` unless `overwrite=True`; a directory
|
|
417
|
+
target of a file copy → `IsADirectoryError`; a directory source needs
|
|
418
|
+
`recursive=True`; copying into its own subtree → `OSError(EINVAL)`; onto
|
|
419
|
+
the same file (or a case-insensitive alias) → `OSError(EINVAL)`. Two
|
|
420
|
+
files of this machine (`LocalPath`, a subclass, `pathlib.Path`, a
|
|
421
|
+
`file:` URI) are compared by identity whatever their classes, a
|
|
422
|
+
directory reached through a symlink included. A type that answers
|
|
423
|
+
`_node_key()` is compared by that, across classes. Otherwise "same" is
|
|
424
|
+
`samefile()` where `stat()` carries `st_dev`/`st_ino`, else equal paths
|
|
425
|
+
of one type that `_same_filesystem()` places together.
|
|
426
|
+
- The source is opened before the target is touched; a failed stream
|
|
427
|
+
removes the partial target. `overwrite=True` removes an existing file
|
|
428
|
+
target with `unlink(missing_ok=True)` just before writing: a backend
|
|
429
|
+
whose write replaces the file and which cannot delete implements
|
|
430
|
+
`unlink()` so that `missing_ok=True` returns without removing.
|
|
431
|
+
- A recursive copy runs inside `target._write_batch()`: onto a zip
|
|
432
|
+
archive it is one rewrite of the file, and a copy that raises leaves
|
|
433
|
+
the archive as it was.
|
|
434
|
+
- A recursive copy refuses any child name that would not stay inside
|
|
435
|
+
`target` — `..`, and when the target reads names with Windows rules
|
|
436
|
+
(`providers.is_windows_flavoured()`) `\`, `:`, a trailing dot or space, and
|
|
437
|
+
the reserved device names (`CON`, `PRN`, `AUX`, `NUL`, `COM1`-`COM9`,
|
|
438
|
+
`LPT1`-`LPT9`, with or without an extension). The names come from a
|
|
439
|
+
listing the destination does not control (an archive, a remote index,
|
|
440
|
+
an object-store key); on a Windows target `"C:x"` joins to a
|
|
441
|
+
drive-relative path outside it and `"report."` is stored as `"report"`.
|
|
442
|
+
Raised as `ValueError` through `ignore_error`, per child, like any other
|
|
443
|
+
child failure.
|
|
444
|
+
- `follow_symlinks=False` on a symlink recreates the link
|
|
445
|
+
(`NotImplementedError` if either side cannot); only the target text is
|
|
446
|
+
copied, not the link's own mode, times or owner. A dangling symlink at
|
|
447
|
+
the target is written through, as `shutil.copyfile` does.
|
|
448
|
+
- `preserve_metadata=True` copies permission bits only, and only a mode the
|
|
449
|
+
source backend really reported (`FileStat.mode_known`): a file's, and a
|
|
450
|
+
directory's after its children are copied, so a read-only directory
|
|
451
|
+
still receives them. Between two different classes the setuid, setgid
|
|
452
|
+
and sticky bits are dropped, so a mode read from a tar member or a
|
|
453
|
+
remote server never makes a privileged file here.
|
|
454
|
+
- `ignore_error`: `True` suppresses child errors of a recursive copy,
|
|
455
|
+
`False`/`None` raise; a callable is called as `ignore_error(error)` and
|
|
456
|
+
the error is **always** suppressed (its return value is ignored — not
|
|
457
|
+
`rm()`'s `(error, path)` predicate).
|
|
458
|
+
- `on_error`: `rm()`'s predicate for a recursive copy, `(error, path) ->
|
|
459
|
+
bool` (or a bool), asked once per child error with `path` the source
|
|
460
|
+
entry that failed. `True` skips that entry and the copy goes on, `False`
|
|
461
|
+
raises the error, and a predicate that raises ends the copy with its own
|
|
462
|
+
exception. An error it declined, or raised, is not offered again by the
|
|
463
|
+
directories above (not even for a nested `copy()`). Errors outside the
|
|
464
|
+
children — the root, the target directory, a refused prefix of an object
|
|
465
|
+
store — are raised and never offered. Passing `ignore_error` and
|
|
466
|
+
`on_error` together raises `TypeError` before anything is copied.
|
|
467
|
+
Every `copy()` override takes it and passes it on only when it was given;
|
|
468
|
+
a native walk may call it from a worker thread, and children already
|
|
469
|
+
running when one is declined are each offered their own error before the
|
|
470
|
+
copy stops.
|
|
471
|
+
- `progress(path, bytes_copied, total_size | None)` per chunk of each file
|
|
472
|
+
streamed; not called by a native transfer that bypasses the streaming copy.
|
|
473
|
+
- A `str` target goes through `_coerce_target()`: `with_segments()` by
|
|
474
|
+
default, a URI parse on a class for URI references (so a `str` can name
|
|
475
|
+
another scheme).
|
|
476
|
+
- `copy_into()` — `copy(target_dir / self.name, ...)`: the same
|
|
477
|
+
keywords and defaults as `copy()`, and the new path is returned (pathlib
|
|
478
|
+
3.14 returns it too). A `str` directory is a path on this backend, as for
|
|
479
|
+
`copy()`. `ValueError` for a path with no name. Present on every backend
|
|
480
|
+
and version; on 3.14 it replaces stdlib's, which overwrote and copied
|
|
481
|
+
trees by default.
|
|
482
|
+
- `move_into()` — `move(target_dir /
|
|
483
|
+
self.name, overwrite=overwrite)`, returning the new path.
|
|
484
|
+
- `replace()` — `move(target, overwrite=True)`, returning
|
|
485
|
+
`target` as a path. A file replaces a file and a directory an empty
|
|
486
|
+
directory; a directory that holds anything raises `OSError(ENOTEMPTY)`
|
|
487
|
+
and a file onto a directory `IsADirectoryError`, both before anything is
|
|
488
|
+
removed. `LocalPath` keeps stdlib's `os.replace()`.
|
|
489
|
+
- `move()` — validates first (missing source →
|
|
490
|
+
`FileNotFoundError`, file onto directory → `IsADirectoryError`, existing
|
|
491
|
+
target without `overwrite` → `FileExistsError`; a same-file spelling is
|
|
492
|
+
renamed in place), and refuses before it removes anything a target that
|
|
493
|
+
holds the source (`OSError(ENOTEMPTY)`), lies inside it
|
|
494
|
+
(`OSError(EINVAL)`) or is a link to the same file (`OSError(EINVAL)`).
|
|
495
|
+
Sameness is decided as for `copy()`. Two hard links of one file are two
|
|
496
|
+
names: an existing target needs `overwrite=True`, and the source name is
|
|
497
|
+
then removed, so the target name keeps the content. Tries `rename()`
|
|
498
|
+
when `_rename_compatible(target)` (default: only onto a path of the very
|
|
499
|
+
same class; a class for URI references leaves it to `rename()`, which raises
|
|
500
|
+
`NotImplementedError` for a target it cannot reach; `LocalPath` requires a
|
|
501
|
+
local target), falling back to `copy(recursive=True)` + `rm`/`unlink` on
|
|
502
|
+
`NotImplementedError` or `OSError(EXDEV)`. The fallback recreates a
|
|
503
|
+
symlink, and the links inside a moved tree, at a target whose class
|
|
504
|
+
implements `_symlink_to()`, so the result does not depend on whether
|
|
505
|
+
`rename()` was available; any other target receives what the links point
|
|
506
|
+
at. `overwrite=True` replaces a
|
|
507
|
+
local file atomically (`replace()`); elsewhere the target is unlinked just
|
|
508
|
+
before the rename. Returns `rename()`'s result (`None` on the fallback).
|
|
509
|
+
|
|
510
|
+
### `FsPathLike`, `PathLike`, `PurePathLike`
|
|
511
|
+
|
|
512
|
+
```python
|
|
513
|
+
class FsPathLike(Protocol):
|
|
514
|
+
def __fspath__(self) -> str: ...
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
`Protocol` with `__fspath__()`, for annotations. A class qualifies by defining
|
|
518
|
+
`__fspath__` itself, which is also how `os.PathLike` recognises it:
|
|
519
|
+
`LocalPath`, `PosixPathname` and `WindowsPathname` do (from `pathlib.PurePath`),
|
|
520
|
+
and `Pathname`, `MemPath`, `FTPPath` and the archive paths do not, so
|
|
521
|
+
`isinstance(path, os.PathLike)` is False for them and `os.fspath(path)` is a
|
|
522
|
+
`TypeError`. A class of your own that has a local name defines `__fspath__`
|
|
523
|
+
and is then an `os.PathLike`.
|
|
524
|
+
**`PathLike`** = `str | Path`; **`PurePathLike`** = `str | Pathname`.
|
|
525
|
+
|
|
526
|
+
### For path implementers (`pathapi.path`, `pathapi.providers`, `pathapi.archive`)
|
|
527
|
+
|
|
528
|
+
The names a class built on `Path` overrides or calls beyond the stubs above,
|
|
529
|
+
in one place: the classes of the package use all of them, so they are contract. The
|
|
530
|
+
helpers are imported from `pathapi.providers`, except `detect_format`,
|
|
531
|
+
which is in `pathapi.archive`.
|
|
532
|
+
|
|
533
|
+
```python
|
|
534
|
+
class Pathname:
|
|
535
|
+
def _anchor_segments(self) -> tuple[str, ...]: ...
|
|
536
|
+
|
|
537
|
+
class Path(Pathname, Move, Copy, Identity, Create, Symlink, Remove, Listing,
|
|
538
|
+
Resolve, Readlink, Hardlink, Owned, Stat, Chmod, Chown, Utime, BinaryOpen):
|
|
539
|
+
def _scandir(self) -> Iterator[Tuple[str, Optional[FileStat]]]: ...
|
|
540
|
+
def _same_filesystem(self, other) -> bool: ...
|
|
541
|
+
def _coerce_target(self, target) -> Path: ...
|
|
542
|
+
def _open(self, mode="r", buffering=-1) -> IOBase: ...
|
|
543
|
+
def _chown(self, uid, gid, *, follow_symlinks=True) -> None: ...
|
|
544
|
+
def _utime(self, *, atime=None, mtime=None) -> None: ...
|
|
545
|
+
def _copy_from(
|
|
546
|
+
self, source, follow_symlinks=True, preserve_metadata=False,
|
|
547
|
+
) -> None: ...
|
|
548
|
+
def _write_batch(self) -> ContextManager[object]: ...
|
|
549
|
+
|
|
550
|
+
def check_follow(name, policy) -> None: ...
|
|
551
|
+
def contains(src, target) -> bool: ...
|
|
552
|
+
def copy_error_predicate(ignore_error, on_error) -> OfferedOnce | None: ...
|
|
553
|
+
def detect_format(name, peek=None) -> str: ...
|
|
554
|
+
|
|
555
|
+
class DiscardOnError:
|
|
556
|
+
def discard(self) -> None: ...
|
|
557
|
+
def _holds_uncommitted(self) -> bool: ...
|
|
558
|
+
|
|
559
|
+
class CommitOnClose(DiscardOnError, BytesIO):
|
|
560
|
+
def __init__(self, commit, initial=None): ...
|
|
561
|
+
def discard(self) -> None: ...
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
- Primitives, by protocol ("Protocols" has the table): `stat`; `chmod`;
|
|
565
|
+
`_chown`; `_user_name`, `_group_name` (for `owner()`/`group()`); `_utime`;
|
|
566
|
+
`_mkdir`; `_open`; `iterdir`; `unlink`, `rmdir`; `readlink`; `_symlink_to`;
|
|
567
|
+
`_hardlink_to`; `_cwd`; `rename`; `checksum`. Implement what the backend can
|
|
568
|
+
and leave the rest: `pathapi.protocols.supports(path, Protocol)` then
|
|
569
|
+
tells a caller, without a call that raises, which protocols the class has.
|
|
570
|
+
- `_supports(protocol)` — optional: return `True` or `False` for a protocol
|
|
571
|
+
whose answer depends on the connection (a server that lacks an extension),
|
|
572
|
+
`None` to leave it to the class. `supports()` asks it first for an instance.
|
|
573
|
+
It may make at most one request per backend, cached for the backend's
|
|
574
|
+
lifetime, and it never raises: when the request fails it returns `None`.
|
|
575
|
+
- `_anchor_segments()` — the leading `segments` that anchor the path instead
|
|
576
|
+
of naming a directory (`("",)` for a rooted path, else `()`); `parents`
|
|
577
|
+
stops there. Override it when the root is spelled differently (a class for
|
|
578
|
+
`file:` URIs anchors at a Windows drive or share).
|
|
579
|
+
- `info` — an object with the methods of `pathlib.types.PathInfo`
|
|
580
|
+
(Python 3.14): `exists()`, `is_dir()`, `is_file()` (each with
|
|
581
|
+
`follow_symlinks=True`) and `is_symlink()`. A stat that fails with
|
|
582
|
+
`OSError`/`ValueError` reads as "no such entry". It calls `stat()` at most
|
|
583
|
+
once following links and once not (an entry that is missing or not a link
|
|
584
|
+
shares its non-following stat), and remembers the answers on the info object
|
|
585
|
+
alone: every `path.info` is a fresh question, since a path holds no state.
|
|
586
|
+
`LocalPath` has pathlib's own on 3.14 and this one before.
|
|
587
|
+
- `_copy_from(source, follow_symlinks=True, preserve_metadata=False)` — the
|
|
588
|
+
hook Python 3.14's `pathlib.Path.copy(target)` calls on a target that is
|
|
589
|
+
not a stdlib path (after `target.with_segments`, `target.parents` and `==`
|
|
590
|
+
have said it is not the source): `pathlib.Path(f).copy(MemPath("/x"))`
|
|
591
|
+
works on 3.14, and returns `target.joinpath()`. It copies `source` (anything
|
|
592
|
+
with `info`, `iterdir()`, `readlink()`, `open()` and `stat()`) as 3.14 does:
|
|
593
|
+
an existing file is overwritten, an existing directory raises, children are
|
|
594
|
+
copied one by one, `follow_symlinks=False` makes a link through
|
|
595
|
+
`symlink_to()`, and `preserve_metadata` copies the permission bits and the
|
|
596
|
+
modification time where the class can set them (`chmod()`, `_utime()`).
|
|
597
|
+
`Path.copy()` of this library does not call it. A class that mixes a
|
|
598
|
+
stdlib path keeps stdlib's.
|
|
599
|
+
- `_write_batch()` — a context manager that `copy(recursive=True)` enters
|
|
600
|
+
around the writes it makes below its target and `PathSyncer.sync()`
|
|
601
|
+
around a whole sync of its root target (never for a dry run). Override it
|
|
602
|
+
in a class whose every write is expensive, to hold them and commit them
|
|
603
|
+
together when the block ends (`ArchiveUri` returns its `batch()`): entries
|
|
604
|
+
nest and must join, and an exception must undo what was held. It must
|
|
605
|
+
read nothing and raise nothing; the default does nothing.
|
|
606
|
+
- `_scandir()`, `_same_filesystem()`, `_coerce_target()` — see `Path`.
|
|
607
|
+
`_open()`, `_chown()` and `_utime()` — see `BinaryOpen` and `Chmod`.
|
|
608
|
+
- `check_follow(name, policy)` — `ValueError` naming `name` unless `policy` is
|
|
609
|
+
`True`, `False`, `None` or a callable. An `rm()` override calls it for
|
|
610
|
+
`follow_symlinks` and `follow_binds` before it removes anything.
|
|
611
|
+
- `contains(src, target)` — whether `target` is `src` or lies inside it,
|
|
612
|
+
looking through symlinks and deciding as `copy()` does (`_node_key()`,
|
|
613
|
+
else `_same_filesystem()` and `==`). A `copy()` override that copies a
|
|
614
|
+
tree natively refuses a target inside the source with it.
|
|
615
|
+
- `copy_error_predicate(ignore_error, on_error)` — `None` when `on_error` was
|
|
616
|
+
not given, else the `OfferedOnce` predicate to ask about each failing child;
|
|
617
|
+
`TypeError` when `ignore_error` was given too. A `copy()` override calls it
|
|
618
|
+
first, passes `on_error=` on to `super().copy()` and to its children only
|
|
619
|
+
when it is not `None`, and offers the source child that failed.
|
|
620
|
+
- `detect_format(name, peek=None)` — `"zip"` or `"tar"` from the extension of
|
|
621
|
+
`name`, else from the first bytes `peek()` returns (called only then).
|
|
622
|
+
- `CommitOnClose(commit, initial=None)` — an in-memory `BytesIO` that calls
|
|
623
|
+
`commit(buffer)` once at `close()`, before the buffer is closed: always for
|
|
624
|
+
a new file, only after a write for one opened with `initial` content. Return
|
|
625
|
+
it from `_open()` for a backend that cannot write a file piecewise. It
|
|
626
|
+
inherits `DiscardOnError`; its `discard()` drops the buffer so that a later
|
|
627
|
+
`close()` commits nothing. A buffer garbage collected unclosed while it
|
|
628
|
+
holds something to commit is discarded and emits a `ResourceWarning`: close
|
|
629
|
+
the file to commit it.
|
|
630
|
+
- `DiscardOnError` — mixin for a write stream that commits when it is closed
|
|
631
|
+
(an upload, a rewrite of an archive), so that a write which failed commits
|
|
632
|
+
nothing. Its `__exit__` calls `discard()` when the `with` block over the
|
|
633
|
+
stream ended in an exception (any `BaseException`) and then closes; a block
|
|
634
|
+
that ended normally just closes, and `close()` outside a `with` block
|
|
635
|
+
commits, as before. The subclass defines `discard()` (the mixin's raises
|
|
636
|
+
`NotImplementedError`): drop what was written so that `close()` commits
|
|
637
|
+
nothing, and do nothing more when called twice or after `close()`. Put the
|
|
638
|
+
mixin first in the bases (`class Upload(DiscardOnError, io.BytesIO)`), so
|
|
639
|
+
that its `__exit__` wins over `io.IOBase`'s. The target is then left as it
|
|
640
|
+
was, and is not created if it was absent. A stream garbage collected
|
|
641
|
+
unclosed is discarded the same way, with a `ResourceWarning`, when
|
|
642
|
+
`_holds_uncommitted()` (true by default) says closing would commit something;
|
|
643
|
+
override it to return false for a stream that was never modified.
|
|
644
|
+
- A stream returned by `_open()` — in binary mode `open()` returns it
|
|
645
|
+
untouched, so its own `__exit__` is the mechanism: a stream that defines
|
|
646
|
+
`discard()` without inheriting `DiscardOnError` (or doing the same in its own
|
|
647
|
+
`__exit__`) is not discarded there. In text mode the wrapper `open()` builds
|
|
648
|
+
calls the stream's `discard()` whenever it has a callable one, mixin or not,
|
|
649
|
+
and drops the text it still holds instead of writing it to the stream; it
|
|
650
|
+
also discards when its final flush fails or when it cannot be built (an
|
|
651
|
+
unknown `encoding=`). A stream without `discard()` is used as it is, which is
|
|
652
|
+
right for one that writes through as it goes (`LocalPath`, `MemPath`, an SFTP
|
|
653
|
+
path): a failed block leaves what it had written.
|
|
654
|
+
|
|
655
|
+
## Local filesystem (`pathapi.fspath`)
|
|
656
|
+
|
|
657
|
+
### `LocalPath`
|
|
658
|
+
|
|
659
|
+
```python
|
|
660
|
+
class LocalPath(pathlib.Path, Path):
|
|
661
|
+
def stat(self, *, follow_symlinks=True): ...
|
|
662
|
+
def chmod(self, mode, *, follow_symlinks=True): ...
|
|
663
|
+
def is_dir(self, *, follow_symlinks=True): ...
|
|
664
|
+
def is_file(self, *, follow_symlinks=True): ...
|
|
665
|
+
def exists(self, *, follow_symlinks=True): ...
|
|
666
|
+
def read_text(self, encoding=None, errors=None, newline=None) -> str: ...
|
|
667
|
+
def write_text(self, data, encoding=None, errors=None, newline=None): ...
|
|
668
|
+
def _scandir(self): ...
|
|
669
|
+
def _chown(self, uid, gid, *, follow_symlinks=True) -> None: ...
|
|
670
|
+
def _utime(self, *, atime=None, mtime=None) -> None: ...
|
|
671
|
+
# Path's signatures: glob, rglob, walk, symlink_to, _symlink_to, copy,
|
|
672
|
+
# copy_into, move, move_into, rm
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
`pathlib.WindowsPath`/`PosixPath` (by `os.name`) with
|
|
676
|
+
`Path` mixed in; stdlib behavior except where overridden: `_scandir()`
|
|
677
|
+
(tuples from `os.scandir` lstat), `walk()`, `glob()`,
|
|
678
|
+
`stat()`/`chmod()` (`follow_symlinks=` on 3.9; `chmod` accepts octal
|
|
679
|
+
strings), `is_dir()`/`is_file()` (`follow_symlinks=` before 3.13),
|
|
680
|
+
`_symlink_to()`, `_chown()` (`shutil.chown`; `NotImplementedError` where
|
|
681
|
+
`os.chown` is missing, i.e. Windows), `_utime()` (`os.utime`), plus pathapi's `copy`, `move`,
|
|
682
|
+
`copy_into`, `move_into`, `exists`, `rglob`, `read_text`, `write_text` and
|
|
683
|
+
`symlink_to`.
|
|
684
|
+
`exists()` returns `False` for any `OSError`/`ValueError` on every Python
|
|
685
|
+
version; `is_dir()`, `is_file()`, `is_fifo()`, `is_socket()`,
|
|
686
|
+
`is_block_device()` and `is_char_device()` are stdlib's: before 3.13 they
|
|
687
|
+
raise for an error other than ENOENT, ENOTDIR, EBADF or ELOOP (a
|
|
688
|
+
`PermissionError` from `stat()`, say), from 3.13 they return `False`. A
|
|
689
|
+
stdlib `pathlib.Path` is not a `pathapi.Path`, and `MemPath` is not a
|
|
690
|
+
stdlib path.
|
|
691
|
+
|
|
692
|
+
### `PosixPathname`, `WindowsPathname`
|
|
693
|
+
|
|
694
|
+
```python
|
|
695
|
+
class PosixPathname(PurePosixPath, Pathname): ...
|
|
696
|
+
class WindowsPathname(PureWindowsPath, Pathname): ...
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
pure classes over
|
|
700
|
+
`PurePosixPath`/`PureWindowsPath` implementing `Pathname`.
|
|
701
|
+
|
|
702
|
+
## In-memory filesystem (`pathapi.mempath`)
|
|
703
|
+
|
|
704
|
+
### `MemPath`
|
|
705
|
+
|
|
706
|
+
```python
|
|
707
|
+
class MemPath(Path):
|
|
708
|
+
def __init__(self, *segments, backend=None): ...
|
|
709
|
+
def relative_to(self, other, *, walk_up=False) -> Self: ...
|
|
710
|
+
backend
|
|
711
|
+
normalized
|
|
712
|
+
|
|
713
|
+
class MemBytesIO(BytesIO):
|
|
714
|
+
def __init__(self, dest, *, append=False): ...
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
`Path` over nested dicts; the
|
|
718
|
+
reference `Path` subclass. Segments may be `str`, `Pathname` or `MemPath`
|
|
719
|
+
(the first `MemPath` argument names the backend, as the left operand does
|
|
720
|
+
for `/` and `joinpath()`, and an explicit `backend=` wins over it; another
|
|
721
|
+
`Path` → `NotImplementedError`). Joined and normalized like `PurePosixPath`.
|
|
722
|
+
An unknown keyword → `TypeError`.
|
|
723
|
+
|
|
724
|
+
- `backend` (a `MemPathBackend`); `normalized` is the list of names with `..`
|
|
725
|
+
applied to the text and clamped at the root (`[""]` for the root);
|
|
726
|
+
`segments` is a tuple (`("", "a")` for `/a`); `parts` is `(segments, backend)`; `as_uri()` →
|
|
727
|
+
`mempath:<quoted posix path>` (no `mempath:` scheme is registered; build
|
|
728
|
+
`MemPath` directly).
|
|
729
|
+
- `..` is applied to the tree, as on a POSIX filesystem: the directory it
|
|
730
|
+
leaves must exist (`/missing/../a` → `FileNotFoundError`, `/f/../a` with
|
|
731
|
+
`f` a file → `NotADirectoryError`), and above the root it stays at the
|
|
732
|
+
root.
|
|
733
|
+
- `stat()` → `FileStat` with `st_size` and `st_mtime` (time of the last
|
|
734
|
+
write); the mode is a placeholder (`mode_known=False`).
|
|
735
|
+
- `open()` supports `r`, `w`, `x`, `a` (binary or text); `+` modes →
|
|
736
|
+
`NotImplementedError`. A binary handle is a `MemBytesIO`, a `BytesIO` that
|
|
737
|
+
writes its buffer back into the file's `bytearray` (`dest`) on `flush()` and
|
|
738
|
+
close; with `append=True` every write lands at the end. Writes are visible after `flush()`/`close()`; two
|
|
739
|
+
`a` handles on one file both land (each adds only what it wrote). A `with`
|
|
740
|
+
block that raised leaves what it had written, as a local file does.
|
|
741
|
+
- `rmdir()` of the root → `OSError(EBUSY)`, so `root.rm(recursive=True)`
|
|
742
|
+
empties the tree and then raises that.
|
|
743
|
+
- `_utime()` sets a file's modification time (the access time is not
|
|
744
|
+
kept); a directory has none (`NotImplementedError`), so `touch()` leaves
|
|
745
|
+
one alone.
|
|
746
|
+
- `relative_to(other, *, walk_up=False)` takes a `MemPath` of the same
|
|
747
|
+
backend or a `str` and returns the remainder as a relative `MemPath` on that
|
|
748
|
+
backend, comparing the names as written (no `..` applied), as `pathlib` does.
|
|
749
|
+
`ValueError` when `self` is not below `other`, when only one of them is
|
|
750
|
+
rooted, or when the backends differ. With `walk_up=True` (on every
|
|
751
|
+
interpreter) a path beside `other` gets a `..` per level climbed; a `..` in
|
|
752
|
+
`other` cannot be walked (`ValueError`).
|
|
753
|
+
- Not implemented (`NotImplementedError`): `rename()` (`move()` copies),
|
|
754
|
+
`chmod()`, `symlink_to()`.
|
|
755
|
+
- A `str` destination to `copy()`/`move()` stays on the same backend.
|
|
756
|
+
|
|
757
|
+
### `MemPathBackend`
|
|
758
|
+
|
|
759
|
+
```python
|
|
760
|
+
class MemPathBackend(dict): ...
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
Storage: `dict` value = directory,
|
|
764
|
+
`bytearray` (`MemFile`, carrying `mtime`, which `copy`, `deepcopy` and
|
|
765
|
+
`pickle` keep on every version) = file. Pass one instance as `backend=` to
|
|
766
|
+
share a tree; each root `MemPath()` otherwise gets its own.
|
|
767
|
+
|
|
768
|
+
## FTP (`pathapi.ftp`)
|
|
769
|
+
|
|
770
|
+
Standard library only (`ftplib`, `ssl`); needs no extra and no URI.
|
|
771
|
+
`pathapi.FTPPath` is the same class, imported on first use.
|
|
772
|
+
|
|
773
|
+
### `FTPPath`
|
|
774
|
+
|
|
775
|
+
```python
|
|
776
|
+
class FTPPath(Path):
|
|
777
|
+
backend
|
|
778
|
+
def __init__(
|
|
779
|
+
self, *segments, backend=None, host=None, port=21, user=None,
|
|
780
|
+
password=None, tls=False, timeout=30.0, ssl_context=None, verify=True,
|
|
781
|
+
) -> None: ...
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
A `Path` for a directory tree on an FTP server. Nothing connects when a path
|
|
785
|
+
is built.
|
|
786
|
+
|
|
787
|
+
- **Construction.** `FTPPath("/pub/x", host="h", user="u", tls=True)` builds
|
|
788
|
+
the path's own `FTPBackend` from the endpoint keywords (the same as
|
|
789
|
+
`FTPBackend`'s). `backend=` instead shares one connection cache between
|
|
790
|
+
paths built separately, or plugs in a `BaseFTPBackend` of your own. Giving
|
|
791
|
+
`backend=` together with any endpoint keyword is a `TypeError`, and so is
|
|
792
|
+
giving neither (unless a segment is an `FTPPath`, whose backend is kept).
|
|
793
|
+
Segments are `str`, `FTPPath` or a pure path; an absolute segment restarts
|
|
794
|
+
the join, empty and `.` segments collapse. Every path derived from one
|
|
795
|
+
(`/`, `joinpath`, `parent`, `with_name`, `iterdir`, `relative_to`) keeps its
|
|
796
|
+
backend.
|
|
797
|
+
- **Names.** The tree has no working directory: `pub/x` and `/pub/x` name one
|
|
798
|
+
file, read from the server root, and `/` is sent for the root (never an
|
|
799
|
+
empty argument, which a server reads as its working directory). `..` is
|
|
800
|
+
sent as written.
|
|
801
|
+
- **Text forms.** `str()` is the path alone. `repr()` names the endpoint
|
|
802
|
+
(`FTPPath('/pub/x', host='h', port=21)`, plus `user=` and `tls=True` when
|
|
803
|
+
set) and never a password. `as_uri()` is `ftp://[user@]host[:port]/path`
|
|
804
|
+
(`ftps:` with `tls`), without a password; `NotImplementedError` on a
|
|
805
|
+
backend that is not an `FTPBackend`.
|
|
806
|
+
- **Identity.** `==` compares the names only (as `MemPath` does); two paths are
|
|
807
|
+
one tree (`copy()`, `move()` and `PathSyncer` ask) when they share a backend
|
|
808
|
+
object or their backends have equal `endpoint`s. `_node_key()` follows the
|
|
809
|
+
same rule and applies `..` to the text. A pickle holds the text and the
|
|
810
|
+
backend, which carries no connection and a password only if it was given
|
|
811
|
+
one; `copy.copy` and `copy.deepcopy` share the backend.
|
|
812
|
+
- **Behavior.** Everything under "What a path does" below.
|
|
813
|
+
|
|
814
|
+
### `FTPBackend`, `BaseFTPBackend`
|
|
815
|
+
|
|
816
|
+
```python
|
|
817
|
+
DEFAULT_PORT = 21
|
|
818
|
+
DEFAULT_TIMEOUT = 30.0
|
|
819
|
+
IDLE_PROBE_SECONDS = 1.0
|
|
820
|
+
|
|
821
|
+
class FTPEndpoint(NamedTuple):
|
|
822
|
+
host: str
|
|
823
|
+
port: int
|
|
824
|
+
user: str | None
|
|
825
|
+
tls: bool
|
|
826
|
+
|
|
827
|
+
class BaseFTPBackend:
|
|
828
|
+
endpoint
|
|
829
|
+
def client(self) -> ftplib.FTP: ...
|
|
830
|
+
def close(self) -> None: ...
|
|
831
|
+
def __enter__(self) -> Self: ...
|
|
832
|
+
def __exit__(self, *exc_info) -> None: ...
|
|
833
|
+
|
|
834
|
+
class FTPBackend(BaseFTPBackend):
|
|
835
|
+
host
|
|
836
|
+
port
|
|
837
|
+
user
|
|
838
|
+
password
|
|
839
|
+
tls
|
|
840
|
+
timeout
|
|
841
|
+
ssl_context
|
|
842
|
+
verify
|
|
843
|
+
def __init__(
|
|
844
|
+
self, host, port=21, *, user=None, password=None, tls=False,
|
|
845
|
+
timeout=30.0, ssl_context=None, verify=True,
|
|
846
|
+
) -> None: ...
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
- `FTPBackend` is the connections to one server and account. A thread opens
|
|
850
|
+
its control connection on first use and keeps it, so paths that share a
|
|
851
|
+
backend share a connection per thread; connections are cached per (backend,
|
|
852
|
+
endpoint, thread) in an LRU of 128, closed on eviction and when their thread
|
|
853
|
+
ends. A connection that carried a command within the last
|
|
854
|
+
`IDLE_PROBE_SECONDS` is used as it is; one idle for longer, or not yet used,
|
|
855
|
+
is probed with `NOOP` and replaced when dead.
|
|
856
|
+
- `port` defaults to 21; `tls=True` is explicit FTPS on that same port (port
|
|
857
|
+
990 is never assumed) with `PROT P`. The certificate and host name are
|
|
858
|
+
verified with `ssl.create_default_context()` before `USER`/`PASS` are sent;
|
|
859
|
+
`ssl_context` (e.g. `ssl.create_default_context(cafile=...)`) wins over
|
|
860
|
+
`verify`; `verify=False` accepts any certificate. Data connections reuse the
|
|
861
|
+
TLS session. No `user` logs in as `anonymous`. `timeout` bounds connect,
|
|
862
|
+
replies and transfers (`None` = forever).
|
|
863
|
+
- `close()` closes every control connection the backend holds, whichever
|
|
864
|
+
thread opened it; one a command is running on closes when that command
|
|
865
|
+
ends. It is harmless twice and before any use, and a later use connects
|
|
866
|
+
afresh. Leaving a `with` block calls it. Both belong to `BaseFTPBackend`,
|
|
867
|
+
so a backend of your own is closed the same way. A `ValueError` for an
|
|
868
|
+
empty `host`.
|
|
869
|
+
- `BaseFTPBackend.client()` is what to override to supply connections: a new
|
|
870
|
+
connected, logged-in `ftplib.FTP`. `endpoint`, when set, is a hashable value
|
|
871
|
+
naming the server and account (`FTPBackend`'s is an `FTPEndpoint`, which holds
|
|
872
|
+
no password); without one only the same backend object is the same tree.
|
|
873
|
+
|
|
874
|
+
### What a path does
|
|
875
|
+
|
|
876
|
+
- A listing uses `MLSD` (UTC `modify`; mode from `unix.mode` or the `perm`
|
|
877
|
+
fact); `stat()` uses `MLST` of the entry itself where the server's `FEAT`
|
|
878
|
+
lists it (one reply on the control connection, `FEAT` asked once per
|
|
879
|
+
connection), else the parent's `MLSD`; a server without `MLSD` falls back
|
|
880
|
+
to `NLST` for a listing and `SIZE`/`CWD` for a stat. A `size` fact that is
|
|
881
|
+
not a plain non-negative number is unknown (`st_size` 0). A listing is
|
|
882
|
+
read whole inside the guarded call: a transient `4xx` reply is
|
|
883
|
+
`OSError(EAGAIN)`, a name the client cannot decode is `OSError(EILSEQ)`
|
|
884
|
+
for that directory only, and a listing that dies mid-transfer drops the
|
|
885
|
+
connection, so the next request reconnects. `stat()` of another entry in
|
|
886
|
+
a directory that holds an undecodable name is answered by `MLST`, or on a
|
|
887
|
+
server with `MLSD` and no `MLST` by `SIZE`/`CWD` (after a reconnect).
|
|
888
|
+
- Reads download the whole file into memory; `"w"`/`"x"`/`"a"` (`APPE`)/
|
|
889
|
+
`"r+"` buffer in memory and upload on close (`"x"` checks then writes). A
|
|
890
|
+
`with` block that raised uploads nothing: the file is left as it was, or not
|
|
891
|
+
created.
|
|
892
|
+
- `_utime()` sends `MFMT` (the modification time, UTC, whole seconds; the
|
|
893
|
+
access time cannot be set) when the server's `FEAT` lists it (asked once
|
|
894
|
+
per connection), else raises `NotImplementedError` without sending it.
|
|
895
|
+
- `rename()` on the same server (a `str` target is a sibling name; a path of
|
|
896
|
+
another server is `NotImplementedError`, so `move()` copies). Whether an
|
|
897
|
+
existing target is replaced is the server's: a POSIX one replaces it, a
|
|
898
|
+
Windows one answers `550 File exists`, which is `FileExistsError` when the
|
|
899
|
+
target is there (a reply that names a permission problem stays
|
|
900
|
+
`PermissionError`). `chmod()` via `SITE CHMOD` (`NotImplementedError`,
|
|
901
|
+
chained to the server's reply, when the server lacks it or refuses it, or
|
|
902
|
+
`follow_symlinks=False`).
|
|
903
|
+
- Errors are the `OSError` family: a refused login is `PermissionError`, a
|
|
904
|
+
connection that cannot be made or drops is `ConnectionError`.
|
|
905
|
+
|
|
906
|
+
## Archives (`pathapi.archive`)
|
|
907
|
+
|
|
908
|
+
Standard library only (`zipfile`, `tarfile`); needs no extra and no URI.
|
|
909
|
+
`pathapi.ArchivePath`, `ZipPath` and `TarPath` are the same classes,
|
|
910
|
+
imported on first use.
|
|
911
|
+
|
|
912
|
+
### `ArchivePath`, `ZipPath`, `TarPath`
|
|
913
|
+
|
|
914
|
+
```python
|
|
915
|
+
MEMBER_SPOOL_BYTES = 16777216
|
|
916
|
+
MAX_LINKS = 40
|
|
917
|
+
|
|
918
|
+
class ArchivePath(Path):
|
|
919
|
+
archive
|
|
920
|
+
backend
|
|
921
|
+
def __init__(self, *segments, archive=None) -> None: ...
|
|
922
|
+
@classmethod
|
|
923
|
+
def backend_for(cls, archive, *, shared=True, identity=None): ...
|
|
924
|
+
def refresh(self) -> None: ...
|
|
925
|
+
def batch(self) -> ContextManager[None]: ...
|
|
926
|
+
def readlink(self) -> LinkTarget: ...
|
|
927
|
+
def _rm_tree(self, on_error, follows=False) -> bool: ...
|
|
928
|
+
class ZipPath(ArchivePath): ...
|
|
929
|
+
class TarPath(ArchivePath): ...
|
|
930
|
+
class LinkTarget(PurePosixPath):
|
|
931
|
+
path: str
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
The members of an archive file. The segments are the member path; `archive=`
|
|
935
|
+
is the archive file as a `Path`, or a `str` or other `os.PathLike` (a
|
|
936
|
+
`LocalPath`). `ZipPath` and `TarPath` fix the format, `ArchivePath` reads it
|
|
937
|
+
from the extension (`.zip`/`.jar` vs `.tar`/`.tgz`/`.tar.*`), else from the
|
|
938
|
+
first bytes (a `PK` magic sniff) on first use.
|
|
939
|
+
|
|
940
|
+
- **Construction.** `ZipPath("d/x", archive="a.zip")`. A path with no
|
|
941
|
+
`archive=` takes the one of a segment that is a path of the same family
|
|
942
|
+
(`ZipPath(root, "x")`, `/`, `joinpath`, `parent`, `with_name`, `iterdir`,
|
|
943
|
+
`relative_to` all keep it); with neither it is a `TypeError`, as it is for
|
|
944
|
+
an `archive=` that is not a path, a `str` or an `os.PathLike`. `archive=`
|
|
945
|
+
may also be the backend of another path of the archive: `backend_for(
|
|
946
|
+
archive, *, shared=True, identity=None)` returns the backend to pass, the
|
|
947
|
+
one shared by every path of that file and format, or with `shared=False`
|
|
948
|
+
one of its own; `identity` names a non-local archive file in the registry
|
|
949
|
+
(else its `_node_key()`). It reads nothing.
|
|
950
|
+
- **Names.** Member names are read from the archive root: `d/x` and `/d/x`
|
|
951
|
+
are one member, and the empty path or `/` is the archive itself.
|
|
952
|
+
- **Text forms.** `str()` is the member path alone. `repr()` names the
|
|
953
|
+
archive (`ZipPath('a/b', archive=LocalPath('x.zip'))`) without reading it.
|
|
954
|
+
`as_uri()` is `zip:<archive uri>!/<member>` (`tar:`, `archive:`), with a
|
|
955
|
+
literal `!/` in a member name written `%21/`.
|
|
956
|
+
- **Identity.** `==` compares the names only; two paths are one tree
|
|
957
|
+
(`copy()`, `move()` and `PathSyncer` ask) when they share a handle, and
|
|
958
|
+
`_node_key()` is that handle and the normalized member. A pickle holds the
|
|
959
|
+
text and the archive file; `copy.copy` and `copy.deepcopy` share the handle.
|
|
960
|
+
- `archive` is the archive file, `backend` the shared handle (it settles the
|
|
961
|
+
format of an `ArchivePath` on the first call), `refresh()` is described
|
|
962
|
+
below, `batch()` and `readlink()` under Writes and Links. `_rm_tree(
|
|
963
|
+
on_error, follows=False)` is the one-rewrite removal `rm(recursive=True)`
|
|
964
|
+
uses, for a wrapper that keeps the generic walk for the rest: it returns
|
|
965
|
+
False when the case is not that one, and asks `on_error(error, self)` about
|
|
966
|
+
a failure.
|
|
967
|
+
- `ArchivePath` settles the format on first use, not when the path is built:
|
|
968
|
+
constructing, printing, joining, copying and pickling a path read nothing,
|
|
969
|
+
a non-local archive file that had to be read to decide is not fetched a second
|
|
970
|
+
time, and a failure to read the outer propagates and decides nothing, so
|
|
971
|
+
the next call tries again (an `ArchivePath` to an archive file that is missing
|
|
972
|
+
and has no extension raises `FileNotFoundError` on a write, as it does on
|
|
973
|
+
a read).
|
|
974
|
+
- One shared handle per archive file and format (keyed by the real local
|
|
975
|
+
path, or by the node -- else the URI -- of a non-local archive file),
|
|
976
|
+
released when no path references it, so paths built separately for one
|
|
977
|
+
file see each other's writes. A local zip or tar is read from its file as
|
|
978
|
+
needed (a compressed tar is decompressed from its start for each member it
|
|
979
|
+
reaches) and no OS handle is held between calls; a non-local archive file is
|
|
980
|
+
read whole into memory, once, and kept while any path to it lives.
|
|
981
|
+
`refresh()` forgets what is held of it, so the next use reads it again
|
|
982
|
+
(every path to the same archive shares it); it is how a change at a remote
|
|
983
|
+
source is seen, and a local archive needs none (it is checked against the
|
|
984
|
+
file on every use).
|
|
985
|
+
- Cost. The members, the directories and each entry's stat data are derived
|
|
986
|
+
once per open handle, so a listing, a `walk()` or a `stat()` costs one
|
|
987
|
+
validation of the file and work in the size of the directory, not of the
|
|
988
|
+
archive. A member read is copied out of the shared handle: in memory up to
|
|
989
|
+
`MEMBER_SPOOL_BYTES` (16 MiB) and past that into an
|
|
990
|
+
unnamed temporary file, so memory stays small however large the member is,
|
|
991
|
+
and the disk used is the member's size as the archive declares it (an
|
|
992
|
+
archive can declare more than it holds). `open("r+")` and a write buffer
|
|
993
|
+
hold the whole member in memory. A zip mutation copies the entries it does
|
|
994
|
+
not change as they lie in the archive, without decompressing them, so it
|
|
995
|
+
costs the archive's stored size; an encrypted member, or one stored with a
|
|
996
|
+
compression method `zipfile` lacks, does not stop a change to another
|
|
997
|
+
member.
|
|
998
|
+
- **Member names are normalized POSIX relative paths**, whatever the
|
|
999
|
+
writer emitted and whichever format: a leading `./` (`tar -C dir .`,
|
|
1000
|
+
`shutil.make_archive`), empty segments (`a//b`) and interior `.`/`..`
|
|
1001
|
+
(`a/./b`, `a/b/../c`) resolve, so one member has one name and a listing
|
|
1002
|
+
and a lookup always agree. The spelling as written still addresses the
|
|
1003
|
+
member. A name that would leave the root -- `../x`, `/abs`, or a `..`
|
|
1004
|
+
with nothing to spend it on -- has no name inside the archive: it is
|
|
1005
|
+
never listed and never readable. A name only a *Windows destination* would
|
|
1006
|
+
misread (`C:drive.txt`, `a\b`) IS a member, because it is an ordinary POSIX
|
|
1007
|
+
filename; refusing to join it is the destination's rule, applied by
|
|
1008
|
+
whatever writes there (see `copy()` below, `PathSyncer`,
|
|
1009
|
+
`unpack_archive()`). On Windows `zipfile` rewrites `\` to `/` in a zip
|
|
1010
|
+
member name; elsewhere (and in a tar) a backslash name is a member like any
|
|
1011
|
+
other. When two spellings normalize to one name the
|
|
1012
|
+
later member wins, as in `zipfile`/`tarfile`. Exception types are POSIX on every
|
|
1013
|
+
platform.
|
|
1014
|
+
- The archive root is the archive: `exists()`/`is_dir()`/`stat()` of it
|
|
1015
|
+
open the archive, so a missing one does not exist (`FileNotFoundError`
|
|
1016
|
+
from `stat()`) and a file that is not an archive raises
|
|
1017
|
+
`zipfile.BadZipFile` / `tarfile.ReadError`, as a listing does. So does any
|
|
1018
|
+
other damage that stops the archive from being read -- a bad deflate
|
|
1019
|
+
stream, a short or inconsistent header, a zip version `zipfile` does not
|
|
1020
|
+
know, a truncated or corrupt gzip/bzip2/xz stream -- whichever decoder
|
|
1021
|
+
noticed it, and never as `zlib.error`, `struct.error`, `EOFError` or an
|
|
1022
|
+
`OSError` without an errno. A member that is encrypted raises
|
|
1023
|
+
`RuntimeError`, and one stored with a compression method `zipfile` lacks
|
|
1024
|
+
`NotImplementedError`, as `zipfile` raises them. A member another thread's
|
|
1025
|
+
rewrite removed between the lookup and its stat is `FileNotFoundError`.
|
|
1026
|
+
- **Links.** A tar `SYMTYPE` member and a zip entry made on Unix whose mode
|
|
1027
|
+
says link (`S_IFLNK`; its data is the text) are symbolic links.
|
|
1028
|
+
`is_symlink()` is True and `stat(follow_symlinks=False)` / `lstat()`, in a
|
|
1029
|
+
listing too, is a link (`S_IFLNK`, `st_size` the length of the text).
|
|
1030
|
+
`readlink()` is the text as a `LinkTarget`: a `PurePosixPath`, so spelled
|
|
1031
|
+
normalized, whose `path` is the text exactly as stored (what `copy()` and
|
|
1032
|
+
`PathSyncer` recreate a link from); `OSError(EINVAL)` for a member that is
|
|
1033
|
+
not a link, `OSError(EIO)` when the text cannot be read (a zip link over
|
|
1034
|
+
4096 bytes, or damaged data). `stat()`, `exists()`, `is_dir()`, `is_file()`,
|
|
1035
|
+
`open()` and `iterdir()` follow links inside the archive, on the way
|
|
1036
|
+
(`ln/x` is the member `x` of what `ln` points at) and at the end; a relative
|
|
1037
|
+
text is read from the directory that holds the link. A link that is
|
|
1038
|
+
absolute, goes above the root, has no readable text or points at nothing
|
|
1039
|
+
resolves to nothing: `FileNotFoundError` whose `filename` names the link,
|
|
1040
|
+
never a path outside the archive. A loop, or more than `MAX_LINKS` (40)
|
|
1041
|
+
links followed by one lookup, is `OSError(ELOOP)`. `walk()` and `glob()`
|
|
1042
|
+
list a link once and by default do not enter it; `rm(recursive=True)`
|
|
1043
|
+
removes the entry, never what it points at; `copy(follow_symlinks=False)`
|
|
1044
|
+
and a `PathSyncer(follow_symlinks=False)` recreate it with `symlink_to()`
|
|
1045
|
+
where the target can hold one. With `follow_symlinks=True` (`copy()`'s
|
|
1046
|
+
default) a listing follows one directory link per path and never a link to
|
|
1047
|
+
the directory that holds it or to one above it: such a link, and one met
|
|
1048
|
+
below another link, answers `OSError(ELOOP)` to a following `stat()`,
|
|
1049
|
+
`is_dir()` or read, so `walk()` and `glob()` list it as an entry and
|
|
1050
|
+
`copy()`, `PathSyncer` and `rm()` meet it as one that cannot be entered
|
|
1051
|
+
(an error for that entry offered to `ignore_error`, for `rm()` a link
|
|
1052
|
+
removed as a link). The entries a recursive operation visits are at most
|
|
1053
|
+
`(members + links) * (directory links + 1)` however the links point; spell
|
|
1054
|
+
the path (`root / "l1" / "l2" / "f"`, or start the walk there) to go
|
|
1055
|
+
further. Nothing is written
|
|
1056
|
+
through a link (`NotImplementedError`: a write, `mkdir()`, `unlink()` or
|
|
1057
|
+
`rename()` whose path passes through one, or a write that names one);
|
|
1058
|
+
`unlink()`, `rename()` and `rm()` of the link itself act on its entry, and
|
|
1059
|
+
no link can be created in an archive. A tar hard link is a second name for
|
|
1060
|
+
a member, not a symlink: it reads and stats as that member and
|
|
1061
|
+
`is_symlink()` is False.
|
|
1062
|
+
- A file has no children: with a file `y` and a member `y/z`, `y/z` does
|
|
1063
|
+
not exist for `exists()`, `stat()`, `open()`, `unlink()` and `rename()`
|
|
1064
|
+
(`FileNotFoundError`; writing or `mkdir()` below `y` is
|
|
1065
|
+
`NotADirectoryError`), and no listing shows it.
|
|
1066
|
+
- Writes: zip only, and only with a local archive file (else
|
|
1067
|
+
`NotImplementedError`). `"w"`/`"x"`/`"r+"`, `mkdir()`, `unlink()`,
|
|
1068
|
+
`rmdir()`, `rename()` (same archive; replaces like POSIX `rename`;
|
|
1069
|
+
a destination below a file or a missing directory is
|
|
1070
|
+
`NotADirectoryError` / `FileNotFoundError`, a directory into itself
|
|
1071
|
+
`OSError(EINVAL)`); parents must exist; `"a"` unsupported. A `with` block
|
|
1072
|
+
over a written member that raised changes nothing (inside `batch()` the
|
|
1073
|
+
member is not staged and the block's other changes still commit). Every mutation
|
|
1074
|
+
replaces the archive atomically (temp file + `os.replace`) and keeps
|
|
1075
|
+
other members' metadata, the comment and any prefix bytes. A write uses
|
|
1076
|
+
the normalized name. A new member is stored deflated; an existing member that is
|
|
1077
|
+
overwritten keeps its compression method. The archive is replaced by a
|
|
1078
|
+
new file, so another hard link to it keeps the old content and the
|
|
1079
|
+
writing user owns the result; an archive the caller may not write
|
|
1080
|
+
(`os.access(..., W_OK)`) is refused with `PermissionError`, on every
|
|
1081
|
+
platform, before anything is written.
|
|
1082
|
+
- `batch()` — for a writable archive (`NotImplementedError` otherwise, as
|
|
1083
|
+
for any write): inside the block every change (`"w"`/`"x"`/`"r+"`,
|
|
1084
|
+
`mkdir()`, `unlink()`, `rmdir()`, `rename()`, `rm()`) goes to a member
|
|
1085
|
+
list held in memory (written data up to `MEMBER_SPOOL_BYTES`, then an
|
|
1086
|
+
unnamed temporary file), every path of the archive reads it, and the
|
|
1087
|
+
file is not touched. When the outermost block ends without an exception
|
|
1088
|
+
the archive is replaced ONCE, with the members, order and metadata that
|
|
1089
|
+
the same changes made one by one would leave; an archive that does not
|
|
1090
|
+
exist yet is created then. An exception leaves the file byte for byte as
|
|
1091
|
+
it was and drops the changes, and so does an archive another process
|
|
1092
|
+
replaced meanwhile (`OSError(EBUSY)`; their file stays). A block inside
|
|
1093
|
+
another joins it and commits or drops nothing itself. The block holds the
|
|
1094
|
+
archive for its thread: another thread's use of the archive waits for it.
|
|
1095
|
+
A change patches the pending member list, so the list is derived twice
|
|
1096
|
+
however many changes the block makes (once for the archive as the block
|
|
1097
|
+
finds it, once when it first holds a change); only a change to a name
|
|
1098
|
+
the archive spells two ways derives it again.
|
|
1099
|
+
`copy(recursive=True)` into a zip and `PathSyncer.sync()` onto one enter
|
|
1100
|
+
it themselves, so each is one replacement; a copy or sync that raises
|
|
1101
|
+
leaves the archive as it was, where the members written before the error
|
|
1102
|
+
used to stay.
|
|
1103
|
+
- Removing a file never removes its directory: when `unlink()`, `rename()`
|
|
1104
|
+
or `rm()` takes the last member out of a directory that exists only
|
|
1105
|
+
through its members, the directory's own entry (`name/`) is written in
|
|
1106
|
+
the same rewrite. `rmdir()` of the root succeeds on an empty archive
|
|
1107
|
+
(`OSError(ENOTEMPTY)` otherwise) and removes nothing.
|
|
1108
|
+
`rm(recursive=True)` of a directory is ONE rewrite (unless a policy other
|
|
1109
|
+
than `follow_symlinks=False` has to meet each link under it), and raises
|
|
1110
|
+
`OSError(ENOTEMPTY)` removing nothing when the tree holds a member under
|
|
1111
|
+
a file.
|
|
1112
|
+
- A member the archive spells several ways (`norm` and `./norm`) is one
|
|
1113
|
+
member: `unlink()`, `rename()`, `rmdir()` and `rm()` act on every entry
|
|
1114
|
+
that names it, and renaming over it replaces them all.
|
|
1115
|
+
- `TarPath` (plain, gz, bz2, xz) is read-only.
|
|
1116
|
+
|
|
1117
|
+
### `make_archive`, `unpack_archive` (`pathapi.archive`)
|
|
1118
|
+
|
|
1119
|
+
```python
|
|
1120
|
+
def make_archive(src, format, target) -> None: ...
|
|
1121
|
+
def unpack_archive(archive, dest) -> None: ...
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
`make_archive()` (`format` `"zip"` or
|
|
1125
|
+
`"tar"`, else `ValueError`; `src` file or directory of any `Path`; built in
|
|
1126
|
+
a temporary buffer, written to `target` only when complete; zip64 always;
|
|
1127
|
+
every directory is a member, empty ones too; a member carries the source's
|
|
1128
|
+
modification time and, in a tar, its permission bits — a source that
|
|
1129
|
+
reports no time gets 1980-01-01 (zip) or the epoch (tar), and one that
|
|
1130
|
+
reports no mode gets 0o644 for a file and 0o755 for a directory; a tar
|
|
1131
|
+
member is sized from the bytes read, not from `stat()`)
|
|
1132
|
+
and `unpack_archive()` (format from the name, else magic
|
|
1133
|
+
bytes; creates `dest`; non-seekable streams are buffered; members that
|
|
1134
|
+
would leave `dest` are skipped, and for a Windows-flavoured `dest` so are
|
|
1135
|
+
members with a part Windows would rewrite or send to a device
|
|
1136
|
+
(`UserWarning`); tar hard links and links to regular files are extracted as
|
|
1137
|
+
copies, other links — to nothing, to a directory, or in a loop — skipped
|
|
1138
|
+
with `UserWarning`).
|
|
1139
|
+
|
|
1140
|
+
## HTTP requests (`pathapi.http`)
|
|
1141
|
+
|
|
1142
|
+
Standard library only (`http.client`, `ssl`); needs no extra and no URI.
|
|
1143
|
+
`import pathapi.http` loads `http.client` and `ssl`; `import pathapi`
|
|
1144
|
+
does not. The package sends a request through a **transport**, follows
|
|
1145
|
+
redirects under one rule for credentials, and maps a failed request to the
|
|
1146
|
+
`OSError` types `pathlib` raises. Path classes for HTTP servers in other
|
|
1147
|
+
distributions are built on it.
|
|
1148
|
+
|
|
1149
|
+
```python
|
|
1150
|
+
class HTTPClient:
|
|
1151
|
+
headers: dict
|
|
1152
|
+
timeout: float | tuple | None
|
|
1153
|
+
user_agent: str
|
|
1154
|
+
def __init__(
|
|
1155
|
+
self, transport=None, *, headers=None, timeout=(10, 60), user_agent=None,
|
|
1156
|
+
): ...
|
|
1157
|
+
transport
|
|
1158
|
+
def request(
|
|
1159
|
+
self, method, url, *, data=None, headers=None, stream=False,
|
|
1160
|
+
allow_redirects=None, timeout=..., auth=None,
|
|
1161
|
+
): ...
|
|
1162
|
+
def redirect(
|
|
1163
|
+
self, method, url, resp, *, headers=None, stream=False, timeout=...,
|
|
1164
|
+
auth=None,
|
|
1165
|
+
): ...
|
|
1166
|
+
def close(self): ...
|
|
1167
|
+
|
|
1168
|
+
DEFAULT_TIMEOUT = (10, 60)
|
|
1169
|
+
MAX_REPLY_BYTES = 8388608
|
|
1170
|
+
```
|
|
1171
|
+
|
|
1172
|
+
- **`HTTPClient(transport=None, *, headers=None, timeout=(10, 60),
|
|
1173
|
+
user_agent=None)`** holds a transport and what every request is sent with.
|
|
1174
|
+
`transport` is any object with `send()` (the contract below); without one the
|
|
1175
|
+
client builds the transport that `PATHAPI_HTTP_TRANSPORT` names (the
|
|
1176
|
+
built-in `HTTPTransport` by default) on first use, and `close()` or leaving
|
|
1177
|
+
its `with` block closes a transport it built, never one it was given. A
|
|
1178
|
+
transport without a `send()` method is `TypeError`. `headers` are sent with
|
|
1179
|
+
every request, the call's headers merged over them by name (case-blind, a
|
|
1180
|
+
value of `None` removes the name) over `User-Agent: <user_agent>` (default
|
|
1181
|
+
`pathapi/<version>`) and `Accept: */*`. Every request gets
|
|
1182
|
+
`timeout=(10, 60)` seconds (`connect`, `read`) unless the client or the call
|
|
1183
|
+
gives one; `timeout=None` waits without limit. `repr()` shows no header.
|
|
1184
|
+
- **`request(method, url, *, data=None, headers=None, stream=False,
|
|
1185
|
+
allow_redirects=None, timeout, auth=None)`** returns the final
|
|
1186
|
+
`HTTPResponse`. `data` is `bytes`, `str` (UTF-8), a binary file or an
|
|
1187
|
+
iterable of `bytes`; one that is not `bytes` cannot be sent twice, so it is
|
|
1188
|
+
not sent again after a redirect or a dropped connection. `auth` is a
|
|
1189
|
+
`(user, password)` pair sent as `Authorization: Basic`, or a `str` sent as the
|
|
1190
|
+
`Authorization` value (`"Bearer <token>"`), to the origin of `url` unless a
|
|
1191
|
+
header names an `Authorization`. A `url` that carries credentials,
|
|
1192
|
+
a URL or header value with a control character (CR and LF included) and
|
|
1193
|
+
another scheme than `http:` or `https:` are `ValueError`, before anything is
|
|
1194
|
+
sent. A failure of the connection is `TimeoutError`, `ConnectionError`,
|
|
1195
|
+
`ssl.SSLError` or `OSError(EIO)` as the transport raises them. A status of
|
|
1196
|
+
400 or more is **not** an error here: it is the reply (`raise_for_status()`).
|
|
1197
|
+
With `stream=False` the body has been read when `request()` returns, and one
|
|
1198
|
+
that decodes to more than `MAX_REPLY_BYTES` is `OSError(EFBIG)`; with
|
|
1199
|
+
`stream=True` the caller reads the body and closes the response.
|
|
1200
|
+
- **Redirects.** `GET`, `HEAD` and `OPTIONS` follow up to 30 redirects (a `303`
|
|
1201
|
+
becomes a `GET`); `allow_redirects=False` returns the first answer. A redirect
|
|
1202
|
+
whose `Location` is missing or that cannot be requested (another scheme, a
|
|
1203
|
+
port out of range, a host that is not a name) is `OSError(EIO)` naming the
|
|
1204
|
+
status, never `ValueError`. A redirect that changes the scheme, host or port
|
|
1205
|
+
drops `Authorization` for that hop and every later one (a return to the first
|
|
1206
|
+
origin does not bring it back), also from `https:` to `http:`, and nothing
|
|
1207
|
+
else: every header other than `Authorization` goes to the new host as well,
|
|
1208
|
+
so keep a secret in `Authorization`. Every other method (`PUT`, `POST`,
|
|
1209
|
+
`PATCH`, `DELETE`, `PROPFIND`, ...) is sent without following: a 307 or 308
|
|
1210
|
+
(for `PROPFIND` also a 301, 302 or 303) to the same scheme, host and port is
|
|
1211
|
+
sent once more with the same method, headers and body, and any other 3xx, a
|
|
1212
|
+
redirect to another origin or a second redirect is `OSError(EIO)` naming the
|
|
1213
|
+
status and the `Location` (userinfo, query and fragment removed). A body
|
|
1214
|
+
that is not `bytes` cannot be sent again, so it raises too.
|
|
1215
|
+
- **`redirect(method, url, resp, *, headers=None, stream=False, timeout,
|
|
1216
|
+
auth=None)`** sends `method` to where `resp`, the answer to a request for
|
|
1217
|
+
`url` made with `allow_redirects=False`, redirects to, and follows what that
|
|
1218
|
+
redirects to in turn. `Authorization` is left off whenever the target is
|
|
1219
|
+
another scheme, host or port than `url`'s. No usable `Location` is
|
|
1220
|
+
`OSError(EIO)`.
|
|
1221
|
+
|
|
1222
|
+
```python
|
|
1223
|
+
class HTTPTransport:
|
|
1224
|
+
def __init__(self, *, ssl_context=None, verify=True, proxies=None): ...
|
|
1225
|
+
def send(
|
|
1226
|
+
self, method, url, *, headers, body=None, timeout, stream=False,
|
|
1227
|
+
): ...
|
|
1228
|
+
def close(self): ...
|
|
1229
|
+
|
|
1230
|
+
class HTTPResponse:
|
|
1231
|
+
status: int
|
|
1232
|
+
reason: str
|
|
1233
|
+
headers: Headers
|
|
1234
|
+
url: str
|
|
1235
|
+
def __init__(
|
|
1236
|
+
self, status, reason, headers, url, body=b"", *, decode=True,
|
|
1237
|
+
): ...
|
|
1238
|
+
def read(self, size=-1): ...
|
|
1239
|
+
def iter_bytes(self, chunk_size=65536): ...
|
|
1240
|
+
def buffered(self): ...
|
|
1241
|
+
def close(self): ...
|
|
1242
|
+
|
|
1243
|
+
class Headers:
|
|
1244
|
+
def get_all(self, name): ...
|
|
1245
|
+
```
|
|
1246
|
+
|
|
1247
|
+
- **The transport contract.** Any object with `send(method, url, *, headers,
|
|
1248
|
+
body=None, timeout, stream=False) -> HTTPResponse` and `close()` is a
|
|
1249
|
+
transport (a context manager is optional). `send()` makes exactly one
|
|
1250
|
+
request: it follows no redirect, adds no credentials and retries nothing
|
|
1251
|
+
but what is stated below. `body` is `None`, `bytes`, a binary file or an
|
|
1252
|
+
iterable of `bytes` (never `str`: `TypeError`). A body is sent under a
|
|
1253
|
+
`Content-Length` when its length is known without reading it: `bytes`, a
|
|
1254
|
+
list or tuple of `bytes`, a file that is seekable (the bytes from `tell()`
|
|
1255
|
+
to its end, the position restored) and a regular file (its size less
|
|
1256
|
+
`tell()`). Any other body (an iterator, a generator, a pipe or another
|
|
1257
|
+
stream that cannot report its size) is sent `Transfer-Encoding: chunked`. A
|
|
1258
|
+
`Content-Length` in `headers` is trusted: that many bytes are sent and
|
|
1259
|
+
nothing is chunked, a longer body is cut there, a count that is not a number
|
|
1260
|
+
or is larger than a body whose length is known is `ValueError` before
|
|
1261
|
+
anything is sent, and a body that runs out first is `OSError(EIO)` with its
|
|
1262
|
+
connection closed. No body is sent past its length (a file that grew after it
|
|
1263
|
+
was measured is cut there). A file or iterator is never rewound and never
|
|
1264
|
+
sent twice. `timeout` is seconds, a `(connect, read)` pair or `None` (no
|
|
1265
|
+
limit); the read half bounds each wait on the server, not the whole reply.
|
|
1266
|
+
With `stream=False` the body has been read when `send()` returns; with
|
|
1267
|
+
`stream=True` it is read from the response, which the caller closes (an
|
|
1268
|
+
unread rest of a body closes its connection). A transport raises only
|
|
1269
|
+
`TimeoutError`, `ConnectionError`, `ssl.SSLError`, `ValueError` (before
|
|
1270
|
+
anything is sent, for a header or URL with a control character, a URL with
|
|
1271
|
+
credentials, or another scheme) or else `OSError(EIO)`; no third-party
|
|
1272
|
+
exception and no URL or header value is in its text. A reply below 200 is
|
|
1273
|
+
never the one `send()` returns: `HTTPTransport` skips interim (1xx) replies,
|
|
1274
|
+
at most 8, and past that is `OSError(EIO)` with its connection closed.
|
|
1275
|
+
- **`HTTPResponse`**: `status`, `reason`, `headers` (read-only, names compare
|
|
1276
|
+
without case, a name sent twice reads as `"a, b"` and `headers.get_all()`
|
|
1277
|
+
lists them; `repr()` shows names only), `url` (the URL requested, without
|
|
1278
|
+
fragment; an empty path is `/`), `read(size=-1)`, `iter_bytes(chunk_size)`,
|
|
1279
|
+
`buffered()` (reads the rest of the body into memory, closes the connection
|
|
1280
|
+
and returns the response, which stays readable: what `send(stream=False)`
|
|
1281
|
+
returns), `close()` and a context manager. A `gzip` or `deflate` body is
|
|
1282
|
+
decoded, in steps of at most 256 KiB of output per step, unless the request
|
|
1283
|
+
set an `Accept-Encoding` naming a coding of its own (`identity` counts as
|
|
1284
|
+
none); `headers` stay as received, so `Content-Length` counts the encoded
|
|
1285
|
+
bytes. A body that ends before the length the server stated, a chunked body
|
|
1286
|
+
cut short, and a coded body that is not valid are `OSError(EIO)`, never a
|
|
1287
|
+
shorter read. A transport builds one from `bytes` or from an object with
|
|
1288
|
+
`read(amount)` and `close()`; a fake transport in a test does the same.
|
|
1289
|
+
- **`HTTPTransport(ssl_context=None, verify=True, proxies=None)`** is standard
|
|
1290
|
+
library only. `https:` is verified against `ssl_context`, else against
|
|
1291
|
+
`ssl.create_default_context()` (the system trust store, not certifi);
|
|
1292
|
+
`verify=False` is the one way to turn verification off (each transport built
|
|
1293
|
+
with it logs a warning under the `pathapi.http` logger), and `ssl_context`
|
|
1294
|
+
wins over it. Each thread keeps one idle connection per server (at most 8,
|
|
1295
|
+
closed when the thread ends or on `close()`), reused for its next request
|
|
1296
|
+
unless the server has closed it or sent bytes on it since (checked before
|
|
1297
|
+
anything is sent). A request that fails before any byte of a reply on a
|
|
1298
|
+
reused connection that the server had closed is sent once more on a new
|
|
1299
|
+
connection **only** if it is `GET`, `HEAD`, `OPTIONS`, `PUT`, `DELETE` or
|
|
1300
|
+
`PROPFIND` and its body is `None` or in memory; any other request uses an
|
|
1301
|
+
idle connection only if it carried a reply within the last second. A timeout
|
|
1302
|
+
is never retried. `stream=False` holds the whole body in memory; read a body
|
|
1303
|
+
that may be large with `stream=True`.
|
|
1304
|
+
- **Proxies.** `proxies=None` reads the environment (`HTTP_PROXY`,
|
|
1305
|
+
`HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY`, through `urllib.request`, which
|
|
1306
|
+
matches names and suffixes in `NO_PROXY`; a CIDR block there, such as
|
|
1307
|
+
`10.0.0.0/8` or `fd00::/8`, also sends an IP-literal host direct, and a host
|
|
1308
|
+
name is never resolved to be matched against one). Otherwise `proxies` is
|
|
1309
|
+
indexed with the request URL (no fragment): it returns `None` (direct), one
|
|
1310
|
+
proxy, or an iterable of them to try in order when a connection cannot be
|
|
1311
|
+
made (`None` in the order is a direct attempt, and an empty iterable is
|
|
1312
|
+
direct as well); `KeyError` means direct and the environment is not read;
|
|
1313
|
+
`{}` sends everything direct (unlike other clients, which still read the
|
|
1314
|
+
environment); an exception of the provider other than `KeyError` reaches the
|
|
1315
|
+
caller as it is. A proxy is a `str` URL or any object with a `url`
|
|
1316
|
+
attribute, so a proxy resolver from another library works as it is, with none
|
|
1317
|
+
imported here:
|
|
1318
|
+
|
|
1319
|
+
```
|
|
1320
|
+
class Resolver: # tries the first proxy, then connects directly
|
|
1321
|
+
def __getitem__(self, url):
|
|
1322
|
+
return ["http://user:secret@proxy.example:3128", None]
|
|
1323
|
+
|
|
1324
|
+
transport = HTTPTransport(proxies=Resolver())
|
|
1325
|
+
```
|
|
1326
|
+
|
|
1327
|
+
A plain `dict` may use the keys the `requests` library does (`"http"`,
|
|
1328
|
+
`"https"`, `"all"`, `"scheme://host"`). Only `http://` proxies work:
|
|
1329
|
+
absolute-form requests for `http:` and `CONNECT` for `https:`, with
|
|
1330
|
+
`Proxy-Authorization: Basic` from the proxy URL's userinfo (sent to the proxy
|
|
1331
|
+
only); any other scheme (`socks5`, `https`) is `OSError(EPROTONOSUPPORT)`
|
|
1332
|
+
naming the scheme and `pathapi-requests`. A proxy that refuses `CONNECT`
|
|
1333
|
+
is a `ConnectionError` and the next proxy is not tried. Proxy credentials are
|
|
1334
|
+
in no message, `repr()` or traceback.
|
|
1335
|
+
|
|
1336
|
+
### Replies and errors (`pathapi.http`)
|
|
1337
|
+
|
|
1338
|
+
```python
|
|
1339
|
+
class HTTPStatusError(Exception):
|
|
1340
|
+
response: HTTPResponse
|
|
1341
|
+
def __init__(self, response): ...
|
|
1342
|
+
|
|
1343
|
+
def raise_for_status(resp): ...
|
|
1344
|
+
def read_limited(resp, what, limit=None): ...
|
|
1345
|
+
def json_body(resp, what, limit=None): ...
|
|
1346
|
+
def drop_body(resp, limit=None): ...
|
|
1347
|
+
def translate_errors(subject, *, conflict=FileExistsError): ...
|
|
1348
|
+
def os_error(error_class, subject): ...
|
|
1349
|
+
def server_size(text): ...
|
|
1350
|
+
def peer_text(text, limit=120): ...
|
|
1351
|
+
def split_userinfo(url): ...
|
|
1352
|
+
def is_redirect(resp): ...
|
|
1353
|
+
```
|
|
1354
|
+
|
|
1355
|
+
- `raise_for_status(resp)` raises `HTTPStatusError` (holding `response`) for a
|
|
1356
|
+
status of 400 or more.
|
|
1357
|
+
- `read_limited(resp, what, limit=None)` is the whole decoded body of `resp`,
|
|
1358
|
+
which may be at most `limit` bytes (default `MAX_REPLY_BYTES`, 8 MiB after
|
|
1359
|
+
decoding, however small it is on the wire; assign
|
|
1360
|
+
`pathapi.http.MAX_REPLY_BYTES` to allow more, it is read each time):
|
|
1361
|
+
more is `OSError(EFBIG)` naming `what`, and the connection is closed.
|
|
1362
|
+
`json_body()` is that, parsed (`ValueError` when it is not JSON).
|
|
1363
|
+
- `drop_body(resp, limit=None)` gives up the body of a reply nobody looks at: it
|
|
1364
|
+
is read up to `limit` bytes so the connection can serve the next request, and
|
|
1365
|
+
`resp` is closed, which closes the connection when the body was longer.
|
|
1366
|
+
- `translate_errors(subject, *, conflict=FileExistsError)` is a context manager
|
|
1367
|
+
that maps what a request raised to what `pathlib` raises for `subject` (a path
|
|
1368
|
+
or URL, shown through `str()`): an `HTTPStatusError` of 404 or 410 becomes
|
|
1369
|
+
`FileNotFoundError`, 401 or 403 `PermissionError`, 405 or 501 `PermissionError`
|
|
1370
|
+
naming the status, 409 `conflict` (a write passes `FileNotFoundError`: RFC 4918
|
|
1371
|
+
uses 409 for a missing parent collection), any other status `OSError(EIO)`
|
|
1372
|
+
naming it (`HTTP Error 502 Bad Gateway for <subject>`). A `TimeoutError` or
|
|
1373
|
+
`ConnectionError` is raised again with a message holding `subject` and the
|
|
1374
|
+
exception's type name. The cause is never chained, so a URL, a header or a
|
|
1375
|
+
credential that the transport's error quoted is in no message or traceback.
|
|
1376
|
+
`os_error(error_class, subject)` builds one such exception as `pathlib` does,
|
|
1377
|
+
with `errno` and `filename` set.
|
|
1378
|
+
- `peer_text(text, limit=120)` is text that came from the other end (a reason
|
|
1379
|
+
phrase, a `Location`, a header value) as an error message may show it:
|
|
1380
|
+
control, line-separator and bidirectional characters escaped, cut to `limit`.
|
|
1381
|
+
`server_size(text)` is a byte count a server stated as an `int`, or `None` when
|
|
1382
|
+
it is not a plain non-negative decimal number. `split_userinfo(url)` is
|
|
1383
|
+
`(url_without_userinfo, (user, password) | None)`, each part the octets its
|
|
1384
|
+
percent-escapes stand for (`str` when ASCII, else `bytes`); `is_redirect(resp)`
|
|
1385
|
+
says whether a reply is a 301, 302, 303, 307 or 308 that names a `Location`.
|
|
1386
|
+
|
|
1387
|
+
### For transport implementers (`pathapi.http`)
|
|
1388
|
+
|
|
1389
|
+
```python
|
|
1390
|
+
TRANSPORT_GROUP = "pathapi.http_transports"
|
|
1391
|
+
TRANSPORT_ENV = "PATHAPI_HTTP_TRANSPORT"
|
|
1392
|
+
def transport_names() -> tuple[str, ...]: ...
|
|
1393
|
+
def get_transport(name: str) -> type: ...
|
|
1394
|
+
def default_transport(): ...
|
|
1395
|
+
|
|
1396
|
+
class PreparedRequest(NamedTuple):
|
|
1397
|
+
method: str
|
|
1398
|
+
url: str
|
|
1399
|
+
headers: dict[str, str]
|
|
1400
|
+
body: Any
|
|
1401
|
+
timeout: tuple[float | None, float | None]
|
|
1402
|
+
decode: bool
|
|
1403
|
+
|
|
1404
|
+
def prepare_request(
|
|
1405
|
+
method, url, *, headers, body=None, timeout,
|
|
1406
|
+
) -> PreparedRequest: ...
|
|
1407
|
+
```
|
|
1408
|
+
|
|
1409
|
+
A distribution provides an HTTP transport by declaring an entry point in the
|
|
1410
|
+
group `pathapi.http_transports`: the name is the transport's (`requests`)
|
|
1411
|
+
and the value names a class that is built with no arguments (`pyproject.toml`:
|
|
1412
|
+
`[project.entry-points."pathapi.http_transports"]`, `requests =
|
|
1413
|
+
"pathapi_requests:RequestsTransport"`). The class implements "The transport
|
|
1414
|
+
contract" above: `send()` and `close()`.
|
|
1415
|
+
|
|
1416
|
+
- `transport_names()` — the transport names, sorted: `builtin`, every entry of
|
|
1417
|
+
the group of an installed distribution, and `requests` whether or not its
|
|
1418
|
+
distribution is installed. Nothing is imported to list them.
|
|
1419
|
+
- `get_transport(name)` — the class called `name`; the name is case-sensitive.
|
|
1420
|
+
`builtin` is always `HTTPTransport`: an entry called `builtin` in the group is
|
|
1421
|
+
ignored. A transport that another distribution provides and that is not
|
|
1422
|
+
installed raises `ImportError` naming it (`pip install pathapi-requests`
|
|
1423
|
+
for `requests`); any other name is a `ValueError` listing `transport_names()`;
|
|
1424
|
+
an entry that does not load a class is a `TypeError`.
|
|
1425
|
+
- `default_transport()` — a new transport of the kind
|
|
1426
|
+
`PATHAPI_HTTP_TRANSPORT` names, which is what a client given no transport
|
|
1427
|
+
builds: `get_transport(name)()`. An empty, differently cased or unknown value
|
|
1428
|
+
is a `ValueError` naming the choices.
|
|
1429
|
+
- `prepare_request(method, url, *, headers, body=None, timeout)` — the arguments
|
|
1430
|
+
of `send()` as the contract reads them, so a transport that starts with it
|
|
1431
|
+
refuses what the built-in transport refuses and frames a body the same way.
|
|
1432
|
+
`method` is upper-cased and the fragment is dropped from `url`. `headers` come
|
|
1433
|
+
back as a `dict` of text values with the `Content-Length` of a body whose
|
|
1434
|
+
length is known (`0` for a `PATCH`, `POST` or `PUT` without one) and
|
|
1435
|
+
`Accept-Encoding: gzip, deflate` unless the caller named an encoding. `body` is
|
|
1436
|
+
`None`, `bytes`, an object that yields exactly the `Content-Length` bytes (a
|
|
1437
|
+
file or a list of `bytes`, cut at the length it was measured at) or an
|
|
1438
|
+
iterable of `bytes` to send chunked. `timeout` is the `(connect, read)` pair.
|
|
1439
|
+
`decode` is what to pass as `HTTPResponse(..., decode=decode)`. It raises
|
|
1440
|
+
`ValueError`, before anything is sent, for a method or header name that is not
|
|
1441
|
+
a token, a header value or URL with a control character, a URL with
|
|
1442
|
+
credentials or another scheme, a `Content-Length` that is not a count of bytes
|
|
1443
|
+
or exceeds the body, and a timeout that is not a positive number, `None` or a
|
|
1444
|
+
pair of them; and `TypeError` for a `str` body. No message quotes the URL or a
|
|
1445
|
+
header value.
|
|
1446
|
+
|
|
1447
|
+
The suite that holds a transport to these promises ships with the package:
|
|
1448
|
+
"Testing support" under "Testing helpers".
|
|
1449
|
+
|
|
1450
|
+
## Protocols (`pathapi.protocols`)
|
|
1451
|
+
|
|
1452
|
+
`Path` is a class statement over these protocols. Each is a `typing.Protocol`
|
|
1453
|
+
with default bodies, subclassed explicitly as a mixin: a class lists the ones
|
|
1454
|
+
it implements, supplies the **primitives** (stubs that raise
|
|
1455
|
+
`NotImplementedError`) and gets the derived operations. A read-only backend is
|
|
1456
|
+
`class X(Pathname, Listing, Stat, BinaryOpen)` with no `Path`; list a protocol
|
|
1457
|
+
before the ones it is built on (`Listing` before `Stat`), as Python's MRO
|
|
1458
|
+
requires. `Path` lists `Copy` before `BinaryOpen`, so `Path.copy` is the path
|
|
1459
|
+
copy and not the stream copy.
|
|
1460
|
+
|
|
1461
|
+
| Protocol | Module | Primitives | Derived | Bases |
|
|
1462
|
+
| --- | --- | --- | --- | --- |
|
|
1463
|
+
| `Named` | `pure` | none (structural only) | `name`, `segments`, `parent`, `with_segments()`, `joinpath()`, `/`, `as_posix()` | none |
|
|
1464
|
+
| `FileStatLike` | `fs` | `st_mode`, `st_size`, `st_mtime` | | none |
|
|
1465
|
+
| `Stat` | `fs` | `stat()` | `lstat()`, `exists()`, `is_dir()`, `is_file()`, `is_symlink()`, `is_block_device()`, `is_char_device()`, `is_fifo()`, `is_socket()`, `info`, `is_junction()`, `is_mount()`, `is_dir_binding()` | none |
|
|
1466
|
+
| `Chmod` | `fs` | `chmod()` | `lchmod()` | `Chown`, `Utime` |
|
|
1467
|
+
| `Owned` | `fs` | `_user_name()`, `_group_name()` | `owner()`, `group()` | `Stat` |
|
|
1468
|
+
| `Chown` | `fs` | `_chown()` | `chown()` | none |
|
|
1469
|
+
| `Utime` | `fs` | `_utime()` | | none |
|
|
1470
|
+
| `Create` | `fs` | `_mkdir()` | `mkdir()`, `touch()` | `Stat`, `Chmod`, `Utime` |
|
|
1471
|
+
| `BinaryOpen` | `io` | `_open()` | `open()`, `read_bytes()`, `read_text()`, `write_bytes()`, `write_text()`, `copy()` | none |
|
|
1472
|
+
| `Listing` | `tree` | `iterdir()` | `_scandir()`, `glob()`, `rglob()`, `walk()`, iteration | `Stat` |
|
|
1473
|
+
| `Remove` | `tree` | `unlink()`, `rmdir()` | `rm()` | `Listing` |
|
|
1474
|
+
| `Readlink` | `links` | `readlink()` | | `Stat` |
|
|
1475
|
+
| `Symlink` | `links` | `_symlink_to()` | `symlink_to()`, `_symlink_target()` | `Remove` |
|
|
1476
|
+
| `Hardlink` | `links` | `_hardlink_to()` | `hardlink_to()` | `Stat` |
|
|
1477
|
+
| `Resolve` | `resolve` | `_cwd()` | `absolute()`, `resolve()` | `Readlink` |
|
|
1478
|
+
| `Identity` | `transfer` | none | `samefile()`, `_same_filesystem()`, `_node_key()` | `Stat` |
|
|
1479
|
+
| `Copy` | `transfer` | none | `copy()`, `copy_into()`, `_copy_from()`, `_coerce_target()`, `_write_batch()` | `Identity`, `Create`, `Symlink`, `Readlink`, `BinaryOpen` |
|
|
1480
|
+
| `Move` | `transfer` | `rename()` | `move()`, `move_into()`, `replace()` | `Copy`, `Remove` |
|
|
1481
|
+
| `NativeChecksum` | `checksum` | `checksum()` | `supported_checksums()` | none |
|
|
1482
|
+
|
|
1483
|
+
One protocol per primitive that a backend can have without the others: an
|
|
1484
|
+
archive reads links and makes none (`Readlink` and not `Symlink`), a server may
|
|
1485
|
+
set times and not modes, and `Owned` reads names where `Chown` writes ids.
|
|
1486
|
+
`BinaryOpen` is one protocol: a single `_open(mode)` serves reading and writing,
|
|
1487
|
+
and a mode the backend cannot honour raises `NotImplementedError` per call.
|
|
1488
|
+
`Create` also calls `open()` and `unlink()` in `touch()`. All names are
|
|
1489
|
+
imported from `pathapi.protocols`; the module column is where each one
|
|
1490
|
+
is defined.
|
|
1491
|
+
|
|
1492
|
+
### `supports`, `capabilities`
|
|
1493
|
+
|
|
1494
|
+
```python
|
|
1495
|
+
def supports(obj, *protocols) -> bool: ...
|
|
1496
|
+
def capabilities(obj) -> FrozenSet[type]: ...
|
|
1497
|
+
```
|
|
1498
|
+
|
|
1499
|
+
`supports(obj_or_class, *protocols)` is true when every primitive of every
|
|
1500
|
+
protocol named is something other than its default stub; `capabilities()`
|
|
1501
|
+
lists the protocols for which that holds. Ask once instead of calling and
|
|
1502
|
+
catching `NotImplementedError`. A primitive also counts as implemented when
|
|
1503
|
+
another library defines the operation it feeds: `pathlib`'s `mkdir` for
|
|
1504
|
+
`_mkdir`, `open` for `_open`, `absolute` for `_cwd`, `hardlink_to` for
|
|
1505
|
+
`_hardlink_to`, `chown` for `_chown`, `symlink_to` for `_symlink_to`, `owner`
|
|
1506
|
+
for `_user_name` and `group` for `_group_name`; so `LocalPath` supports `Owned`
|
|
1507
|
+
where `pathlib` defines `owner()` and `group()`, which includes Windows, where
|
|
1508
|
+
they exist and raise `NotImplementedError` when called. A protocol with no primitives
|
|
1509
|
+
(`Identity`, `Copy`) is supported by a class that has it as a base. A class
|
|
1510
|
+
whose answer depends on its connection defines `_supports(protocol) -> bool |
|
|
1511
|
+
None`, asked first for an instance; `None` falls back to the answer from the
|
|
1512
|
+
class. It may make at most one request per backend, cached for the backend's
|
|
1513
|
+
lifetime, and never raises: a failed request answers `None`. The answer is
|
|
1514
|
+
advisory, like `supported_checksums()`: a primitive may still raise
|
|
1515
|
+
`NotImplementedError` for an argument it cannot handle.
|
|
1516
|
+
`TypeError` for no protocol and for something that is not a protocol of this
|
|
1517
|
+
package (`Named`, `FileStatLike` and the like have no primitives to ask).
|
|
1518
|
+
|
|
1519
|
+
### `pure.Named`
|
|
1520
|
+
|
|
1521
|
+
```python
|
|
1522
|
+
class Named(Protocol):
|
|
1523
|
+
name
|
|
1524
|
+
segments
|
|
1525
|
+
parent
|
|
1526
|
+
def with_segments(self, *segments): ...
|
|
1527
|
+
def joinpath(self, *args): ...
|
|
1528
|
+
def as_posix(self): ...
|
|
1529
|
+
```
|
|
1530
|
+
|
|
1531
|
+
Structural only: `Pathname` and `pathlib.PurePath` satisfy it without naming
|
|
1532
|
+
it, and nothing subclasses it. It is the pure half that `glob`, `walk` and
|
|
1533
|
+
`sync` read.
|
|
1534
|
+
|
|
1535
|
+
### `fs.FileStatLike`
|
|
1536
|
+
|
|
1537
|
+
```python
|
|
1538
|
+
class FileStatLike(Protocol):
|
|
1539
|
+
st_mode: int
|
|
1540
|
+
st_size: int
|
|
1541
|
+
st_mtime: float
|
|
1542
|
+
```
|
|
1543
|
+
|
|
1544
|
+
`st_mode`, `st_size`, `st_mtime`.
|
|
1545
|
+
|
|
1546
|
+
### `fs.Stat`
|
|
1547
|
+
|
|
1548
|
+
```python
|
|
1549
|
+
class Stat(Protocol):
|
|
1550
|
+
def stat(self, *, follow_symlinks=True) -> FileStatLike: ...
|
|
1551
|
+
def lstat(self) -> FileStatLike: ...
|
|
1552
|
+
def exists(self, *, follow_symlinks=True): ...
|
|
1553
|
+
def is_dir(self, *, follow_symlinks=True): ...
|
|
1554
|
+
def is_file(self, *, follow_symlinks=True): ...
|
|
1555
|
+
def is_symlink(self): ...
|
|
1556
|
+
def is_block_device(self): ...
|
|
1557
|
+
def is_char_device(self): ...
|
|
1558
|
+
def is_fifo(self): ...
|
|
1559
|
+
def is_socket(self): ...
|
|
1560
|
+
info
|
|
1561
|
+
def is_junction(self) -> bool: ...
|
|
1562
|
+
def is_mount(self) -> bool: ...
|
|
1563
|
+
def is_dir_binding(self) -> bool: ...
|
|
1564
|
+
```
|
|
1565
|
+
|
|
1566
|
+
`stat()` (stub). A stat may report `size_known=False` (`FileStat`): the
|
|
1567
|
+
backend knows the entry's type and not its size or time, as TFTP without the
|
|
1568
|
+
`tsize` option or an HTTP reply without `Content-Length` do; `st_size` and
|
|
1569
|
+
`st_mtime` are then placeholders, and a consumer that needs them asks again or
|
|
1570
|
+
does without. Derives `lstat()`,
|
|
1571
|
+
`exists()`, `is_dir()`,
|
|
1572
|
+
`is_file()`, `is_symlink()`, `is_block_device()`,
|
|
1573
|
+
`is_char_device()`, `is_fifo()`, `is_socket()`; any `OSError`/`ValueError`
|
|
1574
|
+
from `stat()` reads as `False`. `info`, `is_junction()`, `is_mount()` and
|
|
1575
|
+
`is_dir_binding()` are described under `Path`.
|
|
1576
|
+
|
|
1577
|
+
### `fs.Chmod`
|
|
1578
|
+
|
|
1579
|
+
```python
|
|
1580
|
+
class Chmod(Chown, Utime, Protocol):
|
|
1581
|
+
def chmod(self, mode, *, follow_symlinks=True): ...
|
|
1582
|
+
def lchmod(self, mode): ...
|
|
1583
|
+
```
|
|
1584
|
+
|
|
1585
|
+
`chmod()` (stub; every
|
|
1586
|
+
implementation normalizes through `providers.parse_mode()`, so `"0755"` works)
|
|
1587
|
+
and `lchmod()`. `supports(x, Chmod)` asks for `chmod()` alone. `Chmod`
|
|
1588
|
+
inherits `Chown` and `Utime` for compatibility, because `chown()`, `_chown()`
|
|
1589
|
+
and `_utime()` were documented with it: a class that lists `Chmod` has them,
|
|
1590
|
+
and a new class lists the protocol it means.
|
|
1591
|
+
|
|
1592
|
+
### `fs.Owned`
|
|
1593
|
+
|
|
1594
|
+
```python
|
|
1595
|
+
class Owned(Stat, Protocol):
|
|
1596
|
+
def owner(self, *, follow_symlinks=True) -> str: ...
|
|
1597
|
+
def group(self, *, follow_symlinks=True) -> str: ...
|
|
1598
|
+
def _user_name(self, uid) -> str: ...
|
|
1599
|
+
def _group_name(self, gid) -> str: ...
|
|
1600
|
+
```
|
|
1601
|
+
|
|
1602
|
+
`owner()` and `group()` (the 3.13 signature) take `st_uid` and `st_gid` from
|
|
1603
|
+
`stat()` and map them through `_user_name(uid)` and `_group_name(gid)` (stubs);
|
|
1604
|
+
a stat without the field, or a class without the mapping, raises
|
|
1605
|
+
`NotImplementedError`. `LocalPath` keeps pathlib's.
|
|
1606
|
+
|
|
1607
|
+
### `fs.Chown`
|
|
1608
|
+
|
|
1609
|
+
```python
|
|
1610
|
+
class Chown(Protocol):
|
|
1611
|
+
def _chown(self, uid, gid, *, follow_symlinks=True) -> None: ...
|
|
1612
|
+
def chown(self, uid=None, gid=None, *, follow_symlinks=True) -> None: ...
|
|
1613
|
+
```
|
|
1614
|
+
|
|
1615
|
+
`_chown()` (stub, receives a
|
|
1616
|
+
canonical pair) / `chown()` —
|
|
1617
|
+
`None` or `-1` leaves a field unchanged, `int` is an id, `str` a name; an
|
|
1618
|
+
all-unchanged call returns without touching the backend.
|
|
1619
|
+
|
|
1620
|
+
### `fs.Utime`
|
|
1621
|
+
|
|
1622
|
+
```python
|
|
1623
|
+
class Utime(Protocol):
|
|
1624
|
+
def _utime(self, *, atime=None, mtime=None) -> None: ...
|
|
1625
|
+
```
|
|
1626
|
+
|
|
1627
|
+
`_utime(*, atime=None,
|
|
1628
|
+
mtime=None)` (stub) is the primitive for setting an existing entry's times, in
|
|
1629
|
+
seconds since the epoch (float): `None` leaves that time alone, a backend that
|
|
1630
|
+
keeps whole seconds or no access time sets what it can, and one that cannot set
|
|
1631
|
+
a time at all raises `NotImplementedError`. `touch()` of an existing file and
|
|
1632
|
+
`PathSyncer(preserve_mtime=True)` call it.
|
|
1633
|
+
|
|
1634
|
+
### `fs.Create`
|
|
1635
|
+
|
|
1636
|
+
```python
|
|
1637
|
+
class Create(Stat, Chmod, Utime, Protocol):
|
|
1638
|
+
def touch(self, mode=None, exist_ok=True): ...
|
|
1639
|
+
def _mkdir(self, mode): ...
|
|
1640
|
+
def mkdir(self, mode=0o777, parents=False, exist_ok=False): ...
|
|
1641
|
+
```
|
|
1642
|
+
|
|
1643
|
+
`_mkdir(mode)` (stub) creates this one directory; `mkdir()` and `touch()` are
|
|
1644
|
+
described under `Path`.
|
|
1645
|
+
|
|
1646
|
+
### `io.BinaryOpen`
|
|
1647
|
+
|
|
1648
|
+
```python
|
|
1649
|
+
class BinaryOpen(Protocol):
|
|
1650
|
+
def open(
|
|
1651
|
+
self, mode="r", buffering=-1, encoding=None, errors=None,
|
|
1652
|
+
newline=None,
|
|
1653
|
+
) -> _io.IOBase: ...
|
|
1654
|
+
def read_bytes(self) -> bytes: ...
|
|
1655
|
+
def read_text(self, encoding=None, errors=None, newline=None) -> str: ...
|
|
1656
|
+
def write_bytes(self, data): ...
|
|
1657
|
+
def write_text(self, data, encoding=None, errors=None, newline=None): ...
|
|
1658
|
+
def copy(self, target, *, progress=None, chunk_size=...): ...
|
|
1659
|
+
def _open(self, mode="r", buffering=-1) -> _io.IOBase: ...
|
|
1660
|
+
```
|
|
1661
|
+
|
|
1662
|
+
`_open()` (stub).
|
|
1663
|
+
`open()`
|
|
1664
|
+
validates the mode like builtin `open()` (`ValueError`) and passes
|
|
1665
|
+
`_open()` a canonical `r`/`w`/`x`/`a` plus optional `+` (never `b`/`t`);
|
|
1666
|
+
text mode wraps in `TextIOWrapper` and closes the handle if wrapping fails.
|
|
1667
|
+
An `_open()` that cannot honor a mode raises `NotImplementedError` per
|
|
1668
|
+
call: one `_open(mode)` primitive serves reading and writing, so there is no
|
|
1669
|
+
separate read-only protocol. A stream
|
|
1670
|
+
that commits on close inherits `providers.DiscardOnError`, so a `with` block
|
|
1671
|
+
over the file that raised commits nothing (see "For path implementers");
|
|
1672
|
+
`write_bytes()`, `write_text()` and `copy()` whose data fails half way commit
|
|
1673
|
+
nothing there either. Binary mode returns the stream `_open()` gave it as it
|
|
1674
|
+
is. Derives
|
|
1675
|
+
`read_bytes()`, `read_text()`,
|
|
1676
|
+
`write_bytes(data)`, `write_text()`, `copy()` (`chunk_size` defaults to `shutil.COPY_BUFSIZE`; `progress(bytes_copied, total_size | None)`; an empty file reports once).
|
|
1677
|
+
This `copy()` streams into any target that has `open()`; `Copy.copy()` is the
|
|
1678
|
+
path copy, and a class that lists `Copy` before `BinaryOpen` has that one.
|
|
1679
|
+
|
|
1680
|
+
### `tree.Listing`
|
|
1681
|
+
|
|
1682
|
+
```python
|
|
1683
|
+
class Listing(Stat, Protocol):
|
|
1684
|
+
def iterdir(self) -> Iterator[Self]: ...
|
|
1685
|
+
def _scandir(self) -> Iterator[Tuple[str, Optional[FileStat]]]: ...
|
|
1686
|
+
def glob(
|
|
1687
|
+
self, pattern, *, case_sensitive=None, include_hidden=True,
|
|
1688
|
+
recursive=None, dironly=None, recurse_symlinks=False, native=True,
|
|
1689
|
+
on_error=None, bound_loops=False,
|
|
1690
|
+
): ...
|
|
1691
|
+
def rglob(
|
|
1692
|
+
self, pattern, *, case_sensitive=None, include_hidden=True,
|
|
1693
|
+
recursive=True, dironly=None, recurse_symlinks=False, native=True,
|
|
1694
|
+
on_error=None, bound_loops=False,
|
|
1695
|
+
): ...
|
|
1696
|
+
def walk(self, top_down=True, on_error=None, follow_symlinks=False): ...
|
|
1697
|
+
```
|
|
1698
|
+
|
|
1699
|
+
`iterdir()` (stub). `_scandir()`, `glob()`, `rglob()`, `walk()` and
|
|
1700
|
+
`__iter__()` derive from it and from `stat()`; their contracts are under
|
|
1701
|
+
`Path`.
|
|
1702
|
+
|
|
1703
|
+
### `tree.Remove`
|
|
1704
|
+
|
|
1705
|
+
```python
|
|
1706
|
+
class Remove(Listing, Protocol):
|
|
1707
|
+
def unlink(self, missing_ok=False): ...
|
|
1708
|
+
def rmdir(self): ...
|
|
1709
|
+
def rm(
|
|
1710
|
+
self, recursive=False, missing_ok=False, ignore_error=False, *,
|
|
1711
|
+
follow_symlinks=False, follow_binds=False,
|
|
1712
|
+
): ...
|
|
1713
|
+
```
|
|
1714
|
+
|
|
1715
|
+
`unlink()` and `rmdir()` (stubs); `rm()` derives from them and from
|
|
1716
|
+
`_scandir()` (see `Path`).
|
|
1717
|
+
|
|
1718
|
+
### `links.Readlink`
|
|
1719
|
+
|
|
1720
|
+
```python
|
|
1721
|
+
class Readlink(Stat, Protocol):
|
|
1722
|
+
def readlink(self) -> Self: ...
|
|
1723
|
+
```
|
|
1724
|
+
|
|
1725
|
+
`readlink()` (stub) returns a path of this class whose `str()` and
|
|
1726
|
+
`as_posix()` are the link text, unresolved (`OSError(EINVAL)` where the path
|
|
1727
|
+
is not a link). A read-only backend such as `ZipPath` and `TarPath` has this
|
|
1728
|
+
and not `Symlink`.
|
|
1729
|
+
|
|
1730
|
+
### `links.Symlink`
|
|
1731
|
+
|
|
1732
|
+
```python
|
|
1733
|
+
class Symlink(Remove, Protocol):
|
|
1734
|
+
def _symlink_to(self, target, target_is_directory=False) -> None: ...
|
|
1735
|
+
def _symlink_target(self, target) -> Self: ...
|
|
1736
|
+
def symlink_to(
|
|
1737
|
+
self, target, target_is_directory=False, *, force=False,
|
|
1738
|
+
) -> None: ...
|
|
1739
|
+
```
|
|
1740
|
+
|
|
1741
|
+
`_symlink_to()` (stub) creates a link, and `symlink_to()` is described under
|
|
1742
|
+
`Path`.
|
|
1743
|
+
|
|
1744
|
+
### `links.Hardlink`
|
|
1745
|
+
|
|
1746
|
+
```python
|
|
1747
|
+
class Hardlink(Stat, Protocol):
|
|
1748
|
+
def _hardlink_to(self, target) -> None: ...
|
|
1749
|
+
def hardlink_to(self, target) -> None: ...
|
|
1750
|
+
```
|
|
1751
|
+
|
|
1752
|
+
`_hardlink_to(target)` (stub) makes this path a hard link to `target`, a
|
|
1753
|
+
path of this class; `hardlink_to()` turns a `str` target into a path of the
|
|
1754
|
+
same backend first. `LocalPath` has pathlib's `hardlink_to()` and, before
|
|
1755
|
+
3.10, makes the link with `os.link`.
|
|
1756
|
+
|
|
1757
|
+
### `resolve.Resolve`
|
|
1758
|
+
|
|
1759
|
+
```python
|
|
1760
|
+
class Resolve(Readlink, Protocol):
|
|
1761
|
+
def _cwd(self) -> Self: ...
|
|
1762
|
+
def absolute(self) -> Self: ...
|
|
1763
|
+
def resolve(self, strict=False) -> Self: ...
|
|
1764
|
+
```
|
|
1765
|
+
|
|
1766
|
+
`_cwd()` (stub) is the directory a relative path is relative to; `MemPath`
|
|
1767
|
+
and the tree paths answer with their root. `absolute()` is the path itself
|
|
1768
|
+
when it is absolute, else `_cwd()` joined with it. `resolve(strict=False)`
|
|
1769
|
+
makes it absolute, applies `..` and replaces every link by its `readlink()`
|
|
1770
|
+
text, at most 40 of them (a rooted link text restarts at the root). It is
|
|
1771
|
+
lexical where the class cannot read links. `strict=True` raises
|
|
1772
|
+
`FileNotFoundError` for a missing component. A link loop raises what `pathlib`
|
|
1773
|
+
raises on the running interpreter: `RuntimeError` before 3.13, and from 3.13
|
|
1774
|
+
`OSError(ELOOP)` when `strict` (otherwise the looping link is kept).
|
|
1775
|
+
`LocalPath` keeps pathlib's.
|
|
1776
|
+
|
|
1777
|
+
### `transfer.Identity`
|
|
1778
|
+
|
|
1779
|
+
```python
|
|
1780
|
+
class Identity(Stat, Protocol):
|
|
1781
|
+
def _same_filesystem(self, other) -> bool: ...
|
|
1782
|
+
def _node_key(self) -> tuple[object, tuple[str, ...]] | None: ...
|
|
1783
|
+
def samefile(self, other_path): ...
|
|
1784
|
+
```
|
|
1785
|
+
|
|
1786
|
+
No primitives: the hooks are described under `Path`.
|
|
1787
|
+
|
|
1788
|
+
### `transfer.Copy`
|
|
1789
|
+
|
|
1790
|
+
```python
|
|
1791
|
+
class Copy(Identity, Create, Symlink, Readlink, BinaryOpen, Protocol):
|
|
1792
|
+
def copy(
|
|
1793
|
+
self, target, *, overwrite=False, follow_symlinks=True,
|
|
1794
|
+
preserve_metadata=True, recursive=False, ignore_error=None,
|
|
1795
|
+
progress=None, on_error=None,
|
|
1796
|
+
): ...
|
|
1797
|
+
def copy_into(
|
|
1798
|
+
self, target_dir, *, overwrite=False, follow_symlinks=True,
|
|
1799
|
+
preserve_metadata=True, recursive=False, ignore_error=None,
|
|
1800
|
+
progress=None, on_error=None,
|
|
1801
|
+
) -> Path: ...
|
|
1802
|
+
def _copy_from(
|
|
1803
|
+
self, source, follow_symlinks=True, preserve_metadata=False,
|
|
1804
|
+
) -> None: ...
|
|
1805
|
+
def _coerce_target(self, target) -> Path: ...
|
|
1806
|
+
def _write_batch(self) -> ContextManager[object]: ...
|
|
1807
|
+
```
|
|
1808
|
+
|
|
1809
|
+
No primitives: `copy()` is built from the protocols it lists. Contracts are
|
|
1810
|
+
under `Path`.
|
|
1811
|
+
|
|
1812
|
+
### `transfer.Move`
|
|
1813
|
+
|
|
1814
|
+
```python
|
|
1815
|
+
class Move(Copy, Remove, Protocol):
|
|
1816
|
+
def rename(self, target): ...
|
|
1817
|
+
def move(self, target, *, overwrite=False): ...
|
|
1818
|
+
def move_into(self, target_dir, *, overwrite=False) -> Path: ...
|
|
1819
|
+
def replace(self, target) -> Path: ...
|
|
1820
|
+
def _rename_compatible(self, target) -> bool: ...
|
|
1821
|
+
```
|
|
1822
|
+
|
|
1823
|
+
`rename()` (stub). `move()` falls back to a copy and a removal where it is
|
|
1824
|
+
missing, so `supports(x, Move)` says whether the backend renames natively, not
|
|
1825
|
+
whether `move()` works.
|
|
1826
|
+
|
|
1827
|
+
### `checksum.NativeChecksum`
|
|
1828
|
+
|
|
1829
|
+
```python
|
|
1830
|
+
class NativeChecksum(Protocol):
|
|
1831
|
+
def checksum(self, algorithm="md5") -> str: ...
|
|
1832
|
+
def supported_checksums(self) -> FrozenSet[str]: ...
|
|
1833
|
+
```
|
|
1834
|
+
|
|
1835
|
+
Opt-in (not on `Path`):
|
|
1836
|
+
`checksum()` MUST raise `NotImplementedError` whenever
|
|
1837
|
+
it cannot return a genuine content digest under exactly that algorithm
|
|
1838
|
+
(never a different algorithm, never a hash-shaped value such as a
|
|
1839
|
+
multipart ETag); callers catch only `NotImplementedError`. `supported_checksums()` (default
|
|
1840
|
+
empty) is advisory and never raises.
|
|
1841
|
+
|
|
1842
|
+
## Syncing trees (`pathapi.sync`)
|
|
1843
|
+
|
|
1844
|
+
```python
|
|
1845
|
+
class PathSyncer:
|
|
1846
|
+
EVENT_LOG_FORMAT = "[%s] Source:%s Target:%s DryRun:%s"
|
|
1847
|
+
def __init__(
|
|
1848
|
+
self, checksum=None, /, remove_missing=False, follow_symlinks=True,
|
|
1849
|
+
symlink_mode="preserve", hook=None, ignore_error=False,
|
|
1850
|
+
quick_check=True, preserve_mtime=True, follow_mounts=True,
|
|
1851
|
+
) -> None: ...
|
|
1852
|
+
def sync(self, source, target, /, dry_run=False, ignore_error=None): ...
|
|
1853
|
+
def log(self, msg, *args): ...
|
|
1854
|
+
```
|
|
1855
|
+
|
|
1856
|
+
One-way tree sync between any two
|
|
1857
|
+
`Path` implementations.
|
|
1858
|
+
|
|
1859
|
+
- `.sync()` — `None` uses
|
|
1860
|
+
the constructor policy; a bool or callable overrides it for this call.
|
|
1861
|
+
- `checksum=None`: a native digest when both sides share an algorithm
|
|
1862
|
+
(`NativeChecksum`), else a streamed md5 on both sides; a callable
|
|
1863
|
+
`checksum(entry: PathAndStat)` is compared with `==`.
|
|
1864
|
+
- `quick_check=True`: when either side is non-local (`is_local()`), equal
|
|
1865
|
+
`st_size` and `st_mtime` skip the checksum; `st_mtime == 0` never matches;
|
|
1866
|
+
a mismatch still checksums. Times match at the resolution of the coarser
|
|
1867
|
+
side: in whole seconds when either has no fractional part (SFTP, FTP and the
|
|
1868
|
+
object stores report seconds), else to a microsecond. So 100.0 and 100.9
|
|
1869
|
+
match, 100.0 and 101.0 do not, and neither do 100.25 and 100.5.
|
|
1870
|
+
- `preserve_mtime=True`: after it copies a file, the sync gives the target the
|
|
1871
|
+
source's `st_mtime` through `_utime()` (not for a directory or a link),
|
|
1872
|
+
so the next run's quick check can match. A source that reports no time and a
|
|
1873
|
+
target that cannot set one (`NotImplementedError`), or refuses
|
|
1874
|
+
(`OSError`), keep the time the copy gave them, and the sync does not
|
|
1875
|
+
report it. `False` leaves every copy with the time it was written at.
|
|
1876
|
+
- A `sync()` that is not a dry run runs inside the root target's
|
|
1877
|
+
`_write_batch()`: onto a zip archive the whole tree is one rewrite
|
|
1878
|
+
of the file, and a sync that raises leaves the archive as it was.
|
|
1879
|
+
- `hook(source: PathAndStat, target: PathAndStat, event: SyncEvent,
|
|
1880
|
+
dry_run: bool)` is called for every event (structural ones included) with
|
|
1881
|
+
the call's `dry_run`.
|
|
1882
|
+
- `ignore_error`: bool or `callable(error, source, target, event) -> bool`
|
|
1883
|
+
(`source` and `target` are `PathAndStat`), offered once per error, and a
|
|
1884
|
+
callable that raises ends the sync with its own exception, which is not
|
|
1885
|
+
offered back to it; a tolerated error is logged at WARNING on logger
|
|
1886
|
+
`pathapi.sync` and reported to `hook` as `SyncEvent.Error`.
|
|
1887
|
+
`.log(msg, *args)` (INFO on the same logger) is overridable by a subclass;
|
|
1888
|
+
`EVENT_LOG_FORMAT` (`"[%s] Source:%s Target:%s DryRun:%s"`) is the format
|
|
1889
|
+
it receives for each event.
|
|
1890
|
+
- Safety, all through `ignore_error`: a missing root `source` →
|
|
1891
|
+
`FileNotFoundError`; overlapping `source`/`target` (one inside the other,
|
|
1892
|
+
or two names of one file: decided like `copy()`/`move()` decide "same
|
|
1893
|
+
file", so two files of this machine whatever their classes, two spellings
|
|
1894
|
+
of one `MemPath` or archive node, and else one type that
|
|
1895
|
+
`_same_filesystem()` places together) → `ValueError`; a child
|
|
1896
|
+
name that would leave `target` (`..`, or on a Windows target `\`, `:`, a
|
|
1897
|
+
trailing dot or space, a device name) → `ValueError`, decided on the name
|
|
1898
|
+
as listed, in the source's listing and the target's alike; a symlink
|
|
1899
|
+
inside `target` is replaced, never followed, and so is a Windows
|
|
1900
|
+
junction below its root (event `TypeMismatch`; `rmdir()` removes the
|
|
1901
|
+
junction and nothing behind it). A mount point inside `target` is part
|
|
1902
|
+
of the tree and is synced into (`follow_mounts=True`, the default).
|
|
1903
|
+
With `follow_mounts=False` it is an error for that entry instead:
|
|
1904
|
+
`OSError(EXDEV)` naming it, offered to `ignore_error` with event
|
|
1905
|
+
`TypeMismatch`, and nothing behind it is listed, written or removed, in a
|
|
1906
|
+
dry run too. A junction is replaced as above under either value; the root
|
|
1907
|
+
`target` is used as given.
|
|
1908
|
+
Listing entries with unknown
|
|
1909
|
+
stats are re-stat'd. A name the source lists is never "missing": a listed
|
|
1910
|
+
link that cannot be resolved (dangling, a loop, an unmounted volume) is an
|
|
1911
|
+
error for that entry (`FileNotFoundError` or the stat's `OSError`, event
|
|
1912
|
+
`SyncStart`) and the same-named target entry is left alone, whatever
|
|
1913
|
+
`remove_missing` is; only a name the listing no longer holds is removed.
|
|
1914
|
+
- Cost: each root is stat'd once and each directory of each side is listed
|
|
1915
|
+
once; the listing's stat answers for every entry that is not a link, so a
|
|
1916
|
+
sync that changes nothing makes no other `stat()` call on a backend whose
|
|
1917
|
+
`_scandir()` returns stats. A link, an entry whose listing stat is `None`
|
|
1918
|
+
and a target name the listing did not report are stat'd, one call each. A
|
|
1919
|
+
backend with no `_scandir()` of its own pays the default's `stat()` per
|
|
1920
|
+
entry of every directory it lists.
|
|
1921
|
+
- `remove_missing=True` deletes target entries absent from the source. With
|
|
1922
|
+
`False`, a non-empty target directory whose source became a file or link
|
|
1923
|
+
is kept (`IsADirectoryError`, event `TypeMismatch`).
|
|
1924
|
+
- `follow_symlinks=False` + `symlink_mode="preserve"` recreates source links
|
|
1925
|
+
with the raw `readlink()` text (target must implement `symlink_to()`, else
|
|
1926
|
+
`NotImplementedError` through `ignore_error`, in a dry run too);
|
|
1927
|
+
`"reject"` raises `NotImplementedError`.
|
|
1928
|
+
- Changed files are written to a hidden temporary sibling
|
|
1929
|
+
(`.NAME.<12 hex>.pathapi-tmp`, NAME cut to keep the whole within 255
|
|
1930
|
+
bytes) and renamed over the target where the target supports `rename()`;
|
|
1931
|
+
where `rename()` refuses an existing target the old file is removed first,
|
|
1932
|
+
and if the second rename fails the new version is kept under its
|
|
1933
|
+
temporary name and the error (an `OSError`) names it. A name of exactly
|
|
1934
|
+
that form is the library's own: never a source, never removed as
|
|
1935
|
+
"missing"; a stale one (not being written by this process, modification
|
|
1936
|
+
time known and over a day old) is removed from a target directory when a
|
|
1937
|
+
changed file or link in it is next written, and by `remove_missing=True`
|
|
1938
|
+
(event `RemovedMissing`). FIFOs, sockets and devices are skipped. A dry run makes the same
|
|
1939
|
+
decisions without changes.
|
|
1940
|
+
|
|
1941
|
+
### `SyncEvent`
|
|
1942
|
+
|
|
1943
|
+
```python
|
|
1944
|
+
class SyncEvent(Enum):
|
|
1945
|
+
Copy = ...
|
|
1946
|
+
RemovedMissing = ...
|
|
1947
|
+
Synced = ...
|
|
1948
|
+
CreatedDirectory = ...
|
|
1949
|
+
SyncStart = ...
|
|
1950
|
+
TypeMismatch = ...
|
|
1951
|
+
CheckTargetChild = ...
|
|
1952
|
+
CheckTargetChildren = ...
|
|
1953
|
+
SyncChild = ...
|
|
1954
|
+
SyncChildren = ...
|
|
1955
|
+
Symlink = ...
|
|
1956
|
+
Compare = ...
|
|
1957
|
+
Skipped = ...
|
|
1958
|
+
Error = ...
|
|
1959
|
+
```
|
|
1960
|
+
|
|
1961
|
+
Members: `Copy`, `RemovedMissing`, `Synced`,
|
|
1962
|
+
`CreatedDirectory`, `SyncStart`, `TypeMismatch`, `CheckTargetChild`,
|
|
1963
|
+
`CheckTargetChildren`, `SyncChild`, `SyncChildren`, `Symlink`, `Compare`
|
|
1964
|
+
(a comparison failed; only passed to `ignore_error`), `Skipped` (not a
|
|
1965
|
+
file, directory or link), `Error` (an error was tolerated).
|
|
1966
|
+
|
|
1967
|
+
### `PathAndStat`
|
|
1968
|
+
|
|
1969
|
+
```python
|
|
1970
|
+
class PathAndStat:
|
|
1971
|
+
def __init__(self, path, *, follow_symlink=True) -> None: ...
|
|
1972
|
+
@classmethod
|
|
1973
|
+
def from_stat(cls, path, stat) -> PathAndStat: ...
|
|
1974
|
+
def exists(self): ...
|
|
1975
|
+
def refresh(self, follow_symlink=True): ...
|
|
1976
|
+
```
|
|
1977
|
+
|
|
1978
|
+
`path` plus cached
|
|
1979
|
+
`stat` (`FileStat | None`); `from_stat(path, stat)`, `exists()`,
|
|
1980
|
+
`refresh(follow_symlink=True)`; `is_*` attributes delegate to the stat
|
|
1981
|
+
(always-`False` callables when missing).
|
|
1982
|
+
|
|
1983
|
+
## Globbing (`pathapi.glob`)
|
|
1984
|
+
|
|
1985
|
+
```python
|
|
1986
|
+
def glob(
|
|
1987
|
+
path, *, dironly=False, root_dir=None, recursive=False,
|
|
1988
|
+
include_hidden=False, case_sensitive=None, native=True, on_error=None,
|
|
1989
|
+
bound_loops=False, recurse_symlinks=False,
|
|
1990
|
+
) -> Iterator[_Globable]: ...
|
|
1991
|
+
def compile_pattern(pat, case_sensitive): ...
|
|
1992
|
+
WILCARD_PATTERN = WILDCARD_PATTERN
|
|
1993
|
+
def parse_pattern(pattern, *, native=True) -> Tuple[List[str], bool]: ...
|
|
1994
|
+
def select(
|
|
1995
|
+
base, parts, *, dironly=False, recursive=True, include_hidden=True,
|
|
1996
|
+
case_sensitive=None, native=True, on_error=None, bound_loops=False,
|
|
1997
|
+
recurse_symlinks=False,
|
|
1998
|
+
) -> Iterator[_Globable]: ...
|
|
1999
|
+
def full_match(segments, pattern, case_sensitive) -> bool: ...
|
|
2000
|
+
|
|
2001
|
+
RECURSIVE = '**'
|
|
2002
|
+
```
|
|
2003
|
+
|
|
2004
|
+
`glob.glob()`: the pattern
|
|
2005
|
+
is itself a path (`LocalPath("/x/**/*.py")`); like stdlib `glob`, hidden
|
|
2006
|
+
names need `include_hidden=True`. It is split at the first wildcard, never
|
|
2007
|
+
in the path's anchor (a drive, `\\?\C:\`); a bad pattern raises when it is
|
|
2008
|
+
called, the selection is lazy. `glob.parse_pattern()` (`ValueError`/`NonRelativePatternError`),
|
|
2009
|
+
`glob.select()` (the engine behind
|
|
2010
|
+
`Path.glob()`), `glob.full_match()`,
|
|
2011
|
+
`glob.RECURSIVE = "**"`, `glob.compile_pattern()` (one shell-style component
|
|
2012
|
+
as a compiled regular expression, cached), `glob.ANY_PATTERN` (a lone `*`) and
|
|
2013
|
+
`glob.WILDCARD_PATTERN` (a wildcard character), both compiled expressions, and
|
|
2014
|
+
`glob.WILCARD_PATTERN`, the old misspelling of `WILDCARD_PATTERN`.
|
|
2015
|
+
|
|
2016
|
+
## Checksums (`pathapi.checksum`)
|
|
2017
|
+
|
|
2018
|
+
```python
|
|
2019
|
+
def md5(path, chunk_size=65536) -> str: ...
|
|
2020
|
+
def sha256(path, chunk_size=65536) -> str: ...
|
|
2021
|
+
def stream(path, algorithm="md5", chunk_size=65536) -> str: ...
|
|
2022
|
+
def native(path, algorithm="md5") -> str | None: ...
|
|
2023
|
+
```
|
|
2024
|
+
|
|
2025
|
+
`md5()`, `sha256()`, `stream()`
|
|
2026
|
+
(streamed through `open("rb")`, `usedforsecurity=False`),
|
|
2027
|
+
`native()` (`None` when the protocol is
|
|
2028
|
+
missing or raises `NotImplementedError`).
|
|
2029
|
+
|
|
2030
|
+
## Providers (`pathapi.providers`)
|
|
2031
|
+
|
|
2032
|
+
```python
|
|
2033
|
+
def is_safe_child_name(name, *, windows=False) -> bool: ...
|
|
2034
|
+
def is_windows_flavoured(path) -> bool: ...
|
|
2035
|
+
def name_stem(name) -> str: ...
|
|
2036
|
+
def name_suffix(name) -> str: ...
|
|
2037
|
+
def name_suffixes(name) -> list[str]: ...
|
|
2038
|
+
class TreePath(Path):
|
|
2039
|
+
@classmethod
|
|
2040
|
+
def _join(cls, segments) -> tuple[str, TreePath | None]: ...
|
|
2041
|
+
def _rebuild(self, text) -> Self: ...
|
|
2042
|
+
segments; parts; parent
|
|
2043
|
+
def with_segments(self, *segments) -> Self: ...
|
|
2044
|
+
def relative_to(self, other, *, walk_up=False) -> Self: ...
|
|
2045
|
+
def parse_mode(mode) -> int: ...
|
|
2046
|
+
def parse_owner(uid, gid) -> Tuple[Optional[int], Optional[int]]: ...
|
|
2047
|
+
def as_error_handler(
|
|
2048
|
+
ignore_error, *, default=False,
|
|
2049
|
+
) -> Callable[..., bool]: ...
|
|
2050
|
+
def as_error_predicate(on_error) -> OfferedOnce: ...
|
|
2051
|
+
class OfferedOnce:
|
|
2052
|
+
def __call__(self, error, path) -> bool: ...
|
|
2053
|
+
def notimplemented(method): ...
|
|
2054
|
+
def derived_from(*primitives): ...
|
|
2055
|
+
def primitive(method): ...
|
|
2056
|
+
|
|
2057
|
+
class DiscardOnError:
|
|
2058
|
+
def discard(self) -> None: ...
|
|
2059
|
+
|
|
2060
|
+
class CommitOnClose(DiscardOnError, BytesIO):
|
|
2061
|
+
def __init__(self, commit, initial=None): ...
|
|
2062
|
+
def discard(self) -> None: ...
|
|
2063
|
+
|
|
2064
|
+
class LRU:
|
|
2065
|
+
def __init__(self, func, maxsize=128, on_evict=None): ...
|
|
2066
|
+
def discard(self, *args) -> bool: ...
|
|
2067
|
+
def invalidate(self, *args) -> V: ...
|
|
2068
|
+
maxsize
|
|
2069
|
+
UNCHANGED = None
|
|
2070
|
+
```
|
|
2071
|
+
|
|
2072
|
+
- **`is_safe_child_name()`** — `False` for a
|
|
2073
|
+
non-`str`, `""`, `.`, `..`, or a name containing `/` or NUL; with
|
|
2074
|
+
`windows=True` also `\`, `:`, a trailing dot or space, and the reserved
|
|
2075
|
+
device names `CON`, `PRN`, `AUX`, `NUL`, `COM1`-`COM9`, `LPT1`-`LPT9` (any
|
|
2076
|
+
case, with or without an extension: `nul.txt`).
|
|
2077
|
+
**`is_windows_flavoured()`**
|
|
2078
|
+
— `True` for a `PureWindowsPath` or a path whose `filepath` is one.
|
|
2079
|
+
- **`TreePath`** — the base of a path whose names are POSIX segments below the
|
|
2080
|
+
root of a tree a backend serves: a remote directory, the members of an
|
|
2081
|
+
archive. `FTPPath`, `ZipPath`, `TarPath` and `ArchivePath` are built on it.
|
|
2082
|
+
It holds the names only: joined and normalized like `PurePosixPath` (empty
|
|
2083
|
+
and `.` segments collapse, an absolute argument restarts the join), and a
|
|
2084
|
+
relative and a rooted spelling name the same node because the tree has no
|
|
2085
|
+
working directory (`_cwd()` answers the root). A backend where the two
|
|
2086
|
+
spellings are different nodes, because it has a working directory, does not
|
|
2087
|
+
use `TreePath`, or overrides `_cwd()`. A subclass supplies the constructor, the backend it
|
|
2088
|
+
carries and `_rebuild(text)` (a path of its class and backend for a joined
|
|
2089
|
+
text); `_join(segments)` returns the joined text and the first segment that
|
|
2090
|
+
is a path of the same family, whose backend a join keeps.
|
|
2091
|
+
- **`name_stem()`, `name_suffix()`, `name_suffixes()`** — one path component
|
|
2092
|
+
split as `pathlib` of the running interpreter splits it (`"a.tar.gz"` →
|
|
2093
|
+
`"a.tar"`, `".gz"`, `[".tar", ".gz"]`; `".bashrc"` has no suffix). From
|
|
2094
|
+
Python 3.14 a trailing dot is a suffix (`"a."` → `"."`), as it is there.
|
|
2095
|
+
A class that computes `stem` and `suffix` itself calls these to agree with
|
|
2096
|
+
`Pathname`.
|
|
2097
|
+
- **`LRU`** — thread-safe memoizing cache,
|
|
2098
|
+
called like `func`. `on_evict(key_tuple, value)` runs for every dropped
|
|
2099
|
+
value (overflow, `maxsize` shrink, `invalidate`/`discard`, a losing
|
|
2100
|
+
concurrent miss), outside the lock, exceptions suppressed. `discard(*args)
|
|
2101
|
+
-> bool`, `invalidate(*args)` (discard + recompute), settable `maxsize`.
|
|
2102
|
+
`maxsize=None` is unbounded; `maxsize=0` (or less) stores nothing, so every
|
|
2103
|
+
call runs `func` and its result never reaches `on_evict`.
|
|
2104
|
+
- **`parse_mode()`** — `int` passes through; `str` is octal (optional
|
|
2105
|
+
`0o`), any non-octal digit → `ValueError`. A `bool` → `TypeError`; a number
|
|
2106
|
+
below 0 or above `0o177777` (the 16 bits of a `st_mode`, type bits
|
|
2107
|
+
included) → `ValueError`.
|
|
2108
|
+
- **`parse_owner()`** — `-1` → `None`;
|
|
2109
|
+
`str` names pass through; a `bool` → `TypeError`, an id outside
|
|
2110
|
+
0..2**32-1 → `ValueError`. **`UNCHANGED = None`**.
|
|
2111
|
+
- **`as_error_handler()`** — a
|
|
2112
|
+
callable passes through untouched (arity is the call site's: `rm` and
|
|
2113
|
+
`copy(on_error=)` `(error, path)`, `copy(ignore_error=)` `(error)`,
|
|
2114
|
+
`PathSyncer` `(error, source, target, event)`); a bool (or `None` →
|
|
2115
|
+
`default`) becomes a constant-returning callable.
|
|
2116
|
+
**`as_error_predicate()`** — `on_error` as an `OfferedOnce`: a bool becomes a
|
|
2117
|
+
constant answer, a callable is called as `(error, path)`, and an
|
|
2118
|
+
`OfferedOnce` is returned as it is. An `OfferedOnce` answers `False`, without
|
|
2119
|
+
calling the predicate, for an error it already answered `False` for and
|
|
2120
|
+
for an exception the predicate itself raised, so a recursive operation that
|
|
2121
|
+
passes it down never offers one failure twice.
|
|
2122
|
+
- **`notimplemented()`** — decorator that marks a method as the stub of a
|
|
2123
|
+
primitive, bare or with a reason: `@notimplemented` or `@notimplemented("why")`.
|
|
2124
|
+
Calling it raises `NotImplementedError` with the reason, or
|
|
2125
|
+
`"Method not implemented: <name>"`. `protocols.supports()` reads it as absent.
|
|
2126
|
+
- **`primitive()`** — decorator that marks a function as the default of a
|
|
2127
|
+
primitive that raises an error of its own (a `data:` URI's `_listdir()`
|
|
2128
|
+
raises `NotADirectoryError`): `supports()` reads it as absent, as it reads
|
|
2129
|
+
a `notimplemented` stub.
|
|
2130
|
+
- **`derived_from(*primitives)`** — decorator for a method that only wraps
|
|
2131
|
+
other primitives of its class, such as an `iterdir()` that lists through
|
|
2132
|
+
`_listdir()`: `supports()` counts the method as implemented only when one
|
|
2133
|
+
of the named primitives is, so a base class can define the wrapper without
|
|
2134
|
+
claiming a protocol its subclasses do not have.
|
|
2135
|
+
- **`CommitOnClose`** and **`DiscardOnError`** — the write buffer that commits
|
|
2136
|
+
when it is closed, and the mixin that makes a failed `with` block commit
|
|
2137
|
+
nothing; see "For path implementers".
|
|
2138
|
+
|
|
2139
|
+
## Testing helpers (`pathapi.testing`, needs `pytest`)
|
|
2140
|
+
|
|
2141
|
+
### `FIXTURE_TREE`
|
|
2142
|
+
|
|
2143
|
+
```python
|
|
2144
|
+
FIXTURE_TREE = {
|
|
2145
|
+
"a.txt": "a",
|
|
2146
|
+
"b.py": "b",
|
|
2147
|
+
".hidden.txt": "hidden",
|
|
2148
|
+
"sub": None,
|
|
2149
|
+
"sub/c.py": "c",
|
|
2150
|
+
"sub/nested": None,
|
|
2151
|
+
"sub/nested/d.py": "d",
|
|
2152
|
+
"empty_dir": None,
|
|
2153
|
+
}
|
|
2154
|
+
```
|
|
2155
|
+
|
|
2156
|
+
Directories are the entries whose value is `None`.
|
|
2157
|
+
|
|
2158
|
+
### `populate_fixture_tree`
|
|
2159
|
+
|
|
2160
|
+
```python
|
|
2161
|
+
def populate_fixture_tree(root): ...
|
|
2162
|
+
```
|
|
2163
|
+
|
|
2164
|
+
Builds `FIXTURE_TREE` under an
|
|
2165
|
+
existing empty directory through the path's own `mkdir()`/`write_text()`
|
|
2166
|
+
(any `Path`, or a stdlib `pathlib.Path`).
|
|
2167
|
+
|
|
2168
|
+
### `PurePathContract`
|
|
2169
|
+
|
|
2170
|
+
```python
|
|
2171
|
+
class PurePathContract: ...
|
|
2172
|
+
```
|
|
2173
|
+
|
|
2174
|
+
Name/suffix/stem, parents, join, `match()`; needs a
|
|
2175
|
+
`root` fixture only.
|
|
2176
|
+
|
|
2177
|
+
### `ReadPathContract`
|
|
2178
|
+
|
|
2179
|
+
```python
|
|
2180
|
+
class ReadPathContract(PurePathContract):
|
|
2181
|
+
supports_listing = None
|
|
2182
|
+
supports_empty_directories = True
|
|
2183
|
+
distinguishes_file_types = True
|
|
2184
|
+
supports_pickle = True
|
|
2185
|
+
```
|
|
2186
|
+
|
|
2187
|
+
Exists/types, reads and read
|
|
2188
|
+
modes, partial `read(n)`, `iterdir()`, `stat()` (the size of a longer file, a
|
|
2189
|
+
directory told from a file, a numeric `st_mtime`), `_scandir()` agreeing with
|
|
2190
|
+
`stat()` for every child, nothing existing below a file, `glob()`/`rglob()`,
|
|
2191
|
+
`walk()`, with pathlib's exception types, and a pickle round trip that keeps
|
|
2192
|
+
`==` and `hash()`. Capability attributes: `supports_listing`,
|
|
2193
|
+
`supports_empty_directories`, `distinguishes_file_types`, `supports_pickle`.
|
|
2194
|
+
|
|
2195
|
+
### `PathContract`
|
|
2196
|
+
|
|
2197
|
+
```python
|
|
2198
|
+
class PathContract(ReadPathContract):
|
|
2199
|
+
supports_rename = None
|
|
2200
|
+
supports_append = True
|
|
2201
|
+
supports_exclusive_create = True
|
|
2202
|
+
enforces_directory_hierarchy = True
|
|
2203
|
+
supports_mkdir = None
|
|
2204
|
+
supports_delete = None
|
|
2205
|
+
supports_move = True
|
|
2206
|
+
supports_unusual_names = True
|
|
2207
|
+
supports_utime = None
|
|
2208
|
+
supports_question_mark_names = False
|
|
2209
|
+
```
|
|
2210
|
+
|
|
2211
|
+
`mkdir()`, writes and write/append/
|
|
2212
|
+
exclusive modes, a payload of about 290 KiB holding every byte value and
|
|
2213
|
+
runs of CR/LF written whole and in pieces and read back whole and in parts,
|
|
2214
|
+
names holding a space, `#`, `%`, `+`, `&`, `=` and mixed case written,
|
|
2215
|
+
listed, renamed and removed as written, `unlink()`, `rmdir()`, `rm()` (also
|
|
2216
|
+
with `follow_symlinks=`/`follow_binds=`: an implementation that overrides
|
|
2217
|
+
`rm()` must accept both), `copy()` (recursive), `move()`, `rename()` (also
|
|
2218
|
+
onto a non-empty directory, which must raise), `touch()`, copying a file
|
|
2219
|
+
onto a separately built spelling of itself (`OSError`, content kept) and
|
|
2220
|
+
`_same_filesystem()` within one root. Capability attributes:
|
|
2221
|
+
`supports_rename`, `supports_append`, `supports_exclusive_create`,
|
|
2222
|
+
`enforces_directory_hierarchy` (also covers rename onto a directory),
|
|
2223
|
+
`supports_mkdir` (every test that creates a directory), `supports_delete`
|
|
2224
|
+
(`unlink()`, `rmdir()`, `rm()`, and the overwriting half of `copy()`),
|
|
2225
|
+
`supports_move` (`move()`, by `rename()` or copy and delete),
|
|
2226
|
+
`supports_unusual_names`, `supports_utime` (`_utime()` sets an existing
|
|
2227
|
+
file's modification time and `touch()` moves it; set it `False` for a backend
|
|
2228
|
+
that cannot set a time and leaves `_utime()` raising `NotImplementedError`),
|
|
2229
|
+
and `supports_question_mark_names` (names holding `?`; **False by default**,
|
|
2230
|
+
because a Windows file system cannot store one: set it `True` for a URI
|
|
2231
|
+
scheme or a remote store).
|
|
2232
|
+
|
|
2233
|
+
- Five switches default to `None`, which means "ask `protocols.supports()` on
|
|
2234
|
+
`root`": `supports_listing` (`Listing`), `supports_mkdir` (`Create`),
|
|
2235
|
+
`supports_delete` (`Remove`), `supports_utime` (`Utime`) and `supports_rename`
|
|
2236
|
+
(`Move`). A class without the protocol skips those tests with no switch set,
|
|
2237
|
+
and the skip reason names it (`MyPath does not support Remove`). Set the
|
|
2238
|
+
attribute `True` or `False` on the test class to override the answer, for a
|
|
2239
|
+
class that implements the protocol but cannot honour the test.
|
|
2240
|
+
|
|
2241
|
+
- Both I/O contracts need `root` to be a **fresh, function-scoped** directory
|
|
2242
|
+
populated with `populate_fixture_tree()` (which uses `mkdir()` and
|
|
2243
|
+
`write_text()`: a backend without them seeds `root` another way); two
|
|
2244
|
+
contract classes must not share one. Capability attributes default to
|
|
2245
|
+
`True` except `supports_question_mark_names`; setting one `False` makes the
|
|
2246
|
+
tests it covers skip. `DIRECTORY_ERRORS = (IsADirectoryError,
|
|
2247
|
+
PermissionError)` and `NOT_EMPTY_ERRNOS = (ENOTEMPTY, EEXIST)` are the
|
|
2248
|
+
accepted platform variants.
|
|
2249
|
+
|
|
2250
|
+
```python
|
|
2251
|
+
import pytest
|
|
2252
|
+
from pathapi import LocalPath as MyPath # your Path subclass
|
|
2253
|
+
from pathapi.testing import PathContract, populate_fixture_tree
|
|
2254
|
+
|
|
2255
|
+
class TestMyPath(PathContract):
|
|
2256
|
+
@pytest.fixture
|
|
2257
|
+
def root(self, tmp_path):
|
|
2258
|
+
return populate_fixture_tree(MyPath(tmp_path))
|
|
2259
|
+
```
|
|
2260
|
+
|
|
2261
|
+
### Testing support: the HTTP transport suite (`pathapi.http.testing`, needs `pytest`)
|
|
2262
|
+
|
|
2263
|
+
This is testing support, not part of the stable API: it is importable and
|
|
2264
|
+
documented here, but its contents, including test names, fixtures and helper
|
|
2265
|
+
signatures, may change in any release. `pathapi.http` never imports it and
|
|
2266
|
+
the root does not export it.
|
|
2267
|
+
|
|
2268
|
+
```python
|
|
2269
|
+
# pathapi.http.testing
|
|
2270
|
+
def suite_directory() -> pathlib.Path: ...
|
|
2271
|
+
def run(arguments=None) -> int: ...
|
|
2272
|
+
```
|
|
2273
|
+
|
|
2274
|
+
A distribution that provides a transport holds it to the promises of the
|
|
2275
|
+
built-in one by running the tests `pathapi` runs over its own, with the
|
|
2276
|
+
distribution and `pytest` installed:
|
|
2277
|
+
|
|
2278
|
+
```
|
|
2279
|
+
python -m pathapi.http.testing --http-transports requests -q
|
|
2280
|
+
```
|
|
2281
|
+
|
|
2282
|
+
Everything after `python -m pathapi.http.testing` goes to pytest.
|
|
2283
|
+
`--http-transports` takes names that `transport_names()` lists, separated by
|
|
2284
|
+
commas (default `builtin`): each test that depends on the transport runs once
|
|
2285
|
+
per name, with `PATHAPI_HTTP_TRANSPORT` set to it, so the clients the
|
|
2286
|
+
test builds use `get_transport(name)()`. The tests are `HTTPClient` over
|
|
2287
|
+
loopback servers (redirects and the rule for `Authorization`, replies cut short
|
|
2288
|
+
or too large, errors, headers) and the transport contract against servers
|
|
2289
|
+
scripted at the byte level (what the server received is what is asserted). The
|
|
2290
|
+
modules also hold tests that do not depend on the transport; they pass whatever
|
|
2291
|
+
the name. Every connection stays on the loopback interface, a test that tries
|
|
2292
|
+
another host fails, and the proxy and certificate settings of the environment
|
|
2293
|
+
are removed for the run. The TLS tests need `cryptography` and skip without it.
|
|
2294
|
+
Importing `pathapi.http.testing` loads `pathapi.http` but not `pytest`.
|
|
2295
|
+
|
|
2296
|
+
- `run(arguments=None)` runs the suite with pytest and returns the exit status;
|
|
2297
|
+
it raises `ImportError` without pytest.
|
|
2298
|
+
- `suite_directory()` is the directory that holds the test modules,
|
|
2299
|
+
`MODULES` (their file names) and `HELPERS` (the files they import):
|
|
2300
|
+
`pathapi/http/testing/suite` in an installed package, `tests/transport`
|
|
2301
|
+
in a source checkout; `FileNotFoundError` when neither is there.
|
|
2302
|
+
|
|
2303
|
+
## Exceptions
|
|
2304
|
+
|
|
2305
|
+
A failure is the exception `pathlib` raises for the same condition
|
|
2306
|
+
(`FileNotFoundError`, `FileExistsError`, `IsADirectoryError`,
|
|
2307
|
+
`NotADirectoryError`, `PermissionError`, or an `OSError` with an `errno`),
|
|
2308
|
+
`NotImplementedError` for an operation a class does not support, and
|
|
2309
|
+
`ValueError`/`TypeError` for a bad argument. Those the package defines are
|
|
2310
|
+
listed here; the conditions that raise them are in the entries.
|
|
2311
|
+
|
|
2312
|
+
### `pathapi.exceptions` (also exported from `pathapi`)
|
|
2313
|
+
|
|
2314
|
+
```python
|
|
2315
|
+
class PathError(Exception): ...
|
|
2316
|
+
class NonRelativePatternError(PathError, NotImplementedError, ValueError): ...
|
|
2317
|
+
```
|
|
2318
|
+
|
|
2319
|
+
`PathError` is the base of every exception the core defines for a failure at
|
|
2320
|
+
run time that no builtin describes; an I/O failure is never a `PathError`.
|
|
2321
|
+
`glob()` raises `NonRelativePatternError` for an absolute pattern, and it is
|
|
2322
|
+
caught as `PathError`, `NotImplementedError` or `ValueError`.
|
|
2323
|
+
|
|
2324
|
+
## Environment variables
|
|
2325
|
+
|
|
2326
|
+
The package reads one.
|
|
2327
|
+
|
|
2328
|
+
### `PATHAPI_HTTP_TRANSPORT` (`pathapi.http`)
|
|
2329
|
+
|
|
2330
|
+
`builtin` (the default when unset), or the name of a
|
|
2331
|
+
transport an installed distribution declares in the group
|
|
2332
|
+
`pathapi.http_transports` (`requests`, from `pathapi-requests`): the
|
|
2333
|
+
transport an `HTTPClient` builds when it was given none. `requests` without
|
|
2334
|
+
that distribution raises `ImportError` naming it; the value is case-sensitive,
|
|
2335
|
+
and an empty or unrecognised one raises `ValueError` naming the choices
|
|
2336
|
+
(`transport_names()`). It is read when a client first needs its transport,
|
|
2337
|
+
once per client, so a change reaches clients that need theirs afterwards.
|
|
2338
|
+
|
|
2339
|
+
## Gotchas
|
|
2340
|
+
|
|
2341
|
+
- A `str` argument to `is_relative_to()` is parsed standalone via
|
|
2342
|
+
`self.with_segments(other)`, which keeps per-instance state (a `MemPath`
|
|
2343
|
+
backend). Normalize strings the same way in subclasses: `type(self)(x)`
|
|
2344
|
+
drops that state.
|
|
2345
|
+
|
|
2346
|
+
- **A binding is not a symlink.** `is_junction()` (pathlib 3.12 parity:
|
|
2347
|
+
a Windows junction) and `is_mount()` (a mount point, bind mounts
|
|
2348
|
+
included) report a directory that is another tree's second NAME.
|
|
2349
|
+
Neither is a symlink: `is_symlink()` is False, `readlink()` says
|
|
2350
|
+
nothing, and a non-following stat calls it an ordinary directory — which
|
|
2351
|
+
is why a symlink check cannot protect a walk from one. `is_dir_binding()`
|
|
2352
|
+
is the pair, and is what `rm(recursive=True)` consults: it removes the
|
|
2353
|
+
binding itself rather than the contents behind it (`rmdir()` on a live
|
|
2354
|
+
mount fails instead of emptying the mounted filesystem).
|
|
2355
|
+
Default False everywhere; `LocalPath` answers for real.
|