omlish 0.0.0.dev161__py3-none-any.whl → 0.0.0.dev162__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
omlish/__about__.py CHANGED
@@ -1,5 +1,5 @@
1
- __version__ = '0.0.0.dev161'
2
- __revision__ = '60fcd526dba581c88e3b27a1defac7fb75614a7b'
1
+ __version__ = '0.0.0.dev162'
2
+ __revision__ = 'd9129e1bcec87862768d3a2fc10011def8f3fd52'
3
3
 
4
4
 
5
5
  #
@@ -0,0 +1,6 @@
1
+ Based on bluelet ( https://github.com/sampsyo/bluelet ) by Adrian Sampson, original license:
2
+
3
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
4
+ WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
5
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
6
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
File without changes
@@ -0,0 +1,67 @@
1
+ # ruff: noqa: I001
2
+ from .api import ( # noqa
3
+ bluelet,
4
+ )
5
+
6
+ from .core import ( # noqa
7
+ BlueletCoro as Coro,
8
+ BlueletExcInfo as ExcInfo,
9
+ CoreBlueletEvent as CoreEvent,
10
+ DelegationBlueletEvent as DelegationEvent,
11
+ ExceptionBlueletEvent as ExceptionEvent,
12
+ JoinBlueletEvent as JoinEvent,
13
+ KillBlueletEvent as KillEvent,
14
+ ReturnBlueletEvent as ReturnEvent,
15
+ SleepBlueletEvent as SleepEvent,
16
+ SpawnBlueletEvent as SpawnEvent,
17
+ ValueBlueletEvent as ValueEvent,
18
+ )
19
+
20
+ from .events import ( # noqa
21
+ BlueletEvent as Event,
22
+ BlueletFuture as Future,
23
+ BlueletHasFileno as HasFileno,
24
+ BlueletWaitable as Waitable,
25
+ BlueletWaitables as Waitables,
26
+ WaitableBlueletEvent as WaitableEvent,
27
+ )
28
+
29
+ from .files import ( # noqa
30
+ FileBlueletEvent as FileEvent,
31
+ ReadBlueletEvent as ReadEvent,
32
+ WriteBlueletEvent as WriteEvent,
33
+ )
34
+
35
+ from .runner import ( # noqa
36
+ BlueletCoroException as CoroException,
37
+ )
38
+
39
+ from .sockets import ( # noqa
40
+ AcceptBlueletEvent as AcceptEvent,
41
+ BlueletConnection as Connection,
42
+ BlueletListener as Listener,
43
+ ReceiveBlueletEvent as ReceiveEvent,
44
+ SendBlueletEvent as SendEvent,
45
+ SocketBlueletEvent as SocketEvent,
46
+ SocketClosedBlueletError as SocketClosedError,
47
+ )
48
+
49
+
50
+ ##
51
+
52
+
53
+ call = bluelet.call
54
+ end = bluelet.end
55
+ join = bluelet.join
56
+ kill = bluelet.kill
57
+ null = bluelet.null
58
+ sleep = bluelet.sleep
59
+ spawn = bluelet.spawn
60
+
61
+ read = bluelet.read
62
+ write = bluelet.write
63
+
64
+ run = bluelet.run
65
+
66
+ connect = bluelet.connect
67
+ server = bluelet.server
@@ -0,0 +1,23 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ # Based on bluelet ( https://github.com/sampsyo/bluelet ) by Adrian Sampson, original license:
4
+ # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
5
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
6
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
7
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8
+ from .core import _CoreBlueletApi
9
+ from .files import _FilesBlueletApi
10
+ from .runner import _RunnerBlueletApi
11
+ from .sockets import _SocketsBlueletApi
12
+
13
+
14
+ class BlueletApi(
15
+ _RunnerBlueletApi,
16
+ _SocketsBlueletApi,
17
+ _FilesBlueletApi,
18
+ _CoreBlueletApi,
19
+ ):
20
+ pass
21
+
22
+
23
+ bluelet = BlueletApi()
@@ -0,0 +1,178 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ # Based on bluelet ( https://github.com/sampsyo/bluelet ) by Adrian Sampson, original license:
4
+ # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
5
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
6
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
7
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8
+ import abc
9
+ import dataclasses as dc
10
+ import time
11
+ import types
12
+ import typing as ta
13
+
14
+ from .events import BlueletEvent
15
+ from .events import BlueletFuture
16
+ from .events import WaitableBlueletEvent
17
+
18
+
19
+ T = ta.TypeVar('T')
20
+
21
+ BlueletExcInfo = ta.Tuple[ta.Type[BaseException], BaseException, types.TracebackType] # ta.TypeAlias
22
+
23
+ BlueletCoro = ta.Generator[ta.Union['BlueletEvent', 'BlueletCoro'], ta.Any, None] # ta.TypeAlias
24
+
25
+ BlueletSpawnable = ta.Union[BlueletCoro, ta.Awaitable] # ta.TypeAlias
26
+
27
+
28
+ ##
29
+
30
+
31
+ @dc.dataclass(frozen=True, eq=False)
32
+ class _BlueletAwaitableDriver:
33
+ a: ta.Awaitable
34
+
35
+ def __call__(self) -> BlueletCoro:
36
+ g = self.a.__await__()
37
+ gi = iter(g)
38
+ while True:
39
+ try:
40
+ f = gi.send(None)
41
+ except StopIteration as e:
42
+ yield ReturnBlueletEvent(e.value)
43
+ break
44
+ else:
45
+ if not isinstance(f, BlueletFuture):
46
+ raise TypeError(f)
47
+ res = yield f.event
48
+ f.done = True
49
+ f.result = res
50
+
51
+
52
+ ##
53
+
54
+
55
+ class CoreBlueletEvent(BlueletEvent, abc.ABC): # noqa
56
+ pass
57
+
58
+
59
+ @dc.dataclass(frozen=True, eq=False)
60
+ class ValueBlueletEvent(CoreBlueletEvent, ta.Generic[T]):
61
+ """An event that does nothing but return a fixed value."""
62
+
63
+ value: T
64
+
65
+
66
+ @dc.dataclass(frozen=True, eq=False)
67
+ class ExceptionBlueletEvent(CoreBlueletEvent):
68
+ """Raise an exception at the yield point. Used internally."""
69
+
70
+ exc_info: BlueletExcInfo
71
+
72
+
73
+ @dc.dataclass(frozen=True, eq=False)
74
+ class SpawnBlueletEvent(CoreBlueletEvent):
75
+ """Add a new coroutine coro to the scheduler."""
76
+
77
+ spawned: BlueletSpawnable
78
+
79
+
80
+ @dc.dataclass(frozen=True, eq=False)
81
+ class JoinBlueletEvent(CoreBlueletEvent):
82
+ """Suspend the coro until the specified child coro has completed."""
83
+
84
+ child: BlueletCoro
85
+
86
+
87
+ @dc.dataclass(frozen=True, eq=False)
88
+ class KillBlueletEvent(CoreBlueletEvent):
89
+ """Unschedule a child coro."""
90
+
91
+ child: BlueletCoro
92
+
93
+
94
+ @dc.dataclass(frozen=True, eq=False)
95
+ class DelegationBlueletEvent(CoreBlueletEvent):
96
+ """
97
+ Suspend execution of the current coro, start a new coro and, once the child coro finished, return control to
98
+ the parent coro.
99
+ """
100
+
101
+ spawned: BlueletCoro
102
+
103
+
104
+ @dc.dataclass(frozen=True, eq=False)
105
+ class ReturnBlueletEvent(CoreBlueletEvent, ta.Generic[T]):
106
+ """Return a value the current coro's delegator at the point of delegation. Ends the current (delegate) coro."""
107
+
108
+ value: ta.Optional[T]
109
+
110
+
111
+ @dc.dataclass(frozen=True, eq=False)
112
+ class SleepBlueletEvent(WaitableBlueletEvent, CoreBlueletEvent):
113
+ """Suspend the coro for a given duration."""
114
+
115
+ wakeup_time: float
116
+
117
+ def time_left(self) -> float:
118
+ return max(self.wakeup_time - time.time(), 0.)
119
+
120
+
121
+ ##
122
+
123
+
124
+ class _CoreBlueletApi:
125
+ def value(self, v: T) -> ValueBlueletEvent[T]:
126
+ """Event: yield a value."""
127
+
128
+ return ValueBlueletEvent(v)
129
+
130
+ def null(self) -> ValueBlueletEvent[None]:
131
+ """Event: yield to the scheduler without doing anything special."""
132
+
133
+ return ValueBlueletEvent(None)
134
+
135
+ def spawn(self, spawned: BlueletSpawnable) -> SpawnBlueletEvent:
136
+ """Event: add another coroutine to the scheduler. Both the parent and child coroutines run concurrently."""
137
+
138
+ if isinstance(spawned, types.CoroutineType):
139
+ spawned = _BlueletAwaitableDriver(spawned)()
140
+
141
+ if not isinstance(spawned, types.GeneratorType):
142
+ raise TypeError(f'{spawned} is not spawnable')
143
+
144
+ return SpawnBlueletEvent(spawned)
145
+
146
+ def join(self, coro: BlueletCoro) -> JoinBlueletEvent:
147
+ """Suspend the coro until another, previously `spawn`ed coro completes."""
148
+
149
+ return JoinBlueletEvent(coro)
150
+
151
+ def kill(self, coro: BlueletCoro) -> KillBlueletEvent:
152
+ """Halt the execution of a different `spawn`ed coro."""
153
+
154
+ return KillBlueletEvent(coro)
155
+
156
+ def call(self, spawned: BlueletSpawnable) -> DelegationBlueletEvent:
157
+ """
158
+ Event: delegate to another coroutine. The current coroutine is resumed once the sub-coroutine finishes. If the
159
+ sub-coroutine returns a value using end(), then this event returns that value.
160
+ """
161
+
162
+ if isinstance(spawned, types.CoroutineType):
163
+ spawned = _BlueletAwaitableDriver(spawned)()
164
+
165
+ if not isinstance(spawned, types.GeneratorType):
166
+ raise TypeError(f'{spawned} is not spawnable')
167
+
168
+ return DelegationBlueletEvent(spawned)
169
+
170
+ def end(self, value: ta.Optional[T] = None) -> ReturnBlueletEvent[T]:
171
+ """Event: ends the coroutine and returns a value to its delegator."""
172
+
173
+ return ReturnBlueletEvent(value)
174
+
175
+ def sleep(self, duration: float) -> SleepBlueletEvent:
176
+ """Event: suspend the coro for ``duration`` seconds."""
177
+
178
+ return SleepBlueletEvent(time.time() + duration)
@@ -0,0 +1,78 @@
1
+ # ruff: noqa: UP007
2
+ # @omlish-lite
3
+ # Based on bluelet ( https://github.com/sampsyo/bluelet ) by Adrian Sampson, original license:
4
+ # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
5
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
6
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
7
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8
+ import abc
9
+ import dataclasses as dc
10
+ import typing as ta
11
+
12
+
13
+ R = ta.TypeVar('R')
14
+
15
+ BlueletEventT = ta.TypeVar('BlueletEventT', bound='BlueletEvent') # ta.TypeAlias
16
+
17
+ BlueletWaitable = ta.Union[int, 'BlueletHasFileno'] # ta.TypeAlias
18
+
19
+
20
+ ##
21
+
22
+
23
+ class BlueletEvent(abc.ABC): # noqa
24
+ """
25
+ Just a base class identifying Bluelet events. An event is an object yielded from a Bluelet coro coroutine to
26
+ suspend operation and communicate with the scheduler.
27
+ """
28
+
29
+ def __await__(self):
30
+ return BlueletFuture(self).__await__()
31
+
32
+
33
+ ##
34
+
35
+
36
+ class BlueletHasFileno(ta.Protocol):
37
+ def fileno(self) -> int: ...
38
+
39
+
40
+ @dc.dataclass(frozen=True)
41
+ class BlueletWaitables:
42
+ r: ta.Sequence[BlueletWaitable] = ()
43
+ w: ta.Sequence[BlueletWaitable] = ()
44
+ x: ta.Sequence[BlueletWaitable] = ()
45
+
46
+
47
+ class WaitableBlueletEvent(BlueletEvent, abc.ABC): # noqa
48
+ """
49
+ A waitable event is one encapsulating an action that can be waited for using a select() call. That is, it's an event
50
+ with an associated file descriptor.
51
+ """
52
+
53
+ def waitables(self) -> BlueletWaitables:
54
+ """
55
+ Return "waitable" objects to pass to select(). Should return three iterables for input readiness, output
56
+ readiness, and exceptional conditions (i.e., the three lists passed to select()).
57
+ """
58
+ return BlueletWaitables()
59
+
60
+ def fire(self) -> ta.Any:
61
+ """Called when an associated file descriptor becomes ready (i.e., is returned from a select() call)."""
62
+
63
+
64
+ ##
65
+
66
+
67
+ @dc.dataclass(eq=False)
68
+ class BlueletFuture(ta.Generic[BlueletEventT, R]):
69
+ event: BlueletEventT
70
+ done: bool = False
71
+ result: ta.Optional[R] = None
72
+
73
+ def __await__(self):
74
+ if not self.done:
75
+ yield self
76
+ if not self.done:
77
+ raise RuntimeError("await wasn't used with event future")
78
+ return self.result
@@ -0,0 +1,80 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ # Based on bluelet ( https://github.com/sampsyo/bluelet ) by Adrian Sampson, original license:
4
+ # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
5
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
6
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
7
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8
+ import abc
9
+ import dataclasses as dc
10
+ import typing as ta
11
+
12
+ from .core import DelegationBlueletEvent
13
+ from .core import ReturnBlueletEvent
14
+ from .events import BlueletEvent
15
+ from .events import BlueletWaitables
16
+ from .events import WaitableBlueletEvent
17
+
18
+
19
+ ##
20
+
21
+
22
+ class FileBlueletEvent(BlueletEvent, abc.ABC):
23
+ pass
24
+
25
+
26
+ @dc.dataclass(frozen=True, eq=False)
27
+ class ReadBlueletEvent(WaitableBlueletEvent, FileBlueletEvent):
28
+ """Reads from a file-like object."""
29
+
30
+ fd: ta.IO
31
+ bufsize: int
32
+
33
+ def waitables(self) -> BlueletWaitables:
34
+ return BlueletWaitables(r=[self.fd])
35
+
36
+ def fire(self) -> bytes:
37
+ return self.fd.read(self.bufsize)
38
+
39
+
40
+ @dc.dataclass(frozen=True, eq=False)
41
+ class WriteBlueletEvent(WaitableBlueletEvent, FileBlueletEvent):
42
+ """Writes to a file-like object."""
43
+
44
+ fd: ta.IO
45
+ data: bytes
46
+
47
+ def waitables(self) -> BlueletWaitables:
48
+ return BlueletWaitables(w=[self.fd])
49
+
50
+ def fire(self) -> None:
51
+ self.fd.write(self.data)
52
+
53
+
54
+ ##
55
+
56
+
57
+ class _FilesBlueletApi:
58
+ def read(self, fd: ta.IO, bufsize: ta.Optional[int] = None) -> BlueletEvent:
59
+ """Event: read from a file descriptor asynchronously."""
60
+
61
+ if bufsize is None:
62
+ # Read all.
63
+ def reader():
64
+ buf = []
65
+ while True:
66
+ data = yield self.read(fd, 1024)
67
+ if not data:
68
+ break
69
+ buf.append(data)
70
+ yield ReturnBlueletEvent(''.join(buf))
71
+
72
+ return DelegationBlueletEvent(reader())
73
+
74
+ else:
75
+ return ReadBlueletEvent(fd, bufsize)
76
+
77
+ def write(self, fd: ta.IO, data: bytes) -> BlueletEvent:
78
+ """Event: write to a file descriptor asynchronously."""
79
+
80
+ return WriteBlueletEvent(fd, data)
@@ -0,0 +1,416 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ # Based on bluelet ( https://github.com/sampsyo/bluelet ) by Adrian Sampson, original license:
4
+ # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
5
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
6
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
7
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8
+ """
9
+ TODO:
10
+ - use fdio
11
+ - wrap coros in Tasks :|
12
+ - (unit)tests lol
13
+ - * subprocesses
14
+ - canceling
15
+ - timeouts
16
+ - task groups
17
+ - gather
18
+ - locks / semaphores / events / etc
19
+ - rename Coro to Bluelet?
20
+ - shutdown
21
+ - ensure resource cleanup
22
+ - run_thread? whatever?
23
+
24
+ Subprocs:
25
+ - https://github.com/python/cpython/issues/120804 - GH-120804: Remove get_child_watcher and set_child_watcher from
26
+ asyncio
27
+ - https://github.com/python/cpython/pull/17063/files bpo-38692: Add os.pidfd_open
28
+ - clone PidfdChildWatcher + ThreadedChildWatcher
29
+ """
30
+ import collections
31
+ import dataclasses as dc
32
+ import errno
33
+ import logging
34
+ import select
35
+ import sys
36
+ import time
37
+ import traceback
38
+ import types
39
+ import typing as ta
40
+ import weakref
41
+
42
+ from .core import BlueletCoro
43
+ from .core import BlueletExcInfo
44
+ from .core import CoreBlueletEvent
45
+ from .core import DelegationBlueletEvent
46
+ from .core import ExceptionBlueletEvent
47
+ from .core import JoinBlueletEvent
48
+ from .core import KillBlueletEvent
49
+ from .core import ReturnBlueletEvent
50
+ from .core import SleepBlueletEvent
51
+ from .core import SpawnBlueletEvent
52
+ from .core import ValueBlueletEvent
53
+ from .core import _BlueletAwaitableDriver
54
+ from .events import BlueletEvent
55
+ from .events import BlueletWaitable
56
+ from .events import WaitableBlueletEvent
57
+
58
+
59
+ ##
60
+
61
+
62
+ class BlueletCoroException(Exception): # noqa
63
+ def __init__(self, coro: BlueletCoro, exc_info: BlueletExcInfo) -> None:
64
+ super().__init__()
65
+ self.coro = coro
66
+ self.exc_info = exc_info
67
+
68
+ @staticmethod
69
+ def _exc_info() -> BlueletExcInfo:
70
+ return sys.exc_info() # type: ignore
71
+
72
+ @staticmethod
73
+ def _reraise(typ: ta.Type[BaseException], exc: BaseException, tb: types.TracebackType) -> ta.NoReturn: # noqa
74
+ raise exc.with_traceback(tb)
75
+
76
+ def reraise(self) -> ta.NoReturn:
77
+ self._reraise(self.exc_info[0], self.exc_info[1], self.exc_info[2])
78
+
79
+
80
+ ##
81
+
82
+
83
+ def _bluelet_event_select(
84
+ events: ta.Iterable[BlueletEvent],
85
+ *,
86
+ log: ta.Optional[logging.Logger] = None,
87
+ ) -> ta.Set[WaitableBlueletEvent]:
88
+ """
89
+ Perform a select() over all the Events provided, returning the ones ready to be fired. Only WaitableEvents
90
+ (including SleepEvents) matter here; all other events are ignored (and thus postponed).
91
+ """
92
+
93
+ waitable_to_event: ta.Dict[ta.Tuple[str, BlueletWaitable], WaitableBlueletEvent] = {}
94
+ rlist: ta.List[BlueletWaitable] = []
95
+ wlist: ta.List[BlueletWaitable] = []
96
+ xlist: ta.List[BlueletWaitable] = []
97
+ earliest_wakeup: ta.Optional[float] = None
98
+
99
+ # Gather waitables and wakeup times.
100
+ for event in events:
101
+ if isinstance(event, SleepBlueletEvent):
102
+ if not earliest_wakeup:
103
+ earliest_wakeup = event.wakeup_time
104
+ else:
105
+ earliest_wakeup = min(earliest_wakeup, event.wakeup_time)
106
+
107
+ elif isinstance(event, WaitableBlueletEvent):
108
+ ew = event.waitables()
109
+ rlist.extend(ew.r)
110
+ wlist.extend(ew.w)
111
+ xlist.extend(ew.x)
112
+ for waitable in ew.r:
113
+ waitable_to_event[('r', waitable)] = event
114
+ for waitable in ew.w:
115
+ waitable_to_event[('w', waitable)] = event
116
+ for waitable in ew.x:
117
+ waitable_to_event[('x', waitable)] = event
118
+
119
+ # If we have a any sleeping coros, determine how long to sleep.
120
+ if earliest_wakeup:
121
+ timeout = max(earliest_wakeup - time.time(), 0.)
122
+ else:
123
+ timeout = None
124
+
125
+ # Perform select() if we have any waitables.
126
+ if rlist or wlist or xlist:
127
+ if log:
128
+ log.debug('_bluelet_event_select: +select: %r %r %r %r', rlist, wlist, xlist, timeout)
129
+ rready, wready, xready = select.select(rlist, wlist, xlist, timeout)
130
+ if log:
131
+ log.debug('_bluelet_event_select: -select: %r %r %r', rready, wready, xready)
132
+
133
+ else:
134
+ rready, wready, xready = [], [], []
135
+ if timeout:
136
+ if log:
137
+ log.debug('_bluelet_event_select: sleep: %r', timeout)
138
+ time.sleep(timeout)
139
+
140
+ # Gather ready events corresponding to the ready waitables.
141
+ ready_events: ta.Set[WaitableBlueletEvent] = set()
142
+ for ready in rready:
143
+ ready_events.add(waitable_to_event[('r', ready)])
144
+ for ready in wready:
145
+ ready_events.add(waitable_to_event[('w', ready)])
146
+ for ready in xready:
147
+ ready_events.add(waitable_to_event[('x', ready)])
148
+
149
+ # Gather any finished sleeps.
150
+ for event in events:
151
+ if isinstance(event, SleepBlueletEvent) and not event.time_left():
152
+ ready_events.add(event)
153
+
154
+ return ready_events
155
+
156
+
157
+ ##
158
+
159
+
160
+ class _SuspendedBlueletEvent(CoreBlueletEvent):
161
+ pass
162
+
163
+
164
+ _BLUELET_SUSPENDED = _SuspendedBlueletEvent() # Special sentinel placeholder for suspended coros.
165
+
166
+
167
+ @dc.dataclass(frozen=True, eq=False)
168
+ class _DelegatedBlueletEvent(CoreBlueletEvent):
169
+ """Placeholder indicating that a coro has delegated execution to a different coro."""
170
+
171
+ child: BlueletCoro
172
+
173
+
174
+ class _BlueletRunner:
175
+ """
176
+ Schedules a coroutine, running it to completion. This encapsulates the Bluelet scheduler, which the root coroutine
177
+ can add to by spawning new coroutines.
178
+ """
179
+
180
+ def __init__(
181
+ self,
182
+ root_coro: BlueletCoro,
183
+ *,
184
+ log: ta.Optional[logging.Logger] = None,
185
+ ) -> None:
186
+ super().__init__()
187
+
188
+ self._root_coro = root_coro
189
+ self._log = log
190
+
191
+ # The "coros" dictionary keeps track of all the currently-executing and suspended coroutines. It maps
192
+ # coroutines to their currently "blocking" event. The event value may be SUSPENDED if the coroutine is waiting
193
+ # on some other condition: namely, a delegated coroutine or a joined coroutine. In this case, the coroutine
194
+ # should *also* appear as a value in one of the below dictionaries `delegators` or `joiners`.
195
+ self._coros: ta.Dict[BlueletCoro, BlueletEvent] = {self._root_coro: ValueBlueletEvent(None)}
196
+
197
+ # Maps child coroutines to delegating parents.
198
+ self._delegators: ta.Dict[BlueletCoro, BlueletCoro] = {}
199
+
200
+ # Maps child coroutines to joining (exit-waiting) parents.
201
+ self._joiners: ta.MutableMapping[BlueletCoro, ta.List[BlueletCoro]] = collections.defaultdict(list)
202
+
203
+ # History of spawned coroutines for joining of already completed coroutines.
204
+ self._history: ta.MutableMapping[BlueletCoro, ta.Optional[BlueletEvent]] = \
205
+ weakref.WeakKeyDictionary({self._root_coro: None})
206
+
207
+ def _complete_coro(self, coro: BlueletCoro, return_value: ta.Any) -> None:
208
+ """
209
+ Remove a coroutine from the scheduling pool, awaking delegators and joiners as necessary and returning the
210
+ specified value to any delegating parent.
211
+ """
212
+
213
+ del self._coros[coro]
214
+
215
+ # Resume delegator.
216
+ if coro in self._delegators:
217
+ self._coros[self._delegators[coro]] = ValueBlueletEvent(return_value)
218
+ del self._delegators[coro]
219
+
220
+ # Resume joiners.
221
+ if coro in self._joiners:
222
+ for parent in self._joiners[coro]:
223
+ self._coros[parent] = ValueBlueletEvent(None)
224
+ del self._joiners[coro]
225
+
226
+ def _advance_coro(self, coro: BlueletCoro, value: ta.Any, is_exc: bool = False) -> None:
227
+ """
228
+ After an event is fired, run a given coroutine associated with it in the coros dict until it yields again. If
229
+ the coroutine exits, then the coro is removed from the pool. If the coroutine raises an exception, it is
230
+ reraised in a CoroException. If is_exc is True, then the value must be an exc_info tuple and the exception is
231
+ thrown into the coroutine.
232
+ """
233
+
234
+ try:
235
+ if is_exc:
236
+ next_event = coro.throw(*value)
237
+ else:
238
+ next_event = coro.send(value)
239
+
240
+ except StopIteration:
241
+ # Coro is done.
242
+ self._complete_coro(coro, None)
243
+
244
+ except BaseException: # noqa
245
+ # Coro raised some other exception.
246
+ del self._coros[coro]
247
+ # Note: Don't use `raise from` as this should support 3.8.
248
+ raise BlueletCoroException(coro, BlueletCoroException._exc_info()) # noqa
249
+
250
+ else:
251
+ if isinstance(next_event, ta.Generator):
252
+ # Automatically invoke sub-coroutines. (Shorthand for explicit bluelet.call().)
253
+ next_event = DelegationBlueletEvent(next_event)
254
+
255
+ if isinstance(next_event, types.CoroutineType): # type: ignore[unreachable]
256
+ next_event = DelegationBlueletEvent(_BlueletAwaitableDriver(next_event)()) # type: ignore[unreachable]
257
+
258
+ if not isinstance(next_event, BlueletEvent):
259
+ raise TypeError(next_event)
260
+
261
+ self._coros[coro] = next_event
262
+
263
+ def _kill_coro(self, coro: BlueletCoro) -> None:
264
+ """Unschedule this coro and its (recursive) delegates."""
265
+
266
+ # Collect all coroutines in the delegation stack.
267
+ coros = [coro]
268
+ while isinstance((cur := self._coros[coro]), _DelegatedBlueletEvent):
269
+ coro = cur.child # noqa
270
+ coros.append(coro)
271
+
272
+ # Complete each coroutine from the top to the bottom of the stack.
273
+ for coro in reversed(coros):
274
+ self._complete_coro(coro, None)
275
+
276
+ def close(self) -> None:
277
+ # If any coros still remain, kill them.
278
+ for coro in self._coros:
279
+ coro.close()
280
+
281
+ self._coros.clear()
282
+
283
+ def _handle_core_event(self, coro: BlueletCoro, event: CoreBlueletEvent) -> bool:
284
+ if self._log:
285
+ self._log.debug(f'{self.__class__.__name__}._handle_core_event: %r %r', coro, event)
286
+
287
+ if isinstance(event, SpawnBlueletEvent):
288
+ sc = ta.cast(BlueletCoro, event.spawned) # FIXME
289
+ self._coros[sc] = ValueBlueletEvent(None) # Spawn.
290
+ self._history[sc] = None # Record in history.
291
+ self._advance_coro(coro, None)
292
+ return True
293
+
294
+ elif isinstance(event, ValueBlueletEvent):
295
+ self._advance_coro(coro, event.value)
296
+ return True
297
+
298
+ elif isinstance(event, ExceptionBlueletEvent):
299
+ self._advance_coro(coro, event.exc_info, True)
300
+ return True
301
+
302
+ elif isinstance(event, DelegationBlueletEvent):
303
+ self._coros[coro] = _DelegatedBlueletEvent(event.spawned) # Suspend.
304
+ self._coros[event.spawned] = ValueBlueletEvent(None) # Spawn.
305
+ self._history[event.spawned] = None # Record in history.
306
+ self._delegators[event.spawned] = coro
307
+ return True
308
+
309
+ elif isinstance(event, ReturnBlueletEvent):
310
+ # Coro is done.
311
+ self._complete_coro(coro, event.value)
312
+ return True
313
+
314
+ elif isinstance(event, JoinBlueletEvent):
315
+ if event.child not in self._coros and event.child in self._history:
316
+ self._coros[coro] = ValueBlueletEvent(None)
317
+ else:
318
+ self._coros[coro] = _BLUELET_SUSPENDED # Suspend.
319
+ self._joiners[event.child].append(coro)
320
+ return True
321
+
322
+ elif isinstance(event, KillBlueletEvent):
323
+ self._coros[coro] = ValueBlueletEvent(None)
324
+ self._kill_coro(event.child)
325
+ return True
326
+
327
+ elif isinstance(event, (_DelegatedBlueletEvent, _SuspendedBlueletEvent)):
328
+ return False
329
+
330
+ else:
331
+ raise TypeError(event)
332
+
333
+ def _step(self) -> ta.Optional[BlueletCoroException]:
334
+ if self._log:
335
+ self._log.debug(f'{self.__class__.__name__}._step') # Noqa
336
+
337
+ try:
338
+ # Look for events that can be run immediately. Continue running immediate events until nothing is ready.
339
+ while True:
340
+ have_ready = False
341
+ for coro, event in list(self._coros.items()):
342
+ if isinstance(event, CoreBlueletEvent) and not isinstance(event, SleepBlueletEvent):
343
+ have_ready |= self._handle_core_event(coro, event)
344
+ elif isinstance(event, WaitableBlueletEvent):
345
+ pass
346
+ else:
347
+ raise TypeError(f'Unknown event type: {event}') # noqa
348
+
349
+ # Only start the select when nothing else is ready.
350
+ if not have_ready:
351
+ break
352
+
353
+ # Wait and fire.
354
+ event2coro = {v: k for k, v in self._coros.items()}
355
+ for event in _bluelet_event_select(self._coros.values()):
356
+ # Run the IO operation, but catch socket errors.
357
+ try:
358
+ value = event.fire()
359
+ except OSError as exc:
360
+ if isinstance(exc.args, tuple) and exc.args[0] == errno.EPIPE:
361
+ # Broken pipe. Remote host disconnected.
362
+ pass
363
+ elif isinstance(exc.args, tuple) and exc.args[0] == errno.ECONNRESET:
364
+ # Connection was reset by peer.
365
+ pass
366
+ else:
367
+ traceback.print_exc()
368
+ # Abort the coroutine.
369
+ self._coros[event2coro[event]] = ReturnBlueletEvent(None)
370
+ else:
371
+ self._advance_coro(event2coro[event], value)
372
+
373
+ except BlueletCoroException as te:
374
+ if self._log and self._log.isEnabledFor(logging.DEBUG):
375
+ self._log.exception(f'{self.__class__.__name__}._step')
376
+
377
+ # Exception raised from inside a coro.
378
+ event = ExceptionBlueletEvent(te.exc_info)
379
+ if te.coro in self._delegators:
380
+ # The coro is a delegate. Raise exception in its delegator.
381
+ self._coros[self._delegators[te.coro]] = event
382
+ del self._delegators[te.coro]
383
+ else:
384
+ # The coro is root-level. Raise in client code.
385
+ return te
386
+
387
+ except BaseException: # noqa
388
+ ei = BlueletCoroException._exc_info() # noqa
389
+
390
+ if self._log and self._log.isEnabledFor(logging.DEBUG):
391
+ self._log.exception(f'{self.__class__.__name__}._step')
392
+
393
+ # For instance, KeyboardInterrupt during select(). Raise into root coro and terminate others.
394
+ self._coros = {self._root_coro: ExceptionBlueletEvent(ei)} # noqa
395
+
396
+ return None
397
+
398
+ def run(self) -> None:
399
+ # Continue advancing coros until root coro exits.
400
+ exit_ce: BlueletCoroException | None = None
401
+ while self._coros:
402
+ exit_ce = self._step()
403
+
404
+ self.close()
405
+
406
+ # If we're exiting with an exception, raise it in the client.
407
+ if exit_ce:
408
+ exit_ce.reraise()
409
+
410
+
411
+ ##
412
+
413
+
414
+ class _RunnerBlueletApi:
415
+ def run(self, root_coro: BlueletCoro) -> None:
416
+ _BlueletRunner(root_coro).run()
@@ -0,0 +1,214 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ # Based on bluelet ( https://github.com/sampsyo/bluelet ) by Adrian Sampson, original license:
4
+ # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
5
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
6
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
7
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8
+ import abc
9
+ import dataclasses as dc
10
+ import socket
11
+ import typing as ta
12
+
13
+ from .core import BlueletCoro
14
+ from .core import ReturnBlueletEvent
15
+ from .core import ValueBlueletEvent
16
+ from .core import _CoreBlueletApi
17
+ from .events import BlueletEvent
18
+ from .events import BlueletWaitables
19
+ from .events import WaitableBlueletEvent
20
+
21
+
22
+ ##
23
+
24
+
25
+ class SocketClosedBlueletError(Exception):
26
+ pass
27
+
28
+
29
+ class BlueletListener:
30
+ """A socket wrapper object for listening sockets."""
31
+
32
+ def __init__(self, host: str, port: int) -> None:
33
+ """Create a listening socket on the given hostname and port."""
34
+
35
+ super().__init__()
36
+ self._closed = False
37
+ self.host = host
38
+ self.port = port
39
+
40
+ self.sock = sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
41
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
42
+ sock.bind((host, port))
43
+ sock.listen(5)
44
+
45
+ def accept(self) -> 'AcceptBlueletEvent':
46
+ """
47
+ An event that waits for a connection on the listening socket. When a connection is made, the event returns a
48
+ Connection object.
49
+ """
50
+
51
+ if self._closed:
52
+ raise SocketClosedBlueletError
53
+ return AcceptBlueletEvent(self)
54
+
55
+ def close(self) -> None:
56
+ """Immediately close the listening socket. (Not an event.)"""
57
+
58
+ self._closed = True
59
+ self.sock.close()
60
+
61
+
62
+ class BlueletConnection:
63
+ """A socket wrapper object for connected sockets."""
64
+
65
+ def __init__(self, sock: socket.socket, addr: ta.Tuple[str, int]) -> None:
66
+ super().__init__()
67
+ self.sock = sock
68
+ self.addr = addr
69
+ self._buf = bytearray()
70
+ self._closed: bool = False
71
+
72
+ def close(self) -> None:
73
+ """Close the connection."""
74
+
75
+ self._closed = True
76
+ self.sock.close()
77
+
78
+ def recv(self, size: int) -> BlueletEvent:
79
+ """Read at most size bytes of data from the socket."""
80
+
81
+ if self._closed:
82
+ raise SocketClosedBlueletError
83
+
84
+ if self._buf:
85
+ # We already have data read previously.
86
+ out = self._buf[:size]
87
+ self._buf = self._buf[size:]
88
+ return ValueBlueletEvent(bytes(out))
89
+ else:
90
+ return ReceiveBlueletEvent(self, size)
91
+
92
+ def send(self, data: bytes) -> BlueletEvent:
93
+ """Sends data on the socket, returning the number of bytes successfully sent."""
94
+
95
+ if self._closed:
96
+ raise SocketClosedBlueletError
97
+ return SendBlueletEvent(self, data)
98
+
99
+ def sendall(self, data: bytes) -> BlueletEvent:
100
+ """Send all of data on the socket."""
101
+
102
+ if self._closed:
103
+ raise SocketClosedBlueletError
104
+ return SendBlueletEvent(self, data, True)
105
+
106
+ def readline(self, terminator: bytes = b'\n', bufsize: int = 1024) -> BlueletCoro:
107
+ """Reads a line (delimited by terminator) from the socket."""
108
+
109
+ if self._closed:
110
+ raise SocketClosedBlueletError
111
+
112
+ while True:
113
+ if terminator in self._buf:
114
+ line, self._buf = self._buf.split(terminator, 1)
115
+ line += terminator
116
+ yield ReturnBlueletEvent(bytes(line))
117
+ break
118
+
119
+ if (data := (yield ReceiveBlueletEvent(self, bufsize))):
120
+ self._buf += data
121
+ else:
122
+ line = self._buf
123
+ self._buf = bytearray()
124
+ yield ReturnBlueletEvent(bytes(line))
125
+ break
126
+
127
+
128
+ ##
129
+
130
+
131
+ class SocketBlueletEvent(BlueletEvent, abc.ABC): # noqa
132
+ pass
133
+
134
+
135
+ @dc.dataclass(frozen=True, eq=False)
136
+ class AcceptBlueletEvent(WaitableBlueletEvent, SocketBlueletEvent):
137
+ """An event for Listener objects (listening sockets) that suspends execution until the socket gets a connection."""
138
+
139
+ listener: BlueletListener
140
+
141
+ def waitables(self) -> BlueletWaitables:
142
+ return BlueletWaitables(r=[self.listener.sock])
143
+
144
+ def fire(self) -> BlueletConnection:
145
+ sock, addr = self.listener.sock.accept()
146
+ return BlueletConnection(sock, addr)
147
+
148
+
149
+ @dc.dataclass(frozen=True, eq=False)
150
+ class ReceiveBlueletEvent(WaitableBlueletEvent, SocketBlueletEvent):
151
+ """An event for Connection objects (connected sockets) for asynchronously reading data."""
152
+
153
+ conn: BlueletConnection
154
+ bufsize: int
155
+
156
+ def waitables(self) -> BlueletWaitables:
157
+ return BlueletWaitables(r=[self.conn.sock])
158
+
159
+ def fire(self) -> bytes:
160
+ return self.conn.sock.recv(self.bufsize)
161
+
162
+
163
+ @dc.dataclass(frozen=True, eq=False)
164
+ class SendBlueletEvent(WaitableBlueletEvent, SocketBlueletEvent):
165
+ """An event for Connection objects (connected sockets) for asynchronously writing data."""
166
+
167
+ conn: BlueletConnection
168
+ data: bytes
169
+ sendall: bool = False
170
+
171
+ def waitables(self) -> BlueletWaitables:
172
+ return BlueletWaitables(w=[self.conn.sock])
173
+
174
+ def fire(self) -> ta.Optional[int]:
175
+ if self.sendall:
176
+ self.conn.sock.sendall(self.data)
177
+ return None
178
+ else:
179
+ return self.conn.sock.send(self.data)
180
+
181
+
182
+ ##
183
+
184
+
185
+ class _SocketsBlueletApi(_CoreBlueletApi):
186
+ def connect(self, host: str, port: int) -> BlueletEvent:
187
+ """Event: connect to a network address and return a Connection object for communicating on the socket."""
188
+
189
+ addr = (host, port)
190
+ sock = socket.create_connection(addr)
191
+ return ValueBlueletEvent(BlueletConnection(sock, addr))
192
+
193
+ def server(self, host: str, port: int, func) -> BlueletCoro:
194
+ """
195
+ A coroutine that runs a network server. Host and port specify the listening address. func should be a coroutine
196
+ that takes a single parameter, a Connection object. The coroutine is invoked for every incoming connection on
197
+ the listening socket.
198
+ """
199
+
200
+ def handler(conn):
201
+ try:
202
+ yield func(conn)
203
+ finally:
204
+ conn.close()
205
+
206
+ listener = BlueletListener(host, port)
207
+ try:
208
+ while True:
209
+ conn = yield listener.accept()
210
+ yield self.spawn(handler(conn))
211
+ except KeyboardInterrupt:
212
+ pass
213
+ finally:
214
+ listener.close()
omlish/lite/inject.py CHANGED
@@ -610,7 +610,7 @@ class InjectorBinder:
610
610
  def __new__(cls, *args, **kwargs): # noqa
611
611
  raise TypeError
612
612
 
613
- _FN_TYPES: ta.Tuple[type, ...] = (
613
+ _FN_TYPES: ta.ClassVar[ta.Tuple[type, ...]] = (
614
614
  types.FunctionType,
615
615
  types.MethodType,
616
616
 
@@ -632,7 +632,7 @@ class InjectorBinder:
632
632
  cls._FN_TYPES = (*cls._FN_TYPES, icls)
633
633
  return icls
634
634
 
635
- _BANNED_BIND_TYPES: ta.Tuple[type, ...] = (
635
+ _BANNED_BIND_TYPES: ta.ClassVar[ta.Tuple[type, ...]] = (
636
636
  InjectorProvider,
637
637
  )
638
638
 
@@ -811,45 +811,35 @@ def bind_injector_eager_key(key: ta.Any) -> InjectorBinding:
811
811
  ##
812
812
 
813
813
 
814
- class Injection:
815
- def __new__(cls, *args, **kwargs): # noqa
816
- raise TypeError
817
-
814
+ class InjectionApi:
818
815
  # keys
819
816
 
820
- @classmethod
821
- def as_key(cls, o: ta.Any) -> InjectorKey:
817
+ def as_key(self, o: ta.Any) -> InjectorKey:
822
818
  return as_injector_key(o)
823
819
 
824
- @classmethod
825
- def array(cls, o: ta.Any) -> InjectorKey:
820
+ def array(self, o: ta.Any) -> InjectorKey:
826
821
  return dc.replace(as_injector_key(o), array=True)
827
822
 
828
- @classmethod
829
- def tag(cls, o: ta.Any, t: ta.Any) -> InjectorKey:
823
+ def tag(self, o: ta.Any, t: ta.Any) -> InjectorKey:
830
824
  return dc.replace(as_injector_key(o), tag=t)
831
825
 
832
826
  # bindings
833
827
 
834
- @classmethod
835
- def as_bindings(cls, *args: InjectorBindingOrBindings) -> InjectorBindings:
828
+ def as_bindings(self, *args: InjectorBindingOrBindings) -> InjectorBindings:
836
829
  return as_injector_bindings(*args)
837
830
 
838
- @classmethod
839
- def override(cls, p: InjectorBindings, *args: InjectorBindingOrBindings) -> InjectorBindings:
831
+ def override(self, p: InjectorBindings, *args: InjectorBindingOrBindings) -> InjectorBindings:
840
832
  return injector_override(p, *args)
841
833
 
842
834
  # injector
843
835
 
844
- @classmethod
845
- def create_injector(cls, *args: InjectorBindingOrBindings, parent: ta.Optional[Injector] = None) -> Injector:
836
+ def create_injector(self, *args: InjectorBindingOrBindings, parent: ta.Optional[Injector] = None) -> Injector:
846
837
  return _Injector(as_injector_bindings(*args), parent)
847
838
 
848
839
  # binder
849
840
 
850
- @classmethod
851
841
  def bind(
852
- cls,
842
+ self,
853
843
  obj: ta.Any,
854
844
  *,
855
845
  key: ta.Any = None,
@@ -884,32 +874,29 @@ class Injection:
884
874
 
885
875
  # helpers
886
876
 
887
- @classmethod
888
877
  def bind_factory(
889
- cls,
878
+ self,
890
879
  fn: ta.Callable[..., T],
891
880
  cls_: U,
892
881
  ann: ta.Any = None,
893
882
  ) -> InjectorBindingOrBindings:
894
- return cls.bind(make_injector_factory(fn, cls_, ann))
883
+ return self.bind(make_injector_factory(fn, cls_, ann))
895
884
 
896
- @classmethod
897
885
  def bind_array(
898
- cls,
886
+ self,
899
887
  obj: ta.Any = None,
900
888
  *,
901
889
  tag: ta.Any = None,
902
890
  ) -> InjectorBindingOrBindings:
903
891
  return bind_injector_array(obj, tag=tag)
904
892
 
905
- @classmethod
906
893
  def bind_array_type(
907
- cls,
894
+ self,
908
895
  ele: ta.Union[InjectorKey, InjectorKeyCls],
909
896
  cls_: U,
910
897
  ann: ta.Any = None,
911
898
  ) -> InjectorBindingOrBindings:
912
- return cls.bind(make_injector_array_type(ele, cls_, ann))
899
+ return self.bind(make_injector_array_type(ele, cls_, ann))
913
900
 
914
901
 
915
- inj = Injection
902
+ inj = InjectionApi()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: omlish
3
- Version: 0.0.0.dev161
3
+ Version: 0.0.0.dev162
4
4
  Summary: omlish
5
5
  Author: wrmsr
6
6
  License: BSD-3-Clause
@@ -1,5 +1,5 @@
1
1
  omlish/.manifests.json,sha256=RX24SRc6DCEg77PUVnaXOKCWa5TF_c9RQJdGIf7gl9c,1135
2
- omlish/__about__.py,sha256=mO53HhyG-ZTHxwvhTxgq__7mREyycsxsT-5hHzJF95Q,3409
2
+ omlish/__about__.py,sha256=RGH11TXe-0tI-c-75Cmj5Em5DfZ8d1t-Xincc1SM3bE,3409
3
3
  omlish/__init__.py,sha256=SsyiITTuK0v74XpKV8dqNaCmjOlan1JZKrHQv5rWKPA,253
4
4
  omlish/c3.py,sha256=ubu7lHwss5V4UznbejAI0qXhXahrU01MysuHOZI9C4U,8116
5
5
  omlish/cached.py,sha256=UI-XTFBwA6YXWJJJeBn-WkwBkfzDjLBBaZf4nIJA9y0,510
@@ -92,6 +92,15 @@ omlish/asyncs/asyncio/channels.py,sha256=ZbmsEmdK1fV96liHdcVpRqA2dAMkXJt4Q3rFAg3
92
92
  omlish/asyncs/asyncio/streams.py,sha256=Uc9PCWSfBqrK2kdVtfjjQU1eaYTWYmZm8QISDj2xiuw,1004
93
93
  omlish/asyncs/asyncio/subprocesses.py,sha256=XlIWwSxpVB7sMVc75-f7dI6r08JkdipNFRWXUKS8zAw,6960
94
94
  omlish/asyncs/asyncio/timeouts.py,sha256=Rj5OU9BIAPcVZZKp74z7SzUXF5xokh4dgsWkUqOy1aE,355
95
+ omlish/asyncs/bluelet/LICENSE,sha256=VHf3oPQihOHnWyIR8LcXX0dpONa1lgyJnjWC2qVuRR0,559
96
+ omlish/asyncs/bluelet/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
97
+ omlish/asyncs/bluelet/all.py,sha256=aUV6PwnR8DqnEBS9wsZuPW_UtP6G9M8_KY-mmxZeVG0,1516
98
+ omlish/asyncs/bluelet/api.py,sha256=yeM5SauiIbvKj26mDdxn5mOj8s5fFIw2xBSe-P6HVC0,904
99
+ omlish/asyncs/bluelet/core.py,sha256=NpGQhb865aeXPKV_67l-6FiuQzdDJJ85zjfvCR5R8Pc,5430
100
+ omlish/asyncs/bluelet/events.py,sha256=iXpRWmy64YcshT_nuyiJ39jbketZdtj8LrdlX3JmpoY,2440
101
+ omlish/asyncs/bluelet/files.py,sha256=pgcLV_3oGbpqQmOrii8SeizyYLp8XKofQJhqM82RlKw,2389
102
+ omlish/asyncs/bluelet/runner.py,sha256=cwgVjwRRXzqfOqRD0rkPHjt_Udyt9ZKpVs36uAJ_Q7c,15473
103
+ omlish/asyncs/bluelet/sockets.py,sha256=RrC2vU52dOEBYKzvoh1qA39uUE8p3uCB_oxnhaP1AeA,6752
95
104
  omlish/bootstrap/__init__.py,sha256=-Rtsg7uPQNhh1dIT9nqrz96XlqizwoLnWf-FwOEstJI,730
96
105
  omlish/bootstrap/__main__.py,sha256=4jCwsaogp0FrJjJZ85hzF4-WqluPeheHbfeoKynKvNs,194
97
106
  omlish/bootstrap/base.py,sha256=d8hqn4hp1XMMi5PgcJBQXPKmW47epu8CxBlqDZiRZb4,1073
@@ -339,7 +348,7 @@ omlish/lite/__init__.py,sha256=Y3l4WY4JRi2uLG6kgbGp93fuGfkxkKwZDvhsa0Rwgtk,15
339
348
  omlish/lite/cached.py,sha256=O7ozcoDNFm1Hg2wtpHEqYSp_i_nCLNOP6Ueq_Uk-7mU,1300
340
349
  omlish/lite/check.py,sha256=KvcO86LqWlh2j4ORaZXRR4FM8fFb7kUkNqq3lTs0Ta0,12821
341
350
  omlish/lite/contextmanagers.py,sha256=m9JO--p7L7mSl4cycXysH-1AO27weDKjP3DZG61cwwM,1683
342
- omlish/lite/inject.py,sha256=729Qi0TLbQgBtkvx97q1EUMe73VFYA1hu4woXkOTcwM,23572
351
+ omlish/lite/inject.py,sha256=EEaioN9ESAveVCMe2s5osjwI97FPRUVoU8P95vGUiYo,23376
343
352
  omlish/lite/json.py,sha256=7-02Ny4fq-6YAu5ynvqoijhuYXWpLmfCI19GUeZnb1c,740
344
353
  omlish/lite/logs.py,sha256=CWFG0NKGhqNeEgryF5atN2gkPYbUdTINEw_s1phbINM,51
345
354
  omlish/lite/marshal.py,sha256=ldoZs_yiQIUpOjBviV9f4mwm7hSZy0hRLXrvQA-6POU,14257
@@ -526,9 +535,9 @@ omlish/text/glyphsplit.py,sha256=Ug-dPRO7x-OrNNr8g1y6DotSZ2KH0S-VcOmUobwa4B0,329
526
535
  omlish/text/indent.py,sha256=6Jj6TFY9unaPa4xPzrnZemJ-fHsV53IamP93XGjSUHs,1274
527
536
  omlish/text/parts.py,sha256=7vPF1aTZdvLVYJ4EwBZVzRSy8XB3YqPd7JwEnNGGAOo,6495
528
537
  omlish/text/random.py,sha256=jNWpqiaKjKyTdMXC-pWAsSC10AAP-cmRRPVhm59ZWLk,194
529
- omlish-0.0.0.dev161.dist-info/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
530
- omlish-0.0.0.dev161.dist-info/METADATA,sha256=FeXlao_p21yJqF4nL1hTicjLO3Ir0XMm9jii9k8-2ws,4264
531
- omlish-0.0.0.dev161.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
532
- omlish-0.0.0.dev161.dist-info/entry_points.txt,sha256=Lt84WvRZJskWCAS7xnQGZIeVWksprtUHj0llrvVmod8,35
533
- omlish-0.0.0.dev161.dist-info/top_level.txt,sha256=pePsKdLu7DvtUiecdYXJ78iO80uDNmBlqe-8hOzOmfs,7
534
- omlish-0.0.0.dev161.dist-info/RECORD,,
538
+ omlish-0.0.0.dev162.dist-info/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
539
+ omlish-0.0.0.dev162.dist-info/METADATA,sha256=thW6t3ofpA5bodN1Uc7hDhNjXOwZd68S1SqJZuv4hIw,4264
540
+ omlish-0.0.0.dev162.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
541
+ omlish-0.0.0.dev162.dist-info/entry_points.txt,sha256=Lt84WvRZJskWCAS7xnQGZIeVWksprtUHj0llrvVmod8,35
542
+ omlish-0.0.0.dev162.dist-info/top_level.txt,sha256=pePsKdLu7DvtUiecdYXJ78iO80uDNmBlqe-8hOzOmfs,7
543
+ omlish-0.0.0.dev162.dist-info/RECORD,,