Skip to content

Commit ce48db5

Browse files
authored
Merge pull request #8312 from jenshnielsen/close_subclass
Instrument Add option to only close specific subclass
2 parents 30a638c + d5850ca commit ce48db5

3 files changed

Lines changed: 115 additions & 3 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:meth:`.Instrument.close_all` gained an ``only_subclasses`` keyword argument. When
2+
set to ``True``, only instruments that are instances of the class (or its subclasses)
3+
on which ``close_all`` is called are closed, leaving other registered instruments open.
4+
The ``log_status`` and ``only_subclasses`` arguments are now keyword-only.

src/qcodes/instrument/instrument.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,9 @@ def close(self) -> None:
182182
@classmethod
183183
def close_all(
184184
cls,
185+
*,
185186
log_status: bool = False,
187+
only_subclasses: bool = False,
186188
) -> None:
187189
"""
188190
Try to close all instruments registered in
@@ -196,16 +198,25 @@ def close_all(
196198
Args:
197199
log_status: If True, log the status of closing each instrument. Set this to False
198200
if you want to avoid logging during interpreter shutdown, which can cause errors.
201+
only_subclasses: If True, only close instruments that are subclasses of the class
202+
on which this method is called. If False, close all instruments regardless of class.
199203
200204
"""
201205
if log_status:
202206
log.info("Closing all registered instruments")
203207
for inststr in list(cls._all_instruments):
204208
try:
205209
inst: Instrument = cls.find_instrument(inststr)
206-
if log_status:
207-
log.info("Closing %s", inststr)
208-
inst.close()
210+
if only_subclasses and issubclass(type(inst), cls):
211+
should_be_closed = True
212+
elif not only_subclasses:
213+
should_be_closed = True
214+
else:
215+
should_be_closed = False
216+
if should_be_closed:
217+
if log_status:
218+
log.info("Closing %s", inststr)
219+
inst.close()
209220
except Exception:
210221
if log_status:
211222
log.exception("Failed to close %s, ignored", inststr)

tests/test_instrument.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import contextlib
88
import gc
99
import io
10+
import logging
1011
import re
1112
import weakref
1213
from typing import TYPE_CHECKING, Any, assert_type
@@ -22,6 +23,7 @@
2223
find_or_create_instrument,
2324
)
2425
from qcodes.instrument_drivers.mock_instruments import (
26+
DummyBase,
2527
DummyChannelInstrument,
2628
DummyFailingInstrument,
2729
DummyInstrument,
@@ -450,6 +452,101 @@ def test_recreate(request: FixtureRequest) -> None:
450452
assert instr not in Instrument._all_instruments.values()
451453

452454

455+
@pytest.mark.usefixtures("close_before_and_after")
456+
def test_close_all_closes_all_instruments() -> None:
457+
"""``close_all`` closes every registered instrument by default."""
458+
dummy = DummyInstrument(name="dummy", gates=["dac1"])
459+
parabola = MockParabola("parabola")
460+
461+
assert Instrument.is_valid(dummy)
462+
assert Instrument.is_valid(parabola)
463+
464+
Instrument.close_all()
465+
466+
assert not Instrument.is_valid(dummy)
467+
assert not Instrument.is_valid(parabola)
468+
assert Instrument._all_instruments == WeakValueDictionary()
469+
470+
471+
@pytest.mark.usefixtures("close_before_and_after")
472+
def test_close_all_only_subclasses_from_leaf_class() -> None:
473+
"""``only_subclasses`` on a leaf class leaves sibling classes open."""
474+
dummy = DummyInstrument(name="dummy", gates=["dac1"])
475+
parabola = MockParabola("parabola")
476+
477+
# DummyInstrument and MockParabola are siblings (both subclass DummyBase),
478+
# so closing only DummyInstrument subclasses must leave the parabola open.
479+
DummyInstrument.close_all(only_subclasses=True)
480+
481+
assert not Instrument.is_valid(dummy)
482+
assert Instrument.is_valid(parabola)
483+
484+
# The remaining instrument can still be closed with a plain close_all.
485+
Instrument.close_all()
486+
assert not Instrument.is_valid(parabola)
487+
assert Instrument._all_instruments == WeakValueDictionary()
488+
489+
490+
@pytest.mark.usefixtures("close_before_and_after")
491+
def test_close_all_only_subclasses_from_base_class() -> None:
492+
"""``only_subclasses`` closes instances of the class and its subclasses."""
493+
dummy = DummyInstrument(name="dummy", gates=["dac1"])
494+
parabola = MockParabola("parabola")
495+
496+
# Both DummyInstrument and MockParabola are subclasses of DummyBase, so both
497+
# are closed when calling close_all on the shared base class.
498+
DummyBase.close_all(only_subclasses=True)
499+
500+
assert not Instrument.is_valid(dummy)
501+
assert not Instrument.is_valid(parabola)
502+
assert Instrument._all_instruments == WeakValueDictionary()
503+
504+
505+
@pytest.mark.usefixtures("close_before_and_after")
506+
def test_close_all_only_subclasses_false_closes_everything() -> None:
507+
"""``only_subclasses=False`` closes all instruments regardless of class."""
508+
dummy = DummyInstrument(name="dummy", gates=["dac1"])
509+
parabola = MockParabola("parabola")
510+
511+
DummyInstrument.close_all(only_subclasses=False)
512+
513+
assert not Instrument.is_valid(dummy)
514+
assert not Instrument.is_valid(parabola)
515+
assert Instrument._all_instruments == WeakValueDictionary()
516+
517+
518+
@pytest.mark.usefixtures("close_before_and_after")
519+
def test_close_all_log_status(caplog: pytest.LogCaptureFixture) -> None:
520+
"""``log_status=True`` logs the closing of each instrument."""
521+
dummy = DummyInstrument(name="dummy", gates=["dac1"])
522+
523+
with caplog.at_level(logging.INFO, logger="qcodes.instrument.instrument"):
524+
Instrument.close_all(log_status=True)
525+
526+
assert "Closing all registered instruments" in caplog.text
527+
assert "Closing dummy" in caplog.text
528+
assert not Instrument.is_valid(dummy)
529+
530+
531+
@pytest.mark.usefixtures("close_before_and_after")
532+
def test_close_all_no_log_by_default(caplog: pytest.LogCaptureFixture) -> None:
533+
"""``close_all`` does not log anything when ``log_status`` is not set."""
534+
dummy = DummyInstrument(name="dummy", gates=["dac1"])
535+
536+
with caplog.at_level(logging.INFO, logger="qcodes.instrument.instrument"):
537+
Instrument.close_all()
538+
539+
assert "Closing all registered instruments" not in caplog.text
540+
assert "Closing dummy" not in caplog.text
541+
assert not Instrument.is_valid(dummy)
542+
543+
544+
def test_close_all_only_accepts_keyword_arguments() -> None:
545+
"""The ``close_all`` options are keyword-only."""
546+
with pytest.raises(TypeError):
547+
Instrument.close_all(True) # type: ignore[misc]
548+
549+
453550
def test_instrument_metadata(request: FixtureRequest) -> None:
454551
metadatadict = {1: "data", "some": "data"}
455552
instrument = DummyInstrument(

0 commit comments

Comments
 (0)