forked from goodboy/tractor
				
			Impl a proto "unmasker" `@acm` alongside our test
Such that the suite verifies the wip `maybe_raise_from_masking_exc()` will raise from a `trio.Cancelled.__context__` since I can't think of any reason a `Cancelled` should ever be raised in-place of a non-`Cancelled` XD Not sure what should be raised instead (or maybe just a `log.warning()` emitted?) but this starts a draft for refinement at the least. Use the new `@pytest.mark.parametrize` explicit tuple-of-params form with an `pytest.param + `.mark.xfail()` for the default behaviour case.remotes/1757153874605917753/main
							parent
							
								
									bfd1864180
								
							
						
					
					
						commit
						4edf36a895
					
				|  | @ -86,58 +86,113 @@ def test_stashed_child_nursery(use_start_soon): | |||
|         trio.run(main) | ||||
| 
 | ||||
| 
 | ||||
| # @pytest.mark.parametrize( | ||||
| #     'open_tn_outside_acm', | ||||
| #     [True, False] | ||||
| #     # ids='aio_err_triggered={}'.format | ||||
| # ) | ||||
| @pytest.mark.parametrize( | ||||
|     'canc_from_finally', | ||||
|     [True, False] | ||||
|     # ids='aio_err_triggered={}'.format | ||||
|     ('unmask_from_canc', 'canc_from_finally'), | ||||
|     [ | ||||
|         (True, False), | ||||
|         (True, True), | ||||
|         pytest.param(False, True, | ||||
|                      marks=pytest.mark.xfail(reason="never raises!") | ||||
|         ), | ||||
|     ], | ||||
|     # TODO, ask ronny how to impl this .. XD | ||||
|     # ids='unmask_from_canc={0}, canc_from_finally={1}',#.format, | ||||
| ) | ||||
| def test_acm_embedded_nursery_propagates_enter_err( | ||||
|     canc_from_finally: bool, | ||||
|     # open_tn_outside_acm: bool, | ||||
|     unmask_from_canc: bool, | ||||
| ): | ||||
|     # from tractor.trionics import maybe_open_nursery | ||||
|     ''' | ||||
|     Demo how a masking `trio.Cancelled` could be handled by unmasking from the | ||||
|     `.__context__` field when a user (by accident) re-raises from a `finally:`. | ||||
| 
 | ||||
|     # async def canc_then_checkpoint(tn): | ||||
|     #     tn.cancel_scope.cancel() | ||||
|     #     await trio.lowlevel.checkpoint() | ||||
|     ''' | ||||
|     import tractor | ||||
| 
 | ||||
|     @acm | ||||
|     async def wraps_tn_that_always_cancels( | ||||
|         # maybe_tn: trio.Nursery|None = None | ||||
|     async def maybe_raise_from_masking_exc( | ||||
|         tn: trio.Nursery, | ||||
|         unmask_from: BaseException|None = trio.Cancelled | ||||
| 
 | ||||
|         # TODO, maybe offer a collection? | ||||
|         # unmask_from: set[BaseException] = { | ||||
|         #     trio.Cancelled, | ||||
|         # }, | ||||
|     ): | ||||
|         # async with maybe_open_nursery(maybe_tn) as tn: | ||||
|         async with trio.open_nursery() as tn: | ||||
|         if not unmask_from: | ||||
|             yield | ||||
|             return | ||||
| 
 | ||||
|         try: | ||||
|             yield | ||||
|         except* unmask_from as be_eg: | ||||
| 
 | ||||
|             # TODO, if we offer `unmask_from: set` | ||||
|             # for masker_exc_type in unmask_from: | ||||
| 
 | ||||
|             matches, rest = be_eg.split(unmask_from) | ||||
|             if not matches: | ||||
|                 raise | ||||
| 
 | ||||
|             for exc_match in be_eg.exceptions: | ||||
|                 if ( | ||||
|                     (exc_ctx := exc_match.__context__) | ||||
|                     and | ||||
|                     type(exc_ctx) not in { | ||||
|                         # trio.Cancelled,  # always by default? | ||||
|                         unmask_from, | ||||
|                     } | ||||
|                 ): | ||||
|                     exc_ctx.add_note( | ||||
|                         f'\n' | ||||
|                         f'WARNING: the above error was masked by a {unmask_from!r} !?!\n' | ||||
|                         f'Are you always cancelling? Say from a `finally:` ?\n\n' | ||||
| 
 | ||||
|                         f'{tn!r}' | ||||
|                     ) | ||||
|                     raise exc_ctx from exc_match | ||||
| 
 | ||||
| 
 | ||||
|     @acm | ||||
|     async def wraps_tn_that_always_cancels(): | ||||
|         async with ( | ||||
|             trio.open_nursery() as tn, | ||||
|             maybe_raise_from_masking_exc( | ||||
|                 tn=tn, | ||||
|                 unmask_from=( | ||||
|                     trio.Cancelled | ||||
|                     if unmask_from_canc | ||||
|                     else None | ||||
|                 ), | ||||
|             ) | ||||
|         ): | ||||
|             try: | ||||
|                 yield tn | ||||
|             finally: | ||||
|                 if canc_from_finally: | ||||
|                     # await canc_then_checkpoint(tn) | ||||
|                     tn.cancel_scope.cancel() | ||||
|                     await trio.lowlevel.checkpoint() | ||||
| 
 | ||||
|     async def _main(): | ||||
|         # open_nursery = ( | ||||
|         #     trio.open_nursery if open_tn_outside_acm | ||||
|         #     else nullcontext | ||||
|         # ) | ||||
|         with tractor.devx.open_crash_handler() as bxerr: | ||||
|             assert not bxerr.value | ||||
| 
 | ||||
|         async with ( | ||||
|             # open_nursery() as tn, | ||||
|             # wraps_tn_that_always_cancels(maybe_tn=tn) as tn | ||||
|             wraps_tn_that_always_cancels() as tn | ||||
|         ): | ||||
|             assert not tn.cancel_scope.cancel_called | ||||
|             assert 0 | ||||
|             async with ( | ||||
|                 wraps_tn_that_always_cancels() as tn, | ||||
|             ): | ||||
|                 assert not tn.cancel_scope.cancel_called | ||||
|                 assert 0 | ||||
| 
 | ||||
|         assert ( | ||||
|             (err := bxerr.value) | ||||
|             and | ||||
|             type(err) is AssertionError | ||||
|         ) | ||||
| 
 | ||||
|     with pytest.raises(ExceptionGroup) as excinfo: | ||||
|         trio.run(_main) | ||||
| 
 | ||||
|     eg = excinfo.value | ||||
|     eg: ExceptionGroup = excinfo.value | ||||
|     assert_eg, rest_eg = eg.split(AssertionError) | ||||
| 
 | ||||
|     assert len(assert_eg.exceptions) == 1 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue