Skip to content

Commit a31e959

Browse files
committed
fixes #1631
1 parent 2352a4a commit a31e959

3 files changed

Lines changed: 35 additions & 8 deletions

File tree

nbdev/_modidx.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,8 @@
378378
'nbdev.sync._update_nb': ('api/sync.html#_update_nb', 'nbdev/sync.py'),
379379
'nbdev.sync.absolute_import': ('api/sync.html#absolute_import', 'nbdev/sync.py'),
380380
'nbdev.sync.nbdev_update': ('api/sync.html#nbdev_update', 'nbdev/sync.py')},
381-
'nbdev.test': { 'nbdev.test._int_handler': ('api/test.html#_int_handler', 'nbdev/test.py'),
381+
'nbdev.test': { 'nbdev.test._await_chain': ('api/test.html#_await_chain', 'nbdev/test.py'),
382+
'nbdev.test._int_handler': ('api/test.html#_int_handler', 'nbdev/test.py'),
382383
'nbdev.test._keep_file': ('api/test.html#_keep_file', 'nbdev/test.py'),
383384
'nbdev.test._test_nb_sync': ('api/test.html#_test_nb_sync', 'nbdev/test.py'),
384385
'nbdev.test.nbdev_test': ('api/test.html#nbdev_test', 'nbdev/test.py'),

nbdev/test.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
__all__ = ['test_nb', 'nbdev_test']
99

1010
# %% ../nbs/api/12_test.ipynb #45e10c3f
11-
import time,os,sys,traceback,contextlib,inspect,signal,asyncio
11+
import time,os,sys,io,traceback,contextlib,inspect,signal,asyncio
1212
from fastcore.basics import *
1313
from fastcore.imports import *
1414
from fastcore.foundation import *
@@ -27,11 +27,24 @@
2727
# %% ../nbs/api/12_test.ipynb #dc8994ac
2828
_cur_nb = [None]
2929

30+
def _await_chain(t):
31+
"One frame per coroutine in task `t`'s await chain, deepest last: where a suspended hang actually sits"
32+
co = t.get_coro()
33+
while co is not None:
34+
f = getattr(co, 'cr_frame', None) or getattr(co, 'ag_frame', None) or getattr(co, 'gi_frame', None)
35+
if f is not None: yield f, f.f_lineno
36+
co = getattr(co, 'cr_await', None) or getattr(co, 'ag_await', None) or getattr(co, 'gi_yieldfrom', None)
37+
3038
def _int_handler(signum, frame):
3139
"Dump the running notebook's stack and exit; installed on SIGINT by `test_nb`"
3240
if _cur_nb[0] is not None:
33-
stk = ''.join(traceback.format_stack(frame))
34-
os.write(2, f'\n=== nbdev-test interrupted: {_cur_nb[0]} ===\n{stk}'.encode())
41+
buf = io.StringIO()
42+
traceback.print_stack(frame, file=buf)
43+
with contextlib.suppress(RuntimeError): # no running loop: sync frames already cover it
44+
for t in asyncio.all_tasks():
45+
print(f'\n{t}', file=buf)
46+
buf.writelines(traceback.StackSummary.extract(_await_chain(t)).format())
47+
os.write(2, f'\n=== nbdev-test interrupted: {_cur_nb[0]} ===\n{buf.getvalue()}'.encode())
3548
os._exit(130)
3649

3750
# %% ../nbs/api/12_test.ipynb #3f4fa1ad

nbs/api/12_test.ipynb

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"outputs": [],
2929
"source": [
3030
"#| export\n",
31-
"import time,os,sys,traceback,contextlib,inspect,signal,asyncio\n",
31+
"import time,os,sys,io,traceback,contextlib,inspect,signal,asyncio\n",
3232
"from fastcore.basics import *\n",
3333
"from fastcore.imports import *\n",
3434
"from fastcore.foundation import *\n",
@@ -50,7 +50,7 @@
5050
"id": "7fe8a875",
5151
"metadata": {},
5252
"source": [
53-
"When a test run hangs, ctrl-c should say what each notebook was executing. Each worker installs a SIGINT handler that prints the in-flight notebook's stack as a single block (one `os.write`, so parallel workers' dumps don't interleave) and then exits immediately, rather than letting the interrupt surface inside the cell where the shell would catch it and carry on. An idle worker exits silently."
53+
"When a test run hangs, ctrl-c should say what each notebook was executing. Each worker installs a SIGINT handler that prints the in-flight notebook's stack — the sync frames plus every pending asyncio task, since a hang inside an awaited coroutine lives on the task, not the sync stack — as a single block (one `os.write`, so parallel workers' dumps don't interleave) and then exits immediately, rather than letting the interrupt surface inside the cell where the shell would catch it and carry on. An idle worker exits silently."
5454
]
5555
},
5656
{
@@ -63,11 +63,24 @@
6363
"#| export\n",
6464
"_cur_nb = [None]\n",
6565
"\n",
66+
"def _await_chain(t):\n",
67+
" \"One frame per coroutine in task `t`'s await chain, deepest last: where a suspended hang actually sits\"\n",
68+
" co = t.get_coro()\n",
69+
" while co is not None:\n",
70+
" f = getattr(co, 'cr_frame', None) or getattr(co, 'ag_frame', None) or getattr(co, 'gi_frame', None)\n",
71+
" if f is not None: yield f, f.f_lineno\n",
72+
" co = getattr(co, 'cr_await', None) or getattr(co, 'ag_await', None) or getattr(co, 'gi_yieldfrom', None)\n",
73+
"\n",
6674
"def _int_handler(signum, frame):\n",
6775
" \"Dump the running notebook's stack and exit; installed on SIGINT by `test_nb`\"\n",
6876
" if _cur_nb[0] is not None:\n",
69-
" stk = ''.join(traceback.format_stack(frame))\n",
70-
" os.write(2, f'\\n=== nbdev-test interrupted: {_cur_nb[0]} ===\\n{stk}'.encode())\n",
77+
" buf = io.StringIO()\n",
78+
" traceback.print_stack(frame, file=buf)\n",
79+
" with contextlib.suppress(RuntimeError): # no running loop: sync frames already cover it\n",
80+
" for t in asyncio.all_tasks():\n",
81+
" print(f'\\n{t}', file=buf)\n",
82+
" buf.writelines(traceback.StackSummary.extract(_await_chain(t)).format())\n",
83+
" os.write(2, f'\\n=== nbdev-test interrupted: {_cur_nb[0]} ===\\n{buf.getvalue()}'.encode())\n",
7184
" os._exit(130)"
7285
]
7386
},

0 commit comments

Comments
 (0)