From b115a8148dd7aee3bea0d8e63412c513bc59cf6e Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Tue, 22 Aug 2017 00:52:24 -0700 Subject: [PATCH] Deprecate most of the nursery and task APIs For gh-136 This commit contains: - the actual deprecations - changes to _run.py to prevent warnings - changes to test_run.py to prevent warnings Still need to clean up everything else. --- trio/_core/_run.py | 76 +++++++++---- trio/_core/tests/test_run.py | 213 ++++++++++++++++++----------------- 2 files changed, 164 insertions(+), 125 deletions(-) diff --git a/trio/_core/_run.py b/trio/_core/_run.py index 83d82234dd..867e70398f 100644 --- a/trio/_core/_run.py +++ b/trio/_core/_run.py @@ -359,7 +359,7 @@ def __init__(self, parent, cancel_scope): self._children = set() self._pending_starts = 0 self._zombies = set() - self.monitor = _core.UnboundedQueue() + self._monitor = _core.UnboundedQueue() self._closed = False @property @@ -367,19 +367,26 @@ def children(self): return frozenset(self._children) @property + @deprecated("0.2.0", instead=None, issue=136) def zombies(self): return frozenset(self._zombies) + @property + @deprecated("0.2.0", instead=None, issue=136) + def monitor(self): + return self._monitor + def _child_finished(self, task): self._children.remove(task) self._zombies.add(task) - self.monitor.put_nowait(task) + self._monitor.put_nowait(task) def start_soon(self, async_fn, *args, name=None): GLOBAL_RUN_CONTEXT.runner.spawn_impl(async_fn, args, self, name) # Returns the task, unlike start_soon - #@deprecated("nursery.spawn", version="0.2.0", "nursery.start_soon") + @deprecated("0.2.0", thing="nursery.spawn", instead="nursery.start_soon", + issue=136) def spawn(self, async_fn, *args, name=None): return GLOBAL_RUN_CONTEXT.runner.spawn_impl(async_fn, args, self, name) @@ -402,17 +409,25 @@ async def start(self, async_fn, *args, name=None): return task_status._value finally: self._pending_starts -= 1 - self.monitor.put_nowait(None) + self._monitor.put_nowait(None) - def reap(self, task): + def _reap(self, task): try: self._zombies.remove(task) except KeyError: raise ValueError("{} is not a zombie in this nursery".format(task)) + @deprecated("0.2.0", instead=None, issue=136) + def reap(self, task): + return self._reap(task) + + def _reap_and_unwrap(self, task): + self._reap(task) + return task._result.unwrap() + + @deprecated("0.2.0", instead=None, issue=136) def reap_and_unwrap(self, task): - self.reap(task) - return task.result.unwrap() + return self._reap_and_unwrap(task) async def _clean_up(self, pending_exc): cancelled_children = False @@ -435,22 +450,22 @@ async def _clean_up(self, pending_exc): # of remaining tasks, so we have to check first before # blocking on the monitor queue. for task in list(self._zombies): - if type(task.result) is Error: - exceptions.append(task.result.error) - self.reap(task) + if type(task._result) is Error: + exceptions.append(task._result.error) + self._reap(task) if exceptions and not cancelled_children: self.cancel_scope.cancel() clean_up_scope.shield = True cancelled_children = True - if self.children or self._pending_starts: + if self._children or self._pending_starts: try: # We ignore the return value here, and will pick up # the actual tasks from the zombies set after looping # around. (E.g. it's possible there are tasks in the # queue that were already reaped.) - await self.monitor.get_batch() + await self._monitor.get_batch() except (Cancelled, KeyboardInterrupt) as exc: exceptions.append(exc) @@ -483,7 +498,7 @@ async def _clean_up(self, pending_exc): raise mexc def __del__(self): - assert not self.children and not self.zombies + assert not self._children and not self._zombies ################################################################ @@ -513,7 +528,7 @@ class Task: # Invariant: # - for unfinished tasks, result is None # - for finished tasks, result is a Result object - result = attr.ib(default=None) + _result = attr.ib(default=None) # Invariant: # - for unscheduled tasks, _next_send is None # - for scheduled tasks, _next_send is a Result object @@ -537,6 +552,11 @@ class Task: def __repr__(self): return ("".format(self.name, id(self))) + @property + @deprecated("0.2.0", instead=None, issue=136) + def result(self): + return self._result + # For debugging and visualization: @property def parent_task(self): @@ -555,6 +575,7 @@ def parent_task(self): _monitors = attr.ib(default=attr.Factory(set)) + @deprecated("0.2.0", instead=None, issue=136) def add_monitor(self, queue): """Register to be notified when this task exits. @@ -567,6 +588,9 @@ def add_monitor(self, queue): ValueError: if ``queue`` is already registered with this task """ + return self._add_monitor(queue) + + def _add_monitor(self, queue): # Rationale: (a) don't particularly want to create a # callback-in-disguise API by allowing people to stick in some # arbitrary object with a put_nowait method, (b) don't want to have to @@ -577,11 +601,12 @@ def add_monitor(self, queue): raise TypeError("monitor must be an UnboundedQueue object") if queue in self._monitors: raise ValueError("can't add same monitor twice") - if self.result is not None: + if self._result is not None: queue.put_nowait(self) else: self._monitors.add(queue) + @deprecated("0.2.0", instead=None, issue=136) def discard_monitor(self, queue): """Unregister the given queue from being notified about this task exiting. @@ -596,16 +621,17 @@ def discard_monitor(self, queue): self._monitors.discard(queue) + @deprecated("0.2.0", instead=None, issue=136) async def wait(self): """Wait for this task to exit. """ q = _core.UnboundedQueue() - self.add_monitor(q) + self._add_monitor(q) try: await q.get_batch() finally: - self.discard_monitor(q) + self._monitors.discard(q) ################ # Cancellation @@ -916,7 +942,7 @@ def _return_value_looks_like_wrong_library(value): return task def task_exited(self, task, result): - task.result = result + task._result = result while task._cancel_stack: task._cancel_stack[-1]._remove_task(task) self.tasks.remove(task) @@ -1002,14 +1028,16 @@ async def init(self, async_fn, args): self.spawn_system_task( self.call_soon_task, name="" ) - self.main_task = system_nursery.spawn(async_fn, *args) - async for task_batch in system_nursery.monitor: + + self.main_task = self.spawn_impl(async_fn, args, + self.system_nursery, name=None) + async for task_batch in system_nursery._monitor: for task in task_batch: if task is self.main_task: system_nursery.cancel_scope.cancel() - return system_nursery.reap_and_unwrap(task) + return system_nursery._reap_and_unwrap(task) else: - system_nursery.reap_and_unwrap(task) + system_nursery._reap_and_unwrap(task) ################ # Outside Context Problems @@ -1194,7 +1222,7 @@ def _deliver_ki_cb(self): # same time -- so even if KI arrives before main_task is created, we # won't get here until afterwards. assert self.main_task is not None - if self.main_task.result is not None: + if self.main_task._result is not None: # We're already in the process of exiting -- leave ki_pending set # and we'll check it again on our way out of run(). return @@ -1606,7 +1634,7 @@ def run_impl(runner, async_fn, args): runner.instrument("after_task_step", task) del GLOBAL_RUN_CONTEXT.task - return runner.init_task.result + return runner.init_task._result ################################################################ diff --git a/trio/_core/tests/test_run.py b/trio/_core/tests/test_run.py index 32dfc1bd6a..73827185f6 100644 --- a/trio/_core/tests/test_run.py +++ b/trio/_core/tests/test_run.py @@ -91,7 +91,7 @@ async def main(): # pragma: no cover assert "from inside" in str(excinfo.value) -async def test_basic_spawn_wait(): +async def test_basic_spawn_wait(recwarn): async def child(x): return 2 * x @@ -115,7 +115,7 @@ async def test_nursery_warn_use_async_with(): pass -async def test_child_crash_basic(): +async def test_child_crash_basic_deprecated(recwarn): exc = ValueError("uh oh") async def erroring(): @@ -127,15 +127,21 @@ async def erroring(): assert task.result.error is exc nursery.reap(task) +async def test_child_crash_basic_deprecated(recwarn): + exc = ValueError("uh oh") + + async def erroring(): + raise exc + try: # nursery.__aexit__ propagates exception from child back to parent async with _core.open_nursery() as nursery: - nursery.spawn(erroring) + nursery.start_soon(erroring) except ValueError as e: assert e is exc -async def test_reap_bad_task(): +async def test_reap_bad_task(recwarn): async def child(): pass @@ -159,8 +165,8 @@ async def looper(whoami, record): record = [] async with _core.open_nursery() as nursery: - t1 = nursery.spawn(looper, "a", record) - t2 = nursery.spawn(looper, "b", record) + nursery.start_soon(looper, "a", record) + nursery.start_soon(looper, "b", record) check_sequence_matches( record, @@ -186,8 +192,8 @@ async def crasher(): async def main(): async with _core.open_nursery() as nursery: - nursery.spawn(looper) - nursery.spawn(crasher) + nursery.start_soon(looper) + nursery.start_soon(crasher) with pytest.raises(ValueError) as excinfo: _core.run(main) @@ -196,7 +202,7 @@ async def main(): assert excinfo.value.args == ("argh",) -def test_main_and_task_both_crash(): +def test_main_and_task_both_crash(recwarn): # If main crashes and there's also a task crash, then we get both in a # MultiError async def crasher(): @@ -224,8 +230,8 @@ async def crasher(etype): async def main(): async with _core.open_nursery() as nursery: - nursery.spawn(crasher, KeyError) - nursery.spawn(crasher, ValueError) + nursery.start_soon(crasher, KeyError) + nursery.start_soon(crasher, ValueError) with pytest.raises(_core.MultiError) as excinfo: _core.run(main) @@ -234,7 +240,12 @@ async def main(): async def test_reschedule(): + t1 = None + t2 = None + async def child1(): + nonlocal t1, t2 + t1 = _core.current_task() print("child1 start") x = await sleep_forever() print("child1 woke") @@ -244,7 +255,9 @@ async def child1(): print("child1 exit") async def child2(): + nonlocal t1, t2 print("child2 start") + t2 = _core.current_task() _core.reschedule(t1, _core.Value(0)) print("child2 sleep") with pytest.raises(ValueError): @@ -252,13 +265,13 @@ async def child2(): print("child2 successful exit") async with _core.open_nursery() as nursery: - t1 = nursery.spawn(child1) + nursery.start_soon(child1) # let t1 run and fall asleep await _core.yield_briefly() - t2 = nursery.spawn(child2) + nursery.start_soon(child2) -async def test_task_monitor(): +async def test_task_monitor(recwarn): async def child(): return 1 @@ -301,7 +314,7 @@ async def child(): # loop around and do it again -async def test_bad_monitor_object(): +async def test_bad_monitor_object(recwarn): task = _core.current_task() with pytest.raises(TypeError): @@ -338,12 +351,13 @@ async def test_current_clock(mock_clock): async def test_current_task(): + parent_task = _core.current_task() + async def child(): - return _core.current_task() + assert _core.current_task().parent_task is parent_task async with _core.open_nursery() as nursery: - child_task = nursery.spawn(child) - assert child_task == child_task.result.unwrap() + nursery.start_soon(child) def test_out_of_context(): @@ -371,7 +385,7 @@ async def child(): assert stats.call_soon_queue_size == 0 async with _core.open_nursery() as nursery: - task = nursery.spawn(child) + nursery.start_soon(child) await wait_all_tasks_blocked() call_soon = _core.current_call_soon_thread_and_signal_safe() call_soon(lambda: None) @@ -503,15 +517,17 @@ def test_instruments_interleave(): tasks = {} async def two_step1(): + tasks["t1"] = _core.current_task() await _core.yield_briefly() async def two_step2(): + tasks["t2"] = _core.current_task() await _core.yield_briefly() async def main(): async with _core.open_nursery() as nursery: - tasks["t1"] = nursery.spawn(two_step1) - tasks["t2"] = nursery.spawn(two_step2) + nursery.start_soon(two_step1) + nursery.start_soon(two_step2) r = TaskRecorder() _core.run(main, instruments=[r]) @@ -686,19 +702,16 @@ async def crasher(): try: async with _core.open_nursery() as nursery: # Two children that get cancelled by the nursery scope - t1 = nursery.spawn(child) - t2 = nursery.spawn(child) + nursery.start_soon(child) # t1 + nursery.start_soon(child) # t2 nursery.cancel_scope.cancel() with _core.open_cancel_scope(shield=True): - # Make sure they receive the inner cancellation - # exception before we cancel the outer scope - await t1.wait() - await t2.wait() + await wait_all_tasks_blocked() # One child that gets cancelled by the outer scope - t3 = nursery.spawn(child) + nursery.start_soon(child) # t3 outer.cancel() # And one that raises a different error - t4 = nursery.spawn(crasher) + nursery.start_soon(crasher) # t4 except _core.MultiError as multi_exc: # This is outside the nursery scope but inside the outer # scope, so the nursery should have absorbed t1 and t2's @@ -731,7 +744,7 @@ async def blocker(): async with _core.open_nursery() as nursery: nursery.cancel_scope.cancel() - nursery.spawn(blocker) + nursery.start_soon(blocker) assert record == ["started"] @@ -787,20 +800,15 @@ async def leaf(ident): async def worker(ident): async with _core.open_nursery() as nursery: - t1 = nursery.spawn(leaf, ident + "-l1") - t2 = nursery.spawn(leaf, ident + "-l2") - with _core.open_cancel_scope(shield=True): - await t1.wait() - await t2.wait() + nursery.start_soon(leaf, ident + "-l1") + nursery.start_soon(leaf, ident + "-l2") async with _core.open_nursery() as nursery: - w1 = nursery.spawn(worker, "w1") - w2 = nursery.spawn(worker, "w2") + nursery.start_soon(worker, "w1") + nursery.start_soon(worker, "w2") nursery.cancel_scope.cancel() - with _core.open_cancel_scope(shield=True): - await w1.wait() - await w2.wait() - assert record == {"w1-l1", "w1-l2", "w2-l1", "w2-l2"} + + assert record == {"w1-l1", "w1-l2", "w2-l1", "w2-l2"} async def test_cancel_shield_abort(): @@ -819,7 +827,7 @@ async def sleeper(): except _core.Cancelled: record.append("cancelled") - task = nursery.spawn(sleeper) + nursery.start_soon(sleeper) await wait_all_tasks_blocked() assert record == ["sleeping"] # now when we unshield, it should abort the sleep. @@ -830,7 +838,8 @@ async def sleeper(): # written, without these last few lines, the test spuriously # passed, even though shield assignment was buggy.) with _core.open_cancel_scope(shield=True): - await task.wait() + await wait_all_tasks_blocked() + assert record == ["sleeping", "cancelled"] async def test_basic_timeout(mock_clock): @@ -929,10 +938,12 @@ async def test_timekeeping(): async def test_failed_abort(): + stubborn_task = [None] stubborn_scope = [None] record = [] async def stubborn_sleeper(): + stubborn_task[0] = _core.current_task() with _core.open_cancel_scope() as scope: stubborn_scope[0] = scope record.append("sleep") @@ -945,7 +956,7 @@ async def stubborn_sleeper(): record.append("cancelled") async with _core.open_nursery() as nursery: - task = nursery.spawn(stubborn_sleeper) + nursery.start_soon(stubborn_sleeper) await wait_all_tasks_blocked() assert record == ["sleep"] stubborn_scope[0].cancel() @@ -953,7 +964,7 @@ async def stubborn_sleeper(): # cancel didn't wake it up assert record == ["sleep"] # wake it up again by hand - _core.reschedule(task, _core.Value(1)) + _core.reschedule(stubborn_task[0], _core.Value(1)) assert record == ["sleep", "woke", "cancelled"] @@ -1003,8 +1014,8 @@ async def system_task(x): record.append(("ki", _core.currently_ki_protected())) await _core.yield_briefly() - task = _core.spawn_system_task(system_task, 1) - await task.wait() + _core.spawn_system_task(system_task, 1) + await wait_all_tasks_blocked() assert record == [("x", 1), ("ki", True)] @@ -1015,8 +1026,8 @@ async def crasher(): raise KeyError async def main(): - task = _core.spawn_system_task(crasher) - await task.wait() + _core.spawn_system_task(crasher) + await sleep_forever() with pytest.raises(_core.TrioInternalError): _core.run(main) @@ -1031,8 +1042,8 @@ async def crasher2(): async def system_task(): async with _core.open_nursery() as nursery: - nursery.spawn(crasher1) - nursery.spawn(crasher2) + nursery.start_soon(crasher1) + nursery.start_soon(crasher2) async def main(): _core.spawn_system_task(system_task) @@ -1063,8 +1074,8 @@ async def cancelme(): async def system_task(): async with _core.open_nursery() as nursery: - nursery.spawn(crasher) - nursery.spawn(cancelme) + nursery.start_soon(crasher) + nursery.start_soon(cancelme) async def main(): _core.spawn_system_task(system_task) @@ -1152,8 +1163,8 @@ async def child2(): record.append("child2 success") async with _core.open_nursery() as nursery: - nursery.spawn(child1) - nursery.spawn(child2) + nursery.start_soon(child1) + nursery.start_soon(child2) assert record == [ "child1 raise", "child1 sleep", "child2 wake", "child2 sleep again", @@ -1168,7 +1179,12 @@ async def child2(): # # https://bugs.python.org/issue29587 async def test_exc_info_after_yield_error(): + child_task = None + async def child(): + nonlocal child_task + child_task = _core.current_task() + try: raise KeyError except Exception: @@ -1178,32 +1194,34 @@ async def child(): pass raise - async with _core.open_nursery() as nursery: - t = nursery.spawn(child) - await wait_all_tasks_blocked() - _core.reschedule(t, _core.Error(ValueError())) - await t.wait() - with pytest.raises(KeyError): - nursery.reap_and_unwrap(t) + with pytest.raises(KeyError): + async with _core.open_nursery() as nursery: + nursery.start_soon(child) + await wait_all_tasks_blocked() + _core.reschedule(child_task, _core.Error(ValueError())) # Similar to previous test -- if the ValueError() gets sent in via 'throw', -# then Python's normal implicit chaining stuff is broken. We have to +# then Python's normal implicit chaining stuff is broken. async def test_exception_chaining_after_yield_error(): + child_task = None + async def child(): + nonlocal child_task + child_task = _core.current_task() + try: raise KeyError except Exception: await sleep_forever() - async with _core.open_nursery() as nursery: - t = nursery.spawn(child) - await wait_all_tasks_blocked() - _core.reschedule(t, _core.Error(ValueError())) - await t.wait() - with pytest.raises(ValueError) as excinfo: - nursery.reap_and_unwrap(t) - assert isinstance(excinfo.value.__context__, KeyError) + with pytest.raises(ValueError) as excinfo: + async with _core.open_nursery() as nursery: + nursery.start_soon(child) + await wait_all_tasks_blocked() + _core.reschedule(child_task, _core.Error(ValueError())) + + assert isinstance(excinfo.value.__context__, KeyError) async def test_call_soon_basic(): @@ -1456,7 +1474,7 @@ def slow_abort(raise_cancel): async with _core.open_nursery() as nursery: # So we have a task blocked on an operation that can't be # aborted immediately - nursery.spawn(slow_aborter) + nursery.start_soon(slow_aborter) await wait_all_tasks_blocked() assert record == ["sleeping"] # And then we cancel it, so the abort callback gets run @@ -1474,20 +1492,21 @@ def slow_abort(raise_cancel): async def test_parent_task(): + tasks = {} + async def child2(): - pass + tasks["child2"] = _core.current_task() async def child1(): + tasks["child1"] = _core.current_task() async with _core.open_nursery() as nursery: - return nursery.spawn(child2) + return nursery.start_soon(child2) async with _core.open_nursery() as nursery: - t1 = nursery.spawn(child1) - await t1.wait() - t2 = t1.result.unwrap() + nursery.start_soon(child1) - assert t1.parent_task is _core.current_task() - assert t2.parent_task is t1 + assert tasks["child1"].parent_task is _core.current_task() + assert tasks["child2"].parent_task is tasks["child1"] t = _core.current_task() # Make sure that chaining parent_task eventually gives None (and not, for @@ -1500,42 +1519,34 @@ async def test_nursery_closure(): async def child1(nursery): # We can add new tasks to the nursery even after entering __aexit__, # so long as there are still tasks running - nursery.spawn(child2) + nursery.start_soon(child2) async def child2(): pass async with _core.open_nursery() as nursery: - nursery.spawn(child1, nursery) + nursery.start_soon(child1, nursery) # But once we've left __aexit__, the nursery is closed with pytest.raises(RuntimeError): - nursery.spawn(child2) + nursery.start_soon(child2) async def test_spawn_name(): - async def func1(): - pass + async def func1(expected): + task = _core.current_task() + assert expected in task.name async def func2(): # pragma: no cover pass async with _core.open_nursery() as nursery: - for spawn_fn in [nursery.spawn, _core.spawn_system_task]: - t0 = spawn_fn(func1) - assert "func1" in t0.name - - t1 = spawn_fn(func1, name=func2) - assert "func2" in t1.name - - t2 = spawn_fn(func1, name="func3") - assert "func3" == t2.name - - t3 = spawn_fn(functools.partial(func1)) - assert "func1" in t3.name - - t4 = spawn_fn(func1, name=object()) - assert "object" in t4.name + for spawn_fn in [nursery.start_soon, _core.spawn_system_task]: + spawn_fn(func1, "func1") + spawn_fn(func1, "func2", name=func2) + spawn_fn(func1, "func3", name="func3") + spawn_fn(functools.partial(func1, "func1")) + spawn_fn(func1, "object", name=object()) async def test_current_effective_deadline(mock_clock): @@ -1567,7 +1578,7 @@ def bad_call_run(*args): def bad_call_spawn(*args): async def main(): async with _core.open_nursery() as nursery: - nursery.spawn(*args) + nursery.start_soon(*args) _core.run(main) @@ -1629,7 +1640,7 @@ async def misguided(): assert "asyncio" in str(excinfo.value) -async def test_trivial_yields(): +async def test_trivial_yields(recwarn): with assert_yields(): await _core.yield_briefly()