diff --git a/docs/changes/newsfragments/7978.breaking b/docs/changes/newsfragments/7978.breaking new file mode 100644 index 000000000000..f91535e6f8c1 --- /dev/null +++ b/docs/changes/newsfragments/7978.breaking @@ -0,0 +1,14 @@ +A new ``resource`` parameter has been added that accepts an already-opened +``pyvisa.resources.MessageBasedResource``, allowing a ``VisaInstrument`` to be +constructed from an existing PyVISA resource handle. The ``resource`` parameter +cannot be combined with ``address``, ``visalib`` or ``pyvisa_sim_file``. These arguments +are only used to construct a new resource. + +The ``resource_manager``, ``visabackend`` and ``visalib`` ``visa_handle`` attributes of +:class:`.VisaInstrument` have been converted to read-only properties. + +The ``_address`` property is deprecated in favour of ``address``. +The ``visalib`` property is deprecated in favour of ``visabackend``. + +The method ``set_address`` now takes visalib as an argument. If this +is not supplied the default visa library will be used. diff --git a/pyproject.toml b/pyproject.toml index a9745ddacc65..fb01f37cc98b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ dependencies = [ "packaging>=20.0", "pandas>=1.5.0", "pyarrow>=11.0.0", # will become a requirement of pandas. Installing explicitly silences a warning - "pyvisa>=1.11.0, <1.17.0", + "pyvisa>=1.12.0, <1.17.0", "ruamel.yaml>=0.16.0,!=0.16.6", "tabulate>=0.9.0", "typing_extensions>=4.6.0", diff --git a/src/qcodes/instrument/ip_to_visa.py b/src/qcodes/instrument/ip_to_visa.py index 02245d8c448f..80d221341d48 100644 --- a/src/qcodes/instrument/ip_to_visa.py +++ b/src/qcodes/instrument/ip_to_visa.py @@ -24,7 +24,7 @@ # Such a driver is just a two-line class definition. -class IPToVisa(VisaInstrument, IPInstrument): # type: ignore[misc] +class IPToVisa(VisaInstrument, IPInstrument): # type: ignore[override,misc] """ Class to inject an VisaInstrument like behaviour in an IPInstrument that we'd like to use as a VISAInstrument with the @@ -72,8 +72,7 @@ def __init__( traversable_handle = files("qcodes.instrument.sims") / pyvisa_sim_file with as_file(traversable_handle) as sim_visalib_path: - self.visalib = f"{sim_visalib_path!s}@sim" - self.set_address(address=address) + self.set_address(address=address, visalib=f"{sim_visalib_path!s}@sim") if device_clear: self.device_clear() @@ -88,8 +87,10 @@ def close(self) -> None: if getattr(self, "visa_handle", None): self.visa_handle.close() - if getattr(self, "visabackend", None) == "sim" and getattr( - self, "resource_manager", None + if ( + getattr(self, "visabackend", None) == "sim" + and getattr(self, "resource_manager", None) + and self.resource_manager is not None ): # The pyvisa-sim visalib has a session attribute but the resource manager is not generic in the # visalib type so we cannot get it in a type safe way diff --git a/src/qcodes/instrument/sims/cryo_tm620.yaml b/src/qcodes/instrument/sims/cryo_tm620.yaml index cb238bc848ac..5f1f50869120 100644 --- a/src/qcodes/instrument/sims/cryo_tm620.yaml +++ b/src/qcodes/instrument/sims/cryo_tm620.yaml @@ -24,5 +24,5 @@ devices: resources: - GPIB::1::INSTR: + GPIB0::1::INSTR: device: cryomag_tm620 diff --git a/src/qcodes/instrument/visa.py b/src/qcodes/instrument/visa.py index 1556ab683274..66cdabd53ea9 100644 --- a/src/qcodes/instrument/visa.py +++ b/src/qcodes/instrument/visa.py @@ -12,10 +12,11 @@ import pyvisa.constants as vi_const import pyvisa.resources from pyvisa.errors import InvalidSession +from typing_extensions import deprecated import qcodes.validators as vals from qcodes.logger import get_instrument_logger -from qcodes.utils import DelayedKeyboardInterrupt +from qcodes.utils import DelayedKeyboardInterrupt, QCoDeSDeprecationWarning from .instrument import Instrument from .instrument_base import InstrumentBase, InstrumentBaseKWArgs @@ -84,6 +85,14 @@ class VisaInstrumentKWArgs(TypedDict): """ Name of a pyvisa-sim yaml file used to simulate the instrument. """ + resource: NotRequired[pyvisa.resources.MessageBasedResource | None] + """ + An already-opened :class:`pyvisa.resources.MessageBasedResource`. + When provided, the instrument wraps this resource instead of opening + a new connection. The instrument takes ownership and will close the + resource when the instrument is closed or garbage collected. + Mutually exclusive with ``address``, ``visalib`` and ``pyvisa_sim_file``. + """ class VisaInstrument(Instrument): @@ -93,6 +102,12 @@ class VisaInstrument(Instrument): Args: name: What this instrument is called locally. address: The visa resource name to use to connect. + Mutually exclusive with ``resource``. + resource: An already-opened :class:`pyvisa.resources.MessageBasedResource`. + When provided, the instrument wraps this resource instead of opening + a new connection. The instrument takes ownership and will close the + resource when the instrument is closed or garbage collected. + Mutually exclusive with ``address``, ``visalib`` and ``pyvisa_sim_file``. timeout: seconds to allow for responses. If "unset" will read the value from `self.default_timeout`. None means wait forever. Default 5. terminator: Read and write termination character(s). @@ -107,7 +122,7 @@ class VisaInstrument(Instrument): By default the IVI backend is used if found, but '@py' will use the ``pyvisa-py`` backend. Note that QCoDeS does not install (or even require) ANY backends, it is up to the user to do that. see eg: - http://pyvisa.readthedocs.org/en/stable/names.html + https://pyvisa.readthedocs.io/en/stable/introduction/names.html metadata: additional static metadata to add to this instrument's JSON snapshot. pyvisa_sim_file: Name of a pyvisa-sim yaml file used to simulate the instrument. @@ -138,13 +153,14 @@ class VisaInstrument(Instrument): def __init__( self, name: str, - address: str, + address: str | None = None, timeout: float | None | Literal["Unset"] = "Unset", terminator: str | Literal["Unset"] | None = "Unset", # noqa: PYI051 # while unset is redundant here we add it to communicate to the user that unset has special meaning device_clear: bool = True, visalib: str | None = None, pyvisa_sim_file: str | None = None, + resource: pyvisa.resources.MessageBasedResource | None = None, **kwargs: Unpack[InstrumentBaseKWArgs], ): if terminator == "Unset": @@ -163,12 +179,24 @@ def __init__( vals=vals.MultiType(vals.Numbers(min_value=0), vals.Enum(None)), ) - if visalib is not None and pyvisa_sim_file is not None: + if resource is not None: + if address is not None: + raise TypeError("'address' and 'resource' are mutually exclusive") + if visalib is not None or pyvisa_sim_file is not None: + raise TypeError( + "Cannot supply visalib or pyvisa_sim_file when using " + "an existing resource" + ) + visa_handle = resource + address = resource.resource_name + elif address is None: + raise TypeError("Either 'address' or 'resource' must be provided") + elif visalib is not None and pyvisa_sim_file is not None: raise RuntimeError( "It's an error to supply both visalib and pyvisa_sim_file as " "arguments to a VISA instrument" ) - if pyvisa_sim_file is not None: + elif pyvisa_sim_file is not None: if ":" in pyvisa_sim_file: module, pyvisa_sim_file = pyvisa_sim_file.split(":") else: @@ -182,51 +210,99 @@ def __init__( f"file {pyvisa_sim_file} from module: {module}" ) visalib = f"{sim_visalib_path!s}@sim" - ( - visa_handle, - visabackend, - resource_manager, - ) = self._connect_and_handle_error(address, visalib) + visa_handle = self._connect_and_handle_error(address, visalib) else: - visa_handle, visabackend, resource_manager = self._connect_and_handle_error( - address, visalib - ) + visa_handle = self._connect_and_handle_error(address, visalib) finalize(self, _close_visa_handle, visa_handle, str(self.name)) - self.visabackend: str = visabackend - self.visa_handle: pyvisa.resources.MessageBasedResource = visa_handle + self._legacy_address = address + + self._visa_handle: pyvisa.resources.MessageBasedResource = visa_handle + + if device_clear: + self.device_clear() + + self.set_terminator(terminator) + self.timeout.set(timeout) + + @property + @deprecated( + "The _address property is deprecated, use the address property instead.", + category=QCoDeSDeprecationWarning, + ) + def _address(self) -> str | None: """ - The VISA resource used by this instrument. + DEPRECATED: USE self.address INSTEAD. + """ + return self._legacy_address + + @property + def address(self) -> str | None: """ - self.resource_manager = resource_manager + The VISA resource name used to connect to this instrument. + Note that pyvisa normalizes the resource name when connecting, + so this may not be exactly the same as the address that was passed + in when creating the instrument. + """ + return self.visa_handle.resource_name + + @property + def resource_manager(self) -> pyvisa.ResourceManager | None: """ The VISA resource manager used by this instrument. """ - self.visalib: str | None = visalib - self._address = address + return self.visa_handle.visalib.resource_manager - if device_clear: - self.device_clear() + @property + def visa_handle(self) -> pyvisa.resources.MessageBasedResource: + """ + The VISA resource used by this instrument. + """ + return self._visa_handle - self.set_terminator(terminator) - self.timeout.set(timeout) + @property + def visabackend(self) -> str: + """ + The VISA backend used by this instrument. + """ + class_name = self.visa_handle.visalib.__class__.__name__ + if class_name == "SimVisaLibrary": + return "sim" + elif class_name == "IVIVisaLibrary": + return "ivi" + elif class_name == "PyVisaLibrary": + return "py" + else: + self.visa_log.info( + f"Could not determine VISA backend from visa library class name: {class_name} falling back to IVI default." + ) + return "ivi" + + @property + @deprecated( + "The visalib property is deprecated, use the visabackend property instead.", + category=QCoDeSDeprecationWarning, + ) + def visalib(self) -> str | None: + """ + The VISA library used by this instrument. + """ + return f"{self.visa_handle.visalib.library_path}@{self.visabackend}" def _connect_and_handle_error( self, address: str, visalib: str | None - ) -> tuple[pyvisa.resources.MessageBasedResource, str, pyvisa.ResourceManager]: + ) -> pyvisa.resources.MessageBasedResource: try: - visa_handle, visabackend, resource_manager = self._open_resource( - address, visalib - ) + visa_handle = self._open_resource(address, visalib) except Exception as e: self.visa_log.exception(f"Could not connect at {address}") self.close() raise e - return visa_handle, visabackend, resource_manager + return visa_handle def _open_resource( self, address: str, visalib: str | None - ) -> tuple[pyvisa.resources.MessageBasedResource, str, pyvisa.ResourceManager]: + ) -> pyvisa.resources.MessageBasedResource: # in case we're changing the address - close the old handle first if getattr(self, "visa_handle", None): self.visa_handle.close() @@ -236,11 +312,9 @@ def _open_resource( f"Opening PyVISA Resource Manager with visalib: {visalib}" ) resource_manager = pyvisa.ResourceManager(visalib) - visabackend = visalib.split("@")[1] else: self.visa_log.info("Opening PyVISA Resource Manager with default backend.") resource_manager = pyvisa.ResourceManager() - visabackend = "ivi" self.visa_log.info(f"Opening PyVISA resource at address: {address}") resource = resource_manager.open_resource(address) @@ -248,26 +322,28 @@ def _open_resource( resource.close() raise TypeError("QCoDeS only support MessageBasedResource Visa resources") - return resource, visabackend, resource_manager + return resource - def set_address(self, address: str) -> None: + def set_address( + self, + address: str, + visalib: str | None, + ) -> None: """ - Set the address for this instrument. + Set the address for this instrument. Note in most cases + this method is not recommended and it is better to close the instrument and + create a new instance of the instrument with the new address. Args: address: The visa resource name to use to connect. The address should be the actual address and just that. If you wish to - change the backend for VISA, use the self.visalib attribute - (and then call this function). + change the backend for VISA, use the visalib argument + visalib: Visa backend to use when connecting to this instrument. + If not supplied use the default backend. """ - resource, visabackend, resource_manager = self._open_resource( - address, self.visalib - ) - self.visa_handle = resource - self._address = address - self.visabackend = visabackend - self.resource_manager = resource_manager + self._visa_handle = self._open_resource(address, visalib) + self._legacy_address = address def device_clear(self) -> None: """Clear the buffers of the device""" @@ -328,12 +404,17 @@ def close(self) -> None: if getattr(self, "visa_handle", None): self.visa_handle.close() - if getattr(self, "visabackend", None) == "sim" and getattr( - self, "resource_manager", None + if ( + getattr(self, "visabackend", None) == "sim" + and getattr(self, "resource_manager", None) + and self.resource_manager is not None ): # The pyvisa-sim visalib has a session attribute but the resource manager is not generic in the # visalib type so we cannot get it in a type safe way - known_sessions = getattr(self.resource_manager.visalib, "sessions", ()) + + known_sessions: tuple[int, ...] = getattr( + self.resource_manager.visalib, "sessions", () + ) try: this_session = self.resource_manager.session @@ -425,7 +506,7 @@ def snapshot_base( update=update, params_to_skip_update=params_to_skip_update ) - snap["address"] = self._address + snap["address"] = self.address snap["terminator"] = self.visa_handle.read_termination snap["read_terminator"] = self.visa_handle.read_termination snap["write_terminator"] = self.visa_handle.write_termination diff --git a/src/qcodes/instrument_drivers/Keysight/Infiniium.py b/src/qcodes/instrument_drivers/Keysight/Infiniium.py index 9c579b00d370..e411beed9cff 100644 --- a/src/qcodes/instrument_drivers/Keysight/Infiniium.py +++ b/src/qcodes/instrument_drivers/Keysight/Infiniium.py @@ -887,10 +887,7 @@ def __init__( # Check if we are using pyvisa-py as our visa lib and warn users that # this may cause long digitize operations to fail - if ( - self.visa_handle.visalib.library_path == "py" - and not silence_pyvisapy_warning - ): + if self.visabackend == "py" and not silence_pyvisapy_warning: self.log.warning( "Timeout not handled correctly in pyvisa_py. This may cause" " long acquisitions to fail. Either use ni/keysight visalib" diff --git a/src/qcodes/instrument_drivers/QDev/QDac_channels.py b/src/qcodes/instrument_drivers/QDev/QDac_channels.py index d7ec6e79e141..7d0a8fd0dd4f 100644 --- a/src/qcodes/instrument_drivers/QDev/QDac_channels.py +++ b/src/qcodes/instrument_drivers/QDev/QDac_channels.py @@ -762,7 +762,7 @@ def connect_message( """ self.visa_handle.write("status") - log.info(f"Connected to QDac on {self._address}, {self.visa_handle.read()}") + log.info(f"Connected to QDac on {self.address}, {self.visa_handle.read()}") # take care of the rest of the output for _ in range(self._output_n_lines): diff --git a/src/qcodes/instrument_drivers/tektronix/AWG5014.py b/src/qcodes/instrument_drivers/tektronix/AWG5014.py index 23cb84ed93dd..5907bff2730b 100644 --- a/src/qcodes/instrument_drivers/tektronix/AWG5014.py +++ b/src/qcodes/instrument_drivers/tektronix/AWG5014.py @@ -175,7 +175,6 @@ def __init__( """ super().__init__(name, address, **kwargs) - self._address = address self.num_channels = num_channels self._values: dict[str, dict[str, dict[str, npt.NDArray | float | None]]] = {} diff --git a/tests/drivers/test_CopperMountain_M5065.py b/tests/drivers/test_CopperMountain_M5065.py index 27cd161aba06..cbd6d3b24a9a 100644 --- a/tests/drivers/test_CopperMountain_M5065.py +++ b/tests/drivers/test_CopperMountain_M5065.py @@ -25,7 +25,7 @@ def vna(): def test_m5065_instantiation(vna): assert vna.name == "M5065" - assert vna._address == "TCPIP0::localhost::hislip0::INSTR" + assert vna.address == "TCPIP0::localhost::hislip0::INSTR" def test_idn_command(vna): diff --git a/tests/drivers/test_CopperMountain_M5180.py b/tests/drivers/test_CopperMountain_M5180.py index dc011e347e9e..84a55af31074 100644 --- a/tests/drivers/test_CopperMountain_M5180.py +++ b/tests/drivers/test_CopperMountain_M5180.py @@ -25,7 +25,7 @@ def vna(): def test_m5180_instantiation(vna): assert vna.name == "M5180" - assert vna._address == "TCPIP0::localhost::hislip0::INSTR" + assert vna.address == "TCPIP0::localhost::hislip0::INSTR" def test_idn_command(vna): diff --git a/tests/drivers/test_cryomagnetics_4g.py b/tests/drivers/test_cryomagnetics_4g.py index 0dbf37bb725f..ac9943cf6d48 100644 --- a/tests/drivers/test_cryomagnetics_4g.py +++ b/tests/drivers/test_cryomagnetics_4g.py @@ -32,8 +32,10 @@ def fixture_cryo_instrument() -> "Generator[CryomagneticsModel4G,None,None]": def test_initialization(cryo_instrument: CryomagneticsModel4G) -> None: assert cryo_instrument.name == "test_cryo_4g" - assert cryo_instrument._address == "GPIB::1::INSTR" - # assert cryo_instrument.terminator == "\n" + assert cryo_instrument.address == "GPIB0::1::INSTR" + # the address is normalized by pyvisa meaning that in this case an extra 0 is added after GPIB. + assert cryo_instrument.visa_handle.write_termination == "\n" + assert cryo_instrument.visa_handle.read_termination == "\n" def test_get_field(cryo_instrument: CryomagneticsModel4G) -> None: @@ -45,7 +47,8 @@ def test_get_field(cryo_instrument: CryomagneticsModel4G) -> None: def test_initialization_visa_sim(cryo_instrument: CryomagneticsModel4G) -> None: # Test to ensure correct initialization of the CryomagneticsModel4G instrument assert cryo_instrument.name == "test_cryo_4g" - assert cryo_instrument._address == "GPIB::1::INSTR" + assert cryo_instrument.address == "GPIB0::1::INSTR" + # the address is normalized by pyvisa meaning that in this case an extra 0 is added after GPIB. @pytest.mark.parametrize( diff --git a/tests/drivers/test_cryomagnetics_TM620.py b/tests/drivers/test_cryomagnetics_TM620.py index 79cbf404ec1b..294f69ba4568 100644 --- a/tests/drivers/test_cryomagnetics_TM620.py +++ b/tests/drivers/test_cryomagnetics_TM620.py @@ -1,18 +1,22 @@ +from typing import TYPE_CHECKING from unittest.mock import patch import pytest from qcodes.instrument_drivers.cryomagnetics import CryomagneticsModelTM620 +if TYPE_CHECKING: + from collections.abc import Generator + @pytest.fixture(name="tm620", scope="function") -def fixture_tm620(): +def fixture_tm620() -> "Generator[CryomagneticsModelTM620, None, None]": """ Fixture to create and yield a CryomagneticsModelTM620 object and close it after testing. """ instrument = CryomagneticsModelTM620( name="test_cryo_tm620", - address="GPIB::2::INSTR", + address="GPIB0::1::INSTR", terminator="\r\n", pyvisa_sim_file="cryo_tm620.yaml", ) @@ -22,7 +26,7 @@ def fixture_tm620(): def test_initialization(tm620): assert tm620.name == "test_cryo_tm620" - assert tm620._address == "GPIB::2::INSTR" + assert tm620.address == "GPIB0::1::INSTR" assert hasattr(tm620, "shield") assert hasattr(tm620, "magnet") diff --git a/tests/drivers/test_lakeshore_372.py b/tests/drivers/test_lakeshore_372.py index 4ebe96f40f49..e88dbbb95979 100644 --- a/tests/drivers/test_lakeshore_372.py +++ b/tests/drivers/test_lakeshore_372.py @@ -55,7 +55,7 @@ def __init__(self, *args, **kwargs) -> None: # cycle through all methods for func_name in func_names: with warnings.catch_warnings(): - if func_name == "_name": + if func_name in {"_name", "_address", "visalib"}: # silence warning when getting deprecated attribute warnings.simplefilter("ignore", category=QCoDeSDeprecationWarning) diff --git a/tests/test_visa.py b/tests/test_visa.py index 86e2869cecc3..f18ad4d08520 100644 --- a/tests/test_visa.py +++ b/tests/test_visa.py @@ -13,6 +13,7 @@ from qcodes.instrument import Instrument, VisaInstrument from qcodes.instrument_drivers.AimTTi import AimTTiPL601 from qcodes.instrument_drivers.american_magnetics import AMIModel430 +from qcodes.utils import QCoDeSDeprecationWarning from qcodes.validators import Numbers @@ -29,10 +30,14 @@ def __init__(self, *args, **kwargs): def _open_resource( self, address: str, visalib: str | None - ) -> tuple[pyvisa.resources.MessageBasedResource, str, pyvisa.ResourceManager]: + ) -> pyvisa.resources.MessageBasedResource: if visalib is None: visalib = "MockVisaLib" - return MockVisaHandle(), visalib, pyvisa.ResourceManager("@sim") + return MockVisaHandle() + + @property + def visabackend(self) -> str: + return "sim" class MockVisaHandle(pyvisa.resources.MessageBasedResource): @@ -47,6 +52,8 @@ class MockVisaHandle(pyvisa.resources.MessageBasedResource): - a state > 10 throws an error """ + resource_name: str = "MOCK::INSTR" # pyright: ignore[reportIncompatibleVariableOverride] + def __init__(self): self.state = 0 self.closed = False @@ -132,6 +139,7 @@ def use_magnet() -> pyvisa.ResourceManager: terminator="\n", ) assert list(Instrument._all_instruments.keys()) == ["x"] + assert x.resource_manager is not None assert len(x.resource_manager.list_opened_resources()) == 1 assert x.resource_manager.list_opened_resources() == [x.visa_handle] return x.resource_manager @@ -190,7 +198,11 @@ def test_visa_backend(mocker, request: FixtureRequest) -> None: address_opened = [None] class MockBackendVisaInstrument(VisaInstrument): - visa_handle = MockVisaHandle() + _visa_handle = MockVisaHandle() + + @property + def visabackend(self) -> str: + return "sim" class MockRM: def open_resource(self, address): @@ -249,8 +261,19 @@ def test_load_pyvisa_sim_file_implict_module(request: FixtureRequest) -> None: ) request.addfinalizer(driver.close) assert driver.visabackend == "sim" - assert driver.visalib is not None - path_str, backend = driver.visalib.split("@") + + assert Path(driver.visa_handle.visalib.library_path).match( + "qcodes/instrument/sims/AimTTi_PL601P.yaml" + ) + assert driver.visa_handle.visalib.__class__.__name__ == "SimVisaLibrary" + + # legacy + with pytest.warns( + QCoDeSDeprecationWarning, match="The visalib property is deprecated" + ): + visalib = driver.visalib # pyright: ignore[reportDeprecated] + assert visalib is not None + path_str, backend = visalib.split("@") assert backend == "sim" path = Path(path_str) assert path.match("qcodes/instrument/sims/AimTTi_PL601P.yaml") @@ -264,8 +287,17 @@ def test_load_pyvisa_sim_file_explicit_module(request: FixtureRequest) -> None: ) request.addfinalizer(driver.close) assert driver.visabackend == "sim" - assert driver.visalib is not None - path_str, backend = driver.visalib.split("@") + assert Path(driver.visa_handle.visalib.library_path).match( + "qcodes/instrument/sims/AimTTi_PL601P.yaml" + ) + assert driver.visa_handle.visalib.__class__.__name__ == "SimVisaLibrary" + + with pytest.warns( + QCoDeSDeprecationWarning, match="The visalib property is deprecated" + ): + visalib = driver.visalib # pyright: ignore[reportDeprecated] + assert visalib is not None + path_str, backend = visalib.split("@") assert backend == "sim" path = Path(path_str) assert path.match("qcodes/instrument/sims/AimTTi_PL601P.yaml") @@ -295,3 +327,52 @@ def test_load_pyvisa_sim_file_invalid_module_raises(request: FixtureRequest) -> address="GPIB::1::INSTR", pyvisa_sim_file="qcodes.instrument.not_a_module:AimTTi_PL601P.yaml", ) + + +def test_existing_resource(request: FixtureRequest) -> None: + handle = MockVisaHandle() + mv = MockVisa("from_resource", resource=handle) + request.addfinalizer(mv.close) + assert mv.visa_handle is handle + assert mv.address == "MOCK::INSTR" + mv.state.set(5) + assert mv.state.get() == 5 + + +def test_existing_resource_with_address_raises() -> None: + handle = MockVisaHandle() + with pytest.raises( + TypeError, + match=re.escape("'address' and 'resource' are mutually exclusive"), + ): + MockVisa("bad", address="some_address", resource=handle) + + +def test_existing_resource_with_visalib_raises() -> None: + handle = MockVisaHandle() + with pytest.raises( + TypeError, + match=re.escape( + "Cannot supply visalib or pyvisa_sim_file when using an existing resource" + ), + ): + MockVisa("bad", resource=handle, visalib="@py") + + +def test_existing_resource_with_pyvisa_sim_file_raises() -> None: + handle = MockVisaHandle() + with pytest.raises( + TypeError, + match=re.escape( + "Cannot supply visalib or pyvisa_sim_file when using an existing resource" + ), + ): + MockVisa("bad", resource=handle, pyvisa_sim_file="somefile.yaml") + + +def test_no_address_or_resource_raises() -> None: + with pytest.raises( + TypeError, + match=re.escape("Either 'address' or 'resource' must be provided"), + ): + MockVisa("bad")