Skip to content

Commit 0d26f50

Browse files
authored
Merge pull request #6223 from jenshnielsen/backport_keyboard_interrupt
Backport keyboard interrupt fix
2 parents 0c2cb21 + 2dbbd94 commit 0d26f50

File tree

3 files changed

+122
-6
lines changed

3 files changed

+122
-6
lines changed

docs/changes/0.46.0.rst

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
QCoDeS 0.46.0 (2024-06-27)
1+
QCoDeS 0.46.0 (2024-07-04)
22
==========================
33

44
Breaking Changes:
@@ -11,7 +11,7 @@ Breaking Changes:
1111
documented as TypedDics classes that can be used to type `**kwargs` in the subclass constructors.
1212
See `Creating QCoDeS instrument drivers` for usage examples.
1313

14-
This also means that the these arguments **must** be passed as keyword arguments, and not as positional arguments.
14+
This also means that these arguments **must** be passed as keyword arguments, and not as positional arguments.
1515
This specifically includeds passing ``label`` and ``metadata`` to direct subclasses of ``Instrument`` as well as
1616
``terminator`` to subclasses of ``VisaInstrument``.
1717

@@ -27,6 +27,10 @@ Breaking Changes:
2727
If the attribute is not a ParameterBase this will instead warn. It is the intention that this becomes an error in the future.
2828
(:pr:`6174`) (:pr:`6211`)
2929

30+
- Updated dond functions to to re-raise KeyboardInterrupt for better interrupt handling making it easier to stop long-running measurement
31+
loops and reducing the need for kernel restarts. This meas that if you interrupt a `dond`` function with a keyboard interrupt not only
32+
the measurement but any pending code to execute will be interrupted. In the process logging for interrupted measurements has been improved. (:pr:`6192`)
33+
3034

3135
Improved:
3236
---------

src/qcodes/dataset/dond/do_nd_utils.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,26 @@ def _register_actions(
120120

121121

122122
@contextmanager
123-
def catch_interrupts() -> Iterator[Callable[[], MeasInterruptT]]:
124-
interrupt_exception = None
123+
def catch_interrupts() -> Iterator[Callable[[], MeasInterruptT | None]]:
124+
interrupt_exception: MeasInterruptT | None = None
125+
interrupt_raised = False
125126

126-
def get_interrupt_exception() -> MeasInterruptT:
127+
def get_interrupt_exception() -> MeasInterruptT | None:
127128
nonlocal interrupt_exception
128129
return interrupt_exception
129130

130131
try:
131132
yield get_interrupt_exception
132-
except (KeyboardInterrupt, BreakConditionInterrupt) as e:
133+
except KeyboardInterrupt as e:
133134
interrupt_exception = e
135+
interrupt_raised = True
136+
raise # Re-raise KeyboardInterrupt
137+
except BreakConditionInterrupt as e:
138+
interrupt_exception = e
139+
interrupt_raised = True
140+
# Don't re-raise BreakConditionInterrupt
141+
finally:
142+
if interrupt_raised:
143+
log.warning(
144+
f"Measurement has been interrupted, data may be incomplete: {interrupt_exception}"
145+
)
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
2+
import pytest
3+
4+
from qcodes.dataset.dond.do_nd_utils import BreakConditionInterrupt, catch_interrupts
5+
6+
7+
def test_catch_interrupts():
8+
# Test normal execution (no interrupt)
9+
with catch_interrupts() as get_interrupt:
10+
assert get_interrupt() is None
11+
12+
# Test KeyboardInterrupt
13+
with pytest.raises(KeyboardInterrupt):
14+
with catch_interrupts() as get_interrupt:
15+
raise KeyboardInterrupt()
16+
17+
# Test BreakConditionInterrupt
18+
with catch_interrupts() as get_interrupt:
19+
raise BreakConditionInterrupt()
20+
assert isinstance(get_interrupt(), BreakConditionInterrupt)
21+
22+
# Test that cleanup code runs for KeyboardInterrupt
23+
cleanup_ran = False
24+
with pytest.raises(KeyboardInterrupt):
25+
with catch_interrupts():
26+
try:
27+
raise KeyboardInterrupt()
28+
finally:
29+
cleanup_ran = True
30+
assert cleanup_ran
31+
32+
# Test that cleanup code runs for BreakConditionInterrupt
33+
cleanup_ran = False
34+
with catch_interrupts():
35+
try:
36+
raise BreakConditionInterrupt()
37+
finally:
38+
cleanup_ran = True
39+
assert cleanup_ran
40+
41+
# Test that BreakConditionInterrupt is caught and doesn't raise
42+
with catch_interrupts() as get_interrupt:
43+
raise BreakConditionInterrupt()
44+
assert isinstance(get_interrupt(), BreakConditionInterrupt)
45+
46+
47+
def test_catch_interrupts_in_loops():
48+
# Test interruption in a simple loop
49+
loop_count = 0
50+
with pytest.raises(KeyboardInterrupt):
51+
for i in range(5):
52+
with catch_interrupts():
53+
loop_count += 1
54+
if i == 2:
55+
raise KeyboardInterrupt()
56+
assert loop_count == 3 # Loop should stop at the third iteration
57+
58+
# Test interruption in nested loops
59+
outer_count = 0
60+
inner_count = 0
61+
with pytest.raises(KeyboardInterrupt):
62+
for i in range(3):
63+
with catch_interrupts():
64+
outer_count += 1
65+
for j in range(3):
66+
with catch_interrupts():
67+
inner_count += 1
68+
if i == 1 and j == 1:
69+
raise KeyboardInterrupt()
70+
assert outer_count == 2
71+
assert inner_count == 5
72+
73+
74+
def test_catch_interrupts_simulated_sweeps():
75+
def simulated_sweep(interrupt_at=None):
76+
for i in range(5):
77+
with catch_interrupts():
78+
if i == interrupt_at:
79+
raise KeyboardInterrupt()
80+
yield i
81+
82+
# Test interruption in a single sweep
83+
results = []
84+
with pytest.raises(KeyboardInterrupt):
85+
for value in simulated_sweep(interrupt_at=3):
86+
results.append(value)
87+
assert results == [0, 1, 2]
88+
89+
# Test interruption in nested sweeps
90+
outer_results = []
91+
inner_results = []
92+
with pytest.raises(KeyboardInterrupt):
93+
for outer_value in simulated_sweep(interrupt_at=None):
94+
outer_results.append(outer_value)
95+
for inner_value in simulated_sweep(
96+
interrupt_at=2 if outer_value == 1 else None
97+
):
98+
inner_results.append(inner_value)
99+
assert outer_results == [0, 1]
100+
assert inner_results == [0, 1, 2, 3, 4, 0, 1]

0 commit comments

Comments
 (0)