diff --git a/docs/changes/newsfragments/4611.improved b/docs/changes/newsfragments/4611.improved new file mode 100644 index 00000000000..1e5ef36c67d --- /dev/null +++ b/docs/changes/newsfragments/4611.improved @@ -0,0 +1,4 @@ +:class:`~qcodes.instrument.VirtualInstrument` has been added for virtual drivers +that do not communicate with hardware. Its ``get_idn`` returns a default IDN +dict without warning. :meth:`~qcodes.instrument.Instrument.get_idn` remains +unchanged and still warns when communication via ``ask_raw`` is unavailable. diff --git a/src/qcodes/__init__.py b/src/qcodes/__init__.py index e6f0b8d4674..89c8ce36e18 100644 --- a/src/qcodes/__init__.py +++ b/src/qcodes/__init__.py @@ -51,6 +51,7 @@ Instrument, InstrumentChannel, IPInstrument, + VirtualInstrument, VisaInstrument, find_or_create_instrument, ) diff --git a/src/qcodes/instrument/__init__.py b/src/qcodes/instrument/__init__.py index 5713b58b592..e8baaee65b9 100644 --- a/src/qcodes/instrument/__init__.py +++ b/src/qcodes/instrument/__init__.py @@ -16,7 +16,7 @@ ) from .channel import ChannelList, ChannelTuple, InstrumentChannel, InstrumentModule -from .instrument import Instrument, find_or_create_instrument +from .instrument import Instrument, VirtualInstrument, find_or_create_instrument from .instrument_base import InstrumentBase, InstrumentBaseKWArgs from .ip import IPInstrument from .visa import VisaInstrument, VisaInstrumentKWArgs @@ -30,6 +30,7 @@ "InstrumentBaseKWArgs", "InstrumentChannel", "InstrumentModule", + "VirtualInstrument", "VisaInstrument", "VisaInstrumentKWArgs", "find_or_create_instrument", diff --git a/src/qcodes/instrument/instrument.py b/src/qcodes/instrument/instrument.py index 8f2c36b3961..c11f7ec8449 100644 --- a/src/qcodes/instrument/instrument.py +++ b/src/qcodes/instrument/instrument.py @@ -461,6 +461,15 @@ def ask_raw(self, cmd: str) -> str: ) +class VirtualInstrument(Instrument): + """ + Base class for virtual instruments that do not communicate with hardware. + """ + + def get_idn(self) -> dict[str, str | None]: + return {"vendor": None, "model": self.name, "serial": None, "firmware": None} + + def find_or_create_instrument( instrument_class: type[T], name: str, diff --git a/tests/test_instrument.py b/tests/test_instrument.py index fd23723b25e..870657b560e 100644 --- a/tests/test_instrument.py +++ b/tests/test_instrument.py @@ -19,6 +19,7 @@ Instrument, InstrumentBase, InstrumentModule, + VirtualInstrument, find_or_create_instrument, ) from qcodes.instrument_drivers.mock_instruments import ( @@ -256,6 +257,74 @@ def test_get_idn(testdummy: DummyInstrument) -> None: assert testdummy.get_idn() == idn +def test_get_idn_on_virtual_instrument_does_not_warn( + caplog: pytest.LogCaptureFixture, +) -> None: + """``VirtualInstrument.get_idn`` should return a default IDN without warning.""" + + virtual = VirtualInstrument(name="virtual_no_ask") + try: + caplog.clear() + with caplog.at_level("WARNING"): + idn = virtual.get_idn() + assert idn == { + "vendor": None, + "model": "virtual_no_ask", + "serial": None, + "firmware": None, + } + assert "Error getting or interpreting *IDN?" not in caplog.text + finally: + virtual.close() + + +def test_get_idn_on_base_instrument_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """The plain ``Instrument`` base class should still warn without ``ask_raw``.""" + + instrument = Instrument(name="instrument_no_ask") + try: + caplog.clear() + with caplog.at_level("WARNING"): + idn = instrument.get_idn() + assert idn == { + "vendor": None, + "model": "instrument_no_ask", + "serial": None, + "firmware": None, + } + assert "Error getting or interpreting *IDN?" in caplog.text + finally: + instrument.close() + + +def test_get_idn_still_warns_on_other_errors( + caplog: pytest.LogCaptureFixture, +) -> None: + """Unexpected errors in ``*IDN?`` handling should still be surfaced via a + warning so real misbehaviour is not silenced by the virtual-instrument + short-circuit.""" + + class BrokenInstrument(Instrument): + def ask_raw(self, cmd: str) -> str: + raise RuntimeError("communication failure") + + broken = BrokenInstrument(name="broken_ask") + try: + with caplog.at_level("WARNING"): + idn = broken.get_idn() + assert idn == { + "vendor": None, + "model": "broken_ask", + "serial": None, + "firmware": None, + } + assert "Error getting or interpreting *IDN?" in caplog.text + finally: + broken.close() + + def test_repr(testdummy: DummyInstrument) -> None: assert repr(testdummy) == ""