omlish 0.0.0.dev161__py3-none-any.whl → 0.0.0.dev162__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 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,,