omlish 0.0.0.dev102__py3-none-any.whl → 0.0.0.dev104__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.
- 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()
|