@@ -149,13 +149,16 @@ invoke non-blocking functions.**
149
149
Detailed Breakdown
150
150
==================
151
151
152
- The whole approach of awaitlet is overall a little bit of a "dark art". It
152
+ The whole approach of awaitlet is overall a little bit of a "dark art" (though
153
+ actually less "dark" than what gevent and eventlet
154
+ have done for decades). It
153
155
takes a standard and pretty well known part of Python, the ``asyncio ``
154
156
library, and adds some syntactical helpers that were not intended to be part
155
157
of asyncio itself. Inspired by libraries like gevent and eventlet, awaitlet
156
158
makes use of greenlet in a similar way as those libraries do, but then makes
157
159
use of asyncio for non-blocking primitives, rather than going through the
158
- effort of creating its own the way gevent and eventlet do.
160
+ effort of creating its own and often needing to monkeypatch them into the standard
161
+ library the way gevent and eventlet do.
159
162
160
163
The :func: `.async_def ` function call is an awaitable that when invoked,
161
164
internally starts up a greenlet that can be used to "context switch" to
@@ -172,9 +175,9 @@ other greenlets anywhere within the execution of that greenlet::
172
175
173
176
return await my_awaitable
174
177
175
- Above, the ``send_receive_logic() `` function is called within a greenlet, but
176
- not before first before we enter into an actual async def that's behind
177
- the scenes ::
178
+ Above, the ``send_receive_logic() `` function is called within a greenlet that
179
+ itself links to a parent greenlet that's local to the :func: ` .async_def `
180
+ callable (this is the normal way that greenlet works) ::
178
181
179
182
async def async_def(
180
183
fn: Callable[..., _T],
@@ -184,70 +187,91 @@ the scenes::
184
187
) -> _T:
185
188
"""Runs a sync function ``fn`` in a new greenlet."""
186
189
187
- # make a greenlet.greenlet with the given function
190
+ # make a greenlet.greenlet with the given function.
191
+ # getcurrent() is the parent greenlet that is basically where we
192
+ # are right now in the function
188
193
context = _AsyncIoGreenlet(fn, greenlet.getcurrent())
189
194
190
- # switch into it (start the function)
195
+ # switch into the new greenlet (start the function)
191
196
result = context.switch(*args, **kwargs)
192
197
193
198
# ... continued ...
194
199
195
- Then, whenever some part of ``send_receive_logic() `` or some sub-function within
196
- it calls upon :func: `.awaitlet `, that goes back into awaitlet's greenlet code
197
- and uses ``greenlet.switch() `` to **context switch ** back out into the behind-the-scenes
198
- async function, below illustrated in a simplified form of the actual code::
200
+ When this line of code is first called::
199
201
200
- async def async_def(
201
- fn: Callable[..., _T],
202
- *args: Any,
203
- assert_await_occurs: bool = False,
204
- **kwargs: Any,
205
- ) -> _T:
206
- """Runs a sync function ``fn`` in a new greenlet."""
202
+ # switch into the new greenlet (start the function)
203
+ result = context.switch(*args, **kwargs)
207
204
208
- # make a greenlet.greenlet with the given function
209
- context = _AsyncIoGreenlet(fn, greenlet.getcurrent())
205
+ It runs the given function, and blocks until the function is complete.
206
+ However, within the function (which is our ``send_receive_logic() `` call),
207
+ that function can call upon Python awaitables using :func: `.awaitlet `.
208
+ :func: `.awaitlet ` looks like this::
210
209
211
- # switch into it (start the function)
212
- result = context.switch(*args, **kwargs)
210
+ def awaitlet(awaitable: Awaitable[_T]) -> _T:
211
+ """Awaits an async function in a sync method."""
213
212
214
- # we're back! is the context not dead ? (e.g. the funciton has more
215
- # code to run?)
216
- while not context.dead:
217
- # wait for a coroutine from awaitlet and then return its
218
- # result back to it.
219
- value = await result
213
+ current = greenlet.getcurrent()
214
+ return current.parent.switch(awaitable)
220
215
221
- # then switch back in! (in reality there's error handling here also)
222
- result = context.switch(value)
216
+ That is, it does nothing but context switch **back to the parent greenlet **,
217
+ which means back outside of the ``context.switch() `` that got us here.
218
+ The returned value is a real Python awaitable. So inside
219
+ of the ``async_def() `` function, we check that the inner function is not
220
+ complete yet, we then assume the result must be an awaitable, and we await it
221
+ on behalf of our hosted function - remember, we're in a real ``async def ``
222
+ at this level::
223
223
224
- When this line of code is first called::
224
+ # switch into the new greenlet (start the function)
225
+ result = context.switch(*args, **kwargs)
225
226
226
- # switch into it (start the function)
227
+ # loop for the function not done yet
228
+ while not context.dead:
229
+ # await on the result that we expect is awaitable
230
+ value = await result
231
+
232
+ With the awaitable completed, we send the result of
233
+ the awaitable **back into the hosted function and context switch back **::
234
+
235
+ # switch into the new greenlet (start the function)
227
236
result = context.switch(*args, **kwargs)
228
237
229
- It blocks while our function runs. Only when our function exits or
230
- calls :func: `.awaitlet ` do we hit the next line. If the function calls
231
- :func: `.awaitlet `, awaitlet looks like this::
238
+ # loop for the function not done yet
239
+ while not context.dead:
240
+ # await on the result that we expect is awaitable
241
+ value = await result
242
+
243
+ # pass control back into the function, with the return value
244
+ # of the awaitable
245
+ result = context.switch(value)
246
+
247
+ The ``value `` passed in becomes the **return value of the awaitlet() call **::
232
248
233
249
def awaitlet(awaitable: Awaitable[_T]) -> _T:
234
- """Awaits an async function in a sync method."""
250
+ # ...
235
251
236
- current = greenlet.getcurrent()
237
252
return current.parent.switch(awaitable)
238
253
239
- That is, it does nothing but greenlet switch **back to the parent greenlet **,
240
- which means back outside of the ``context.switch() `` that got us here.
241
- The returned value is a real Python awaitable. So inside
242
- of the ``async_def() `` funciton, we await it on behalf of our hosted function::
254
+ and we are then back in the hosted function with an awaitable having proceeded
255
+ and its return value passed back from the :func: `.awaitlet ` call.
256
+
257
+ The loop continues; each time ``context.dead `` is False, we know that
258
+ ``result `` is yet another Python awaitable. Once ``context.dead `` is
259
+ True, then we know the function completed; we return the result!
243
260
261
+ .. sourcecode ::
262
+
263
+ # switch into it (start the function)
264
+ result = context.switch(*args, **kwargs)
265
+
266
+ # loop for the function not done yet
244
267
while not context.dead:
245
268
# await on the result that we expect is awaitable
246
269
value = await result
247
270
248
- We send the result of the awaitable **back into the hosted function and
249
- context switch back **::
271
+ result = context.switch(value)
272
+
273
+ # no more awaits; so this is the result!
274
+ return result
250
275
251
- result = context.switch(value)
252
276
253
- Minus some more robustness details, that's the whole thing!
277
+ Minus error handling and some other robustness details, that's the whole thing!
0 commit comments