omlish 0.0.0.dev229__py3-none-any.whl → 0.0.0.dev231__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/collections/__init__.py +15 -15
- omlish/collections/frozen.py +0 -2
- omlish/collections/identity.py +0 -3
- omlish/collections/indexed.py +0 -2
- omlish/collections/mappings.py +0 -3
- omlish/collections/ordered.py +0 -2
- omlish/collections/persistent/__init__.py +0 -0
- omlish/collections/sorted/__init__.py +0 -0
- omlish/collections/{skiplist.py → sorted/skiplist.py} +0 -1
- omlish/collections/{sorted.py → sorted/sorted.py} +2 -5
- omlish/collections/unmodifiable.py +0 -2
- omlish/daemons/__init__.py +0 -0
- omlish/daemons/daemon.py +200 -0
- omlish/daemons/reparent.py +16 -0
- omlish/daemons/spawning.py +166 -0
- omlish/daemons/targets.py +89 -0
- omlish/daemons/waiting.py +95 -0
- omlish/dispatch/dispatch.py +4 -1
- omlish/dispatch/methods.py +4 -0
- omlish/formats/json/__init__.py +5 -0
- omlish/io/compress/brotli.py +3 -3
- omlish/io/fdio/__init__.py +3 -0
- omlish/lang/__init__.py +0 -2
- omlish/lang/contextmanagers.py +15 -10
- omlish/libc.py +2 -4
- omlish/lite/timeouts.py +1 -1
- omlish/marshal/__init__.py +10 -10
- omlish/marshal/base.py +1 -1
- omlish/marshal/standard.py +2 -2
- omlish/marshal/trivial/__init__.py +0 -0
- omlish/marshal/{forbidden.py → trivial/forbidden.py} +7 -7
- omlish/marshal/{nop.py → trivial/nop.py} +5 -5
- omlish/os/deathpacts/__init__.py +15 -0
- omlish/os/deathpacts/base.py +76 -0
- omlish/os/deathpacts/heartbeatfile.py +85 -0
- omlish/os/{death.py → deathpacts/pipe.py} +20 -90
- omlish/os/forkhooks.py +55 -31
- omlish/os/pidfiles/manager.py +11 -44
- omlish/os/pidfiles/pidfile.py +18 -1
- omlish/reflect/__init__.py +1 -0
- omlish/reflect/inspect.py +43 -0
- omlish/sql/queries/__init__.py +4 -4
- omlish/sql/queries/rendering2.py +248 -0
- omlish/text/parts.py +26 -23
- {omlish-0.0.0.dev229.dist-info → omlish-0.0.0.dev231.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev229.dist-info → omlish-0.0.0.dev231.dist-info}/RECORD +57 -45
- omlish/formats/json/delimted.py +0 -4
- /omlish/collections/{persistent.py → persistent/persistent.py} +0 -0
- /omlish/collections/{treap.py → persistent/treap.py} +0 -0
- /omlish/collections/{treapmap.py → persistent/treapmap.py} +0 -0
- /omlish/marshal/{utils.py → proxy.py} +0 -0
- /omlish/marshal/{singular → trivial}/any.py +0 -0
- /omlish/sql/queries/{building.py → std.py} +0 -0
- {omlish-0.0.0.dev229.dist-info → omlish-0.0.0.dev231.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev229.dist-info → omlish-0.0.0.dev231.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev229.dist-info → omlish-0.0.0.dev231.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev229.dist-info → omlish-0.0.0.dev231.dist-info}/top_level.txt +0 -0
omlish/__about__.py
CHANGED
omlish/collections/__init__.py
CHANGED
@@ -76,39 +76,39 @@ from .ordered import ( # noqa
|
|
76
76
|
OrderedSet,
|
77
77
|
)
|
78
78
|
|
79
|
-
from .persistent import ( # noqa
|
79
|
+
from .persistent.persistent import ( # noqa
|
80
80
|
PersistentMap,
|
81
81
|
)
|
82
82
|
|
83
83
|
if _ta.TYPE_CHECKING:
|
84
|
-
from .
|
84
|
+
from .persistent.treapmap import ( # noqa
|
85
|
+
TreapMap,
|
86
|
+
new_treap_map,
|
87
|
+
)
|
88
|
+
else:
|
89
|
+
_lang.proxy_init(globals(), '.persistent.treapmap', [
|
90
|
+
'TreapMap',
|
91
|
+
'new_treap_map',
|
92
|
+
])
|
93
|
+
|
94
|
+
if _ta.TYPE_CHECKING:
|
95
|
+
from .sorted.skiplist import ( # noqa
|
85
96
|
SkipList,
|
86
97
|
SkipListDict,
|
87
98
|
)
|
88
99
|
else:
|
89
|
-
_lang.proxy_init(globals(), '.skiplist', [
|
100
|
+
_lang.proxy_init(globals(), '.sorted.skiplist', [
|
90
101
|
'SkipList',
|
91
102
|
'SkipListDict',
|
92
103
|
])
|
93
104
|
|
94
|
-
from .sorted import ( # noqa
|
105
|
+
from .sorted.sorted import ( # noqa
|
95
106
|
SortedCollection,
|
96
107
|
SortedListDict,
|
97
108
|
SortedMapping,
|
98
109
|
SortedMutableMapping,
|
99
110
|
)
|
100
111
|
|
101
|
-
if _ta.TYPE_CHECKING:
|
102
|
-
from .treapmap import ( # noqa
|
103
|
-
TreapMap,
|
104
|
-
new_treap_map,
|
105
|
-
)
|
106
|
-
else:
|
107
|
-
_lang.proxy_init(globals(), '.treapmap', [
|
108
|
-
'TreapMap',
|
109
|
-
'new_treap_map',
|
110
|
-
])
|
111
|
-
|
112
112
|
from .unmodifiable import ( # noqa
|
113
113
|
Unmodifiable,
|
114
114
|
UnmodifiableMapping,
|
omlish/collections/frozen.py
CHANGED
@@ -16,7 +16,6 @@ class Frozen(ta.Hashable, abc.ABC):
|
|
16
16
|
|
17
17
|
|
18
18
|
class FrozenDict(ta.Mapping[K, V], Frozen):
|
19
|
-
|
20
19
|
def __new__(cls, *args: ta.Any, **kwargs: ta.Any) -> 'FrozenDict[K, V]': # noqa
|
21
20
|
if len(args) == 1 and Frozen in type(args[0]).__bases__:
|
22
21
|
return args[0]
|
@@ -73,7 +72,6 @@ class FrozenDict(ta.Mapping[K, V], Frozen):
|
|
73
72
|
|
74
73
|
|
75
74
|
class FrozenList(ta.Sequence[T], Frozen):
|
76
|
-
|
77
75
|
def __init__(self, it: ta.Iterable[T] | None = None) -> None:
|
78
76
|
super().__init__()
|
79
77
|
|
omlish/collections/identity.py
CHANGED
@@ -13,7 +13,6 @@ V = ta.TypeVar('V')
|
|
13
13
|
|
14
14
|
|
15
15
|
class IdentityWrapper(ta.Generic[T]):
|
16
|
-
|
17
16
|
def __init__(self, value: T) -> None:
|
18
17
|
super().__init__()
|
19
18
|
self._value = value
|
@@ -36,7 +35,6 @@ class IdentityWrapper(ta.Generic[T]):
|
|
36
35
|
|
37
36
|
|
38
37
|
class IdentityKeyDict(ta.MutableMapping[K, V]):
|
39
|
-
|
40
38
|
def __init__(self, *args, **kwargs) -> None:
|
41
39
|
super().__init__()
|
42
40
|
self._dict: dict[int, tuple[K, V]] = {}
|
@@ -70,7 +68,6 @@ class IdentityKeyDict(ta.MutableMapping[K, V]):
|
|
70
68
|
|
71
69
|
|
72
70
|
class IdentitySet(ta.MutableSet[T]):
|
73
|
-
|
74
71
|
def __init__(self, init: ta.Iterable[T] | None = None) -> None:
|
75
72
|
super().__init__()
|
76
73
|
self._dict: dict[int, T] = {}
|
omlish/collections/indexed.py
CHANGED
@@ -8,7 +8,6 @@ T = ta.TypeVar('T')
|
|
8
8
|
|
9
9
|
|
10
10
|
class IndexedSeq(ta.Sequence[T]):
|
11
|
-
|
12
11
|
def __init__(self, it: ta.Iterable[T], *, identity: bool = False) -> None:
|
13
12
|
super().__init__()
|
14
13
|
|
@@ -42,7 +41,6 @@ class IndexedSeq(ta.Sequence[T]):
|
|
42
41
|
|
43
42
|
|
44
43
|
class IndexedSetSeq(ta.Sequence[ta.AbstractSet[T]]):
|
45
|
-
|
46
44
|
def __init__(self, it: ta.Iterable[ta.Iterable[T]], *, identity: bool = False) -> None:
|
47
45
|
super().__init__()
|
48
46
|
|
omlish/collections/mappings.py
CHANGED
@@ -49,7 +49,6 @@ def yield_dict_init(*args, **kwargs) -> ta.Iterable[tuple[ta.Any, ta.Any]]:
|
|
49
49
|
|
50
50
|
|
51
51
|
class TypeMap(ta.Generic[T]):
|
52
|
-
|
53
52
|
def __init__(self, items: ta.Iterable[T] = ()) -> None:
|
54
53
|
super().__init__()
|
55
54
|
|
@@ -79,7 +78,6 @@ class TypeMap(ta.Generic[T]):
|
|
79
78
|
|
80
79
|
|
81
80
|
class DynamicTypeMap(ta.Generic[V]):
|
82
|
-
|
83
81
|
def __init__(self, items: ta.Iterable[V] = (), *, weak: bool = False) -> None:
|
84
82
|
super().__init__()
|
85
83
|
|
@@ -115,7 +113,6 @@ class DynamicTypeMap(ta.Generic[V]):
|
|
115
113
|
|
116
114
|
|
117
115
|
class MissingDict(dict[K, V]):
|
118
|
-
|
119
116
|
def __init__(self, missing_fn: ta.Callable[[K], V]) -> None:
|
120
117
|
if not callable(missing_fn):
|
121
118
|
raise TypeError(missing_fn)
|
omlish/collections/ordered.py
CHANGED
@@ -5,7 +5,6 @@ T = ta.TypeVar('T')
|
|
5
5
|
|
6
6
|
|
7
7
|
class OrderedSet(ta.MutableSet[T]):
|
8
|
-
|
9
8
|
def __init__(self, iterable: ta.Iterable[T] | None = None) -> None:
|
10
9
|
super().__init__()
|
11
10
|
self._dct: dict[T, ta.Any] = {}
|
@@ -56,7 +55,6 @@ class OrderedSet(ta.MutableSet[T]):
|
|
56
55
|
|
57
56
|
|
58
57
|
class OrderedFrozenSet(ta.FrozenSet[T]): # noqa
|
59
|
-
|
60
58
|
_list: ta.Sequence[T]
|
61
59
|
|
62
60
|
def __new__(cls, items: ta.Iterable[T]) -> frozenset[T]: # type: ignore
|
File without changes
|
File without changes
|
@@ -1,8 +1,8 @@
|
|
1
1
|
import abc
|
2
2
|
import typing as ta
|
3
3
|
|
4
|
-
from
|
5
|
-
from
|
4
|
+
from ... import lang
|
5
|
+
from ..mappings import yield_dict_init
|
6
6
|
|
7
7
|
|
8
8
|
T = ta.TypeVar('T')
|
@@ -12,7 +12,6 @@ V = ta.TypeVar('V')
|
|
12
12
|
|
13
13
|
|
14
14
|
class SortedCollection(lang.Abstract, ta.Collection[T]):
|
15
|
-
|
16
15
|
Comparator = ta.Callable[[U, U], int]
|
17
16
|
|
18
17
|
@staticmethod
|
@@ -55,7 +54,6 @@ class SortedCollection(lang.Abstract, ta.Collection[T]):
|
|
55
54
|
|
56
55
|
|
57
56
|
class SortedMapping(ta.Mapping[K, V]):
|
58
|
-
|
59
57
|
@abc.abstractmethod
|
60
58
|
def items(self) -> ta.Iterator[tuple[K, V]]: # type: ignore
|
61
59
|
raise NotImplementedError
|
@@ -78,7 +76,6 @@ class SortedMutableMapping(ta.MutableMapping[K, V], SortedMapping[K, V]):
|
|
78
76
|
|
79
77
|
|
80
78
|
class SortedListDict(SortedMutableMapping[K, V]):
|
81
|
-
|
82
79
|
@staticmethod
|
83
80
|
def _item_comparator(a: tuple[K, V], b: tuple[K, V]) -> int:
|
84
81
|
return SortedCollection.default_comparator(a[0], b[0])
|
@@ -13,7 +13,6 @@ class Unmodifiable(lang.Abstract):
|
|
13
13
|
|
14
14
|
|
15
15
|
class UnmodifiableSequence(ta.Sequence[T], Unmodifiable, lang.Final):
|
16
|
-
|
17
16
|
def __init__(self, target: ta.Sequence[T]) -> None:
|
18
17
|
super().__init__()
|
19
18
|
|
@@ -65,7 +64,6 @@ class UnmodifiableSequence(ta.Sequence[T], Unmodifiable, lang.Final):
|
|
65
64
|
|
66
65
|
|
67
66
|
class UnmodifiableSet(ta.AbstractSet[T], Unmodifiable, lang.Final):
|
68
|
-
|
69
67
|
def __init__(self, target: ta.AbstractSet[T]) -> None:
|
70
68
|
super().__init__()
|
71
69
|
|
File without changes
|
omlish/daemons/daemon.py
ADDED
@@ -0,0 +1,200 @@
|
|
1
|
+
"""
|
2
|
+
TODO:
|
3
|
+
- OK.. this is useful for non-system-daemons too... which have no pidfile..
|
4
|
+
- split out pidfile concern to.. identity?
|
5
|
+
- async[io] support, really just waiting
|
6
|
+
- helpers for http, json, etc
|
7
|
+
- heartbeat? status checks? heartbeat file?
|
8
|
+
- zmq
|
9
|
+
- cli, cfg reload
|
10
|
+
- bootstrap
|
11
|
+
- rsrc limit
|
12
|
+
- logs
|
13
|
+
- https://github.com/Homebrew/homebrew-services
|
14
|
+
- deathpacts
|
15
|
+
- timebomb
|
16
|
+
- pickle protocol, revision / venv check, multiprocessing manager support
|
17
|
+
"""
|
18
|
+
import contextlib
|
19
|
+
import functools
|
20
|
+
import logging
|
21
|
+
import os.path
|
22
|
+
import threading
|
23
|
+
import time
|
24
|
+
import typing as ta
|
25
|
+
|
26
|
+
from .. import check
|
27
|
+
from .. import dataclasses as dc
|
28
|
+
from .. import lang
|
29
|
+
from ..os.pidfiles.manager import _PidfileManager # noqa
|
30
|
+
from ..os.pidfiles.manager import open_inheritable_pidfile
|
31
|
+
from ..os.pidfiles.pidfile import Pidfile
|
32
|
+
from .reparent import reparent_process
|
33
|
+
from .spawning import InProcessSpawner
|
34
|
+
from .spawning import Spawn
|
35
|
+
from .spawning import Spawner
|
36
|
+
from .spawning import Spawning
|
37
|
+
from .spawning import spawner_for
|
38
|
+
from .targets import Target
|
39
|
+
from .targets import target_runner_for
|
40
|
+
from .waiting import Wait
|
41
|
+
from .waiting import waiter_for
|
42
|
+
|
43
|
+
|
44
|
+
log = logging.getLogger(__name__)
|
45
|
+
|
46
|
+
|
47
|
+
##
|
48
|
+
|
49
|
+
|
50
|
+
class Daemon:
|
51
|
+
@dc.dataclass(frozen=True, kw_only=True)
|
52
|
+
class Config:
|
53
|
+
target: Target
|
54
|
+
spawning: Spawning
|
55
|
+
|
56
|
+
#
|
57
|
+
|
58
|
+
reparent_process: bool = False
|
59
|
+
|
60
|
+
pid_file: str | None = None
|
61
|
+
|
62
|
+
#
|
63
|
+
|
64
|
+
wait: Wait | None = None
|
65
|
+
|
66
|
+
wait_timeout: lang.TimeoutLike = 10.
|
67
|
+
wait_sleep_s: float = .1
|
68
|
+
|
69
|
+
launched_timeout_s: float = 5.
|
70
|
+
|
71
|
+
#
|
72
|
+
|
73
|
+
def __post_init__(self) -> None:
|
74
|
+
check.isinstance(self.pid_file, (str, None))
|
75
|
+
|
76
|
+
def __init__(self, config: Config) -> None:
|
77
|
+
super().__init__()
|
78
|
+
|
79
|
+
self._config = config
|
80
|
+
|
81
|
+
@property
|
82
|
+
def config(self) -> Config:
|
83
|
+
return self._config
|
84
|
+
|
85
|
+
#
|
86
|
+
|
87
|
+
@property
|
88
|
+
def has_pidfile(self) -> bool:
|
89
|
+
return self._config.pid_file is not None
|
90
|
+
|
91
|
+
def _non_inheritable_pidfile(self) -> Pidfile:
|
92
|
+
check.state(self.has_pidfile)
|
93
|
+
return Pidfile(
|
94
|
+
check.non_empty_str(self._config.pid_file),
|
95
|
+
inheritable=False,
|
96
|
+
)
|
97
|
+
|
98
|
+
def is_running(self) -> bool:
|
99
|
+
check.state(self.has_pidfile)
|
100
|
+
|
101
|
+
if not os.path.isfile(check.non_empty_str(self._config.pid_file)):
|
102
|
+
return False
|
103
|
+
|
104
|
+
with self._non_inheritable_pidfile() as pf:
|
105
|
+
return not pf.try_acquire_lock()
|
106
|
+
|
107
|
+
#
|
108
|
+
|
109
|
+
def _inner_launch(
|
110
|
+
self,
|
111
|
+
*,
|
112
|
+
pidfile_manager: ta.ContextManager | None,
|
113
|
+
launched_callback: ta.Callable[[], None] | None = None,
|
114
|
+
) -> None:
|
115
|
+
try:
|
116
|
+
if self._config.reparent_process:
|
117
|
+
log.info('Reparenting')
|
118
|
+
reparent_process()
|
119
|
+
|
120
|
+
with contextlib.ExitStack() as es:
|
121
|
+
pidfile: Pidfile | None = None # noqa
|
122
|
+
if pidfile_manager is not None:
|
123
|
+
pidfile = check.isinstance(es.enter_context(pidfile_manager), Pidfile)
|
124
|
+
pidfile.write()
|
125
|
+
|
126
|
+
if launched_callback is not None:
|
127
|
+
launched_callback()
|
128
|
+
|
129
|
+
runner = target_runner_for(self._config.target)
|
130
|
+
runner.run()
|
131
|
+
|
132
|
+
finally:
|
133
|
+
if launched_callback is not None:
|
134
|
+
launched_callback()
|
135
|
+
|
136
|
+
def launch_no_wait(self) -> bool:
|
137
|
+
with contextlib.ExitStack() as es:
|
138
|
+
spawner: Spawner = es.enter_context(spawner_for(self._config.spawning))
|
139
|
+
|
140
|
+
#
|
141
|
+
|
142
|
+
inherit_fds: set[int] = set()
|
143
|
+
launched_event: threading.Event | None = None
|
144
|
+
|
145
|
+
pidfile: Pidfile | None = None # noqa
|
146
|
+
pidfile_manager: ta.ContextManager | None = None
|
147
|
+
|
148
|
+
if (pid_file := self._config.pid_file) is not None:
|
149
|
+
if not isinstance(spawner, InProcessSpawner):
|
150
|
+
pidfile = es.enter_context(open_inheritable_pidfile(pid_file))
|
151
|
+
pidfile_manager = lang.NopContextManager(pidfile)
|
152
|
+
|
153
|
+
else:
|
154
|
+
check.state(not self._config.reparent_process)
|
155
|
+
pidfile = es.enter_context(Pidfile(pid_file))
|
156
|
+
pidfile_manager = pidfile.dup()
|
157
|
+
launched_event = threading.Event()
|
158
|
+
|
159
|
+
if not pidfile.try_acquire_lock():
|
160
|
+
return False
|
161
|
+
|
162
|
+
inherit_fds.add(check.isinstance(pidfile.fileno(), int))
|
163
|
+
|
164
|
+
#
|
165
|
+
|
166
|
+
spawner.spawn(Spawn(
|
167
|
+
functools.partial(
|
168
|
+
self._inner_launch,
|
169
|
+
pidfile_manager=pidfile_manager,
|
170
|
+
launched_callback=launched_event.set if launched_event is not None else None,
|
171
|
+
),
|
172
|
+
inherit_fds=inherit_fds,
|
173
|
+
))
|
174
|
+
|
175
|
+
if launched_event is not None:
|
176
|
+
check.state(launched_event.wait(timeout=self._config.launched_timeout_s))
|
177
|
+
|
178
|
+
return True
|
179
|
+
|
180
|
+
#
|
181
|
+
|
182
|
+
def wait_sync(self, timeout: lang.TimeoutLike = lang.Timeout.Default) -> None:
|
183
|
+
if self._config.wait is None:
|
184
|
+
return
|
185
|
+
|
186
|
+
timeout = lang.Timeout.of(timeout, self._config.wait_timeout)
|
187
|
+
waiter = waiter_for(self._config.wait)
|
188
|
+
while not waiter.do_wait():
|
189
|
+
timeout()
|
190
|
+
time.sleep(self._config.wait_sleep_s or 0.)
|
191
|
+
|
192
|
+
#
|
193
|
+
|
194
|
+
class _NOT_SET(lang.Marker): # noqa
|
195
|
+
pass
|
196
|
+
|
197
|
+
def launch(self, timeout: lang.TimeoutLike = lang.Timeout.Default) -> None:
|
198
|
+
self.launch_no_wait()
|
199
|
+
|
200
|
+
self.wait_sync(timeout)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
import os
|
2
|
+
import sys
|
3
|
+
|
4
|
+
|
5
|
+
def reparent_process() -> None:
|
6
|
+
if (pid := os.fork()): # noqa
|
7
|
+
sys.exit(0)
|
8
|
+
raise RuntimeError('Unreachable') # noqa
|
9
|
+
|
10
|
+
os.setsid()
|
11
|
+
|
12
|
+
if (pid := os.fork()): # noqa
|
13
|
+
sys.exit(0)
|
14
|
+
|
15
|
+
sys.stdout.flush()
|
16
|
+
sys.stderr.flush()
|
@@ -0,0 +1,166 @@
|
|
1
|
+
import abc
|
2
|
+
import functools
|
3
|
+
import os
|
4
|
+
import sys
|
5
|
+
import threading
|
6
|
+
import typing as ta
|
7
|
+
|
8
|
+
from .. import check
|
9
|
+
from .. import dataclasses as dc
|
10
|
+
from .. import lang
|
11
|
+
from ..diag import pydevd
|
12
|
+
|
13
|
+
|
14
|
+
if ta.TYPE_CHECKING:
|
15
|
+
import multiprocessing as mp
|
16
|
+
import multiprocessing.context
|
17
|
+
import multiprocessing.process # noqa
|
18
|
+
|
19
|
+
from ..multiprocessing import spawn as omp_spawn
|
20
|
+
|
21
|
+
else:
|
22
|
+
mp = lang.proxy_import('multiprocessing', extras=['context', 'process'])
|
23
|
+
subprocess = lang.proxy_import('subprocess')
|
24
|
+
|
25
|
+
omp_spawn = lang.proxy_import('..multiprocessing.spawn', __package__)
|
26
|
+
|
27
|
+
|
28
|
+
##
|
29
|
+
|
30
|
+
|
31
|
+
class Spawning(dc.Case):
|
32
|
+
pass
|
33
|
+
|
34
|
+
|
35
|
+
class Spawn(dc.Frozen, final=True):
|
36
|
+
fn: ta.Callable[[], None]
|
37
|
+
|
38
|
+
_: dc.KW_ONLY
|
39
|
+
|
40
|
+
inherit_fds: ta.Collection[int] | None = None
|
41
|
+
|
42
|
+
|
43
|
+
class Spawner(lang.ContextManaged, abc.ABC):
|
44
|
+
@abc.abstractmethod
|
45
|
+
def spawn(self, spawn: Spawn) -> None:
|
46
|
+
raise NotImplementedError
|
47
|
+
|
48
|
+
|
49
|
+
class InProcessSpawner(Spawner, abc.ABC):
|
50
|
+
pass
|
51
|
+
|
52
|
+
|
53
|
+
@functools.singledispatch
|
54
|
+
def spawner_for(spawning: Spawning) -> Spawner:
|
55
|
+
raise TypeError(spawning)
|
56
|
+
|
57
|
+
|
58
|
+
##
|
59
|
+
|
60
|
+
|
61
|
+
class MultiprocessingSpawning(Spawning, kw_only=True):
|
62
|
+
# Defaults to 'fork' if under pydevd, else 'spawn'
|
63
|
+
start_method: str | None = None
|
64
|
+
|
65
|
+
non_daemon: bool = False
|
66
|
+
|
67
|
+
|
68
|
+
class MultiprocessingSpawner(Spawner):
|
69
|
+
def __init__(self, spawning: MultiprocessingSpawning) -> None:
|
70
|
+
super().__init__()
|
71
|
+
|
72
|
+
self._spawning = spawning
|
73
|
+
self._process: ta.Optional['mp.process.BaseProcess'] = None # noqa
|
74
|
+
|
75
|
+
def _process_cls(self, spawn: Spawn) -> type['mp.process.BaseProcess']:
|
76
|
+
if (start_method := self._spawning.start_method) is None:
|
77
|
+
# Unfortunately, pydevd forces the use of the 'fork' start_method, which cannot be mixed with 'spawn':
|
78
|
+
# https://github.com/python/cpython/blob/a7427f2db937adb4c787754deb4c337f1894fe86/Lib/multiprocessing/spawn.py#L102 # noqa
|
79
|
+
if pydevd.is_running():
|
80
|
+
start_method = 'fork'
|
81
|
+
else:
|
82
|
+
start_method = 'spawn'
|
83
|
+
|
84
|
+
ctx: 'mp.context.BaseContext' # noqa
|
85
|
+
if start_method == 'fork':
|
86
|
+
ctx = mp.get_context(check.non_empty_str(start_method))
|
87
|
+
|
88
|
+
elif start_method == 'spawn':
|
89
|
+
ctx = omp_spawn.ExtrasSpawnContext(omp_spawn.SpawnExtras(
|
90
|
+
pass_fds=frozenset(spawn.inherit_fds) if spawn.inherit_fds is not None else None,
|
91
|
+
))
|
92
|
+
|
93
|
+
else:
|
94
|
+
raise ValueError(start_method)
|
95
|
+
|
96
|
+
return ctx.Process # type: ignore
|
97
|
+
|
98
|
+
def spawn(self, spawn: Spawn) -> None:
|
99
|
+
check.none(self._process)
|
100
|
+
self._process = self._process_cls(spawn)(
|
101
|
+
target=spawn.fn,
|
102
|
+
daemon=not self._spawning.non_daemon,
|
103
|
+
)
|
104
|
+
self._process.start()
|
105
|
+
|
106
|
+
|
107
|
+
@spawner_for.register
|
108
|
+
def _(spawning: MultiprocessingSpawning) -> MultiprocessingSpawner:
|
109
|
+
return MultiprocessingSpawner(spawning)
|
110
|
+
|
111
|
+
|
112
|
+
##
|
113
|
+
|
114
|
+
|
115
|
+
class ForkSpawning(Spawning):
|
116
|
+
pass
|
117
|
+
|
118
|
+
|
119
|
+
class ForkSpawner(Spawner, dc.Frozen):
|
120
|
+
spawning: ForkSpawning
|
121
|
+
|
122
|
+
def spawn(self, spawn: Spawn) -> None:
|
123
|
+
if (pid := os.fork()): # noqa
|
124
|
+
return
|
125
|
+
|
126
|
+
try:
|
127
|
+
spawn.fn()
|
128
|
+
except BaseException: # noqa
|
129
|
+
sys.exit(1)
|
130
|
+
else:
|
131
|
+
sys.exit(0)
|
132
|
+
|
133
|
+
raise RuntimeError('Unreachable') # noqa
|
134
|
+
|
135
|
+
|
136
|
+
@spawner_for.register
|
137
|
+
def _(spawning: ForkSpawning) -> ForkSpawner:
|
138
|
+
return ForkSpawner(spawning)
|
139
|
+
|
140
|
+
|
141
|
+
##
|
142
|
+
|
143
|
+
|
144
|
+
class ThreadSpawning(Spawning, kw_only=True):
|
145
|
+
non_daemon: bool = False
|
146
|
+
|
147
|
+
|
148
|
+
class ThreadSpawner(InProcessSpawner):
|
149
|
+
def __init__(self, spawning: ThreadSpawning) -> None:
|
150
|
+
super().__init__()
|
151
|
+
|
152
|
+
self._spawning = spawning
|
153
|
+
self._thread: threading.Thread | None = None
|
154
|
+
|
155
|
+
def spawn(self, spawn: Spawn) -> None:
|
156
|
+
check.none(self._thread)
|
157
|
+
self._thread = threading.Thread(
|
158
|
+
target=spawn.fn,
|
159
|
+
daemon=not self._spawning.non_daemon,
|
160
|
+
)
|
161
|
+
self._thread.start()
|
162
|
+
|
163
|
+
|
164
|
+
@spawner_for.register
|
165
|
+
def _(spawning: ThreadSpawning) -> ThreadSpawner:
|
166
|
+
return ThreadSpawner(spawning)
|
@@ -0,0 +1,89 @@
|
|
1
|
+
import abc
|
2
|
+
import functools
|
3
|
+
import os
|
4
|
+
import typing as ta
|
5
|
+
|
6
|
+
from .. import check
|
7
|
+
from .. import dataclasses as dc
|
8
|
+
from .. import lang
|
9
|
+
|
10
|
+
|
11
|
+
if ta.TYPE_CHECKING:
|
12
|
+
import runpy
|
13
|
+
else:
|
14
|
+
runpy = lang.proxy_import('runpy')
|
15
|
+
|
16
|
+
|
17
|
+
##
|
18
|
+
|
19
|
+
|
20
|
+
class Target(dc.Case):
|
21
|
+
pass
|
22
|
+
|
23
|
+
|
24
|
+
class TargetRunner(abc.ABC):
|
25
|
+
@abc.abstractmethod
|
26
|
+
def run(self) -> None:
|
27
|
+
raise NotImplementedError
|
28
|
+
|
29
|
+
|
30
|
+
@functools.singledispatch
|
31
|
+
def target_runner_for(target: Target) -> TargetRunner:
|
32
|
+
raise TypeError(target)
|
33
|
+
|
34
|
+
|
35
|
+
##
|
36
|
+
|
37
|
+
|
38
|
+
class FnTarget(Target):
|
39
|
+
fn: ta.Callable[[], None]
|
40
|
+
|
41
|
+
|
42
|
+
class FnTargetRunner(TargetRunner, dc.Frozen):
|
43
|
+
target: FnTarget
|
44
|
+
|
45
|
+
def run(self) -> None:
|
46
|
+
self.target.fn()
|
47
|
+
|
48
|
+
|
49
|
+
@target_runner_for.register
|
50
|
+
def _(target: FnTarget) -> FnTargetRunner:
|
51
|
+
return FnTargetRunner(target)
|
52
|
+
|
53
|
+
|
54
|
+
##
|
55
|
+
|
56
|
+
|
57
|
+
class NameTarget(Target):
|
58
|
+
name: str
|
59
|
+
|
60
|
+
|
61
|
+
class NameTargetRunner(TargetRunner, dc.Frozen):
|
62
|
+
target: NameTarget
|
63
|
+
|
64
|
+
def run(self) -> None:
|
65
|
+
name = self.target.name
|
66
|
+
if lang.can_import(name):
|
67
|
+
runpy._run_module_as_main(name) # type: ignore # noqa
|
68
|
+
else:
|
69
|
+
obj = lang.import_attr(self.target.name)
|
70
|
+
obj()
|
71
|
+
|
72
|
+
|
73
|
+
##
|
74
|
+
|
75
|
+
|
76
|
+
class ExecTarget(Target):
|
77
|
+
cmd: ta.Sequence[str] = dc.xfield(coerce=check.of_not_isinstance(str))
|
78
|
+
|
79
|
+
|
80
|
+
class ExecTargetRunner(TargetRunner, dc.Frozen):
|
81
|
+
target: ExecTarget
|
82
|
+
|
83
|
+
def run(self) -> None:
|
84
|
+
os.execl(*self.target.cmd)
|
85
|
+
|
86
|
+
|
87
|
+
@target_runner_for.register
|
88
|
+
def _(target: ExecTarget) -> ExecTargetRunner:
|
89
|
+
return ExecTargetRunner(target)
|