pathapi 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pathapi/AGENTS.md +2355 -0
- pathapi/README.md +205 -0
- pathapi/__init__.py +62 -0
- pathapi/_commit.py +116 -0
- pathapi/_discard.py +92 -0
- pathapi/_errorpolicy.py +106 -0
- pathapi/_filestat.py +194 -0
- pathapi/_lru.py +138 -0
- pathapi/_names.py +70 -0
- pathapi/_permissions.py +102 -0
- pathapi/_stubs.py +76 -0
- pathapi/_tree.py +136 -0
- pathapi/archive/__init__.py +44 -0
- pathapi/archive/_backend.py +393 -0
- pathapi/archive/_names.py +63 -0
- pathapi/archive/_pack.py +394 -0
- pathapi/archive/_path.py +352 -0
- pathapi/archive/_snapshot.py +269 -0
- pathapi/archive/_tar.py +126 -0
- pathapi/archive/_write.py +359 -0
- pathapi/archive/_zip.py +563 -0
- pathapi/archive/_zipentries.py +272 -0
- pathapi/checksum.py +61 -0
- pathapi/exceptions.py +22 -0
- pathapi/fspath.py +513 -0
- pathapi/ftp/__init__.py +27 -0
- pathapi/ftp/_backend.py +418 -0
- pathapi/ftp/_facts.py +117 -0
- pathapi/ftp/_path.py +582 -0
- pathapi/glob/__init__.py +27 -0
- pathapi/glob/_entries.py +103 -0
- pathapi/glob/_patterns.py +164 -0
- pathapi/glob/_recursion.py +180 -0
- pathapi/glob/_selection.py +303 -0
- pathapi/http/__init__.py +84 -0
- pathapi/http/_body.py +156 -0
- pathapi/http/_builtin.py +553 -0
- pathapi/http/_client.py +211 -0
- pathapi/http/_errors.py +79 -0
- pathapi/http/_follow.py +230 -0
- pathapi/http/_headers.py +233 -0
- pathapi/http/_pool.py +169 -0
- pathapi/http/_proxies.py +142 -0
- pathapi/http/_response.py +255 -0
- pathapi/http/_settings.py +12 -0
- pathapi/http/_status.py +68 -0
- pathapi/http/_transports.py +173 -0
- pathapi/http/testing/__init__.py +80 -0
- pathapi/http/testing/__main__.py +10 -0
- pathapi/http/testing/suite/conftest.py +98 -0
- pathapi/http/testing/suite/test_client_default.py +285 -0
- pathapi/http/testing/suite/test_client_errors.py +181 -0
- pathapi/http/testing/suite/test_client_peer_text.py +86 -0
- pathapi/http/testing/suite/test_client_redirect_origin.py +134 -0
- pathapi/http/testing/suite/test_client_redirects.py +231 -0
- pathapi/http/testing/suite/test_client_reply_limits.py +201 -0
- pathapi/http/testing/suite/test_client_write_redirects.py +239 -0
- pathapi/http/testing/suite/test_transport_body.py +302 -0
- pathapi/http/testing/suite/test_transport_send.py +662 -0
- pathapi/http/testing/suite/transport_fakes.py +133 -0
- pathapi/http/testing/suite/transport_guard.py +174 -0
- pathapi/http/testing/suite/transport_wire.py +511 -0
- pathapi/mempath.py +438 -0
- pathapi/path/__init__.py +19 -0
- pathapi/path/_info.py +10 -0
- pathapi/path/_path.py +198 -0
- pathapi/path/_pure.py +522 -0
- pathapi/path/_relation.py +10 -0
- pathapi/path/_transfer.py +10 -0
- pathapi/protocols/__init__.py +35 -0
- pathapi/protocols/_bundles.py +22 -0
- pathapi/protocols/_errors.py +12 -0
- pathapi/protocols/_info.py +65 -0
- pathapi/protocols/_relation.py +191 -0
- pathapi/protocols/_removal.py +69 -0
- pathapi/protocols/_support.py +122 -0
- pathapi/protocols/_transfer.py +100 -0
- pathapi/protocols/checksum.py +80 -0
- pathapi/protocols/fs.py +429 -0
- pathapi/protocols/io.py +224 -0
- pathapi/protocols/links.py +135 -0
- pathapi/protocols/pure.py +31 -0
- pathapi/protocols/resolve.py +97 -0
- pathapi/protocols/transfer.py +656 -0
- pathapi/protocols/tree.py +492 -0
- pathapi/providers.py +48 -0
- pathapi/py.typed +1 -0
- pathapi/sync/__init__.py +8 -0
- pathapi/sync/_compare.py +157 -0
- pathapi/sync/_events.py +97 -0
- pathapi/sync/_links.py +93 -0
- pathapi/sync/_offer.py +33 -0
- pathapi/sync/_paths.py +50 -0
- pathapi/sync/_replace.py +98 -0
- pathapi/sync/_syncer.py +989 -0
- pathapi/testing/__init__.py +96 -0
- pathapi/testing/_contract.py +522 -0
- pathapi/testing/_fixtures.py +83 -0
- pathapi/testing/_read.py +279 -0
- pathapi-0.1.0.dist-info/METADATA +242 -0
- pathapi-0.1.0.dist-info/RECORD +104 -0
- pathapi-0.1.0.dist-info/WHEEL +4 -0
- pathapi-0.1.0.dist-info/entry_points.txt +2 -0
- pathapi-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import stat as _stat
|
|
4
|
+
import struct as _struct
|
|
5
|
+
import tempfile as _tempfile
|
|
6
|
+
import time as _time
|
|
7
|
+
import zipfile as _zipfile
|
|
8
|
+
|
|
9
|
+
from ._backend import MEMBER_SPOOL_BYTES
|
|
10
|
+
from ._snapshot import NO_LINK as _NO_LINK
|
|
11
|
+
|
|
12
|
+
_ZIP64_EXTRA_ID = 0x0001
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _strip_zip64_extra(extra: bytes) -> bytes:
|
|
16
|
+
"""Drop zip64 extended-information records from a raw extra field.
|
|
17
|
+
`zipfile` writes its own when an entry needs them; a copied one would
|
|
18
|
+
carry the old sizes/offsets and conflict with the rewritten entry."""
|
|
19
|
+
kept = bytearray()
|
|
20
|
+
offset = 0
|
|
21
|
+
while offset + 4 <= len(extra):
|
|
22
|
+
header_id, size = _struct.unpack("<HH", extra[offset : offset + 4])
|
|
23
|
+
end = offset + 4 + size
|
|
24
|
+
if header_id != _ZIP64_EXTRA_ID:
|
|
25
|
+
kept += extra[offset:end]
|
|
26
|
+
offset = end
|
|
27
|
+
return bytes(kept)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _copy_zipinfo(info: _zipfile.ZipInfo, name: str) -> _zipfile.ZipInfo:
|
|
31
|
+
"""A fresh `ZipInfo` for `name` carrying `info`'s member metadata
|
|
32
|
+
(timestamp, compression, permissions, comment, extra) -- the fields
|
|
33
|
+
`writestr()` would otherwise reset. Sizes, CRC and offsets are left for
|
|
34
|
+
`writestr()` to compute."""
|
|
35
|
+
copied = _zipfile.ZipInfo(name, date_time=info.date_time)
|
|
36
|
+
copied.compress_type = info.compress_type
|
|
37
|
+
copied.comment = info.comment
|
|
38
|
+
copied.extra = _strip_zip64_extra(info.extra)
|
|
39
|
+
copied.create_system = info.create_system
|
|
40
|
+
copied.internal_attr = info.internal_attr
|
|
41
|
+
copied.external_attr = info.external_attr
|
|
42
|
+
return copied
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _member_mode(info: _zipfile.ZipInfo) -> "int | None":
|
|
46
|
+
"""The Unix mode stored for `info`, or None when there is none: only a
|
|
47
|
+
Unix-made entry (`create_system == 3`) carries one, in the high 16 bits
|
|
48
|
+
of `external_attr`. A special type other than a symlink (which
|
|
49
|
+
`_ZipBackend._stat_of` reports as one) is not modelled and reports no
|
|
50
|
+
mode either."""
|
|
51
|
+
mode = info.external_attr >> 16 if info.create_system == 3 else 0
|
|
52
|
+
if not _stat.S_IMODE(mode):
|
|
53
|
+
return None
|
|
54
|
+
kind = _stat.S_IFMT(mode)
|
|
55
|
+
if info.filename.endswith("/"):
|
|
56
|
+
kind = _stat.S_IFDIR
|
|
57
|
+
elif kind == 0:
|
|
58
|
+
kind = _stat.S_IFREG
|
|
59
|
+
if kind not in (_stat.S_IFREG, _stat.S_IFDIR):
|
|
60
|
+
return None
|
|
61
|
+
return kind | _stat.S_IMODE(mode)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
_MAX_LINK_TEXT = 4096
|
|
65
|
+
"""The longest text a zip link member may store (`PATH_MAX`); a longer one is
|
|
66
|
+
a link whose text cannot be read."""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _is_link(info: _zipfile.ZipInfo) -> bool:
|
|
70
|
+
"""Whether `info` is a symbolic link: an entry made on Unix whose mode
|
|
71
|
+
says so. Its data is the text of the link."""
|
|
72
|
+
return (
|
|
73
|
+
info.create_system == 3
|
|
74
|
+
and not info.filename.endswith("/")
|
|
75
|
+
and _stat.S_ISLNK(info.external_attr >> 16)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
_COPY_CHUNK = 1024 * 1024
|
|
80
|
+
_LOCAL_HEADER = _struct.Struct("<4s2B4HL2L2H")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _copy_range(source, target, length: int) -> None:
|
|
84
|
+
"""Copy `length` bytes from `source` to `target` in chunks."""
|
|
85
|
+
while length > 0:
|
|
86
|
+
chunk = source.read(min(length, _COPY_CHUNK))
|
|
87
|
+
if not chunk:
|
|
88
|
+
raise _zipfile.BadZipFile("archive ended inside an entry")
|
|
89
|
+
target.write(chunk)
|
|
90
|
+
length -= len(chunk)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _copy_entry(original, dst, info, new_name: str, end: "int | None") -> bool:
|
|
94
|
+
"""Append the entry `info` to the archive being written by `dst` under
|
|
95
|
+
`new_name`, copying its compressed bytes as they are: the local header, the
|
|
96
|
+
data and any data descriptor lie between `info.header_offset` and `end`
|
|
97
|
+
(where the next record starts). Only a renamed entry, or one whose name
|
|
98
|
+
the central directory would write in another encoding than the local
|
|
99
|
+
header, gets a new local header. False, with nothing written, when the
|
|
100
|
+
record is not laid out as its directory says; the caller then recompresses
|
|
101
|
+
it."""
|
|
102
|
+
start = info.header_offset
|
|
103
|
+
if end is None or start < 0 or end <= start:
|
|
104
|
+
return False
|
|
105
|
+
original.seek(start)
|
|
106
|
+
header = original.read(_LOCAL_HEADER.size)
|
|
107
|
+
if len(header) < _LOCAL_HEADER.size:
|
|
108
|
+
return False
|
|
109
|
+
fields = _LOCAL_HEADER.unpack(header)
|
|
110
|
+
header_size = _LOCAL_HEADER.size + fields[10] + fields[11]
|
|
111
|
+
if fields[0] != b"PK\x03\x04" or start + header_size + info.compress_size > end:
|
|
112
|
+
return False
|
|
113
|
+
verbatim = new_name == info.filename and (
|
|
114
|
+
info.filename.isascii() or info.flag_bits & 0x800
|
|
115
|
+
)
|
|
116
|
+
if not verbatim and (
|
|
117
|
+
info.flag_bits & 0x08
|
|
118
|
+
and max(info.file_size, info.compress_size) > _zipfile.ZIP64_LIMIT
|
|
119
|
+
):
|
|
120
|
+
return False
|
|
121
|
+
copied = _copy_zipinfo(info, new_name)
|
|
122
|
+
for name in (
|
|
123
|
+
"create_version",
|
|
124
|
+
"extract_version",
|
|
125
|
+
"reserved",
|
|
126
|
+
"flag_bits",
|
|
127
|
+
"volume",
|
|
128
|
+
"CRC",
|
|
129
|
+
"compress_size",
|
|
130
|
+
"file_size",
|
|
131
|
+
):
|
|
132
|
+
setattr(copied, name, getattr(info, name))
|
|
133
|
+
out = dst.fp
|
|
134
|
+
out.seek(dst.start_dir)
|
|
135
|
+
copied.header_offset = out.tell()
|
|
136
|
+
if verbatim:
|
|
137
|
+
original.seek(start)
|
|
138
|
+
_copy_range(original, out, end - start)
|
|
139
|
+
else:
|
|
140
|
+
out.write(copied.FileHeader())
|
|
141
|
+
original.seek(start + header_size)
|
|
142
|
+
_copy_range(original, out, end - start - header_size)
|
|
143
|
+
dst.start_dir = out.tell()
|
|
144
|
+
dst.filelist.append(copied)
|
|
145
|
+
dst.NameToInfo[copied.filename] = copied
|
|
146
|
+
return True
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _remapper(exclude, rename):
|
|
150
|
+
"""`remap(name) -> (new name | None, renamed)` for a rewrite: a name in
|
|
151
|
+
`exclude` goes, one in `rename` takes its new name, and a `<name>/` key of
|
|
152
|
+
`rename` also renames every entry nested under that prefix."""
|
|
153
|
+
prefixes = {old: new for old, new in rename.items() if old.endswith("/")}
|
|
154
|
+
|
|
155
|
+
def remap(name: str) -> "tuple[str | None, bool]":
|
|
156
|
+
if name in exclude:
|
|
157
|
+
return None, False
|
|
158
|
+
if name in rename:
|
|
159
|
+
return rename[name], True
|
|
160
|
+
for old_prefix, new_prefix in prefixes.items():
|
|
161
|
+
if name.startswith(old_prefix):
|
|
162
|
+
return new_prefix + name[len(old_prefix) :], True
|
|
163
|
+
return name, False
|
|
164
|
+
|
|
165
|
+
return remap
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _collapse(items, remap):
|
|
169
|
+
"""`[(new name, payload, renamed)]` for the `(name, payload)` pairs of an
|
|
170
|
+
archive after `remap`, in archive order. A renamed entry replaces any
|
|
171
|
+
entry at its new name (POSIX rename); otherwise the last of several
|
|
172
|
+
entries with one name is the one `zipfile` reads, and the others go."""
|
|
173
|
+
plan = []
|
|
174
|
+
winner = {}
|
|
175
|
+
for name, payload in items:
|
|
176
|
+
new_name, renamed = remap(name)
|
|
177
|
+
if new_name is None:
|
|
178
|
+
continue
|
|
179
|
+
previous = winner.get(new_name)
|
|
180
|
+
if previous is None or renamed or not plan[previous][2]:
|
|
181
|
+
winner[new_name] = len(plan)
|
|
182
|
+
plan.append((new_name, payload, renamed))
|
|
183
|
+
return [step for index, step in enumerate(plan) if winner[step[0]] == index]
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _now() -> "tuple[int, ...]":
|
|
187
|
+
"""The current local time as a zip stores it: whole even seconds."""
|
|
188
|
+
now = _time.localtime()[:6]
|
|
189
|
+
return (*now[:5], now[5] & ~1)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _new_info(name: str, size: int) -> _zipfile.ZipInfo:
|
|
193
|
+
"""The `ZipInfo` `ZipFile.writestr(name, data)` would give a new entry of
|
|
194
|
+
`size` bytes."""
|
|
195
|
+
info = _zipfile.ZipInfo(name, date_time=_now())
|
|
196
|
+
info.compress_type = _zipfile.ZIP_DEFLATED
|
|
197
|
+
if info.filename.endswith("/"):
|
|
198
|
+
info.external_attr = (0o40775 << 16) | 0x10
|
|
199
|
+
else:
|
|
200
|
+
info.external_attr = 0o600 << 16
|
|
201
|
+
info.file_size = size
|
|
202
|
+
return info
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _renamed_info(info: _zipfile.ZipInfo, name: str) -> _zipfile.ZipInfo:
|
|
206
|
+
"""`_copy_zipinfo()` that also keeps the size a listing reports."""
|
|
207
|
+
copied = _copy_zipinfo(info, name)
|
|
208
|
+
copied.file_size = info.file_size
|
|
209
|
+
return copied
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class _Scratch:
|
|
213
|
+
"""The data a batch holds for members written in it: in memory up to
|
|
214
|
+
`MEMBER_SPOOL_BYTES` altogether, in an unnamed temporary file past that."""
|
|
215
|
+
|
|
216
|
+
__slots__ = ("_file", "_end")
|
|
217
|
+
|
|
218
|
+
def __init__(self):
|
|
219
|
+
self._file = _tempfile.SpooledTemporaryFile(max_size=MEMBER_SPOOL_BYTES)
|
|
220
|
+
self._end = 0
|
|
221
|
+
|
|
222
|
+
def add(self, data: bytes) -> "tuple[int, int]":
|
|
223
|
+
self._file.seek(self._end)
|
|
224
|
+
self._file.write(data)
|
|
225
|
+
reference = (self._end, len(data))
|
|
226
|
+
self._end += len(data)
|
|
227
|
+
return reference
|
|
228
|
+
|
|
229
|
+
def get(self, reference: "tuple[int, int]") -> bytes:
|
|
230
|
+
offset, length = reference
|
|
231
|
+
self._file.seek(offset)
|
|
232
|
+
return self._file.read(length)
|
|
233
|
+
|
|
234
|
+
def close(self) -> None:
|
|
235
|
+
self._file.close()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class _Pending:
|
|
239
|
+
"""One member of a batch: the `ZipInfo` a listing reports for it, the
|
|
240
|
+
entry of the archive file whose stored bytes it is (`origin`, None for a
|
|
241
|
+
member written in the batch) and where the written bytes are (`data`).
|
|
242
|
+
`link` is the text of a link member (None when unreadable), else
|
|
243
|
+
`_NO_LINK`. `seq` is its place in the archive: a renamed member keeps it,
|
|
244
|
+
a new one comes last."""
|
|
245
|
+
|
|
246
|
+
__slots__ = ("info", "origin", "data", "link", "seq")
|
|
247
|
+
|
|
248
|
+
def __init__(self, info, origin, data, link=_NO_LINK, seq=0):
|
|
249
|
+
self.info = info
|
|
250
|
+
self.origin = origin
|
|
251
|
+
self.data = data
|
|
252
|
+
self.link = link
|
|
253
|
+
self.seq = seq
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class _Batch:
|
|
257
|
+
"""What a batch changed. `table` maps each member's name to its `_Pending`
|
|
258
|
+
and is read from the archive on the first change; `signature` is the file
|
|
259
|
+
it was read from (None: there was none), checked again before the archive
|
|
260
|
+
is replaced. `snapshot` is the member list derived from the table, kept up
|
|
261
|
+
to date change by change once it exists; `next_seq` is the place of the
|
|
262
|
+
next new member."""
|
|
263
|
+
|
|
264
|
+
__slots__ = ("table", "signature", "scratch", "dirty", "snapshot", "next_seq")
|
|
265
|
+
|
|
266
|
+
def __init__(self):
|
|
267
|
+
self.table = None
|
|
268
|
+
self.signature = None
|
|
269
|
+
self.scratch = _Scratch()
|
|
270
|
+
self.dirty = False
|
|
271
|
+
self.snapshot = None
|
|
272
|
+
self.next_seq = 0
|
pathapi/checksum.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib as _hashlib
|
|
4
|
+
|
|
5
|
+
__all__ = ["md5", "native", "sha256", "stream"]
|
|
6
|
+
|
|
7
|
+
from .protocols import BinaryOpen as _BinaryOpen
|
|
8
|
+
from .protocols import NativeChecksum as _NativeChecksum
|
|
9
|
+
from .protocols import supports as _supports
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def native(
|
|
13
|
+
path: "_NativeChecksum | _BinaryOpen", algorithm: str = "md5"
|
|
14
|
+
) -> "str | None":
|
|
15
|
+
"""Try `path`'s backend-native `NativeChecksum.checksum()` (see
|
|
16
|
+
`protocols/checksum.py`); return `None` if `path` doesn't implement the
|
|
17
|
+
protocol at all, or if it does but can't produce `algorithm` (raises
|
|
18
|
+
`NotImplementedError` -- the protocol's documented contract for
|
|
19
|
+
"unsupported", never a wrong-algorithm value). Never raises for either
|
|
20
|
+
of those two cases; any other exception the backend raises propagates.
|
|
21
|
+
"""
|
|
22
|
+
if not _supports(path, _NativeChecksum):
|
|
23
|
+
return None
|
|
24
|
+
checksum = getattr(path, "checksum", None)
|
|
25
|
+
if checksum is None:
|
|
26
|
+
return None
|
|
27
|
+
try:
|
|
28
|
+
return checksum(algorithm)
|
|
29
|
+
except NotImplementedError:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def md5(path: "_BinaryOpen", chunk_size: int = 65536) -> str:
|
|
34
|
+
"""Calculate MD5 checksum of the file at `path`."""
|
|
35
|
+
return stream(path, "md5", chunk_size)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def sha256(path: "_BinaryOpen", chunk_size: int = 65536) -> str:
|
|
39
|
+
"""Calculate SHA-256 checksum of the file at `path`."""
|
|
40
|
+
return stream(path, "sha256", chunk_size)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def stream(path: "_BinaryOpen", algorithm: str = "md5", chunk_size: int = 65536) -> str:
|
|
44
|
+
"""Streaming checksum for an arbitrary `hashlib` `algorithm` name (the
|
|
45
|
+
generic form of `md5`/`sha256` above, used where the algorithm is a
|
|
46
|
+
runtime parameter rather than fixed at the call site -- e.g.
|
|
47
|
+
`PathSyncer`'s streaming fallback, which must match whatever algorithm
|
|
48
|
+
a native checksum attempt was made under).
|
|
49
|
+
|
|
50
|
+
The digest compares content; it is not a security use, so it is created
|
|
51
|
+
with `usedforsecurity=False` -- on a FIPS-mode host `hashlib.md5()`
|
|
52
|
+
otherwise raises ValueError.
|
|
53
|
+
"""
|
|
54
|
+
h = _hashlib.new(algorithm, usedforsecurity=False)
|
|
55
|
+
with path.open("rb") as f:
|
|
56
|
+
while True:
|
|
57
|
+
chunk = f.read(chunk_size)
|
|
58
|
+
if not chunk:
|
|
59
|
+
break
|
|
60
|
+
h.update(chunk)
|
|
61
|
+
return h.hexdigest()
|
pathapi/exceptions.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""The exceptions `pathapi` defines.
|
|
2
|
+
|
|
3
|
+
A failure of the file system or of a backend is the `OSError` type that
|
|
4
|
+
`pathlib` raises for the same condition (`FileNotFoundError`,
|
|
5
|
+
`PermissionError`, ...); this module holds what has no such type. A bad
|
|
6
|
+
argument is a plain `ValueError` or `TypeError`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
__all__ = ["NonRelativePatternError", "PathError"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PathError(Exception):
|
|
15
|
+
"""Base class of the exceptions `pathapi` defines for a failure at
|
|
16
|
+
run time that no builtin describes."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class NonRelativePatternError(PathError, NotImplementedError, ValueError):
|
|
20
|
+
"""An absolute or anchored glob pattern. pathlib raises
|
|
21
|
+
`NotImplementedError("Non-relative patterns are unsupported")`; this is
|
|
22
|
+
also a `ValueError`, so either `except` clause catches it."""
|