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.
- pathlib_next/__init__.py +7 -0
- pathlib_next/fspath.py +103 -0
- pathlib_next/path.py +618 -0
- pathlib_next/uri/__init__.py +471 -0
- pathlib_next/uri/query.py +58 -0
- pathlib_next/uri/schemes/__init__.py +10 -0
- pathlib_next/uri/schemes/file.py +62 -0
- pathlib_next/uri/schemes/http.py +136 -0
- pathlib_next/uri/schemes/sftp.py +101 -0
- pathlib_next/uri/source.py +73 -0
- pathlib_next/utils/__init__.py +93 -0
- pathlib_next/utils/glob.py +145 -0
- pathlib_next/utils/stat.py +77 -0
- pathlib_next/utils/sync.py +91 -0
- pathlib_next-0.1.0.dist-info/METADATA +25 -0
- pathlib_next-0.1.0.dist-info/RECORD +18 -0
- pathlib_next-0.1.0.dist-info/WHEEL +4 -0
- pathlib_next-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from ...utils.stat import FileStat
|
|
2
|
+
from .. import UriPath
|
|
3
|
+
from ... import utils as _utils
|
|
4
|
+
import io as _io
|
|
5
|
+
import requests as _req
|
|
6
|
+
import typing as _ty
|
|
7
|
+
import bs4 as _bs4
|
|
8
|
+
import time as _time
|
|
9
|
+
|
|
10
|
+
from htmllistparse import parse as _htmlparse
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _FileEntry(_ty.NamedTuple):
|
|
14
|
+
name: str
|
|
15
|
+
modified: _ty.Optional[_time.struct_time]
|
|
16
|
+
size: _ty.Optional[int]
|
|
17
|
+
description: _ty.Optional[str]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class HttpBackend(_ty.NamedTuple):
|
|
21
|
+
session: _req.Session
|
|
22
|
+
requests_args: dict
|
|
23
|
+
|
|
24
|
+
def request(self, method, uri: "HttpPath|str", **kwargs):
|
|
25
|
+
return self.session.request(
|
|
26
|
+
**self.requests_args,
|
|
27
|
+
**kwargs,
|
|
28
|
+
method=method,
|
|
29
|
+
url=uri if isinstance(uri, str) else uri.as_uri(False),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class HttpPath(UriPath):
|
|
34
|
+
__SCHEMES = ("http", "https")
|
|
35
|
+
__slots__ = ("_isdir", "_session", "_requests_args")
|
|
36
|
+
_isdir: bool
|
|
37
|
+
|
|
38
|
+
if _ty.TYPE_CHECKING:
|
|
39
|
+
backend: HttpBackend
|
|
40
|
+
|
|
41
|
+
def _initbackend(self):
|
|
42
|
+
return HttpBackend(_req.Session(), {})
|
|
43
|
+
|
|
44
|
+
def _listdir(self) -> list[_FileEntry]:
|
|
45
|
+
req = self.backend.session.request("GET", self)
|
|
46
|
+
req.raise_for_status()
|
|
47
|
+
soup = _bs4.BeautifulSoup(req.content, "html5lib")
|
|
48
|
+
_, listing = _htmlparse(soup)
|
|
49
|
+
return listing
|
|
50
|
+
|
|
51
|
+
def iterdir(self):
|
|
52
|
+
_self = self.path.removesuffix("/")
|
|
53
|
+
cls = type(self)
|
|
54
|
+
for path in self._listdir():
|
|
55
|
+
inst = type(self).__new__(cls, backend=self.backend)
|
|
56
|
+
inst._init(self.source, f"{_self}/{path.name}", "", "")
|
|
57
|
+
yield inst
|
|
58
|
+
|
|
59
|
+
def _is_dir(self, resp: _req.Response):
|
|
60
|
+
return (
|
|
61
|
+
resp.is_redirect
|
|
62
|
+
or resp.url.endswith("/")
|
|
63
|
+
or resp.url.endswith("/..")
|
|
64
|
+
or resp.url.endswith("/.")
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def stat(self):
|
|
68
|
+
check = (
|
|
69
|
+
[self.with_path(self.path.removesuffix("/")), self]
|
|
70
|
+
if self.path.endswith("/")
|
|
71
|
+
else [self]
|
|
72
|
+
)
|
|
73
|
+
for uri in check:
|
|
74
|
+
resp = self.backend.request("HEAD", uri, allow_redirects=False)
|
|
75
|
+
resp.close()
|
|
76
|
+
if resp.status_code < 400:
|
|
77
|
+
break
|
|
78
|
+
|
|
79
|
+
if self._isdir is None:
|
|
80
|
+
self._isdir = self._is_dir(resp)
|
|
81
|
+
|
|
82
|
+
if resp.is_redirect:
|
|
83
|
+
resp = self.backend.request("HEAD", uri)
|
|
84
|
+
if resp.status_code == 404:
|
|
85
|
+
raise FileNotFoundError(self)
|
|
86
|
+
elif resp.status_code == 403:
|
|
87
|
+
raise PermissionError(self)
|
|
88
|
+
else:
|
|
89
|
+
resp.raise_for_status()
|
|
90
|
+
|
|
91
|
+
st_size = 0 if self._isdir else int(resp.headers.get("Content-Length", 0))
|
|
92
|
+
lm = resp.headers.get("Last-Modified")
|
|
93
|
+
if lm is None:
|
|
94
|
+
parent = self.parent
|
|
95
|
+
if self != parent:
|
|
96
|
+
try:
|
|
97
|
+
entry = next(
|
|
98
|
+
filter(
|
|
99
|
+
lambda p: p.name.removesuffix("/") == self.name,
|
|
100
|
+
parent._listdir(),
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
if entry and entry.modified:
|
|
104
|
+
lm = entry.modified
|
|
105
|
+
except:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
return FileStat(
|
|
109
|
+
st_size=st_size, st_mtime=_utils.parsedate(lm), is_dir=self._isdir
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def _open(
|
|
113
|
+
self,
|
|
114
|
+
mode="r",
|
|
115
|
+
buffering=-1,
|
|
116
|
+
):
|
|
117
|
+
if mode != "r":
|
|
118
|
+
raise NotImplementedError(mode)
|
|
119
|
+
buffer_size = _io.DEFAULT_BUFFER_SIZE if buffering < 0 else buffering
|
|
120
|
+
req = self.backend.request("GET", self.as_uri(), stream=True)
|
|
121
|
+
return (
|
|
122
|
+
req.raw
|
|
123
|
+
if buffer_size == 0
|
|
124
|
+
else _io.BufferedReader(req.raw, buffer_size=buffer_size)
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def is_dir(self):
|
|
128
|
+
if self._isdir is None:
|
|
129
|
+
self.stat()
|
|
130
|
+
return self._is_dir is not None and self._isdir
|
|
131
|
+
|
|
132
|
+
def is_file(self):
|
|
133
|
+
return self.is_dir is not None and not self.is_dir()
|
|
134
|
+
|
|
135
|
+
def with_session(self, session: _req.Session, **requests_args):
|
|
136
|
+
return type(self)(self, backend=HttpBackend(session, requests_args))
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import typing as _ty
|
|
2
|
+
from .. import UriPath, Source
|
|
3
|
+
from ... import utils as _utils
|
|
4
|
+
import threading as _thread
|
|
5
|
+
import paramiko as _paramiko
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseSftpBackend(object):
|
|
9
|
+
__slots__ = ()
|
|
10
|
+
|
|
11
|
+
@_utils.notimplemented
|
|
12
|
+
def client(self, source: Source) -> _paramiko.SFTPClient: ...
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SftpBackend(BaseSftpBackend):
|
|
16
|
+
__slots__ = ("connect_opts", "hostkeypolicy")
|
|
17
|
+
connect_opts: dict[str, str]
|
|
18
|
+
hostkeypolicy: _paramiko.MissingHostKeyPolicy
|
|
19
|
+
|
|
20
|
+
def __init__(self, connect_opts, hostkeypolicy) -> None:
|
|
21
|
+
self.connect_opts = connect_opts
|
|
22
|
+
self.hostkeypolicy = hostkeypolicy
|
|
23
|
+
|
|
24
|
+
def opts(self, source: Source):
|
|
25
|
+
connect_ops = {
|
|
26
|
+
**self.connect_opts,
|
|
27
|
+
"hostname": str(source.host),
|
|
28
|
+
"port": source.port or 22,
|
|
29
|
+
}
|
|
30
|
+
user, password = source.parsed_userinfo()
|
|
31
|
+
if user:
|
|
32
|
+
connect_ops["username"] = user
|
|
33
|
+
if password:
|
|
34
|
+
connect_ops["password"] = password
|
|
35
|
+
return connect_ops
|
|
36
|
+
|
|
37
|
+
def transport(self, source: Source) -> _paramiko.Transport:
|
|
38
|
+
client = _paramiko.SSHClient()
|
|
39
|
+
client.set_missing_host_key_policy(self.hostkeypolicy)
|
|
40
|
+
client.connect(**self.opts(source))
|
|
41
|
+
transport = client.get_transport()
|
|
42
|
+
if not transport:
|
|
43
|
+
raise Exception()
|
|
44
|
+
return transport
|
|
45
|
+
|
|
46
|
+
def client(self, source: Source):
|
|
47
|
+
return self.transport(source).open_sftp_client()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _create_sftpclient(backend: BaseSftpBackend, source: Source, thread_id: int):
|
|
51
|
+
return backend.client(source)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
_CACHED_CLIENTS = _utils.LRU(_create_sftpclient, maxsize=128)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class SftpPath(UriPath):
|
|
58
|
+
|
|
59
|
+
__SCHEMES = ("sftp",)
|
|
60
|
+
__slots__ = ()
|
|
61
|
+
|
|
62
|
+
if _ty.TYPE_CHECKING:
|
|
63
|
+
backend: BaseSftpBackend
|
|
64
|
+
|
|
65
|
+
def _initbackend(self):
|
|
66
|
+
return SftpBackend({}, _paramiko.MissingHostKeyPolicy)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def _sftpclient(self):
|
|
70
|
+
thead_id = _thread.get_ident()
|
|
71
|
+
client = _CACHED_CLIENTS(self.backend, self.source, thead_id)
|
|
72
|
+
if client is None or not client.sock.active:
|
|
73
|
+
client = _CACHED_CLIENTS.invalidate(self.backend, self.source, thead_id)
|
|
74
|
+
return client
|
|
75
|
+
|
|
76
|
+
def _listdir(self):
|
|
77
|
+
for path in self._sftpclient.listdir(self.path):
|
|
78
|
+
yield path
|
|
79
|
+
|
|
80
|
+
def stat(self):
|
|
81
|
+
return self._sftpclient.stat(self.path)
|
|
82
|
+
|
|
83
|
+
def _open(self, mode="r", buffering=-1):
|
|
84
|
+
return self._sftpclient.open(self.path, mode, buffering)
|
|
85
|
+
|
|
86
|
+
def _mkdir(self, mode):
|
|
87
|
+
return self._sftpclient.mkdir(self.path, mode)
|
|
88
|
+
|
|
89
|
+
def chmod(self, mode):
|
|
90
|
+
return self._sftpclient.chmod(self.path, mode)
|
|
91
|
+
|
|
92
|
+
def unlink(self, missing_ok=False):
|
|
93
|
+
if missing_ok and not self.exists():
|
|
94
|
+
return
|
|
95
|
+
return self._sftpclient.remove(self.path)
|
|
96
|
+
|
|
97
|
+
def rmdir(self):
|
|
98
|
+
return self._sftpclient.rmdir(self.path)
|
|
99
|
+
|
|
100
|
+
def _rename(self, target):
|
|
101
|
+
return self._sftpclient.rename(self.path, target.as_posix())
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import typing as _ty
|
|
2
|
+
import uritools as _uritools
|
|
3
|
+
import socket as _socket
|
|
4
|
+
import ipaddress as _ip
|
|
5
|
+
|
|
6
|
+
from .. import utils as _utils
|
|
7
|
+
|
|
8
|
+
_IPAddress = _ip.IPv4Address | _ip.IPv6Address
|
|
9
|
+
|
|
10
|
+
if _ty.TYPE_CHECKING:
|
|
11
|
+
from . import UriPath
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Source(_ty.NamedTuple):
|
|
15
|
+
scheme: str | None
|
|
16
|
+
userinfo: str | None
|
|
17
|
+
host: str | _IPAddress | None
|
|
18
|
+
port: int | None
|
|
19
|
+
|
|
20
|
+
def __bool__(self):
|
|
21
|
+
for val in self:
|
|
22
|
+
if val == '' or val is None:
|
|
23
|
+
continue
|
|
24
|
+
return True
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
def __str__(self) -> str:
|
|
28
|
+
return _uritools.uricompose(
|
|
29
|
+
scheme=self.scheme, userinfo=self.userinfo, host=self.host, port=self.port
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def from_str(cls, source: str, strict=True):
|
|
34
|
+
uri = _uritools.urisplit(source)
|
|
35
|
+
if strict and (uri.path or uri.fragment or uri.query):
|
|
36
|
+
raise ValueError(source)
|
|
37
|
+
return cls(uri.getscheme(), uri.getuserinfo(), uri.gethost(), uri.getport())
|
|
38
|
+
|
|
39
|
+
def keys(self):
|
|
40
|
+
return self._asdict().keys()
|
|
41
|
+
|
|
42
|
+
def __getitem__(self, key: int | str):
|
|
43
|
+
if not isinstance(key, str):
|
|
44
|
+
key = self._fields[key]
|
|
45
|
+
return getattr(self, key)
|
|
46
|
+
|
|
47
|
+
def parsed_userinfo(self):
|
|
48
|
+
parts = []
|
|
49
|
+
if self.userinfo:
|
|
50
|
+
parts = self.userinfo.split(":", maxsplit=1)
|
|
51
|
+
parts = parts + ["", ""]
|
|
52
|
+
return parts[0], parts[1]
|
|
53
|
+
|
|
54
|
+
def get_scheme_cls(self, schemesmap: _ty.Mapping[str, type["UriPath"]] = None):
|
|
55
|
+
from . import UriPath
|
|
56
|
+
|
|
57
|
+
if self.scheme:
|
|
58
|
+
if schemesmap is None:
|
|
59
|
+
schemesmap = UriPath._schemesmap()
|
|
60
|
+
_cls = schemesmap.get(self.scheme, None)
|
|
61
|
+
return _cls if _cls else UriPath
|
|
62
|
+
return UriPath
|
|
63
|
+
|
|
64
|
+
def is_local(self):
|
|
65
|
+
host = self.host
|
|
66
|
+
if not host or host == "localhost":
|
|
67
|
+
return True
|
|
68
|
+
if isinstance(host, str):
|
|
69
|
+
host = _ip.ip_address(_socket.gethostbyname(host))
|
|
70
|
+
return host.is_loopback or host in _utils.get_machine_ips()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
_NOSOURCE = Source(None, None, None, None)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import functools as _functools
|
|
2
|
+
from threading import RLock
|
|
3
|
+
import typing as _ty
|
|
4
|
+
import time as _time
|
|
5
|
+
from email.utils import parsedate as _parsedate
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
import collections
|
|
9
|
+
|
|
10
|
+
K = _ty.ParamSpec("K")
|
|
11
|
+
V = _ty.TypeVar("V")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LRU(_ty.Generic[K, V]):
|
|
15
|
+
|
|
16
|
+
def __init__(self, func: _ty.Callable[K, V], maxsize=128):
|
|
17
|
+
self.cache = collections.OrderedDict()
|
|
18
|
+
self.func = func
|
|
19
|
+
self._maxsize = maxsize
|
|
20
|
+
self.lock = RLock()
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def maxsize(self):
|
|
24
|
+
return self._maxsize
|
|
25
|
+
|
|
26
|
+
@maxsize.setter
|
|
27
|
+
def maxsize(self, maxsize: int):
|
|
28
|
+
cache = self.cache
|
|
29
|
+
with self.lock:
|
|
30
|
+
self._maxsize = maxsize
|
|
31
|
+
while len(cache) > maxsize:
|
|
32
|
+
cache.pop(last=False)
|
|
33
|
+
|
|
34
|
+
def __call__(self, *args: K.args) -> V:
|
|
35
|
+
cache = self.cache
|
|
36
|
+
with self.lock:
|
|
37
|
+
if args in cache:
|
|
38
|
+
cache.move_to_end(args)
|
|
39
|
+
return cache[args]
|
|
40
|
+
result = self.func(*args)
|
|
41
|
+
with self.lock:
|
|
42
|
+
cache[args] = result
|
|
43
|
+
if len(cache) > self._maxsize:
|
|
44
|
+
cache.popitem(last=False)
|
|
45
|
+
return result
|
|
46
|
+
|
|
47
|
+
def invalidate(self, *args: K.args) -> V:
|
|
48
|
+
with self.lock():
|
|
49
|
+
if args in self.cache:
|
|
50
|
+
self.cache.pop(args, None)
|
|
51
|
+
|
|
52
|
+
return self(*args)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def parsedate(date: _ty.Union[str, _time.struct_time, tuple, float]):
|
|
56
|
+
if date is None:
|
|
57
|
+
return _time.time()
|
|
58
|
+
if isinstance(date, str):
|
|
59
|
+
date = _parsedate(date)
|
|
60
|
+
return _time.mktime(date)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def sizeof_fmt(num: _ty.Union[int, float]):
|
|
64
|
+
for unit in ("", "K", "M", "G", "T", "P", "E", "Z"):
|
|
65
|
+
if abs(num) < 1024:
|
|
66
|
+
if unit:
|
|
67
|
+
return "%3.1f%s" % (num, unit)
|
|
68
|
+
else:
|
|
69
|
+
return int(num)
|
|
70
|
+
num /= 1024.0
|
|
71
|
+
return "%.1f%s" % (num, "Y")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def notimplemented(method):
|
|
75
|
+
@_functools.wraps(method)
|
|
76
|
+
def _notimplemented(*args, **kwargs):
|
|
77
|
+
raise NotImplementedError(f"Not implement method {method.__name__}")
|
|
78
|
+
|
|
79
|
+
return _notimplemented
|
|
80
|
+
|
|
81
|
+
import socket as _socket
|
|
82
|
+
import ipaddress as _ip
|
|
83
|
+
|
|
84
|
+
def get_machine_ips():
|
|
85
|
+
ips:list[_ip.IPv4Address|_ip.IPv6Address] = list()
|
|
86
|
+
for item in _socket.getaddrinfo(_socket.gethostname(), None):
|
|
87
|
+
protocol, *_, (ip, *_) = item
|
|
88
|
+
if protocol == _socket.AddressFamily.AF_INET:
|
|
89
|
+
ips.append(_ip.ip_address(ip))
|
|
90
|
+
elif protocol == _socket.AddressFamily.AF_INET6:
|
|
91
|
+
ips.append(_ip.ip_address(ip))
|
|
92
|
+
|
|
93
|
+
return ips
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Based on glob built-in on python modified to work with Uri/anything that implemetns fspath/iterdir that is similar to pathlib.Path
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
"""Filename globbing utility."""
|
|
5
|
+
|
|
6
|
+
import fnmatch as _fnmatch
|
|
7
|
+
import typing as _ty
|
|
8
|
+
import functools as _func
|
|
9
|
+
import re as _re
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
RECURSIVE = "**"
|
|
13
|
+
ANY_PATTERN = _re.compile(_fnmatch.translate("*"))
|
|
14
|
+
WILCARD_PATTERN = _re.compile("([*?[])")
|
|
15
|
+
|
|
16
|
+
if _ty.TYPE_CHECKING:
|
|
17
|
+
from ..path import P as _Globable
|
|
18
|
+
else:
|
|
19
|
+
|
|
20
|
+
class _Globable(_ty.Protocol): ...
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@_func.lru_cache(maxsize=256, typed=True)
|
|
24
|
+
def compile_pattern(pat: str, case_sensitive: bool):
|
|
25
|
+
flags = _re.NOFLAG if case_sensitive else _re.IGNORECASE
|
|
26
|
+
return _re.compile(_fnmatch.translate(pat), flags)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def glob(
|
|
30
|
+
path: _Globable,
|
|
31
|
+
*,
|
|
32
|
+
dironly: bool = False,
|
|
33
|
+
root_dir: _Globable | None = None,
|
|
34
|
+
recursive: bool = False,
|
|
35
|
+
include_hidden: bool = False,
|
|
36
|
+
case_sensitive: bool | None = None
|
|
37
|
+
) -> _ty.Iterable[_Globable]:
|
|
38
|
+
"""Return an iterator which yields the paths matching a pathname pattern.
|
|
39
|
+
|
|
40
|
+
The pattern may contain simple shell-style wildcards a la
|
|
41
|
+
fnmatch. However, unlike fnmatch, filenames starting with a
|
|
42
|
+
dot are special cases that are not matched by '*' and '?'
|
|
43
|
+
patterns.
|
|
44
|
+
|
|
45
|
+
If recursive is true, the pattern '**' will match any files and
|
|
46
|
+
zero or more directories and subdirectories.
|
|
47
|
+
"""
|
|
48
|
+
if case_sensitive is None:
|
|
49
|
+
case_sensitive = path._is_case_sensitive
|
|
50
|
+
|
|
51
|
+
include_hidden = include_hidden or path.is_hidden()
|
|
52
|
+
pattern = compile_pattern(path.name, case_sensitive) if path.name else ANY_PATTERN
|
|
53
|
+
|
|
54
|
+
name_is_pattern = WILCARD_PATTERN.match(path.name) != None
|
|
55
|
+
wildcard_in_path = name_is_pattern
|
|
56
|
+
parent = next(iter(path.parents), None)
|
|
57
|
+
for segment in path.segments:
|
|
58
|
+
if WILCARD_PATTERN.match(segment) != None:
|
|
59
|
+
wildcard_in_path = True
|
|
60
|
+
break
|
|
61
|
+
|
|
62
|
+
root: _Globable = (
|
|
63
|
+
(root_dir or parent) if not root_dir or not parent else (root_dir / parent)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if recursive and path.name == RECURSIVE:
|
|
67
|
+
globber = _glob_recursive
|
|
68
|
+
else:
|
|
69
|
+
globber = _glob_with_pattern
|
|
70
|
+
|
|
71
|
+
if not parent or not wildcard_in_path:
|
|
72
|
+
yield from globber(
|
|
73
|
+
root or path,
|
|
74
|
+
pattern,
|
|
75
|
+
dironly,
|
|
76
|
+
include_hidden=include_hidden,
|
|
77
|
+
)
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
if parent and name_is_pattern:
|
|
81
|
+
dirs = glob(
|
|
82
|
+
parent,
|
|
83
|
+
root_dir=root_dir,
|
|
84
|
+
recursive=recursive,
|
|
85
|
+
dironly=True,
|
|
86
|
+
include_hidden=include_hidden,
|
|
87
|
+
case_sensitive=case_sensitive,
|
|
88
|
+
)
|
|
89
|
+
else:
|
|
90
|
+
dirs = [parent]
|
|
91
|
+
|
|
92
|
+
for parent in dirs:
|
|
93
|
+
yield from globber(parent, pattern, dironly, include_hidden)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _glob_with_pattern(
|
|
97
|
+
parent: _Globable, pattern: _re.Pattern, dironly: bool, include_hidden=False
|
|
98
|
+
) -> _ty.Iterable[_Globable]:
|
|
99
|
+
if not include_hidden:
|
|
100
|
+
|
|
101
|
+
def _filter(p: _Globable):
|
|
102
|
+
return not p.is_hidden()
|
|
103
|
+
|
|
104
|
+
else:
|
|
105
|
+
|
|
106
|
+
def _filter(p: _Globable):
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
for path in _iterdir(parent, dironly):
|
|
110
|
+
if _filter(path) and pattern.match(path.name):
|
|
111
|
+
yield path
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# This helper function recursively yields relative pathnames inside a literal
|
|
115
|
+
# directory.
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _glob_recursive(
|
|
119
|
+
parent: _Globable, pattern: _re.Pattern, dironly: bool, include_hidden=False
|
|
120
|
+
):
|
|
121
|
+
if parent and parent.is_dir():
|
|
122
|
+
yield parent
|
|
123
|
+
yield from _rlistdir(parent, dironly, include_hidden=include_hidden)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# If dironly is false, yields all file names inside a directory.
|
|
127
|
+
# If dironly is true, yields only directory names.
|
|
128
|
+
def _iterdir(path: _Globable, dironly: bool):
|
|
129
|
+
for entry in path.iterdir():
|
|
130
|
+
try:
|
|
131
|
+
if not dironly or entry.is_dir():
|
|
132
|
+
yield entry
|
|
133
|
+
except OSError:
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# Recursively yields relative pathnames inside a literal directory.
|
|
138
|
+
def _rlistdir(
|
|
139
|
+
dirname: _Globable, dironly: bool, include_hidden=False
|
|
140
|
+
) -> _ty.Iterable[_Globable]:
|
|
141
|
+
for path in _iterdir(dirname, dironly):
|
|
142
|
+
if include_hidden or not path.is_hidden():
|
|
143
|
+
yield path
|
|
144
|
+
for y in _rlistdir(path, dironly, include_hidden):
|
|
145
|
+
yield y
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import stat as _stat
|
|
2
|
+
import typing as _ty
|
|
3
|
+
import abc as _abc
|
|
4
|
+
from .. import utils as _utils
|
|
5
|
+
|
|
6
|
+
class FileStatLike(_ty.Protocol):
|
|
7
|
+
__slots__ = ()
|
|
8
|
+
|
|
9
|
+
@property
|
|
10
|
+
@_abc.abstractmethod
|
|
11
|
+
def st_mode(self) -> int: ...
|
|
12
|
+
@property
|
|
13
|
+
@_abc.abstractmethod
|
|
14
|
+
def st_size(self) -> int: ...
|
|
15
|
+
@property
|
|
16
|
+
@_abc.abstractmethod
|
|
17
|
+
def st_mtime(self) -> int: ...
|
|
18
|
+
|
|
19
|
+
class FileStat(FileStatLike):
|
|
20
|
+
__slots__ = (
|
|
21
|
+
"st_mode",
|
|
22
|
+
"st_nlink",
|
|
23
|
+
"st_uid",
|
|
24
|
+
"st_gid",
|
|
25
|
+
"st_size",
|
|
26
|
+
"st_atime",
|
|
27
|
+
"st_mtime",
|
|
28
|
+
"st_ctime",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
st_mode: int = None,
|
|
34
|
+
st_size: int = 0,
|
|
35
|
+
st_mtime: int = 0,
|
|
36
|
+
is_dir: bool = False,
|
|
37
|
+
):
|
|
38
|
+
self.st_mode = st_mode or (
|
|
39
|
+
_stat.S_IFDIR | 0o555 if is_dir else _stat.S_IFREG | 0o444
|
|
40
|
+
)
|
|
41
|
+
self.st_nlink = 1
|
|
42
|
+
self.st_uid = 0
|
|
43
|
+
self.st_gid = 0
|
|
44
|
+
self.st_size = st_size
|
|
45
|
+
self.st_atime = 0
|
|
46
|
+
self.st_mtime = st_mtime
|
|
47
|
+
self.st_ctime = 0
|
|
48
|
+
|
|
49
|
+
def settime(self, value):
|
|
50
|
+
self.st_atime = self.st_mtime = self.st_ctime = value
|
|
51
|
+
|
|
52
|
+
def setmode(self, value, isdir=None):
|
|
53
|
+
if isdir is None:
|
|
54
|
+
isdir = self.st_mode & _stat.S_IFDIR
|
|
55
|
+
if isdir:
|
|
56
|
+
self.st_mode = _stat.S_IFDIR | value
|
|
57
|
+
else:
|
|
58
|
+
self.st_mode = _stat.S_IFREG | value
|
|
59
|
+
|
|
60
|
+
def __getitem__(self, key):
|
|
61
|
+
return getattr(self, key)
|
|
62
|
+
|
|
63
|
+
def items(self):
|
|
64
|
+
for key in self.__slots__:
|
|
65
|
+
yield key, getattr(self, key)
|
|
66
|
+
|
|
67
|
+
def __repr__(self):
|
|
68
|
+
return "<%s mode=%o, size=%s, mtime=%d>" % (
|
|
69
|
+
type(self).__name__,
|
|
70
|
+
self.st_mode,
|
|
71
|
+
_utils.sizeof_fmt(self.st_size),
|
|
72
|
+
self.st_mtime,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def __str__(self):
|
|
76
|
+
props = [f"{k}={v}" for k, v in self.items()]
|
|
77
|
+
return "<%s %s>" % (type(self).__name__, ", ".join(props))
|