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.
Files changed (58) hide show
  1. omlish/__about__.py +2 -2
  2. omlish/collections/__init__.py +15 -15
  3. omlish/collections/frozen.py +0 -2
  4. omlish/collections/identity.py +0 -3
  5. omlish/collections/indexed.py +0 -2
  6. omlish/collections/mappings.py +0 -3
  7. omlish/collections/ordered.py +0 -2
  8. omlish/collections/persistent/__init__.py +0 -0
  9. omlish/collections/sorted/__init__.py +0 -0
  10. omlish/collections/{skiplist.py → sorted/skiplist.py} +0 -1
  11. omlish/collections/{sorted.py → sorted/sorted.py} +2 -5
  12. omlish/collections/unmodifiable.py +0 -2
  13. omlish/daemons/__init__.py +0 -0
  14. omlish/daemons/daemon.py +200 -0
  15. omlish/daemons/reparent.py +16 -0
  16. omlish/daemons/spawning.py +166 -0
  17. omlish/daemons/targets.py +89 -0
  18. omlish/daemons/waiting.py +95 -0
  19. omlish/dispatch/dispatch.py +4 -1
  20. omlish/dispatch/methods.py +4 -0
  21. omlish/formats/json/__init__.py +5 -0
  22. omlish/io/compress/brotli.py +3 -3
  23. omlish/io/fdio/__init__.py +3 -0
  24. omlish/lang/__init__.py +0 -2
  25. omlish/lang/contextmanagers.py +15 -10
  26. omlish/libc.py +2 -4
  27. omlish/lite/timeouts.py +1 -1
  28. omlish/marshal/__init__.py +10 -10
  29. omlish/marshal/base.py +1 -1
  30. omlish/marshal/standard.py +2 -2
  31. omlish/marshal/trivial/__init__.py +0 -0
  32. omlish/marshal/{forbidden.py → trivial/forbidden.py} +7 -7
  33. omlish/marshal/{nop.py → trivial/nop.py} +5 -5
  34. omlish/os/deathpacts/__init__.py +15 -0
  35. omlish/os/deathpacts/base.py +76 -0
  36. omlish/os/deathpacts/heartbeatfile.py +85 -0
  37. omlish/os/{death.py → deathpacts/pipe.py} +20 -90
  38. omlish/os/forkhooks.py +55 -31
  39. omlish/os/pidfiles/manager.py +11 -44
  40. omlish/os/pidfiles/pidfile.py +18 -1
  41. omlish/reflect/__init__.py +1 -0
  42. omlish/reflect/inspect.py +43 -0
  43. omlish/sql/queries/__init__.py +4 -4
  44. omlish/sql/queries/rendering2.py +248 -0
  45. omlish/text/parts.py +26 -23
  46. {omlish-0.0.0.dev229.dist-info → omlish-0.0.0.dev231.dist-info}/METADATA +1 -1
  47. {omlish-0.0.0.dev229.dist-info → omlish-0.0.0.dev231.dist-info}/RECORD +57 -45
  48. omlish/formats/json/delimted.py +0 -4
  49. /omlish/collections/{persistent.py → persistent/persistent.py} +0 -0
  50. /omlish/collections/{treap.py → persistent/treap.py} +0 -0
  51. /omlish/collections/{treapmap.py → persistent/treapmap.py} +0 -0
  52. /omlish/marshal/{utils.py → proxy.py} +0 -0
  53. /omlish/marshal/{singular → trivial}/any.py +0 -0
  54. /omlish/sql/queries/{building.py → std.py} +0 -0
  55. {omlish-0.0.0.dev229.dist-info → omlish-0.0.0.dev231.dist-info}/LICENSE +0 -0
  56. {omlish-0.0.0.dev229.dist-info → omlish-0.0.0.dev231.dist-info}/WHEEL +0 -0
  57. {omlish-0.0.0.dev229.dist-info → omlish-0.0.0.dev231.dist-info}/entry_points.txt +0 -0
  58. {omlish-0.0.0.dev229.dist-info → omlish-0.0.0.dev231.dist-info}/top_level.txt +0 -0
omlish/__about__.py CHANGED
@@ -1,5 +1,5 @@
1
- __version__ = '0.0.0.dev229'
2
- __revision__ = '646f2d8caaef32d279a6ebcfd121449752fbae93'
1
+ __version__ = '0.0.0.dev231'
2
+ __revision__ = '2eb4140ac802af2814bc4b4d5a8d37613083ae32'
3
3
 
4
4
 
5
5
  #
@@ -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 .skiplist import ( # noqa
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,
@@ -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
 
@@ -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] = {}
@@ -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
 
@@ -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)
@@ -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
@@ -188,6 +188,5 @@ class SkipList(SortedCollection[T]):
188
188
 
189
189
 
190
190
  class SkipListDict(SortedListDict[K, V]):
191
-
192
191
  def __init__(self, *args, **kwargs) -> None:
193
192
  super().__init__(SkipList(comparator=SortedListDict._item_comparator), *args, **kwargs) # noqa
@@ -1,8 +1,8 @@
1
1
  import abc
2
2
  import typing as ta
3
3
 
4
- from .. import lang
5
- from .mappings import yield_dict_init
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
@@ -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)