omlish 0.0.0.dev5__py3-none-any.whl → 0.0.0.dev7__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.

Potentially problematic release.


This version of omlish might be problematic. Click here for more details.

Files changed (163) hide show
  1. omlish/__about__.py +109 -5
  2. omlish/__init__.py +0 -8
  3. omlish/asyncs/__init__.py +9 -9
  4. omlish/asyncs/anyio.py +123 -19
  5. omlish/asyncs/asyncio.py +23 -0
  6. omlish/asyncs/asyncs.py +9 -6
  7. omlish/asyncs/bridge.py +316 -0
  8. omlish/asyncs/trio_asyncio.py +7 -3
  9. omlish/bootstrap.py +737 -0
  10. omlish/check.py +1 -1
  11. omlish/collections/__init__.py +5 -0
  12. omlish/collections/exceptions.py +2 -0
  13. omlish/collections/identity.py +7 -0
  14. omlish/collections/utils.py +38 -9
  15. omlish/configs/strings.py +96 -0
  16. omlish/dataclasses/__init__.py +16 -0
  17. omlish/dataclasses/impl/copy.py +30 -0
  18. omlish/dataclasses/impl/descriptors.py +95 -0
  19. omlish/dataclasses/impl/exceptions.py +6 -0
  20. omlish/dataclasses/impl/fields.py +24 -25
  21. omlish/dataclasses/impl/init.py +4 -2
  22. omlish/dataclasses/impl/main.py +2 -0
  23. omlish/dataclasses/impl/reflect.py +1 -1
  24. omlish/dataclasses/utils.py +67 -0
  25. omlish/{lang/datetimes.py → datetimes.py} +8 -4
  26. omlish/diag/__init__.py +4 -0
  27. omlish/diag/procfs.py +2 -2
  28. omlish/{testing → diag}/pydevd.py +35 -0
  29. omlish/diag/threads.py +131 -48
  30. omlish/dispatch/_dispatch2.py +65 -0
  31. omlish/dispatch/_dispatch3.py +104 -0
  32. omlish/docker.py +16 -1
  33. omlish/fnpairs.py +11 -4
  34. omlish/formats/__init__.py +0 -0
  35. omlish/{configs → formats}/dotenv.py +15 -24
  36. omlish/{json.py → formats/json.py} +2 -1
  37. omlish/formats/yaml.py +223 -0
  38. omlish/graphs/trees.py +1 -1
  39. omlish/http/asgi.py +2 -1
  40. omlish/http/collections.py +15 -0
  41. omlish/http/consts.py +22 -1
  42. omlish/http/sessions.py +10 -3
  43. omlish/inject/__init__.py +49 -17
  44. omlish/inject/binder.py +185 -5
  45. omlish/inject/bindings.py +3 -36
  46. omlish/inject/eagers.py +2 -8
  47. omlish/inject/elements.py +31 -10
  48. omlish/inject/exceptions.py +1 -1
  49. omlish/inject/impl/elements.py +37 -12
  50. omlish/inject/impl/injector.py +72 -25
  51. omlish/inject/impl/inspect.py +33 -5
  52. omlish/inject/impl/origins.py +77 -0
  53. omlish/inject/impl/{private.py → privates.py} +2 -2
  54. omlish/inject/impl/scopes.py +6 -2
  55. omlish/inject/injector.py +8 -4
  56. omlish/inject/inspect.py +18 -0
  57. omlish/inject/keys.py +8 -14
  58. omlish/inject/listeners.py +26 -0
  59. omlish/inject/managed.py +76 -10
  60. omlish/inject/multis.py +68 -18
  61. omlish/inject/origins.py +30 -0
  62. omlish/inject/overrides.py +5 -4
  63. omlish/inject/{private.py → privates.py} +6 -10
  64. omlish/inject/providers.py +12 -85
  65. omlish/inject/scopes.py +13 -6
  66. omlish/inject/types.py +3 -1
  67. omlish/inject/utils.py +18 -0
  68. omlish/iterators.py +69 -2
  69. omlish/lang/__init__.py +24 -9
  70. omlish/lang/cached.py +2 -2
  71. omlish/lang/classes/restrict.py +12 -1
  72. omlish/lang/classes/simple.py +18 -8
  73. omlish/lang/contextmanagers.py +13 -4
  74. omlish/lang/descriptors.py +132 -1
  75. omlish/lang/functions.py +8 -28
  76. omlish/lang/imports.py +67 -0
  77. omlish/lang/iterables.py +60 -1
  78. omlish/lang/maybes.py +3 -0
  79. omlish/lang/objects.py +38 -0
  80. omlish/lang/strings.py +25 -0
  81. omlish/lang/sys.py +9 -0
  82. omlish/lang/typing.py +42 -0
  83. omlish/lifecycles/__init__.py +34 -0
  84. omlish/lifecycles/abstract.py +43 -0
  85. omlish/lifecycles/base.py +51 -0
  86. omlish/lifecycles/contextmanagers.py +74 -0
  87. omlish/lifecycles/controller.py +116 -0
  88. omlish/lifecycles/manager.py +161 -0
  89. omlish/lifecycles/states.py +43 -0
  90. omlish/lifecycles/transitions.py +64 -0
  91. omlish/lite/__init__.py +1 -0
  92. omlish/lite/cached.py +18 -0
  93. omlish/lite/check.py +29 -0
  94. omlish/lite/contextmanagers.py +18 -0
  95. omlish/lite/json.py +30 -0
  96. omlish/lite/logs.py +52 -0
  97. omlish/lite/marshal.py +316 -0
  98. omlish/lite/reflect.py +49 -0
  99. omlish/lite/runtime.py +18 -0
  100. omlish/lite/secrets.py +19 -0
  101. omlish/lite/strings.py +25 -0
  102. omlish/lite/subprocesses.py +112 -0
  103. omlish/logs/configs.py +15 -2
  104. omlish/logs/formatters.py +7 -2
  105. omlish/marshal/__init__.py +32 -0
  106. omlish/marshal/any.py +5 -5
  107. omlish/marshal/base.py +27 -11
  108. omlish/marshal/base64.py +24 -9
  109. omlish/marshal/dataclasses.py +34 -28
  110. omlish/marshal/datetimes.py +74 -18
  111. omlish/marshal/enums.py +14 -8
  112. omlish/marshal/exceptions.py +11 -1
  113. omlish/marshal/factories.py +59 -74
  114. omlish/marshal/forbidden.py +35 -0
  115. omlish/marshal/global_.py +11 -4
  116. omlish/marshal/iterables.py +21 -24
  117. omlish/marshal/mappings.py +23 -26
  118. omlish/marshal/naming.py +4 -0
  119. omlish/marshal/numbers.py +51 -0
  120. omlish/marshal/objects.py +1 -0
  121. omlish/marshal/optionals.py +11 -12
  122. omlish/marshal/polymorphism.py +86 -21
  123. omlish/marshal/primitives.py +4 -5
  124. omlish/marshal/standard.py +13 -8
  125. omlish/marshal/uuids.py +4 -5
  126. omlish/matchfns.py +218 -0
  127. omlish/os.py +64 -0
  128. omlish/reflect/__init__.py +39 -0
  129. omlish/reflect/isinstance.py +38 -0
  130. omlish/reflect/ops.py +84 -0
  131. omlish/reflect/subst.py +110 -0
  132. omlish/reflect/types.py +275 -0
  133. omlish/secrets/__init__.py +23 -0
  134. omlish/secrets/crypto.py +132 -0
  135. omlish/secrets/marshal.py +70 -0
  136. omlish/secrets/openssl.py +207 -0
  137. omlish/secrets/passwords.py +120 -0
  138. omlish/secrets/secrets.py +299 -0
  139. omlish/secrets/subprocesses.py +42 -0
  140. omlish/sql/dbs.py +7 -6
  141. omlish/sql/duckdb.py +136 -0
  142. omlish/sql/exprs.py +12 -0
  143. omlish/sql/secrets.py +10 -0
  144. omlish/sql/sqlean.py +17 -0
  145. omlish/term.py +2 -2
  146. omlish/testing/pytest/__init__.py +3 -2
  147. omlish/testing/pytest/inject/harness.py +3 -3
  148. omlish/testing/pytest/marks.py +4 -7
  149. omlish/testing/pytest/plugins/__init__.py +1 -0
  150. omlish/testing/pytest/plugins/asyncs.py +136 -0
  151. omlish/testing/pytest/plugins/pydevd.py +1 -1
  152. omlish/testing/pytest/plugins/switches.py +54 -19
  153. omlish/text/glyphsplit.py +97 -0
  154. omlish-0.0.0.dev7.dist-info/METADATA +50 -0
  155. omlish-0.0.0.dev7.dist-info/RECORD +268 -0
  156. {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev7.dist-info}/WHEEL +1 -1
  157. omlish/reflect.py +0 -355
  158. omlish-0.0.0.dev5.dist-info/METADATA +0 -34
  159. omlish-0.0.0.dev5.dist-info/RECORD +0 -212
  160. /omlish/{asyncs/futures.py → concurrent.py} +0 -0
  161. /omlish/{configs → formats}/props.py +0 -0
  162. {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev7.dist-info}/LICENSE +0 -0
  163. {omlish-0.0.0.dev5.dist-info → omlish-0.0.0.dev7.dist-info}/top_level.txt +0 -0
