omlish 0.0.0.dev102__py3-none-any.whl → 0.0.0.dev104__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- omlish/__about__.py +2 -2
- omlish/concurrent/threadlets.py +3 -0
- omlish/diag/pycharm/pycharm.py +33 -0
- omlish/fnpairs.py +22 -12
- omlish/formats/json/cli/cli.py +35 -1
- omlish/formats/json/cli/formats.py +7 -0
- omlish/formats/json/cli/io.py +74 -0
- omlish/http/consts.py +2 -0
- omlish/http/jwt.py +179 -0
- omlish/io/__init__.py +3 -0
- omlish/io/pyio.py +2757 -0
- omlish/io/trampoline.py +293 -0
- omlish/lite/cached.py +9 -1
- omlish/lite/contextmanagers.py +34 -0
- omlish/lite/marshal.py +72 -29
- omlish/lite/pidfile.py +1 -1
- omlish/lite/subprocesses.py +18 -0
- omlish/specs/__init__.py +2 -0
- omlish/sync.py +55 -0
- {omlish-0.0.0.dev102.dist-info → omlish-0.0.0.dev104.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev102.dist-info → omlish-0.0.0.dev104.dist-info}/RECORD +25 -20
- {omlish-0.0.0.dev102.dist-info → omlish-0.0.0.dev104.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev102.dist-info → omlish-0.0.0.dev104.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev102.dist-info → omlish-0.0.0.dev104.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev102.dist-info → omlish-0.0.0.dev104.dist-info}/top_level.txt +0 -0
omlish/io/trampoline.py
ADDED
@@ -0,0 +1,293 @@
|
|
1
|
+
"""
|
2
|
+
TODO:
|
3
|
+
- https://docs.python.org/3/library/zlib.html#zlib.compressobj
|
4
|
+
"""
|
5
|
+
import abc
|
6
|
+
import contextlib
|
7
|
+
import io
|
8
|
+
import typing as ta
|
9
|
+
|
10
|
+
from .. import check
|
11
|
+
from .. import lang
|
12
|
+
from ..concurrent import threadlets as tls
|
13
|
+
from ..sync import ConditionDeque
|
14
|
+
|
15
|
+
|
16
|
+
if ta.TYPE_CHECKING:
|
17
|
+
import threading
|
18
|
+
|
19
|
+
from . import pyio # noqa
|
20
|
+
|
21
|
+
else:
|
22
|
+
threading = lang.proxy_import('threading')
|
23
|
+
|
24
|
+
pyio = lang.proxy_import('.pyio', __package__)
|
25
|
+
|
26
|
+
|
27
|
+
T = ta.TypeVar('T')
|
28
|
+
|
29
|
+
BytesLike: ta.TypeAlias = ta.Any
|
30
|
+
|
31
|
+
BufferedReader = io.BufferedReader
|
32
|
+
# BufferedReader = pyio.BufferedReader
|
33
|
+
|
34
|
+
|
35
|
+
##
|
36
|
+
|
37
|
+
|
38
|
+
class ProxyReadFile:
|
39
|
+
def __init__(self, read: ta.Callable[[int], bytes | BaseException]) -> None:
|
40
|
+
super().__init__()
|
41
|
+
|
42
|
+
self._read = read
|
43
|
+
self._eof = False
|
44
|
+
self._closed = False
|
45
|
+
|
46
|
+
#
|
47
|
+
|
48
|
+
def read(self, n: int, /) -> bytes:
|
49
|
+
if self._closed:
|
50
|
+
raise RuntimeError('Closed')
|
51
|
+
if self._eof:
|
52
|
+
return b''
|
53
|
+
d = self._read(n)
|
54
|
+
if isinstance(d, BaseException):
|
55
|
+
raise d
|
56
|
+
if not d:
|
57
|
+
self._eof = True
|
58
|
+
return d
|
59
|
+
|
60
|
+
def readall(self, *args, **kwargs):
|
61
|
+
raise TypeError # FIXME
|
62
|
+
|
63
|
+
def readinto(self, b: BytesLike) -> int | None:
|
64
|
+
d = self.read(len(b))
|
65
|
+
if d:
|
66
|
+
b[:len(d)] = d
|
67
|
+
return len(d)
|
68
|
+
|
69
|
+
#
|
70
|
+
|
71
|
+
def readable(self) -> bool:
|
72
|
+
return not (self._eof or self._closed)
|
73
|
+
|
74
|
+
@property
|
75
|
+
def closed(self) -> bool:
|
76
|
+
return self._closed
|
77
|
+
|
78
|
+
def close(self) -> None:
|
79
|
+
self._closed = True
|
80
|
+
|
81
|
+
#
|
82
|
+
|
83
|
+
def flush(self) -> None:
|
84
|
+
pass
|
85
|
+
|
86
|
+
def seekable(self) -> bool:
|
87
|
+
return False
|
88
|
+
|
89
|
+
def seek(self, n: int, /) -> ta.Any:
|
90
|
+
raise TypeError
|
91
|
+
|
92
|
+
|
93
|
+
##
|
94
|
+
|
95
|
+
|
96
|
+
class NeedMore(lang.Marker):
|
97
|
+
pass
|
98
|
+
|
99
|
+
|
100
|
+
class Exited(lang.Marker):
|
101
|
+
pass
|
102
|
+
|
103
|
+
|
104
|
+
class Shutdown(BaseException): # noqa
|
105
|
+
pass
|
106
|
+
|
107
|
+
|
108
|
+
IoTrampolineTarget: ta.TypeAlias = ta.Callable[[io.BufferedReader], ta.ContextManager[ta.Callable[[], bytes]]]
|
109
|
+
|
110
|
+
|
111
|
+
class IoTrampoline(lang.Abstract):
|
112
|
+
def __init__(
|
113
|
+
self,
|
114
|
+
target: IoTrampolineTarget,
|
115
|
+
*,
|
116
|
+
buffer_size: int | None = None,
|
117
|
+
) -> None:
|
118
|
+
super().__init__()
|
119
|
+
|
120
|
+
self._target = target
|
121
|
+
self._buffer_size = buffer_size
|
122
|
+
|
123
|
+
def _make_buffered_reader(self, raw: ta.Any) -> io.BufferedReader:
|
124
|
+
return io.BufferedReader(
|
125
|
+
raw,
|
126
|
+
**(dict(buffer_size=self._buffer_size) if self._buffer_size is not None else {}),
|
127
|
+
)
|
128
|
+
|
129
|
+
@abc.abstractmethod
|
130
|
+
def close(self, timeout: float | None = None) -> None:
|
131
|
+
raise NotImplementedError
|
132
|
+
|
133
|
+
@abc.abstractmethod
|
134
|
+
def __enter__(self) -> ta.Self:
|
135
|
+
raise NotImplementedError
|
136
|
+
|
137
|
+
@abc.abstractmethod
|
138
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
139
|
+
raise NotImplementedError
|
140
|
+
|
141
|
+
@abc.abstractmethod
|
142
|
+
def feed(self, *data: bytes) -> ta.Iterable[bytes]:
|
143
|
+
raise NotImplementedError
|
144
|
+
|
145
|
+
|
146
|
+
#
|
147
|
+
|
148
|
+
|
149
|
+
class ThreadIoTrampoline(IoTrampoline):
|
150
|
+
def __init__(self, target: IoTrampolineTarget, **kwargs: ta.Any) -> None:
|
151
|
+
super().__init__(target, **kwargs)
|
152
|
+
|
153
|
+
self._in: ConditionDeque[bytes | BaseException] = ConditionDeque()
|
154
|
+
self._out: ConditionDeque[
|
155
|
+
bytes | # noqa
|
156
|
+
type[NeedMore] |
|
157
|
+
type[Exited] |
|
158
|
+
BaseException
|
159
|
+
] = ConditionDeque()
|
160
|
+
|
161
|
+
self._proxy = ProxyReadFile(self._read)
|
162
|
+
|
163
|
+
self._thread = threading.Thread(target=self._thread_proc, daemon=True)
|
164
|
+
|
165
|
+
#
|
166
|
+
|
167
|
+
def close(self, timeout: float | None = None) -> None:
|
168
|
+
if not self._thread.is_alive():
|
169
|
+
return
|
170
|
+
self._out.push(Shutdown())
|
171
|
+
self._thread.join(timeout)
|
172
|
+
if self._thread.is_alive():
|
173
|
+
if timeout is not None:
|
174
|
+
raise TimeoutError
|
175
|
+
else:
|
176
|
+
raise RuntimeError('Failed to join thread')
|
177
|
+
|
178
|
+
#
|
179
|
+
|
180
|
+
def __enter__(self) -> ta.Self:
|
181
|
+
self._thread.start()
|
182
|
+
return self
|
183
|
+
|
184
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
185
|
+
self.close()
|
186
|
+
|
187
|
+
#
|
188
|
+
|
189
|
+
def _read(self, n: int, /) -> bytes | BaseException:
|
190
|
+
return self._in.pop(if_empty=lambda: self._out.push(NeedMore))
|
191
|
+
|
192
|
+
def _thread_proc(self) -> None:
|
193
|
+
try:
|
194
|
+
with contextlib.closing(self._make_buffered_reader(self._proxy)) as bf: # noqa
|
195
|
+
with self._target(bf) as read:
|
196
|
+
while out := read():
|
197
|
+
self._out.push(out)
|
198
|
+
self._out.push(out)
|
199
|
+
except BaseException as e:
|
200
|
+
self._out.push(e)
|
201
|
+
raise
|
202
|
+
finally:
|
203
|
+
self._out.push(Exited)
|
204
|
+
|
205
|
+
def feed(self, *data: bytes) -> ta.Iterable[bytes]:
|
206
|
+
self._in.push(*data)
|
207
|
+
while True:
|
208
|
+
e = self._out.pop()
|
209
|
+
if e is NeedMore:
|
210
|
+
break
|
211
|
+
elif isinstance(e, BaseException):
|
212
|
+
raise e
|
213
|
+
elif e is Exited:
|
214
|
+
raise RuntimeError('IO thread exited')
|
215
|
+
elif isinstance(e, bytes):
|
216
|
+
yield e
|
217
|
+
if not e:
|
218
|
+
return
|
219
|
+
else:
|
220
|
+
raise TypeError(e)
|
221
|
+
|
222
|
+
|
223
|
+
#
|
224
|
+
|
225
|
+
|
226
|
+
class ThreadletIoTrampoline(IoTrampoline):
|
227
|
+
def __init__(
|
228
|
+
self,
|
229
|
+
target: IoTrampolineTarget,
|
230
|
+
threadlets: tls.Threadlets = tls.GREENLET_THREADLETS,
|
231
|
+
** kwargs: ta.Any,
|
232
|
+
) -> None:
|
233
|
+
super().__init__(target, **kwargs)
|
234
|
+
|
235
|
+
self._proxy = ProxyReadFile(self._read)
|
236
|
+
self._tl: tls.Threadlet = threadlets.spawn(self._g_proc)
|
237
|
+
|
238
|
+
#
|
239
|
+
|
240
|
+
def close(self, timeout: float | None = None) -> None:
|
241
|
+
if self._tl.dead:
|
242
|
+
return
|
243
|
+
out = self._tl.switch(Shutdown())
|
244
|
+
if out is not Exited or not self._tl.dead:
|
245
|
+
raise RuntimeError
|
246
|
+
|
247
|
+
#
|
248
|
+
|
249
|
+
def __enter__(self) -> ta.Self:
|
250
|
+
out = self._tl.switch()
|
251
|
+
if out is not NeedMore:
|
252
|
+
raise RuntimeError
|
253
|
+
return self
|
254
|
+
|
255
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
256
|
+
self.close()
|
257
|
+
|
258
|
+
#
|
259
|
+
|
260
|
+
def _read(self, n: int, /) -> bytes:
|
261
|
+
out = check.not_none(self._tl.parent).switch(NeedMore)
|
262
|
+
return out
|
263
|
+
|
264
|
+
def _g_proc(self) -> ta.Any:
|
265
|
+
try:
|
266
|
+
with contextlib.closing(self._make_buffered_reader(self._proxy)) as bf: # noqa
|
267
|
+
with self._target(bf) as read:
|
268
|
+
while out := read():
|
269
|
+
e = check.not_none(self._tl.parent).switch(out)
|
270
|
+
if e is not NeedMore:
|
271
|
+
raise TypeError(e) # noqa
|
272
|
+
e = check.not_none(self._tl.parent).switch(out)
|
273
|
+
if not isinstance(e, Shutdown):
|
274
|
+
raise TypeError(e) # noqa
|
275
|
+
return Exited
|
276
|
+
except BaseException as e:
|
277
|
+
check.not_none(self._tl.parent).throw(e)
|
278
|
+
raise
|
279
|
+
|
280
|
+
def feed(self, *data: bytes) -> ta.Iterable[bytes]:
|
281
|
+
i: bytes | type[NeedMore]
|
282
|
+
for i in data:
|
283
|
+
while True:
|
284
|
+
e = self._tl.switch(i)
|
285
|
+
i = NeedMore
|
286
|
+
if e is NeedMore:
|
287
|
+
break
|
288
|
+
elif isinstance(e, bytes):
|
289
|
+
yield e
|
290
|
+
if not e:
|
291
|
+
return
|
292
|
+
else:
|
293
|
+
raise TypeError(e)
|
omlish/lite/cached.py
CHANGED
@@ -1,7 +1,11 @@
|
|
1
1
|
import functools
|
2
|
+
import typing as ta
|
2
3
|
|
3
4
|
|
4
|
-
|
5
|
+
T = ta.TypeVar('T')
|
6
|
+
|
7
|
+
|
8
|
+
class _cached_nullary: # noqa
|
5
9
|
def __init__(self, fn):
|
6
10
|
super().__init__()
|
7
11
|
self._fn = fn
|
@@ -16,3 +20,7 @@ class cached_nullary: # noqa
|
|
16
20
|
def __get__(self, instance, owner): # noqa
|
17
21
|
bound = instance.__dict__[self._fn.__name__] = self.__class__(self._fn.__get__(instance, owner))
|
18
22
|
return bound
|
23
|
+
|
24
|
+
|
25
|
+
def cached_nullary(fn: ta.Callable[..., T]) -> ta.Callable[..., T]:
|
26
|
+
return _cached_nullary(fn)
|
omlish/lite/contextmanagers.py
CHANGED
@@ -1,4 +1,38 @@
|
|
1
|
+
# ruff: noqa: UP007
|
1
2
|
import contextlib
|
3
|
+
import typing as ta
|
4
|
+
|
5
|
+
from .check import check_not_none
|
6
|
+
from .check import check_state
|
7
|
+
|
8
|
+
|
9
|
+
T = ta.TypeVar('T')
|
10
|
+
ExitStackedT = ta.TypeVar('ExitStackedT', bound='ExitStacked')
|
11
|
+
|
12
|
+
|
13
|
+
##
|
14
|
+
|
15
|
+
|
16
|
+
class ExitStacked:
|
17
|
+
_exit_stack: ta.Optional[contextlib.ExitStack] = None
|
18
|
+
|
19
|
+
def __enter__(self: ExitStackedT) -> ExitStackedT:
|
20
|
+
check_state(self._exit_stack is None)
|
21
|
+
es = self._exit_stack = contextlib.ExitStack()
|
22
|
+
es.__enter__()
|
23
|
+
return self
|
24
|
+
|
25
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
26
|
+
if (es := self._exit_stack) is None:
|
27
|
+
return None
|
28
|
+
return es.__exit__(exc_type, exc_val, exc_tb)
|
29
|
+
|
30
|
+
def _enter_context(self, cm: ta.ContextManager[T]) -> T:
|
31
|
+
es = check_not_none(self._exit_stack)
|
32
|
+
return es.enter_context(cm)
|
33
|
+
|
34
|
+
|
35
|
+
##
|
2
36
|
|
3
37
|
|
4
38
|
@contextlib.contextmanager
|
omlish/lite/marshal.py
CHANGED
@@ -12,6 +12,8 @@ import datetime
|
|
12
12
|
import decimal
|
13
13
|
import enum
|
14
14
|
import fractions
|
15
|
+
import functools
|
16
|
+
import threading
|
15
17
|
import typing as ta
|
16
18
|
import uuid
|
17
19
|
import weakref # noqa
|
@@ -150,7 +152,7 @@ class DataclassObjMarshaler(ObjMarshaler):
|
|
150
152
|
return {k: m.marshal(getattr(o, k)) for k, m in self.fs.items()}
|
151
153
|
|
152
154
|
def unmarshal(self, o: ta.Any) -> ta.Any:
|
153
|
-
return self.ty(**{k: self.fs[k].unmarshal(v) for k, v in o.items() if self.nonstrict or k in self.fs})
|
155
|
+
return self.ty(**{k: self.fs[k].unmarshal(v) for k, v in o.items() if not self.nonstrict or k in self.fs})
|
154
156
|
|
155
157
|
|
156
158
|
@dc.dataclass(frozen=True)
|
@@ -210,7 +212,10 @@ class UuidObjMarshaler(ObjMarshaler):
|
|
210
212
|
return uuid.UUID(o)
|
211
213
|
|
212
214
|
|
213
|
-
|
215
|
+
##
|
216
|
+
|
217
|
+
|
218
|
+
_DEFAULT_OBJ_MARSHALERS: ta.Dict[ta.Any, ObjMarshaler] = {
|
214
219
|
**{t: NopObjMarshaler() for t in (type(None),)},
|
215
220
|
**{t: CastObjMarshaler(t) for t in (int, float, str, bool)},
|
216
221
|
**{t: Base64ObjMarshaler(t) for t in (bytes, bytearray)},
|
@@ -239,20 +244,19 @@ _OBJ_MARSHALER_GENERIC_ITERABLE_TYPES: ta.Dict[ta.Any, type] = {
|
|
239
244
|
}
|
240
245
|
|
241
246
|
|
242
|
-
def
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
def _make_obj_marshaler(ty: ta.Any) -> ObjMarshaler:
|
247
|
+
def _make_obj_marshaler(
|
248
|
+
ty: ta.Any,
|
249
|
+
rec: ta.Callable[[ta.Any], ObjMarshaler],
|
250
|
+
*,
|
251
|
+
nonstrict_dataclasses: bool = False,
|
252
|
+
) -> ObjMarshaler:
|
249
253
|
if isinstance(ty, type):
|
250
254
|
if abc.ABC in ty.__bases__:
|
251
255
|
impls = [ # type: ignore
|
252
256
|
PolymorphicObjMarshaler.Impl(
|
253
257
|
ity,
|
254
258
|
ity.__qualname__,
|
255
|
-
|
259
|
+
rec(ity),
|
256
260
|
)
|
257
261
|
for ity in deep_subclasses(ty)
|
258
262
|
if abc.ABC not in ity.__bases__
|
@@ -268,7 +272,8 @@ def _make_obj_marshaler(ty: ta.Any) -> ObjMarshaler:
|
|
268
272
|
if dc.is_dataclass(ty):
|
269
273
|
return DataclassObjMarshaler(
|
270
274
|
ty,
|
271
|
-
{f.name:
|
275
|
+
{f.name: rec(f.type) for f in dc.fields(ty)},
|
276
|
+
nonstrict=nonstrict_dataclasses,
|
272
277
|
)
|
273
278
|
|
274
279
|
if is_generic_alias(ty):
|
@@ -278,7 +283,7 @@ def _make_obj_marshaler(ty: ta.Any) -> ObjMarshaler:
|
|
278
283
|
pass
|
279
284
|
else:
|
280
285
|
k, v = ta.get_args(ty)
|
281
|
-
return MappingObjMarshaler(mt,
|
286
|
+
return MappingObjMarshaler(mt, rec(k), rec(v))
|
282
287
|
|
283
288
|
try:
|
284
289
|
st = _OBJ_MARSHALER_GENERIC_ITERABLE_TYPES[ta.get_origin(ty)]
|
@@ -286,33 +291,71 @@ def _make_obj_marshaler(ty: ta.Any) -> ObjMarshaler:
|
|
286
291
|
pass
|
287
292
|
else:
|
288
293
|
[e] = ta.get_args(ty)
|
289
|
-
return IterableObjMarshaler(st,
|
294
|
+
return IterableObjMarshaler(st, rec(e))
|
290
295
|
|
291
296
|
if is_union_alias(ty):
|
292
|
-
return OptionalObjMarshaler(
|
297
|
+
return OptionalObjMarshaler(rec(get_optional_alias_arg(ty)))
|
293
298
|
|
294
299
|
raise TypeError(ty)
|
295
300
|
|
296
301
|
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
p.m = m
|
302
|
+
##
|
303
|
+
|
304
|
+
|
305
|
+
_OBJ_MARSHALERS_LOCK = threading.RLock()
|
306
|
+
|
307
|
+
_OBJ_MARSHALERS: ta.Dict[ta.Any, ObjMarshaler] = dict(_DEFAULT_OBJ_MARSHALERS)
|
308
|
+
|
309
|
+
_OBJ_MARSHALER_PROXIES: ta.Dict[ta.Any, ProxyObjMarshaler] = {}
|
310
|
+
|
311
|
+
|
312
|
+
def register_opj_marshaler(ty: ta.Any, m: ObjMarshaler) -> None:
|
313
|
+
with _OBJ_MARSHALERS_LOCK:
|
314
|
+
if ty in _OBJ_MARSHALERS:
|
315
|
+
raise KeyError(ty)
|
312
316
|
_OBJ_MARSHALERS[ty] = m
|
317
|
+
|
318
|
+
|
319
|
+
def get_obj_marshaler(
|
320
|
+
ty: ta.Any,
|
321
|
+
*,
|
322
|
+
no_cache: bool = False,
|
323
|
+
**kwargs: ta.Any,
|
324
|
+
) -> ObjMarshaler:
|
325
|
+
with _OBJ_MARSHALERS_LOCK:
|
326
|
+
if not no_cache:
|
327
|
+
try:
|
328
|
+
return _OBJ_MARSHALERS[ty]
|
329
|
+
except KeyError:
|
330
|
+
pass
|
331
|
+
|
332
|
+
try:
|
333
|
+
return _OBJ_MARSHALER_PROXIES[ty]
|
334
|
+
except KeyError:
|
335
|
+
pass
|
336
|
+
|
337
|
+
rec = functools.partial(
|
338
|
+
get_obj_marshaler,
|
339
|
+
no_cache=no_cache,
|
340
|
+
**kwargs,
|
341
|
+
)
|
342
|
+
|
343
|
+
p = ProxyObjMarshaler()
|
344
|
+
_OBJ_MARSHALER_PROXIES[ty] = p
|
345
|
+
try:
|
346
|
+
m = _make_obj_marshaler(ty, rec, **kwargs)
|
347
|
+
finally:
|
348
|
+
del _OBJ_MARSHALER_PROXIES[ty]
|
349
|
+
p.m = m
|
350
|
+
|
351
|
+
if not no_cache:
|
352
|
+
_OBJ_MARSHALERS[ty] = m
|
313
353
|
return m
|
314
354
|
|
315
355
|
|
356
|
+
##
|
357
|
+
|
358
|
+
|
316
359
|
def marshal_obj(o: ta.Any, ty: ta.Any = None) -> ta.Any:
|
317
360
|
return get_obj_marshaler(ty if ty is not None else type(o)).marshal(o)
|
318
361
|
|
omlish/lite/pidfile.py
CHANGED
omlish/lite/subprocesses.py
CHANGED
@@ -110,3 +110,21 @@ def subprocess_try_output(
|
|
110
110
|
def subprocess_try_output_str(*args: str, **kwargs: ta.Any) -> ta.Optional[str]:
|
111
111
|
out = subprocess_try_output(*args, **kwargs)
|
112
112
|
return out.decode().strip() if out is not None else None
|
113
|
+
|
114
|
+
|
115
|
+
##
|
116
|
+
|
117
|
+
|
118
|
+
def subprocess_close(
|
119
|
+
proc: subprocess.Popen,
|
120
|
+
timeout: ta.Optional[float] = None,
|
121
|
+
) -> None:
|
122
|
+
# TODO: terminate, sleep, kill
|
123
|
+
if proc.stdout:
|
124
|
+
proc.stdout.close()
|
125
|
+
if proc.stderr:
|
126
|
+
proc.stderr.close()
|
127
|
+
if proc.stdin:
|
128
|
+
proc.stdin.close()
|
129
|
+
|
130
|
+
proc.wait(timeout)
|
omlish/specs/__init__.py
CHANGED
omlish/sync.py
CHANGED
@@ -3,6 +3,7 @@ TODO:
|
|
3
3
|
- sync (lol) w/ asyncs.anyio
|
4
4
|
- atomics
|
5
5
|
"""
|
6
|
+
import collections
|
6
7
|
import threading
|
7
8
|
import typing as ta
|
8
9
|
|
@@ -68,3 +69,57 @@ class LazyFn(ta.Generic[T]):
|
|
68
69
|
self._v = lang.just(self._fn())
|
69
70
|
self._once.do(do)
|
70
71
|
return self._v.must()
|
72
|
+
|
73
|
+
|
74
|
+
class ConditionDeque(ta.Generic[T]):
|
75
|
+
def __init__(
|
76
|
+
self,
|
77
|
+
*,
|
78
|
+
cond: ta.Optional['threading.Condition'] = None,
|
79
|
+
deque: collections.deque[T] | None = None,
|
80
|
+
|
81
|
+
lock: ta.Optional['threading.RLock'] = None,
|
82
|
+
maxlen: int | None = None,
|
83
|
+
init: ta.Iterable[T] | None = None,
|
84
|
+
) -> None:
|
85
|
+
super().__init__()
|
86
|
+
|
87
|
+
if cond is None:
|
88
|
+
cond = threading.Condition(lock=lock)
|
89
|
+
if deque is None:
|
90
|
+
deque = collections.deque(maxlen=maxlen)
|
91
|
+
if init:
|
92
|
+
deque.extend(init)
|
93
|
+
|
94
|
+
self._cond = cond
|
95
|
+
self._deque = deque
|
96
|
+
|
97
|
+
@property
|
98
|
+
def cond(self) -> 'threading.Condition':
|
99
|
+
return self._cond
|
100
|
+
|
101
|
+
@property
|
102
|
+
def deque(self) -> collections.deque[T]:
|
103
|
+
return self._deque
|
104
|
+
|
105
|
+
def push(
|
106
|
+
self,
|
107
|
+
*items: T,
|
108
|
+
n: int = 1,
|
109
|
+
) -> None:
|
110
|
+
with self.cond:
|
111
|
+
self.deque.extend(items)
|
112
|
+
self.cond.notify(n)
|
113
|
+
|
114
|
+
def pop(
|
115
|
+
self,
|
116
|
+
timeout: float | None = None,
|
117
|
+
*,
|
118
|
+
if_empty: ta.Callable[[], None] | None = None,
|
119
|
+
) -> T:
|
120
|
+
with self.cond:
|
121
|
+
if not self.deque and if_empty is not None:
|
122
|
+
if_empty()
|
123
|
+
while not self.deque:
|
124
|
+
self.cond.wait(timeout)
|
125
|
+
return self.deque.popleft()
|