pathlib-next 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.
@@ -0,0 +1,471 @@
1
+ import os
2
+ import typing as _ty
3
+ import uritools
4
+ import posixpath as _posix
5
+ import pathlib as _pathlib
6
+
7
+
8
+ if _ty.TYPE_CHECKING:
9
+ from typing import Self
10
+
11
+ from .. import utils as _utils
12
+ from .query import Query
13
+ from .source import Source
14
+
15
+ from ..path import Path, Pathname
16
+
17
+ UriLike: _ty.TypeAlias = "str | Uri | os.PathLike"
18
+
19
+ _NOSOURCE = Source(None, None, None, None)
20
+
21
+ _U = _ty.TypeVar("_U", bound="Uri")
22
+
23
+
24
+ def _uriencode(text: str, safe=""):
25
+ return uritools.uriencode(text, safe=safe).decode()
26
+
27
+
28
+ class Uri(Pathname):
29
+
30
+ __slots__ = (
31
+ "_raw_uris",
32
+ "_source",
33
+ "_path",
34
+ "_query",
35
+ "_fragment",
36
+ "_uri",
37
+ "_initiated",
38
+ "_normalized_path",
39
+ )
40
+
41
+ def __new__(cls, *uris, **options):
42
+ inst = object.__new__(cls)
43
+ for cls in cls.__mro__:
44
+ for slot in getattr(cls, "__slots__", ()):
45
+ if not hasattr(inst, slot):
46
+ setattr(inst, slot, None)
47
+ return inst
48
+
49
+ def __init__(self, *uris: UriLike, **options):
50
+ if self._raw_uris or self._initiated:
51
+ return
52
+ _uris: list[str | Uri] = []
53
+ for uri in uris:
54
+ if not uri:
55
+ uri = ""
56
+ if isinstance(uri, Uri):
57
+ _uris.append(uri)
58
+ elif isinstance(uri, (_pathlib.Path, Path)):
59
+ try:
60
+ uri = uri.as_uri()
61
+ except:
62
+ uri = f"file:{_uriencode(uri.as_posix(), safe='/')}"
63
+ _uris.append(uri)
64
+ elif isinstance(uri, (_pathlib.PurePath, Pathname)):
65
+ _uris.append(f"{_uriencode(uri.as_posix(), safe='/')}")
66
+ elif hasattr(uri, "as_uri"):
67
+ path = uri.as_uri
68
+ if callable(path):
69
+ path = path()
70
+ _uris.append(path)
71
+ elif isinstance(uri, str):
72
+ _uris.append(uri)
73
+ elif isinstance(uri, bytes):
74
+ _uris.append(uri.decode())
75
+ else:
76
+ path = None
77
+ try:
78
+ path = os.fspath(uri)
79
+ except (TypeError, NotImplementedError):
80
+ pass
81
+ if not isinstance(path, str):
82
+ raise TypeError(
83
+ "argument should be a str or an os.PathLike "
84
+ "object where __fspath__ returns a str, "
85
+ f"not {type(path).__name__!r}"
86
+ )
87
+ _uris.append(f"{_uriencode(uri.as_posix(), safe='/')}")
88
+ self._raw_uris = _uris
89
+
90
+ @classmethod
91
+ def _parse_uri(cls, uri: str) -> tuple[Source, str, Query, str]:
92
+ parsed = uritools.urisplit(uri)
93
+ return (
94
+ Source(
95
+ parsed.getscheme(),
96
+ parsed.getuserinfo(),
97
+ parsed.gethost() or "",
98
+ parsed.getport(),
99
+ ),
100
+ parsed.getpath(),
101
+ Query(parsed.getquery() or ""),
102
+ parsed.getfragment() or "",
103
+ )
104
+
105
+ @property
106
+ def parts(self):
107
+ return (self.source, self.path, self.query, self.fragment)
108
+
109
+ def _load_parts(self):
110
+ uris = self._raw_uris
111
+ source = _NOSOURCE
112
+ query = fragment = None
113
+ _path = ""
114
+
115
+ if not uris:
116
+ pass
117
+ elif len(uris) == 1 and isinstance(uris[0], Uri):
118
+ source, _path, query, fragment = uris[0].parts
119
+ else:
120
+ paths: list[str] = []
121
+ for _uri in uris:
122
+ src, path, query, fragment = (
123
+ _uri.parts if isinstance(_uri, Uri) else self._parse_uri(_uri)
124
+ )
125
+ if bool(src):
126
+ source = src
127
+ paths.append(path)
128
+
129
+ for path in reversed(paths):
130
+ if not path:
131
+ continue
132
+ if path.endswith("/"):
133
+ _path = f"{path}{_path}"
134
+ elif _path:
135
+ _path = f"{path}/{_path}"
136
+ else:
137
+ _path = path
138
+ if _path.startswith("/"):
139
+ break
140
+
141
+ self._init(source, _path, query, fragment)
142
+
143
+ def _init(self, source: Source, path: str, query: str, fragment: str, **kwargs):
144
+ if self._initiated:
145
+ pass
146
+ # raise Exception(f"Uri._init should only be called once")
147
+ self._initiated = True
148
+ self._source = source
149
+ self._path = path
150
+ self._query = query
151
+ self._fragment = fragment
152
+
153
+ def _from_parsed_parts(
154
+ self, source: Source, path: str, query: str, fragment: str, /, **kwargs
155
+ ):
156
+ cls = type(self)
157
+ uri = cls.__new__(cls)
158
+ uri._init(source, path, query, fragment, **kwargs)
159
+ return uri
160
+
161
+ @classmethod
162
+ def _format_parsed_parts(
163
+ cls,
164
+ source: Source,
165
+ path: str,
166
+ query: str,
167
+ fragment: str,
168
+ /,
169
+ sanitize=True,
170
+ ) -> str:
171
+ parts = {
172
+ "path": path,
173
+ }
174
+ if query:
175
+ parts["query"] = query
176
+ if fragment:
177
+ parts["fragment"] = fragment
178
+ if source:
179
+ source_ = source._asdict()
180
+ if sanitize:
181
+ source_["userinfo"] = (source_["userinfo"] or "").split(
182
+ ":", maxsplit=1
183
+ )[0]
184
+ parts.update(source_)
185
+
186
+ return uritools.uricompose(**{k: v for k, v in parts.items() if v})
187
+
188
+ def __str__(self):
189
+ """Return the string representation of the path, suitable for
190
+ passing to system calls."""
191
+ return self.as_uri(sanitize=True)
192
+
193
+ def __fspath__(self):
194
+ if (self.source.scheme or "file") == "file":
195
+ if not self.source.host or self.is_local():
196
+ return self.path
197
+ elif os.name == "nt":
198
+ return f"//{self.source.host}/{self.path.removeprefix('/')}"
199
+ else:
200
+ raise NotImplementedError(f"OS Support for not local fspath")
201
+
202
+ raise NotImplementedError(f"fspath for {self.source.scheme}")
203
+
204
+ def __repr__(self):
205
+ return "{}({!r})".format(type(self).__name__, str(self))
206
+
207
+ def as_uri(self, /, sanitize=False):
208
+ if self._uri is None or sanitize:
209
+ uri = self._format_parsed_parts(
210
+ self.source, self.path, self.query, self.fragment, sanitize=sanitize
211
+ )
212
+ if not sanitize:
213
+ self._uri = uri
214
+ return uri
215
+ else:
216
+ return self._uri
217
+
218
+ @property
219
+ def source(self) -> Source:
220
+ if not self._initiated:
221
+ self._load_parts()
222
+ return self._source
223
+
224
+ @property
225
+ def path(self) -> str:
226
+ if not self._initiated:
227
+ self._load_parts()
228
+ return self._path
229
+
230
+ @property
231
+ def query(self) -> str:
232
+ if not self._initiated:
233
+ self._load_parts()
234
+ return self._query
235
+
236
+ @property
237
+ def fragment(self) -> str:
238
+ if not self._initiated:
239
+ self._load_parts()
240
+ return self._fragment
241
+
242
+ def _make_child_relpath(self, name: str, **kwargs) -> _ty.Self:
243
+ cls = type(self)
244
+ inst = cls.__new__(cls)
245
+ inst._init(
246
+ self.source, f"{self.path}/{name}" if self.path else name, "", "", **kwargs
247
+ )
248
+ return inst
249
+
250
+ def with_source(self, source: Source):
251
+ return self._from_parsed_parts(source, self.path, self.query, self.fragment)
252
+
253
+ def with_segments(self, *segments: str):
254
+ if not segments:
255
+ return self.with_path("")
256
+ return self.with_path("/".join(segments))
257
+
258
+ def with_path(self, path: str | Pathname):
259
+ return self._from_parsed_parts(
260
+ self.source,
261
+ path.as_posix() if isinstance(path, Pathname) else path,
262
+ self.query,
263
+ self.fragment,
264
+ )
265
+
266
+ def with_query(self, query: str):
267
+ if not isinstance(query, Query):
268
+ query = Query(query)
269
+ return self._from_parsed_parts(self.source, self.path, query, self.fragment)
270
+
271
+ def with_fragment(self, fragment: str):
272
+ return self._from_parsed_parts(self.source, self.path, self.query, fragment)
273
+
274
+ @property
275
+ def segments(self):
276
+ if not self.path:
277
+ return ()
278
+ return self.path.split("/")
279
+
280
+ @property
281
+ def parent(self):
282
+ """The logical parent of the path."""
283
+ segments = self.segments
284
+ if not segments or len(segments) == 2 and segments[1] == "":
285
+ return self
286
+ return self.with_path("/".join(segments[:-1]))
287
+
288
+ @property
289
+ def normalized_path(self):
290
+ if self._normalized_path is None:
291
+ self._normalized_path = _posix.normpath(self.path)
292
+ return self._normalized_path
293
+
294
+ def is_absolute(self):
295
+ """True if the path is absolute."""
296
+ return bool(self.source) and self.path.startswith("/")
297
+
298
+ def is_relative_to(self, other: UriLike):
299
+ """Return True if the path is relative to another path or False."""
300
+ other = other if isinstance(other, Uri) else Uri(self, _ROOT, other)
301
+ if not (
302
+ (other.source == self.source)
303
+ or not (bool(self.source) and bool(other.source))
304
+ ):
305
+ return False
306
+ _other = other.normalized_path
307
+ _self = self.normalized_path
308
+ return _self.startswith(_other)
309
+
310
+ def relative_to(self, other: UriLike, *, walk_up=False):
311
+ other = other if isinstance(other, Uri) else Uri(other)
312
+ if not self.is_relative_to(other):
313
+ raise ValueError(f"{str(self)!r} is not in the subpath of {str(other)!r}")
314
+
315
+ for step, path in enumerate([other] + list(other.parents)):
316
+ if self.is_relative_to(path):
317
+ break
318
+ elif not walk_up:
319
+ raise ValueError(
320
+ f"{str(self)!r} is not in the subpath of {str(other)!r}"
321
+ )
322
+ elif path.name == "..":
323
+ raise ValueError(f"'..' segment in {str(other)!r} cannot be walked")
324
+ else:
325
+ raise ValueError(f"{str(self)!r} and {str(other)!r} have different anchors")
326
+ parts = [".."] * step + self.segments[len(path.segments) :]
327
+ return self._from_parsed_parts(
328
+ _NOSOURCE, "/".join(parts), self.query, self.fragment
329
+ )
330
+
331
+ def is_local(self):
332
+ return self.source.is_local()
333
+
334
+ def __eq__(self, other: Pathname | str):
335
+ uri = other.as_uri() if isinstance(other, Pathname) else other
336
+ return self.as_uri() == uri
337
+
338
+ def as_posix(self):
339
+ source = self.source
340
+ host = None
341
+ posix = self.path
342
+ if source.host:
343
+ host = source.host
344
+ user, password = source.parsed_userinfo()
345
+ if user:
346
+ posix = f"{user}@{host}:{posix}"
347
+ else:
348
+ posix = f"{host}:{posix}"
349
+ return posix
350
+
351
+
352
+ class UriPath(Uri, Path):
353
+ __slots__ = ("_backend",)
354
+ __SCHEMES: _ty.Sequence[str] = ()
355
+ __SCHEMESMAP: _ty.Mapping[str, type["Self"]] = None
356
+
357
+ @classmethod
358
+ def _schemesmap(cls, reload=False) -> _ty.Mapping[str, type["Self"]]:
359
+ _propname = f"_{cls.__name__}__SCHEMESMAP"
360
+ if not reload:
361
+ try:
362
+ schemesmap = getattr(cls, _propname)
363
+ if schemesmap is not None:
364
+ return schemesmap
365
+ except AttributeError:
366
+ pass
367
+ schemesmap = cls._get_schemesmap()
368
+ setattr(cls, _propname, schemesmap)
369
+ return schemesmap
370
+
371
+ @classmethod
372
+ def _schemes(cls) -> _ty.Sequence[str]:
373
+ try:
374
+ return getattr(cls, f"_{cls.__name__}__SCHEMES")
375
+ except AttributeError as _e:
376
+ return ()
377
+
378
+ @classmethod
379
+ def _get_schemesmap(cls):
380
+ schemesmap = {scheme: cls for scheme in cls._schemes()}
381
+ for scls in cls.__subclasses__():
382
+ schemesmap.update(scls._get_schemesmap())
383
+ return schemesmap
384
+
385
+ def __new__(
386
+ cls,
387
+ *args,
388
+ schemesmap: dict[str, type["Self"]] = None,
389
+ findclass=False,
390
+ **kwargs,
391
+ ) -> "UriPath":
392
+ if cls is UriPath or findclass:
393
+ uri = Uri(*args, **kwargs)
394
+ cls: type[UriPath] = uri.source.get_scheme_cls(schemesmap)
395
+ if cls is UriPath:
396
+ inst = Uri.__new__(cls, *args, **kwargs)
397
+ else:
398
+ inst = cls.__new__(cls, *args, **kwargs)
399
+ inst._init(uri.source, uri.path, uri.query, uri.fragment, **kwargs)
400
+ else:
401
+ inst = Uri.__new__(cls, *args, **kwargs)
402
+ return inst
403
+
404
+ def _initbackend(self):
405
+ return None
406
+
407
+ def _init(
408
+ self,
409
+ source: Source,
410
+ path: str,
411
+ query: str,
412
+ fragment: str,
413
+ /,
414
+ backend=None,
415
+ **kwargs,
416
+ ):
417
+ if backend is not None:
418
+ self._backend = backend
419
+ super()._init(source, path, query, fragment, **kwargs)
420
+
421
+ @property
422
+ def backend(self):
423
+ if self._backend is None:
424
+ self._backend = self._initbackend()
425
+ return self._backend
426
+
427
+ def with_backend(self, backend):
428
+ return self._from_parsed_parts(*self.parts, backend=backend)
429
+
430
+ def _load_parts(self):
431
+ super()._load_parts()
432
+ if self.source and self.source.scheme and self._backend is None:
433
+ for uri in reversed(self._raw_uris):
434
+ if (
435
+ isinstance(uri, UriPath)
436
+ and uri.source.scheme == self.source.scheme
437
+ and uri._backend
438
+ ):
439
+ self._backend = uri._backend
440
+ break
441
+
442
+ def __truediv__(self, key: str | Uri | os.PathLike):
443
+ try:
444
+ return type(self)(self, key, findclass=True)
445
+ except (TypeError, NotImplementedError):
446
+ return NotImplemented
447
+
448
+ def with_source(self, source: Source):
449
+ cls = type(self)
450
+ if not source:
451
+ inst = Uri.__new__(UriPath)
452
+ elif source.scheme not in cls._schemes():
453
+ inst = UriPath.__new__(UriPath, source.scheme + ":")
454
+ else:
455
+ inst = cls.__new__(cls, backend=self._backend)
456
+ inst._init(source, self.path, self.query, self.fragment)
457
+ return inst
458
+
459
+ @_utils.notimplemented
460
+ def _listdir(self) -> "_ty.Iterator[str]": ...
461
+
462
+ def _make_child_relpath(self, name: str, **kwargs) -> _ty.Self:
463
+ inst = super()._make_child_relpath(name, backend=self.backend, **kwargs)
464
+ return inst
465
+
466
+ def iterdir(self) -> "_ty.Iterator[Self]":
467
+ for path in self._listdir():
468
+ yield self._make_child_relpath(path)
469
+
470
+
471
+ _ROOT = Uri("/")
@@ -0,0 +1,58 @@
1
+ import typing as _ty
2
+ import uritools as _uritools
3
+
4
+
5
+ class Query(str):
6
+ __slots__ = ("_encoding", "_separator")
7
+ SEPARATOR = "&"
8
+ ENCODING = "utf-8"
9
+
10
+ def __new__(
11
+ cls,
12
+ query: (
13
+ str
14
+ | _ty.Sequence[tuple[str, str | None]]
15
+ | _ty.Mapping[str, str | None | _ty.Sequence[str | None]]
16
+ ),
17
+ *,
18
+ encoding=ENCODING,
19
+ separator=SEPARATOR
20
+ ):
21
+ if isinstance(query, Query):
22
+ _encoding = query._encoding
23
+ _separator = query._separator
24
+ else:
25
+ _encoding = None
26
+ _separator = None
27
+
28
+ encoding = encoding or _encoding or cls.ENCODING
29
+ separator = separator or _separator or cls.SEPARATOR
30
+ if isinstance(query, str):
31
+ pass
32
+ else:
33
+ if isinstance(query, _ty.Mapping):
34
+ query: str = _uritools._querydict(query, separator, encoding).decode()
35
+ else:
36
+ query = _uritools._querylist(query, separator, encoding).decode()
37
+
38
+ obj = str.__new__(cls, query)
39
+ obj._encoding = encoding
40
+ obj._separator = separator
41
+ return obj
42
+
43
+ def decode(query) -> list[tuple[str, str | None]]:
44
+ return _uritools.SplitResultString("", "", "", str(query), "").getquerylist(
45
+ query._separator, query._encoding
46
+ )
47
+
48
+ def __iter__(self):
49
+ return iter(self.decode())
50
+
51
+ def to_dict(query, *, single=False):
52
+ query_: dict[str, list[str | None]] = {}
53
+ for k, v in query.decode():
54
+ if single:
55
+ query_[k] = v
56
+ else:
57
+ query_.setdefault(k, []).append(v)
58
+ return query_
@@ -0,0 +1,10 @@
1
+ from .file import FileUri
2
+
3
+ try:
4
+ from .http import HttpPath
5
+ except ImportError:
6
+ pass
7
+ try:
8
+ from .sftp import SftpPath
9
+ except ImportError:
10
+ pass
@@ -0,0 +1,62 @@
1
+ from ...fspath import LocalPath as _Local
2
+ from ...path import FsPathLike
3
+ import os as _os
4
+ from .. import UriPath, Source
5
+
6
+
7
+ class FileUri(UriPath):
8
+ __SCHEMES = ("file",)
9
+ __slots__ = ("_filepath",)
10
+
11
+ @property
12
+ def filepath(self):
13
+ if self._filepath is None:
14
+ self._filepath = _Local(self.__fspath__())
15
+ return self._filepath
16
+
17
+ def _init(
18
+ self,
19
+ source: Source,
20
+ path: str,
21
+ query: str,
22
+ fragment: str,
23
+ /,
24
+ **kwargs,
25
+ ):
26
+ if _os.name == "nt" and path and path[0] == "/":
27
+ root, *_ = path[1:].split("/", maxsplit=1)
28
+ if root and root[-1] == ":":
29
+ path = path.removeprefix("/")
30
+ super()._init(source, path, query, fragment, **kwargs)
31
+
32
+ def _listdir(self):
33
+ yield from _os.listdir(self.filepath)
34
+
35
+ def stat(self, *, follow_symlinks=True):
36
+ return self.filepath.stat(follow_symlinks=follow_symlinks)
37
+
38
+ def open(self, mode="r", buffering=-1, encoding=None, errors=None, newline=None):
39
+ return self.filepath.open(mode, buffering, encoding, errors, newline)
40
+
41
+ def mkdir(self, mode=511, parents=False, exist_ok=False):
42
+ return self.filepath.mkdir(mode, parents, exist_ok)
43
+
44
+ def chmod(self, mode, *, follow_symlinks=True):
45
+ return self.filepath.chmod(mode, follow_symlinks=follow_symlinks)
46
+
47
+ def unlink(self, missing_ok=False):
48
+ return self.filepath.unlink(missing_ok)
49
+
50
+ def rmdir(self):
51
+ return self.filepath.rmdir()
52
+
53
+ def rename(self, target: FsPathLike | str):
54
+ try:
55
+ _target = _os.fspath(target)
56
+ except (TypeError, NotImplementedError):
57
+ _target = NotImplemented
58
+
59
+ if _target is NotImplemented:
60
+ raise NotImplementedError("rename", target)
61
+
62
+ return self.filepath.rename(_target)