omlish/lang/iterables.py CHANGED
@@ -1,8 +1,11 @@
1
+ import dataclasses as dc
1
2
  import itertools
2
3
  import typing as ta
3
4
 
4
5
 
5
6
  T = ta.TypeVar('T')
7
+ S = ta.TypeVar('S')
8
+ R = ta.TypeVar('R')
6
9
 
7
10
 
8
11
  BUILTIN_SCALAR_ITERABLE_TYPES: tuple[type, ...] = (
@@ -34,7 +37,13 @@ def peek(vs: ta.Iterable[T]) -> tuple[T, ta.Iterator[T]]:
34
37
  return v, itertools.chain(iter((v,)), it)
35
38
 
36
39
 
37
- Rangeable: ta.TypeAlias = int | tuple[int] | tuple[int, int] | ta.Iterable[int]
40
+ Rangeable: ta.TypeAlias = ta.Union[ # noqa
41
+ int,
42
+ tuple[int],
43
+ tuple[int, int],
44
+ tuple[int, int, int],
45
+ ta.Iterable[int],
46
+ ]
38
47
 
39
48
 
40
49
  def asrange(i: Rangeable) -> ta.Iterable[int]:
@@ -52,3 +61,53 @@ def prodrange(*dims: Rangeable) -> ta.Iterable[ta.Sequence[int]]:
52
61
  if not dims:
53
62
  return []
54
63
  return itertools.product(*map(asrange, dims))
64
+
65
+
66
+ @dc.dataclass(frozen=True)
67
+ class itergen(ta.Generic[T]): # noqa
68
+ fn: ta.Callable[[], ta.Iterable[T]]
69
+
70
+ def __iter__(self):
71
+ return iter(self.fn())
72
+
73
+
74
+ def renumerate(it: ta.Iterable[T]) -> ta.Iterable[tuple[T, int]]:
75
+ return ((e, i) for i, e in enumerate(it))
76
+
77
+
78
+ flatten = itertools.chain.from_iterable
79
+
80
+
81
+ def flatmap(fn: ta.Callable[[T], ta.Iterable[R]], it: ta.Iterable[T]) -> ta.Iterable[R]:
82
+ return flatten(map(fn, it))
83
+
84
+
85
+ class Generator(ta.Generator[T, S, R]):
86
+ def __init__(self, gen: ta.Generator[T, S, R]) -> None:
87
+ super().__init__()
88
+ self.gen = gen
89
+
90
+ value: R
91
+
92
+ def __iter__(self):
93
+ return self
94
+
95
+ def __next__(self):
96
+ return self.send(None)
97
+
98
+ def send(self, v):
99
+ try:
100
+ return self.gen.send(v)
101
+ except StopIteration as e:
102
+ self.value = e.value
103
+ raise
104
+
105
+ def throw(self, *args):
106
+ try:
107
+ return self.gen.throw(*args)
108
+ except StopIteration as e:
109
+ self.value = e.value
110
+ raise
111
+
112
+ def close(self):
113
+ self.gen.close()
omlish/lang/maybes.py CHANGED
@@ -65,6 +65,9 @@ class Maybe(abc.ABC, ta.Generic[T]):
65
65
  class _Maybe(Maybe[T], tuple):
66
66
  __slots__ = ()
67
67
 
68
+ def __init_subclass__(cls, **kwargs):
69
+ raise TypeError
70
+
68
71
  @property
69
72
  def present(self) -> bool:
70
73
  return bool(self)
omlish/lang/objects.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import types
2
2
  import typing as ta
3
+ import weakref
3
4
 
4
5
 
5
6
  T = ta.TypeVar('T')
@@ -28,6 +29,28 @@ def opt_repr(obj: ta.Any) -> str | None:
28
29
  ##
29
30
 
30
31
 
32
+ _CAN_WEAKREF_TYPE_MAP: ta.MutableMapping[type, bool] = weakref.WeakKeyDictionary()
33
+
34
+
35
+ def can_weakref(obj: ta.Any) -> bool:
36
+ _type = type(obj)
37
+ try:
38
+ return _CAN_WEAKREF_TYPE_MAP[_type]
39
+ except KeyError:
40
+ pass
41
+ try:
42
+ weakref.ref(obj)
43
+ except TypeError:
44
+ ret = False
45
+ else:
46
+ ret = True
47
+ _CAN_WEAKREF_TYPE_MAP[_type] = ret
48
+ return ret
49
+
50
+
51
+ ##
52
+
53
+
31
54
  def new_type(
32
55
  name: str,
33
56
  bases: ta.Sequence[ta.Any],
@@ -62,6 +85,21 @@ def super_meta(
62
85
  ##
63
86
 
64
87
 
88
+ def deep_subclasses(cls: type) -> ta.Iterator[type]:
89
+ seen = set()
90
+ todo = list(reversed(cls.__subclasses__()))
91
+ while todo:
92
+ cur = todo.pop()
93
+ if cur in seen:
94
+ continue
95
+ seen.add(cur)
96
+ yield cur
97
+ todo.extend(reversed(cur.__subclasses__()))
98
+
99
+
100
+ ##
101
+
102
+
65
103
  class SimpleProxy(ta.Generic[T]):
66
104
 
67
105
  class Descriptor:
omlish/lang/strings.py CHANGED
@@ -126,3 +126,28 @@ def is_ident_cont(c: str) -> bool:
126
126
 
127
127
  def is_ident(name: str) -> bool:
128
128
  return is_ident_start(name[0]) and all(is_ident_cont(c) for c in name[1:])
129
+
130
+
131
+ ##
132
+
133
+
134
+ BOOL_STRINGS: ta.Sequence[tuple[str, str]] = [
135
+ ('n', 'y'),
136
+ ('no', 'yes'),
137
+ ('f', 't'),
138
+ ('false', 'true'),
139
+ ('off', 'on'),
140
+ ('0', '1'),
141
+ ]
142
+
143
+ BOOL_FALSE_STRINGS = frozenset(tup[0] for tup in BOOL_STRINGS)
144
+ BOOL_TRUE_STRINGS = frozenset(tup[1] for tup in BOOL_STRINGS)
145
+
146
+ STRING_BOOL_VALUES: ta.Mapping[str, bool] = {
147
+ k: v
148
+ for ks, v in [
149
+ (BOOL_FALSE_STRINGS, False),
150
+ (BOOL_TRUE_STRINGS, True),
151
+ ]
152
+ for k in ks
153
+ }
omlish/lang/sys.py CHANGED
@@ -1,6 +1,15 @@
1
1
  import sys
2
2
 
3
3
 
4
+ REQUIRED_PYTHON_VERSION = (3, 12)
5
+
6
+
7
+ def check_runtime_version() -> None:
8
+ if sys.version_info < REQUIRED_PYTHON_VERSION:
9
+ raise OSError(
10
+ f'Requires python {REQUIRED_PYTHON_VERSION}, got {sys.version_info} from {sys.executable}') # noqa
11
+
12
+
4
13
  def is_gil_enabled() -> bool:
5
14
  if (fn := getattr(sys, '_is_gil_enabled', None)) is not None:
6
15
  return fn()
omlish/lang/typing.py CHANGED
@@ -5,6 +5,7 @@ TODO:
5
5
  - probably need to gen types per inst
6
6
  - typed_factory
7
7
  """
8
+ import dataclasses as dc
8
9
  import functools
9
10
  import inspect
10
11
  import typing as ta
@@ -12,6 +13,11 @@ import typing as ta
12
13
 
13
14
  Ty = ta.TypeVar('Ty', bound=type)
14
15
 
16
+ T = ta.TypeVar('T')
17
+ A0 = ta.TypeVar('A0')
18
+ A1 = ta.TypeVar('A1')
19
+ A2 = ta.TypeVar('A2')
20
+
15
21
 
16
22
  BytesLike: ta.TypeAlias = bytes | bytearray
17
23
 
@@ -91,3 +97,39 @@ def typed_partial(obj, **kw): # noqa
91
97
  },
92
98
  )(inner)
93
99
  return _update_wrapper_no_anns(lam, obj)
100
+
101
+
102
+ ##
103
+ # A workaround for typing deficiencies (like `Argument 2 to NewType(...) must be subclassable`).
104
+
105
+
106
+ @dc.dataclass(frozen=True)
107
+ class Func0(ta.Generic[T]):
108
+ fn: ta.Callable[[], T]
109
+
110
+ def __call__(self) -> T:
111
+ return self.fn()
112
+
113
+
114
+ @dc.dataclass(frozen=True)
115
+ class Func1(ta.Generic[A0, T]):
116
+ fn: ta.Callable[[A0], T]
117
+
118
+ def __call__(self, a0: A0) -> T:
119
+ return self.fn(a0)
120
+
121
+
122
+ @dc.dataclass(frozen=True)
123
+ class Func2(ta.Generic[A0, A1, T]):
124
+ fn: ta.Callable[[A0, A1], T]
125
+
126
+ def __call__(self, a0: A0, a1: A1) -> T:
127
+ return self.fn(a0, a1)
128
+
129
+
130
+ @dc.dataclass(frozen=True)
131
+ class Func3(ta.Generic[A0, A1, A2, T]):
132
+ fn: ta.Callable[[A0, A1, A2], T]
133
+
134
+ def __call__(self, a0: A0, a1: A1, a2: A2) -> T:
135
+ return self.fn(a0, a1, a2)
@@ -0,0 +1,34 @@
1
+ from .abstract import ( # noqa
2
+ AbstractLifecycle,
3
+ )
4
+
5
+ from .base import ( # noqa
6
+ CallbackLifecycle,
7
+ Lifecycle,
8
+ LifecycleCallback,
9
+ )
10
+
11
+ from .contextmanagers import ( # noqa
12
+ ContextManagerLifecycle,
13
+ LifecycleContextManager,
14
+ )
15
+
16
+ from .controller import ( # noqa
17
+ LifecycleController,
18
+ LifecycleListener,
19
+ )
20
+
21
+ from .manager import ( # noqa
22
+ LifecycleManager,
23
+ )
24
+
25
+ from .states import ( # noqa
26
+ LifecycleState,
27
+ LifecycleStateError,
28
+ LifecycleStates,
29
+ )
30
+
31
+ from .transitions import ( # noqa
32
+ LifecycleTransition,
33
+ LifecycleTransitions,
34
+ )
@@ -0,0 +1,43 @@
1
+ import typing as ta
2
+
3
+ from .. import cached
4
+ from .. import dataclasses as dc
5
+ from .. import lang
6
+ from .base import Lifecycle
7
+
8
+
9
+ AbstractLifecycleT = ta.TypeVar('AbstractLifecycleT', bound='AbstractLifecycle')
10
+
11
+
12
+ class AbstractLifecycle(lang.Abstract):
13
+ @dc.dataclass(frozen=True)
14
+ class _Lifecycle(Lifecycle, lang.Final, ta.Generic[AbstractLifecycleT]):
15
+ obj: AbstractLifecycleT
16
+
17
+ def lifecycle_construct(self) -> None:
18
+ self.obj._lifecycle_construct() # noqa
19
+
20
+ def lifecycle_start(self) -> None:
21
+ self.obj._lifecycle_start() # noqa
22
+
23
+ def lifecycle_stop(self) -> None:
24
+ self.obj._lifecycle_stop() # noqa
25
+
26
+ def lifecycle_destroy(self) -> None:
27
+ self.obj._lifecycle_destroy() # noqa
28
+
29
+ @cached.property
30
+ def _lifecycle(self) -> _Lifecycle[ta.Self]:
31
+ return AbstractLifecycle._Lifecycle(self)
32
+
33
+ def _lifecycle_construct(self) -> None:
34
+ pass
35
+
36
+ def _lifecycle_start(self) -> None:
37
+ pass
38
+
39
+ def _lifecycle_stop(self) -> None:
40
+ pass
41
+
42
+ def _lifecycle_destroy(self) -> None:
43
+ pass
@@ -0,0 +1,51 @@
1
+ import typing as ta
2
+
3
+ from .. import dataclasses as dc
4
+ from .. import lang
5
+
6
+
7
+ LifecycleT = ta.TypeVar('LifecycleT', bound='Lifecycle')
8
+ LifecycleCallback: ta.TypeAlias = ta.Callable[[LifecycleT], None]
9
+
10
+
11
+ class Lifecycle:
12
+
13
+ def lifecycle_construct(self) -> None:
14
+ pass
15
+
16
+ def lifecycle_start(self) -> None:
17
+ pass
18
+
19
+ def lifecycle_stop(self) -> None:
20
+ pass
21
+
22
+ def lifecycle_destroy(self) -> None:
23
+ pass
24
+
25
+
26
+ @dc.dataclass(frozen=True, kw_only=True)
27
+ class CallbackLifecycle(Lifecycle, lang.Final, ta.Generic[LifecycleT]):
28
+ on_construct: LifecycleCallback['CallbackLifecycle[LifecycleT]'] | None = None
29
+ on_start: LifecycleCallback['CallbackLifecycle[LifecycleT]'] | None = None
30
+ on_stop: LifecycleCallback['CallbackLifecycle[LifecycleT]'] | None = None
31
+ on_destroy: LifecycleCallback['CallbackLifecycle[LifecycleT]'] | None = None
32
+
33
+ @ta.override
34
+ def lifecycle_construct(self) -> None:
35
+ if self.on_construct is not None:
36
+ self.on_construct(self)
37
+
38
+ @ta.override
39
+ def lifecycle_start(self) -> None:
40
+ if self.on_start is not None:
41
+ self.on_start(self)
42
+
43
+ @ta.override
44
+ def lifecycle_stop(self) -> None:
45
+ if self.on_stop is not None:
46
+ self.on_stop(self)
47
+
48
+ @ta.override
49
+ def lifecycle_destroy(self) -> None:
50
+ if self.on_destroy is not None:
51
+ self.on_destroy(self)
@@ -0,0 +1,74 @@
1
+ import types
2
+ import typing as ta
3
+
4
+ from .. import dataclasses as dc
5
+ from .. import defs
6
+ from .. import lang
7
+ from .base import Lifecycle
8
+ from .controller import LifecycleController
9
+ from .states import LifecycleState
10
+ from .states import LifecycleStates
11
+
12
+
13
+ LifecycleT = ta.TypeVar('LifecycleT', bound='Lifecycle')
14
+ ContextManagerT = ta.TypeVar('ContextManagerT', bound=ta.ContextManager)
15
+
16
+
17
+ @dc.dataclass(frozen=True)
18
+ class ContextManagerLifecycle(Lifecycle, lang.Final, ta.Generic[ContextManagerT]):
19
+ cm: ContextManagerT
20
+
21
+ @ta.override
22
+ def lifecycle_start(self) -> None:
23
+ self.cm.__enter__()
24
+
25
+ @ta.override
26
+ def lifecycle_stop(self) -> None:
27
+ self.cm.__exit__(None, None, None)
28
+
29
+
30
+ class LifecycleContextManager(ta.Generic[LifecycleT]):
31
+
32
+ def __init__(self, lifecycle: LifecycleT) -> None:
33
+ super().__init__()
34
+ self._lifecycle = lifecycle
35
+ self._controller = lifecycle if isinstance(lifecycle, LifecycleController) else LifecycleController(lifecycle)
36
+
37
+ defs.repr('lifecycle', 'state')
38
+
39
+ @property
40
+ def lifecycle(self) -> LifecycleT:
41
+ return self._lifecycle
42
+
43
+ @property
44
+ def controller(self) -> LifecycleController:
45
+ return self._controller
46
+
47
+ @property
48
+ def state(self) -> LifecycleState:
49
+ return self._controller.state
50
+
51
+ def __enter__(self) -> ta.Self:
52
+ try:
53
+ self._controller.lifecycle_construct()
54
+ self._controller.lifecycle_start()
55
+ except Exception:
56
+ self._controller.lifecycle_destroy()
57
+ raise
58
+ return self
59
+
60
+ def __exit__(
61
+ self,
62
+ exc_type: type[BaseException] | None,
63
+ exc_val: BaseException | None,
64
+ exc_tb: types.TracebackType | None,
65
+ ) -> bool | None:
66
+ try:
67
+ if self._controller.state is LifecycleStates.STARTED:
68
+ self._controller.lifecycle_stop()
69
+ except Exception:
70
+ self._controller.lifecycle_destroy()
71
+ raise
72
+ else:
73
+ self._controller.lifecycle_destroy()
74
+ return None
@@ -0,0 +1,116 @@
1
+ import typing as ta
2
+
3
+ from .. import check
4
+ from .. import defs
5
+ from .. import lang
6
+ from .base import Lifecycle
7
+ from .states import LifecycleState
8
+ from .states import LifecycleStates
9
+ from .transitions import LifecycleTransition
10
+ from .transitions import LifecycleTransitions
11
+
12
+
13
+ LifecycleT = ta.TypeVar('LifecycleT', bound='Lifecycle')
14
+
15
+
16
+ class LifecycleListener(ta.Generic[LifecycleT]):
17
+
18
+ def on_starting(self, obj: LifecycleT) -> None:
19
+ pass
20
+
21
+ def on_started(self, obj: LifecycleT) -> None:
22
+ pass
23
+
24
+ def on_stopping(self, obj: LifecycleT) -> None:
25
+ pass
26
+
27
+ def on_stopped(self, obj: LifecycleT) -> None:
28
+ pass
29
+
30
+
31
+ class LifecycleController(Lifecycle, ta.Generic[LifecycleT]):
32
+
33
+ def __init__(
34
+ self,
35
+ lifecycle: LifecycleT,
36
+ *,
37
+ lock: lang.DefaultLockable = None,
38
+ ) -> None:
39
+ super().__init__()
40
+
41
+ self._lifecycle: LifecycleT = check.isinstance(lifecycle, Lifecycle) # type: ignore
42
+ self._lock = lang.default_lock(lock, False)
43
+
44
+ self._state = LifecycleStates.NEW
45
+ self._listeners: list[LifecycleListener[LifecycleT]] = []
46
+
47
+ defs.repr('lifecycle', 'state')
48
+
49
+ @property
50
+ def lifecycle(self) -> LifecycleT:
51
+ return self._lifecycle
52
+
53
+ @property
54
+ def state(self) -> LifecycleState:
55
+ return self._state
56
+
57
+ def add_listener(self, listener: LifecycleListener[LifecycleT]) -> 'LifecycleController':
58
+ self._listeners.append(check.isinstance(listener, LifecycleListener)) # type: ignore
59
+ return self
60
+
61
+ def _advance(
62
+ self,
63
+ transition: LifecycleTransition,
64
+ lifecycle_fn: ta.Callable[[], None],
65
+ pre_listener_fn: ta.Callable[[LifecycleListener[LifecycleT]], ta.Callable[[LifecycleT], None]] | None = None, # noqa
66
+ post_listener_fn: ta.Callable[[LifecycleListener[LifecycleT]], ta.Callable[[LifecycleT], None]] | None = None, # noqa
67
+ ) -> None:
68
+ with self._lock():
69
+ if pre_listener_fn is not None:
70
+ for listener in self._listeners:
71
+ pre_listener_fn(listener)(self._lifecycle)
72
+ check.state(self._state in transition.old)
73
+ self._state = transition.new_intermediate
74
+ try:
75
+ lifecycle_fn()
76
+ except Exception:
77
+ self._state = transition.new_failed
78
+ raise
79
+ self._state = transition.new_succeeded
80
+ if post_listener_fn is not None:
81
+ for listener in self._listeners:
82
+ post_listener_fn(listener)(self._lifecycle)
83
+
84
+ ##
85
+
86
+ @ta.override
87
+ def lifecycle_construct(self) -> None:
88
+ self._advance(
89
+ LifecycleTransitions.CONSTRUCT,
90
+ self._lifecycle.lifecycle_construct,
91
+ )
92
+
93
+ @ta.override
94
+ def lifecycle_start(self) -> None:
95
+ self._advance(
96
+ LifecycleTransitions.START,
97
+ self._lifecycle.lifecycle_start,
98
+ lambda l: l.on_starting,
99
+ lambda l: l.on_started,
100
+ )
101
+
102
+ @ta.override
103
+ def lifecycle_stop(self) -> None:
104
+ self._advance(
105
+ LifecycleTransitions.STOP,
106
+ self._lifecycle.lifecycle_stop,
107
+ lambda l: l.on_stopping,
108
+ lambda l: l.on_stopped,
109
+ )
110
+
111
+ @ta.override
112
+ def lifecycle_destroy(self) -> None:
113
+ self._advance(
114
+ LifecycleTransitions.DESTROY,
115
+ self._lifecycle.lifecycle_destroy,
116
+ )