39
39
Nursery ,
40
40
)
41
41
42
- class MaybeOutcome (Struct ):
43
42
44
- _ready : Event = trio .Event ()
45
- _outcome : Outcome | None = None
46
- _result : Any | None = None
43
+ class TaskOutcome (Struct ):
44
+ '''
45
+ The outcome of a scheduled ``trio`` task which includes an interface
46
+ for synchronizing to the completion of the task's runtime and access
47
+ to the eventual boxed result/value or raised exception.
48
+
49
+ '''
50
+ _exited : Event = trio .Event () # as per `trio.Runner.task_exited()`
51
+ _outcome : Outcome | None = None # as per `outcome.Outcome`
52
+ _result : Any | None = None # the eventual maybe-returned-value
47
53
48
54
@property
49
55
def result (self ) -> Any :
@@ -55,27 +61,35 @@ def result(self) -> Any:
55
61
raise RuntimeError (
56
62
# f'Task {task.name} is not complete.\n'
57
63
f'Outcome is not complete.\n '
58
- 'wait on `await MaybeOutcome.unwrap ()` first!'
64
+ 'wait on `await TaskOutcome.wait_for_result ()` first!'
59
65
)
60
66
return self ._result
61
67
62
68
def _set_outcome (
63
69
self ,
64
70
outcome : Outcome ,
65
71
):
72
+ '''
73
+ Set the ``Outcome`` for this task.
74
+
75
+ This method should only ever be called by the task's supervising
76
+ nursery implemenation.
77
+
78
+ '''
66
79
self ._outcome = outcome
67
80
self ._result = outcome .unwrap ()
68
- self ._ready .set ()
69
-
70
- # TODO: maybe a better name like,
71
- # - .wait_and_unwrap()
72
- # - .wait_unwrap()
73
- # - .aunwrap() ?
74
- async def unwrap (self ) -> Any :
75
- if self ._ready .is_set ():
81
+ self ._exited .set ()
82
+
83
+ async def wait_for_result (self ) -> Any :
84
+ '''
85
+ Unwind the underlying task's ``Outcome`` by async waiting for
86
+ the task to first complete and then unwrap it's result-value.
87
+
88
+ '''
89
+ if self ._exited .is_set ():
76
90
return self ._result
77
91
78
- await self ._ready .wait ()
92
+ await self ._exited .wait ()
79
93
80
94
out = self ._outcome
81
95
if out is None :
@@ -84,13 +98,6 @@ async def unwrap(self) -> Any:
84
98
return self .result
85
99
86
100
87
- class TaskHandle (Struct ):
88
- task : Task
89
- cs : CancelScope
90
- exited : Event | None = None
91
- _outcome : Outcome | None = None
92
-
93
-
94
101
class ScopePerTaskNursery (Struct ):
95
102
_n : Nursery
96
103
_scopes : dict [
@@ -122,73 +129,96 @@ async def start_soon(
122
129
cs = CancelScope ()
123
130
new_task : Task | None = None
124
131
to_return : tuple [Any ] | None = None
125
- maybe_outcome = MaybeOutcome ()
126
132
127
133
sm = self .scope_manager
128
134
if sm is None :
129
135
mngr = nullcontext ([cs ])
130
136
else :
131
- mngr = sm (
132
- nursery = n ,
133
- scope = cs ,
134
- maybe_outcome = maybe_outcome ,
135
- )
137
+ # NOTE: what do we enforce as a signature for the
138
+ # `@task_scope_manager` here?
139
+ mngr = sm (nursery = n , scope = cs )
136
140
137
141
async def _start_wrapped_in_scope (
138
142
task_status : TaskStatus [
139
143
tuple [CancelScope , Task ]
140
144
] = trio .TASK_STATUS_IGNORED ,
141
145
142
146
) -> None :
143
- nonlocal maybe_outcome
144
- nonlocal to_return
147
+
148
+ # TODO: this was working before?!
149
+ # nonlocal to_return
145
150
146
151
with cs :
147
152
148
153
task = trio .lowlevel .current_task ()
149
154
self ._scopes [cs ] = task
150
155
151
- # TODO: instead we should probably just use
152
- # `Outcome.send(mngr)` here no and inside a custom
153
- # decorator `@trio.cancel_scope_manager` enforce
154
- # that it's a single yield generator?
155
- with mngr as to_return :
156
-
157
- # TODO: relay through whatever the
158
- # started task passes back via `.started()` ?
159
- # seems like that won't work with also returning
160
- # a "task handle"?
161
- task_status .started ()
156
+ # execute up to the first yield
157
+ try :
158
+ to_return : tuple [Any ] = next (mngr )
159
+ except StopIteration :
160
+ raise RuntimeError ("task manager didn't yield" ) from None
161
+
162
+ # TODO: how do we support `.start()` style?
163
+ # - relay through whatever the
164
+ # started task passes back via `.started()` ?
165
+ # seems like that won't work with also returning
166
+ # a "task handle"?
167
+ # - we were previously binding-out this `to_return` to
168
+ # the parent's lexical scope, why isn't that working
169
+ # now?
170
+ task_status .started (to_return )
171
+
172
+ # invoke underlying func now that cs is entered.
173
+ outcome = await acapture (async_fn , * args )
174
+
175
+ # execute from the 1st yield to return and expect
176
+ # generator-mngr `@task_scope_manager` thinger to
177
+ # terminate!
178
+ try :
179
+ mngr .send (outcome )
180
+
181
+ # NOTE: this will instead send the underlying
182
+ # `.value`? Not sure if that's better or not?
183
+ # I would presume it's better to have a handle to
184
+ # the `Outcome` entirely? This method sends *into*
185
+ # the mngr this `Outcome.value`; seems like kinda
186
+ # weird semantics for our purposes?
187
+ # outcome.send(mngr)
188
+
189
+ except StopIteration :
190
+ return
191
+ else :
192
+ raise RuntimeError (f"{ mngr } didn't stop!" )
193
+
194
+ to_return = await n .start (_start_wrapped_in_scope )
195
+ assert to_return is not None
162
196
163
- # invoke underlying func now that cs is entered.
164
- outcome = await acapture (async_fn , * args )
197
+ # TODO: use the fancy type-check-time type signature stuff from
198
+ # mypy i guess..to like, relay the type of whatever the
199
+ # generator yielded through? betcha that'll be un-grokable XD
200
+ return to_return
165
201
166
- # TODO: instead, mngr.send(outcome) so that we don't
167
- # tie this `.start_soon()` impl to the
168
- # `MaybeOutcome` type? Use `Outcome.send(mngr)`
169
- # right?
170
- maybe_outcome ._set_outcome (outcome )
171
202
172
- await n .start (_start_wrapped_in_scope )
173
- assert to_return is not None
174
203
175
- # TODO: better way to concat the values delivered by the user
176
- # provided `.scope_manager` and the outcome?
177
- return tuple ([maybe_outcome ] + to_return )
204
+ # TODO: you could wrap your output task handle in this?
205
+ # class TaskHandle(Struct):
206
+ # task: Task
207
+ # cs: CancelScope
208
+ # outcome: TaskOutcome
178
209
179
210
180
211
# TODO: maybe just make this a generator with a single yield that also
181
212
# delivers a value (of some std type) from the yield expression?
182
- # @trio.cancel_scope_manager
183
- @cm
213
+ # @trio.task_scope_manager
184
214
def add_task_handle_and_crash_handling (
185
215
nursery : Nursery ,
186
216
scope : CancelScope ,
187
- maybe_outcome : MaybeOutcome ,
188
217
189
218
) -> Generator [None , list [Any ]]:
190
219
191
220
cs : CancelScope = CancelScope ()
221
+ task_outcome = TaskOutcome ()
192
222
193
223
# if you need it you can ask trio for the task obj
194
224
task : Task = trio .lowlevel .current_task ()
@@ -197,12 +227,11 @@ def add_task_handle_and_crash_handling(
197
227
try :
198
228
# yields back when task is terminated, cancelled, returns?
199
229
with cs :
200
- # the yielded values here are what are returned to the
201
- # nursery's `.start_soon()` caller
202
230
203
- # TODO: actually make this work so that `MaybeOutcome` isn't
204
- # tied to the impl of `.start_soon()` on our custom nursery!
205
- task_outcome : Outcome = yield [cs ]
231
+ # the yielded value(s) here are what are returned to the
232
+ # nursery's `.start_soon()` caller B)
233
+ lowlevel_outcome : Outcome = yield (task_outcome , cs )
234
+ task_outcome ._set_outcome (lowlevel_outcome )
206
235
207
236
except Exception as err :
208
237
# Adds "crash handling" from `pdbp` by entering
@@ -247,7 +276,7 @@ async def main():
247
276
248
277
val : str = 'yoyoyo'
249
278
val_outcome , cs = await sn .start_soon (sleep_then_return_val , val )
250
- res = await val_outcome .unwrap ()
279
+ res = await val_outcome .wait_for_result ()
251
280
assert res == val
252
281
print (f'GOT EXPECTED TASK VALUE: { res } ' )
253
282
0 commit comments