From d5850cac26f121d06bc5600f62df59fc9a083c5c Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 20 Jul 2026 11:47:38 +0200 Subject: [PATCH] Add option to only close specific subclass Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0908787e-17e8-469b-a939-af9fc746464e --- docs/changes/newsfragments/8312.improved | 4 + src/qcodes/instrument/instrument.py | 17 ++++- tests/test_instrument.py | 97 ++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 docs/changes/newsfragments/8312.improved diff --git a/docs/changes/newsfragments/8312.improved b/docs/changes/newsfragments/8312.improved new file mode 100644 index 00000000000..b836c51a35c --- /dev/null +++ b/docs/changes/newsfragments/8312.improved @@ -0,0 +1,4 @@ +:meth:`.Instrument.close_all` gained an ``only_subclasses`` keyword argument. When +set to ``True``, only instruments that are instances of the class (or its subclasses) +on which ``close_all`` is called are closed, leaving other registered instruments open. +The ``log_status`` and ``only_subclasses`` arguments are now keyword-only. diff --git a/src/qcodes/instrument/instrument.py b/src/qcodes/instrument/instrument.py index 2f52e6670df..2352e271c16 100644 --- a/src/qcodes/instrument/instrument.py +++ b/src/qcodes/instrument/instrument.py @@ -182,7 +182,9 @@ def close(self) -> None: @classmethod def close_all( cls, + *, log_status: bool = False, + only_subclasses: bool = False, ) -> None: """ Try to close all instruments registered in @@ -196,6 +198,8 @@ def close_all( Args: log_status: If True, log the status of closing each instrument. Set this to False if you want to avoid logging during interpreter shutdown, which can cause errors. + only_subclasses: If True, only close instruments that are subclasses of the class + on which this method is called. If False, close all instruments regardless of class. """ if log_status: @@ -203,9 +207,16 @@ def close_all( for inststr in list(cls._all_instruments): try: inst: Instrument = cls.find_instrument(inststr) - if log_status: - log.info("Closing %s", inststr) - inst.close() + if only_subclasses and issubclass(type(inst), cls): + should_be_closed = True + elif not only_subclasses: + should_be_closed = True + else: + should_be_closed = False + if should_be_closed: + if log_status: + log.info("Closing %s", inststr) + inst.close() except Exception: if log_status: log.exception("Failed to close %s, ignored", inststr) diff --git a/tests/test_instrument.py b/tests/test_instrument.py index fd23723b25e..cbd4ce12631 100644 --- a/tests/test_instrument.py +++ b/tests/test_instrument.py @@ -7,6 +7,7 @@ import contextlib import gc import io +import logging import re import weakref from typing import TYPE_CHECKING, Any, assert_type @@ -22,6 +23,7 @@ find_or_create_instrument, ) from qcodes.instrument_drivers.mock_instruments import ( + DummyBase, DummyChannelInstrument, DummyFailingInstrument, DummyInstrument, @@ -450,6 +452,101 @@ def test_recreate(request: FixtureRequest) -> None: assert instr not in Instrument._all_instruments.values() +@pytest.mark.usefixtures("close_before_and_after") +def test_close_all_closes_all_instruments() -> None: + """``close_all`` closes every registered instrument by default.""" + dummy = DummyInstrument(name="dummy", gates=["dac1"]) + parabola = MockParabola("parabola") + + assert Instrument.is_valid(dummy) + assert Instrument.is_valid(parabola) + + Instrument.close_all() + + assert not Instrument.is_valid(dummy) + assert not Instrument.is_valid(parabola) + assert Instrument._all_instruments == WeakValueDictionary() + + +@pytest.mark.usefixtures("close_before_and_after") +def test_close_all_only_subclasses_from_leaf_class() -> None: + """``only_subclasses`` on a leaf class leaves sibling classes open.""" + dummy = DummyInstrument(name="dummy", gates=["dac1"]) + parabola = MockParabola("parabola") + + # DummyInstrument and MockParabola are siblings (both subclass DummyBase), + # so closing only DummyInstrument subclasses must leave the parabola open. + DummyInstrument.close_all(only_subclasses=True) + + assert not Instrument.is_valid(dummy) + assert Instrument.is_valid(parabola) + + # The remaining instrument can still be closed with a plain close_all. + Instrument.close_all() + assert not Instrument.is_valid(parabola) + assert Instrument._all_instruments == WeakValueDictionary() + + +@pytest.mark.usefixtures("close_before_and_after") +def test_close_all_only_subclasses_from_base_class() -> None: + """``only_subclasses`` closes instances of the class and its subclasses.""" + dummy = DummyInstrument(name="dummy", gates=["dac1"]) + parabola = MockParabola("parabola") + + # Both DummyInstrument and MockParabola are subclasses of DummyBase, so both + # are closed when calling close_all on the shared base class. + DummyBase.close_all(only_subclasses=True) + + assert not Instrument.is_valid(dummy) + assert not Instrument.is_valid(parabola) + assert Instrument._all_instruments == WeakValueDictionary() + + +@pytest.mark.usefixtures("close_before_and_after") +def test_close_all_only_subclasses_false_closes_everything() -> None: + """``only_subclasses=False`` closes all instruments regardless of class.""" + dummy = DummyInstrument(name="dummy", gates=["dac1"]) + parabola = MockParabola("parabola") + + DummyInstrument.close_all(only_subclasses=False) + + assert not Instrument.is_valid(dummy) + assert not Instrument.is_valid(parabola) + assert Instrument._all_instruments == WeakValueDictionary() + + +@pytest.mark.usefixtures("close_before_and_after") +def test_close_all_log_status(caplog: pytest.LogCaptureFixture) -> None: + """``log_status=True`` logs the closing of each instrument.""" + dummy = DummyInstrument(name="dummy", gates=["dac1"]) + + with caplog.at_level(logging.INFO, logger="qcodes.instrument.instrument"): + Instrument.close_all(log_status=True) + + assert "Closing all registered instruments" in caplog.text + assert "Closing dummy" in caplog.text + assert not Instrument.is_valid(dummy) + + +@pytest.mark.usefixtures("close_before_and_after") +def test_close_all_no_log_by_default(caplog: pytest.LogCaptureFixture) -> None: + """``close_all`` does not log anything when ``log_status`` is not set.""" + dummy = DummyInstrument(name="dummy", gates=["dac1"]) + + with caplog.at_level(logging.INFO, logger="qcodes.instrument.instrument"): + Instrument.close_all() + + assert "Closing all registered instruments" not in caplog.text + assert "Closing dummy" not in caplog.text + assert not Instrument.is_valid(dummy) + + +def test_close_all_only_accepts_keyword_arguments() -> None: + """The ``close_all`` options are keyword-only.""" + with pytest.raises(TypeError): + Instrument.close_all(True) # type: ignore[misc] + + def test_instrument_metadata(request: FixtureRequest) -> None: metadatadict = {1: "data", "some": "data"} instrument = DummyInstrument(