pathapi-uri 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_uri-0.1.0.dist-info/METADATA +259 -0
- pathapi_uri-0.1.0.dist-info/RECORD +48 -0
- pathapi_uri-0.1.0.dist-info/WHEEL +4 -0
- pathapi_uri-0.1.0.dist-info/entry_points.txt +17 -0
- pathapi_uri-0.1.0.dist-info/licenses/LICENSE +21 -0
- uripath/AGENTS.md +1094 -0
- uripath/README.md +213 -0
- uripath/__init__.py +27 -0
- uripath/__main__.py +7 -0
- uripath/_pathtext.py +32 -0
- uripath/_rfc3986.py +67 -0
- uripath/_transfer.py +108 -0
- uripath/_uri.py +1007 -0
- uripath/_version.py +11 -0
- uripath/cli/__init__.py +54 -0
- uripath/cli/_common.py +48 -0
- uripath/cli/_root.py +70 -0
- uripath/cli/cp.py +61 -0
- uripath/cli/read.py +22 -0
- uripath/cli/rm.py +38 -0
- uripath/cli/sync.py +78 -0
- uripath/cli/write.py +49 -0
- uripath/exceptions.py +38 -0
- uripath/py.typed +1 -0
- uripath/query.py +240 -0
- uripath/schemes/__init__.py +120 -0
- uripath/schemes/_archive.py +338 -0
- uripath/schemes/_data.py +99 -0
- uripath/schemes/_dav.py +459 -0
- uripath/schemes/_delegate.py +127 -0
- uripath/schemes/_elsewhere.py +102 -0
- uripath/schemes/_file.py +186 -0
- uripath/schemes/_ftp.py +200 -0
- uripath/schemes/_http/__init__.py +4 -0
- uripath/schemes/_http/_backend.py +132 -0
- uripath/schemes/_http/_headers.py +8 -0
- uripath/schemes/_http/_listing.py +403 -0
- uripath/schemes/_http/_path.py +371 -0
- uripath/schemes/_http/_settings.py +6 -0
- uripath/schemes/_http/_streams.py +230 -0
- uripath/schemes/_httpdate.py +48 -0
- uripath/schemes/_path.py +629 -0
- uripath/schemes/_registry.py +134 -0
- uripath/source/__init__.py +20 -0
- uripath/source/_compose.py +212 -0
- uripath/source/_locality.py +44 -0
- uripath/source/_parse.py +183 -0
- uripath/source/_source.py +239 -0
uripath/schemes/_path.py
ADDED
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
"""`URIPath`: the class a URI scheme subclasses, and the backend bookkeeping it shares between the paths of one endpoint."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import stat as _stat
|
|
6
|
+
import threading as _threading
|
|
7
|
+
import typing as _ty
|
|
8
|
+
import weakref as _weakref
|
|
9
|
+
import re as _re
|
|
10
|
+
|
|
11
|
+
from pathapi import FileStat, Path
|
|
12
|
+
from pathapi import providers as _providers
|
|
13
|
+
from .._uri import URI, URILike
|
|
14
|
+
from .._uri import _DEFAULT_CHILD_BUILDERS
|
|
15
|
+
from ..exceptions import UnsupportedSchemeError
|
|
16
|
+
from ..source import Source
|
|
17
|
+
from ..source._source import _same_authority
|
|
18
|
+
from ._registry import (
|
|
19
|
+
_BUILTIN_SCHEMES,
|
|
20
|
+
_CLAIMS,
|
|
21
|
+
_CLAIMS_LOCK,
|
|
22
|
+
_ENTRY_POINTS_LOADED,
|
|
23
|
+
_claim_schemes,
|
|
24
|
+
_declared_entry_points,
|
|
25
|
+
_forget_entry_points,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if _ty.TYPE_CHECKING:
|
|
29
|
+
from typing import Self
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
#: An RFC 3986 scheme followed by its colon, used to tell a URI destination
|
|
33
|
+
#: from a path one (see `_looks_like_uri`, which `as_path()` also applies).
|
|
34
|
+
_URI_SCHEME_RE = _re.compile(r"[A-Za-z][A-Za-z0-9+.\-]*:")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
#: Backends a path built for itself through `_initbackend()`, by `id()` (the `is`
|
|
38
|
+
#: test on lookup makes a reused id harmless). It covers a derived backend that
|
|
39
|
+
#: a scheme hands on through `backend=`, which would read as supplied.
|
|
40
|
+
_DERIVED_BACKENDS: "_weakref.WeakValueDictionary[int, object]" = (
|
|
41
|
+
_weakref.WeakValueDictionary()
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class DerivedBackend:
|
|
46
|
+
"""Marks a backend class whose instances only ever come from
|
|
47
|
+
`_initbackend()`, for a backend that cannot be weakly referenced and so
|
|
48
|
+
cannot be recorded in `_DERIVED_BACKENDS` (a tuple, say)."""
|
|
49
|
+
|
|
50
|
+
__slots__ = ()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
_DerivedBackend = DerivedBackend
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class _BackendCell:
|
|
57
|
+
"""The slot that paths derived from one another on one endpoint share for
|
|
58
|
+
the backend they derive for themselves. It starts empty, so deriving a
|
|
59
|
+
path builds nothing; the first of them to need a backend fills it and the
|
|
60
|
+
rest read it. `lock` makes two threads that ask at once build one."""
|
|
61
|
+
|
|
62
|
+
__slots__ = ("backend", "lock")
|
|
63
|
+
|
|
64
|
+
def __init__(self):
|
|
65
|
+
self.backend = None
|
|
66
|
+
self.lock = _threading.RLock()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
#: Makes "create the cell of a path unless it has one" atomic.
|
|
70
|
+
_CELL_LOCK = _threading.Lock()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _rebuild_path(cls, uri, options, backend, schemesmap):
|
|
74
|
+
"""Unpickle a `URIPath`: the class built from its URI text again."""
|
|
75
|
+
if backend is not None:
|
|
76
|
+
options = {**options, "backend": backend}
|
|
77
|
+
if schemesmap is not None:
|
|
78
|
+
options = {**options, "schemesmap": schemesmap}
|
|
79
|
+
return cls(uri, **options)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _looks_like_uri(value: str, schemesmap=None) -> bool:
|
|
83
|
+
"""Whether a destination string is URI syntax rather than a path.
|
|
84
|
+
|
|
85
|
+
Needs an RFC 3986 scheme prefix; without `://` the scheme must also be
|
|
86
|
+
one a class registers, so an ordinary relative name whose first segment
|
|
87
|
+
happens to carry a colon (`notes:draft`, `Fedora-42:latest.tar`) stays a
|
|
88
|
+
path. A one-letter scheme is a Windows drive (`C:/Temp`). `as_path()`
|
|
89
|
+
applies the same rule to a path given as text.
|
|
90
|
+
`schemesmap` is the allow-list the scheme is looked up in, if any.
|
|
91
|
+
"""
|
|
92
|
+
match = _URI_SCHEME_RE.match(value)
|
|
93
|
+
if match is None:
|
|
94
|
+
return False
|
|
95
|
+
if match.end() == 2: # "C:" -- a drive, not a scheme
|
|
96
|
+
return False
|
|
97
|
+
if "://" in value:
|
|
98
|
+
return True
|
|
99
|
+
scheme = value[: match.end() - 1].lower()
|
|
100
|
+
try:
|
|
101
|
+
return (
|
|
102
|
+
Source(scheme, None, None, None).get_scheme_cls(schemesmap) is not URIPath
|
|
103
|
+
)
|
|
104
|
+
except Exception:
|
|
105
|
+
# A scheme plugin that fails to load: treat it as a URI, as the
|
|
106
|
+
# scheme lookup would have.
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class URIPath(URI, Path):
|
|
111
|
+
"""`URI` + `Path` (I/O) + scheme dispatch. `URIPath(...)` constructs
|
|
112
|
+
the concrete subclass registered for the URI's scheme (via `__SCHEMES`)
|
|
113
|
+
-- e.g. `URIPath("http://...")` returns an `HTTPURIPath`. Subclass this
|
|
114
|
+
and set `__SCHEMES` to add a new scheme (Track B of extending this
|
|
115
|
+
library; see `docs/guides/extending.md`): defining the class registers
|
|
116
|
+
it, a name another class holds raises `ValueError` unless the class
|
|
117
|
+
statement passes `replace=True`, and `schemes()` / `scheme_class()` read
|
|
118
|
+
the registry. Implement the I/O surface
|
|
119
|
+
(`_listdir` or `_scandir`, `stat`, `_open`, ...) documented in
|
|
120
|
+
`docs/guides/extending.md`. Prefer overriding `_scandir()` over `_listdir()` when the
|
|
121
|
+
listing call already returns type/size/mtime metadata (PROPFIND, MLSD,
|
|
122
|
+
`listdir_attr`, an S3 list page, ...) -- `walk()`/`glob()` then answer
|
|
123
|
+
`is_dir()` on the results for free, without a stat request per entry."""
|
|
124
|
+
|
|
125
|
+
#: Slots: `_backend` (None until built or inherited), `_backend_derived` (the path
|
|
126
|
+
#: built it), `_backend_candidates` (join segments' backends until the endpoint is
|
|
127
|
+
#: known), `_derived_cell` (shared on one endpoint), `_schemes_in_use` (`schemesmap=`).
|
|
128
|
+
__slots__ = (
|
|
129
|
+
"_backend",
|
|
130
|
+
"_backend_derived",
|
|
131
|
+
"_backend_candidates",
|
|
132
|
+
"_derived_cell",
|
|
133
|
+
"_stat_hint",
|
|
134
|
+
"_schemes_in_use",
|
|
135
|
+
)
|
|
136
|
+
__SCHEMES: _ty.Sequence[str] = ()
|
|
137
|
+
#: A scheme that takes keyword arguments of its own adds their names.
|
|
138
|
+
_OPTIONS = frozenset({"backend", "schemesmap", "findclass"})
|
|
139
|
+
|
|
140
|
+
def __init_subclass__(cls, replace: bool = False, **kwargs):
|
|
141
|
+
super().__init_subclass__(**kwargs)
|
|
142
|
+
_claim_schemes(cls, replace)
|
|
143
|
+
_forget_entry_points()
|
|
144
|
+
|
|
145
|
+
@classmethod
|
|
146
|
+
def schemes(cls) -> "tuple[str, ...]":
|
|
147
|
+
"""The registered scheme names, sorted: those of every class defined
|
|
148
|
+
so far, every built-in scheme and every entry point of the
|
|
149
|
+
`uripath.schemes` group, whether or not its module is imported
|
|
150
|
+
yet. The schemes another distribution provides are listed too."""
|
|
151
|
+
from . import _elsewhere
|
|
152
|
+
|
|
153
|
+
with _CLAIMS_LOCK:
|
|
154
|
+
claimed = list(_CLAIMS)
|
|
155
|
+
return tuple(
|
|
156
|
+
sorted(
|
|
157
|
+
{
|
|
158
|
+
*claimed,
|
|
159
|
+
*_BUILTIN_SCHEMES,
|
|
160
|
+
*_elsewhere.SCHEMES,
|
|
161
|
+
*_declared_entry_points(),
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def scheme_class(cls, name: str) -> "type[URIPath]":
|
|
168
|
+
"""The class registered for the scheme `name` (case-insensitive),
|
|
169
|
+
importing its module or loading its entry point first when needed.
|
|
170
|
+
|
|
171
|
+
Raises `UnsupportedSchemeError` naming the registered schemes when there is
|
|
172
|
+
none, and `ImportError` for a built-in module that cannot be
|
|
173
|
+
imported and for a scheme another distribution provides that is not
|
|
174
|
+
installed (the error names what to install).
|
|
175
|
+
"""
|
|
176
|
+
found = cls._find_scheme(name.lower(), strict=True)
|
|
177
|
+
if found is None:
|
|
178
|
+
raise UnsupportedSchemeError(
|
|
179
|
+
f"no URI scheme {name!r} is registered; registered: "
|
|
180
|
+
+ ", ".join(cls.schemes())
|
|
181
|
+
)
|
|
182
|
+
return found
|
|
183
|
+
|
|
184
|
+
@classmethod
|
|
185
|
+
def _find_scheme(cls, scheme: str, strict: bool = False):
|
|
186
|
+
"""The class that registered `scheme`, or `None`: a class already
|
|
187
|
+
defined, then the built-in module, then an entry point, then the class
|
|
188
|
+
that stands for a scheme another distribution provides. With `strict`
|
|
189
|
+
a built-in module that fails to import, and a scheme provided
|
|
190
|
+
elsewhere that is not installed, raise instead of reading as "no
|
|
191
|
+
class" and as that stand-in."""
|
|
192
|
+
found = _CLAIMS.get(scheme)
|
|
193
|
+
if found is None and (
|
|
194
|
+
cls._load_builtin_scheme(scheme, strict=strict)
|
|
195
|
+
or cls._load_entry_point(scheme)
|
|
196
|
+
):
|
|
197
|
+
found = _CLAIMS.get(scheme)
|
|
198
|
+
if found is None:
|
|
199
|
+
from . import _elsewhere
|
|
200
|
+
|
|
201
|
+
if scheme not in _elsewhere.SCHEMES:
|
|
202
|
+
return None
|
|
203
|
+
if strict:
|
|
204
|
+
raise _elsewhere.not_installed(scheme)
|
|
205
|
+
found = _elsewhere.ProvidedElsewhere
|
|
206
|
+
return found
|
|
207
|
+
|
|
208
|
+
@classmethod
|
|
209
|
+
def _schemes(cls) -> _ty.Sequence[str]:
|
|
210
|
+
try:
|
|
211
|
+
return getattr(cls, f"_{cls.__name__}__SCHEMES")
|
|
212
|
+
except AttributeError:
|
|
213
|
+
return ()
|
|
214
|
+
|
|
215
|
+
@classmethod
|
|
216
|
+
def _load_entry_point(cls, scheme: str) -> bool:
|
|
217
|
+
"""Attempt to load a scheme class from package entry points.
|
|
218
|
+
|
|
219
|
+
Looks for entry points in the 'uripath.schemes' group where
|
|
220
|
+
the name matches the requested scheme.
|
|
221
|
+
"""
|
|
222
|
+
ep = _declared_entry_points().get(scheme)
|
|
223
|
+
if ep is None or scheme in _ENTRY_POINTS_LOADED:
|
|
224
|
+
return False
|
|
225
|
+
ep.load()
|
|
226
|
+
_ENTRY_POINTS_LOADED.add(scheme)
|
|
227
|
+
return True
|
|
228
|
+
|
|
229
|
+
@classmethod
|
|
230
|
+
def _load_builtin_scheme(cls, scheme: str, strict: bool = False) -> bool:
|
|
231
|
+
"""Attempt to load a builtin scheme module dynamically on-demand."""
|
|
232
|
+
module_name = _BUILTIN_SCHEMES.get(scheme)
|
|
233
|
+
if module_name:
|
|
234
|
+
import importlib
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
importlib.import_module(module_name)
|
|
238
|
+
return True
|
|
239
|
+
except ImportError:
|
|
240
|
+
if strict:
|
|
241
|
+
raise
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
def __new__(
|
|
245
|
+
cls,
|
|
246
|
+
*args,
|
|
247
|
+
schemesmap: "_ty.Mapping[str, type[URIPath]] | None" = None,
|
|
248
|
+
findclass=False,
|
|
249
|
+
**kwargs,
|
|
250
|
+
) -> "URIPath":
|
|
251
|
+
if cls is URIPath or findclass:
|
|
252
|
+
uri = URI(*args)
|
|
253
|
+
cls: type[URIPath] = uri.source.get_scheme_cls(schemesmap)
|
|
254
|
+
if cls is URIPath:
|
|
255
|
+
inst = URI.__new__(cls, *args, **kwargs)
|
|
256
|
+
else:
|
|
257
|
+
inst = cls.__new__(cls, *args, **kwargs)
|
|
258
|
+
inst._schemes_in_use = schemesmap
|
|
259
|
+
inst._init(uri.source, uri.path, uri.query, uri.fragment, **kwargs)
|
|
260
|
+
else:
|
|
261
|
+
inst = URI.__new__(cls, *args, **kwargs)
|
|
262
|
+
inst._schemes_in_use = schemesmap
|
|
263
|
+
backend = kwargs.get("backend", None)
|
|
264
|
+
if backend is not None:
|
|
265
|
+
inst._backend = backend
|
|
266
|
+
else:
|
|
267
|
+
# Which of these a path inherits depends on the endpoint it
|
|
268
|
+
# ends up with, known once it is parsed (`_inherit_backend`).
|
|
269
|
+
for segment in args:
|
|
270
|
+
if isinstance(segment, cls):
|
|
271
|
+
if segment._backend_candidates is not None:
|
|
272
|
+
# Parsing the segment settles its own inheritance.
|
|
273
|
+
segment._load_parts()
|
|
274
|
+
if inst._backend_candidates is None:
|
|
275
|
+
inst._backend_candidates = []
|
|
276
|
+
inst._backend_candidates.append(
|
|
277
|
+
(segment, segment._backend, segment._backend_derived)
|
|
278
|
+
)
|
|
279
|
+
return inst
|
|
280
|
+
|
|
281
|
+
def _initbackend(self):
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
def _inherit_backend(self, source: Source) -> None:
|
|
285
|
+
"""Settle which join segment's backend this path takes now that its
|
|
286
|
+
endpoint is `source`: the rightmost one that holds a backend and
|
|
287
|
+
belongs to that same endpoint (scheme, userinfo, host, port), else
|
|
288
|
+
none. A backend never crosses to another endpoint, so credentials and
|
|
289
|
+
sessions stay put. With none, the path joins the family of the
|
|
290
|
+
rightmost segment on its endpoint (`_family_for()`)."""
|
|
291
|
+
candidates = self._backend_candidates
|
|
292
|
+
if candidates is None:
|
|
293
|
+
return
|
|
294
|
+
family = None
|
|
295
|
+
for segment, backend, derived in reversed(candidates):
|
|
296
|
+
if not _same_authority(segment.source, source):
|
|
297
|
+
continue
|
|
298
|
+
if backend is not None:
|
|
299
|
+
self._backend, self._backend_derived = backend, derived
|
|
300
|
+
family = None
|
|
301
|
+
break
|
|
302
|
+
if family is None and source:
|
|
303
|
+
family = segment._family()
|
|
304
|
+
if family is not None:
|
|
305
|
+
self._derived_cell = family
|
|
306
|
+
self._backend_candidates = None
|
|
307
|
+
|
|
308
|
+
def _family(self) -> _BackendCell:
|
|
309
|
+
"""The cell this path and the paths derived from it share, made when
|
|
310
|
+
it is first asked for."""
|
|
311
|
+
cell = self._derived_cell
|
|
312
|
+
if cell is None:
|
|
313
|
+
with _CELL_LOCK:
|
|
314
|
+
cell = self._derived_cell
|
|
315
|
+
if cell is None:
|
|
316
|
+
cell = self._derived_cell = _BackendCell()
|
|
317
|
+
if self._backend is not None and self._backend_derived:
|
|
318
|
+
# Already built on its own: the family reads that one.
|
|
319
|
+
cell.backend = self._backend
|
|
320
|
+
return cell
|
|
321
|
+
|
|
322
|
+
def _family_for(self, source: Source) -> "_BackendCell | None":
|
|
323
|
+
"""The cell a path at `source` shares with this one: this path's own
|
|
324
|
+
when `source` is its endpoint, else none. Never builds a backend, and
|
|
325
|
+
a path that holds one hands that on instead (`_backend_for()`)."""
|
|
326
|
+
if not self._initiated:
|
|
327
|
+
self._load_parts()
|
|
328
|
+
if self._backend is not None:
|
|
329
|
+
return None
|
|
330
|
+
mine = self._source
|
|
331
|
+
if mine and (source is mine or _same_authority(source, mine)):
|
|
332
|
+
return self._family()
|
|
333
|
+
return None
|
|
334
|
+
|
|
335
|
+
def _backend_for(self, source: Source):
|
|
336
|
+
"""`(backend, derived)` a path at `source` inherits from this one:
|
|
337
|
+
what this path already holds, if `source` is its own endpoint, else
|
|
338
|
+
`(None, None)`. Never builds a backend."""
|
|
339
|
+
if not self._initiated:
|
|
340
|
+
self._load_parts()
|
|
341
|
+
backend = self._backend
|
|
342
|
+
if backend is not None:
|
|
343
|
+
mine = self._source
|
|
344
|
+
if source is mine or _same_authority(source, mine):
|
|
345
|
+
return backend, self._backend_derived
|
|
346
|
+
return None, None
|
|
347
|
+
|
|
348
|
+
def _from_parsed_parts(
|
|
349
|
+
self, source: Source, path: str, query: str, fragment: str, /, **kwargs
|
|
350
|
+
):
|
|
351
|
+
if "backend" in kwargs or (
|
|
352
|
+
self._backend is None and self._backend_candidates is None
|
|
353
|
+
):
|
|
354
|
+
# Given, or nothing to carry: the usual case before any I/O.
|
|
355
|
+
inst = super()._from_parsed_parts(source, path, query, fragment, **kwargs)
|
|
356
|
+
if "backend" not in kwargs:
|
|
357
|
+
# A path that already has a family and stays on its own
|
|
358
|
+
# source hands the cell on without asking again.
|
|
359
|
+
cell = self._derived_cell
|
|
360
|
+
inst._derived_cell = (
|
|
361
|
+
cell
|
|
362
|
+
if cell is not None and source is self._source
|
|
363
|
+
else self._family_for(source)
|
|
364
|
+
)
|
|
365
|
+
else:
|
|
366
|
+
backend, derived = self._backend_for(source)
|
|
367
|
+
if backend is not None:
|
|
368
|
+
kwargs["backend"] = backend
|
|
369
|
+
inst = super()._from_parsed_parts(source, path, query, fragment, **kwargs)
|
|
370
|
+
if backend is not None and inst._backend is backend:
|
|
371
|
+
inst._backend_derived = derived
|
|
372
|
+
elif backend is None:
|
|
373
|
+
inst._derived_cell = self._family_for(source)
|
|
374
|
+
inst._schemes_in_use = self._schemes_in_use
|
|
375
|
+
return inst
|
|
376
|
+
|
|
377
|
+
def _init(
|
|
378
|
+
self,
|
|
379
|
+
source: Source,
|
|
380
|
+
path: str,
|
|
381
|
+
query: str,
|
|
382
|
+
fragment: str,
|
|
383
|
+
/,
|
|
384
|
+
backend=None,
|
|
385
|
+
**kwargs,
|
|
386
|
+
):
|
|
387
|
+
self._inherit_backend(source)
|
|
388
|
+
if backend is not None:
|
|
389
|
+
self._backend = backend
|
|
390
|
+
self._backend_derived = None
|
|
391
|
+
super()._init(source, path, query, fragment, **kwargs)
|
|
392
|
+
|
|
393
|
+
@property
|
|
394
|
+
def backend(self):
|
|
395
|
+
"""The connection or session state backend instance."""
|
|
396
|
+
if not self._initiated:
|
|
397
|
+
# Settles a backend inherited from a join segment first.
|
|
398
|
+
self._load_parts()
|
|
399
|
+
backend = self._backend
|
|
400
|
+
if backend is None:
|
|
401
|
+
cell = self._derived_cell
|
|
402
|
+
built = False
|
|
403
|
+
if cell is None:
|
|
404
|
+
backend = self._initbackend()
|
|
405
|
+
built = backend is not None
|
|
406
|
+
else:
|
|
407
|
+
# The family builds one, whichever of them asks first.
|
|
408
|
+
with cell.lock:
|
|
409
|
+
backend = cell.backend
|
|
410
|
+
if backend is None:
|
|
411
|
+
backend = self._initbackend()
|
|
412
|
+
if backend is not None:
|
|
413
|
+
cell.backend = backend
|
|
414
|
+
built = True
|
|
415
|
+
if backend is not None:
|
|
416
|
+
self._backend = backend
|
|
417
|
+
self._backend_derived = True
|
|
418
|
+
if built:
|
|
419
|
+
try:
|
|
420
|
+
_DERIVED_BACKENDS[id(backend)] = backend
|
|
421
|
+
except TypeError:
|
|
422
|
+
pass
|
|
423
|
+
return backend
|
|
424
|
+
|
|
425
|
+
def _supplied_backend(self):
|
|
426
|
+
"""The backend the caller gave this path (`backend=`,
|
|
427
|
+
`with_backend()`, or inherited from a path that had one), or None
|
|
428
|
+
when it has none yet or derived its own. Never builds one."""
|
|
429
|
+
backend = self._backend
|
|
430
|
+
if (
|
|
431
|
+
backend is None
|
|
432
|
+
or self._backend_derived
|
|
433
|
+
or isinstance(backend, _DerivedBackend)
|
|
434
|
+
or _DERIVED_BACKENDS.get(id(backend)) is backend
|
|
435
|
+
):
|
|
436
|
+
return None
|
|
437
|
+
return backend
|
|
438
|
+
|
|
439
|
+
def _same_filesystem(self, other: "URIPath") -> bool:
|
|
440
|
+
# A URI's equality includes its authority, so two paths built separately for one
|
|
441
|
+
# URL are the same file; only two distinct SUPPLIED backends (separate
|
|
442
|
+
# connections, or fakes for two hosts) are not provably the same tree.
|
|
443
|
+
mine, theirs = self._supplied_backend(), other._supplied_backend()
|
|
444
|
+
return mine is None or theirs is None or mine is theirs
|
|
445
|
+
|
|
446
|
+
def _rename_compatible(self, target) -> bool:
|
|
447
|
+
# Every scheme's `rename()` reads its target through
|
|
448
|
+
# `_rename_target()`, which raises NotImplementedError for one it
|
|
449
|
+
# cannot reach, so `move()` falls back to copy + delete by itself.
|
|
450
|
+
return True
|
|
451
|
+
|
|
452
|
+
def _coerce_target(self, target: str) -> "URIPath":
|
|
453
|
+
"""A `str` destination for `copy()`/`move()`, read by its shape.
|
|
454
|
+
|
|
455
|
+
With a scheme (`s3://bucket/key`, `file:///tmp/x`) it is a URI, so a
|
|
456
|
+
cross-scheme copy keeps working. Without one it is a decoded path on
|
|
457
|
+
THIS endpoint -- absolute replaces the path, relative is a sibling,
|
|
458
|
+
as `rename()` resolves it -- keeping this path's source and backend
|
|
459
|
+
instead of building a sourceless path plus a second connection.
|
|
460
|
+
|
|
461
|
+
A single-letter scheme is a Windows drive, not a scheme: `C:/Temp/x`
|
|
462
|
+
is a path (the same rule as `as_path()`).
|
|
463
|
+
"""
|
|
464
|
+
if _looks_like_uri(target, self._schemes_in_use):
|
|
465
|
+
destination = type(self)(
|
|
466
|
+
target, findclass=True, schemesmap=self._schemes_in_use
|
|
467
|
+
)
|
|
468
|
+
# Same endpoint: reuse the connection (and whatever auth was
|
|
469
|
+
# configured on it) rather than opening a second, bare one.
|
|
470
|
+
backend, derived = self._backend_for(destination.source)
|
|
471
|
+
if backend is not None:
|
|
472
|
+
destination._backend, destination._backend_derived = backend, derived
|
|
473
|
+
else:
|
|
474
|
+
destination._derived_cell = self._family_for(destination.source)
|
|
475
|
+
return destination
|
|
476
|
+
# `/` joins a decoded path on this endpoint and carries the backend; the
|
|
477
|
+
# destination is another resource, so it takes no query or fragment.
|
|
478
|
+
return self._sibling_base() / target
|
|
479
|
+
|
|
480
|
+
def with_backend(self, backend):
|
|
481
|
+
"""Return a new path instance sharing the same backend state."""
|
|
482
|
+
return self._from_parsed_parts(*self.parts, backend=backend)
|
|
483
|
+
|
|
484
|
+
def _reduce_options(self) -> dict:
|
|
485
|
+
"""Constructor keywords a pickle rebuilds this path with, beside its
|
|
486
|
+
URI text. A scheme with per-path state that is not a secret returns
|
|
487
|
+
it here; the backend and the stat hint are never part of it."""
|
|
488
|
+
return {}
|
|
489
|
+
|
|
490
|
+
def __reduce__(self):
|
|
491
|
+
# A path pickles as its URI text (userinfo included), never its connection. A
|
|
492
|
+
# supplied backend goes along only if it declares `picklable = True`; a
|
|
493
|
+
# derived one is rebuilt on demand.
|
|
494
|
+
backend = self._supplied_backend()
|
|
495
|
+
if backend is not None and getattr(backend, "picklable", False) is not True:
|
|
496
|
+
backend = None
|
|
497
|
+
return (
|
|
498
|
+
_rebuild_path,
|
|
499
|
+
(
|
|
500
|
+
type(self),
|
|
501
|
+
self.as_uri(sanitize=False),
|
|
502
|
+
self._reduce_options(),
|
|
503
|
+
backend,
|
|
504
|
+
self._schemes_in_use,
|
|
505
|
+
),
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
def __copy__(self):
|
|
509
|
+
# The same URI on the same backend object, supplied or derived: a
|
|
510
|
+
# copy must not open a second connection or change which paths
|
|
511
|
+
# `_same_filesystem()` places together.
|
|
512
|
+
return self._from_parsed_parts(*self.parts)
|
|
513
|
+
|
|
514
|
+
def __deepcopy__(self, memo):
|
|
515
|
+
# A connection is not part of the value, so it is shared here too.
|
|
516
|
+
inst = memo[id(self)] = self.__copy__()
|
|
517
|
+
return inst
|
|
518
|
+
|
|
519
|
+
def _join_object(self, *parts) -> "URIPath":
|
|
520
|
+
# The class comes from the joined scheme, and from the allow-list.
|
|
521
|
+
return type(self)(*parts, findclass=True, schemesmap=self._schemes_in_use)
|
|
522
|
+
|
|
523
|
+
def with_source(self, source: Source):
|
|
524
|
+
cls = type(self)
|
|
525
|
+
if not source or not source.scheme:
|
|
526
|
+
# Nothing to dispatch on (`source.scheme + ":"` raised TypeError
|
|
527
|
+
# for a host-only Source): a plain URIPath.
|
|
528
|
+
inst = URI.__new__(URIPath)
|
|
529
|
+
elif source.scheme not in cls._schemes():
|
|
530
|
+
inst = cls.__new__(
|
|
531
|
+
cls,
|
|
532
|
+
source.scheme + ":",
|
|
533
|
+
findclass=True,
|
|
534
|
+
schemesmap=self._schemes_in_use,
|
|
535
|
+
)
|
|
536
|
+
else:
|
|
537
|
+
inst = cls.__new__(cls)
|
|
538
|
+
backend, derived = self._backend_for(source)
|
|
539
|
+
if backend is not None:
|
|
540
|
+
inst._backend, inst._backend_derived = backend, derived
|
|
541
|
+
else:
|
|
542
|
+
inst._derived_cell = self._family_for(source)
|
|
543
|
+
inst._schemes_in_use = self._schemes_in_use
|
|
544
|
+
inst._init(source, self.path, self.query, self.fragment)
|
|
545
|
+
return inst
|
|
546
|
+
|
|
547
|
+
def _symlink_target(self, target: "URILike") -> "_ty.Self":
|
|
548
|
+
"""`Path._symlink_target()` for URI-backed paths: a `str` target is
|
|
549
|
+
an already-decoded path, never URI syntax (see
|
|
550
|
+
`URI._from_decoded_path`).
|
|
551
|
+
|
|
552
|
+
The default `type(self)(target)` ran the link target back through
|
|
553
|
+
the URI parser, so `symlink_to("/mnt/cache?v=2")` created a link
|
|
554
|
+
pointing at `/mnt/cache`. Unlike `_rename_target()` this never
|
|
555
|
+
anchors at `self.parent`: a symlink target is stored as given, so
|
|
556
|
+
a relative one stays relative.
|
|
557
|
+
"""
|
|
558
|
+
if isinstance(target, str):
|
|
559
|
+
return self._from_decoded_path(target)
|
|
560
|
+
return target
|
|
561
|
+
|
|
562
|
+
@_providers.notimplemented
|
|
563
|
+
def _listdir(self) -> "_ty.Iterator[str]": ...
|
|
564
|
+
|
|
565
|
+
@_providers.derived_from("_listdir")
|
|
566
|
+
def _scandir(self) -> "_ty.Iterator[_ty.Tuple[str, _ty.Optional[FileStat]]]":
|
|
567
|
+
"""Default: derives from `_listdir()` + one `stat()` per child (no
|
|
568
|
+
round-trip savings over `iterdir()`). Schemes whose listing
|
|
569
|
+
call already returns metadata should override this directly instead
|
|
570
|
+
(see docs/guides/extending.md) -- HTTPURIPath/DAVURIPath/SFTPURIPath/FTPURIPath/
|
|
571
|
+
S3URIPath all do. A name that is not one path component
|
|
572
|
+
(`providers.is_safe_child_name()`) is skipped, and an override must skip
|
|
573
|
+
it too."""
|
|
574
|
+
for name in self._listdir():
|
|
575
|
+
# A listing is untrusted input: a name that is not one component
|
|
576
|
+
# ("..", "a/b", "") must never become a child path.
|
|
577
|
+
if not _providers.is_safe_child_name(name):
|
|
578
|
+
continue
|
|
579
|
+
child = self._make_child_relpath(name)
|
|
580
|
+
try:
|
|
581
|
+
# Non-following, like Path._scandir(): rm(recursive=True) and
|
|
582
|
+
# walk() trust this stat, and a following one reported a
|
|
583
|
+
# directory symlink as a real directory to descend into.
|
|
584
|
+
stat = FileStat.from_path(child, follow_symlink=False)
|
|
585
|
+
except OSError:
|
|
586
|
+
stat = None
|
|
587
|
+
yield name, stat
|
|
588
|
+
|
|
589
|
+
def _make_child_relpath(
|
|
590
|
+
self, name: str, stat_hint: "FileStat" = None, **kwargs
|
|
591
|
+
) -> _ty.Self:
|
|
592
|
+
# The child takes the backend this path already holds and builds none
|
|
593
|
+
# (`_from_parsed_parts`): naming a child is a pure-path operation. A
|
|
594
|
+
# listing has one by the time it gets here, so its children share it.
|
|
595
|
+
inst = super()._make_child_relpath(name, **kwargs)
|
|
596
|
+
inst._stat_hint = stat_hint
|
|
597
|
+
return inst
|
|
598
|
+
|
|
599
|
+
def _pop_stat_hint(self) -> "FileStat | None":
|
|
600
|
+
"""Consume this instance's pre-seeded stat (from `_scandir()`), if
|
|
601
|
+
any -- single-use: the next `stat()` call on this same object always
|
|
602
|
+
re-fetches, so a live mutation is never masked by a stale hint. A
|
|
603
|
+
type-only hint (`size_known` False) is consumed and not returned:
|
|
604
|
+
`stat()` has to give a size and a time, which it does not hold."""
|
|
605
|
+
hint = self._stat_hint
|
|
606
|
+
self._stat_hint = None
|
|
607
|
+
if hint is not None and not hint.size_known:
|
|
608
|
+
return None
|
|
609
|
+
return hint
|
|
610
|
+
|
|
611
|
+
def _st_mode(self, *, follow_symlinks=True):
|
|
612
|
+
# The type questions are answered by a type-only hint, without a request. The
|
|
613
|
+
# hint answers a non-following stat, so it speaks for a followed one only when
|
|
614
|
+
# the entry is not a link.
|
|
615
|
+
hint = self._stat_hint
|
|
616
|
+
if hint is not None and not hint.size_known:
|
|
617
|
+
self._stat_hint = None
|
|
618
|
+
mode = hint.st_mode
|
|
619
|
+
if _stat.S_IFMT(mode) and (not follow_symlinks or not _stat.S_ISLNK(mode)):
|
|
620
|
+
return mode
|
|
621
|
+
return super()._st_mode(follow_symlinks=follow_symlinks)
|
|
622
|
+
|
|
623
|
+
@_providers.derived_from("_scandir")
|
|
624
|
+
def iterdir(self) -> "_ty.Iterator[Self]":
|
|
625
|
+
for name, stat in self._scandir():
|
|
626
|
+
yield self._make_child_relpath(name, stat_hint=stat)
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
_DEFAULT_CHILD_BUILDERS.add(URIPath._make_child_relpath)
|