Skip to content

Commit 692a2e6

Browse files
authored
Merge pull request #5341 from jenshnielsen/shutdown_visa
Use weakref finalize to close visa connections
2 parents 119caca + a033320 commit 692a2e6

File tree

5 files changed

+99
-13
lines changed

5 files changed

+99
-13
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
The `pyvisa.ResourceManager` of a VISA instrument is now exposed
2+
as `instr.resource_manager`. All VISA instruments will now use `weakref.finalize`
3+
to close the visa resource on shutdown of the instrument. This should reduce the
4+
chance that an instrument resource is not cleanup correctly on exit.

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,9 @@ markers = "serial"
243243
filterwarnings = [
244244
"ignore:The distutils package is deprecated and slated for removal in Python 3.12:DeprecationWarning",
245245
"ignore:distutils Version classes are deprecated:DeprecationWarning",
246-
"ignore:SelectableGroups dict interface is deprecated:DeprecationWarning"
246+
"ignore:SelectableGroups dict interface is deprecated:DeprecationWarning",
247+
'ignore:Deprecated call to `pkg_resources\.declare_namespace:DeprecationWarning',
248+
'ignore:pkg_resources is deprecated as an API',
247249
]
248250

249251
[tool.ruff]

qcodes/instrument/visa.py

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from collections.abc import Sequence
66
from importlib.resources import as_file, files
77
from typing import Any
8+
from weakref import finalize
89

910
import pyvisa
1011
import pyvisa.constants as vi_const
@@ -22,6 +23,16 @@
2223
log = logging.getLogger(__name__)
2324

2425

26+
def _close_visa_handle(
27+
handle: pyvisa.resources.MessageBasedResource, name: str
28+
) -> None:
29+
log.info(
30+
"Closing VISA handle to %s as there are no non weak "
31+
"references to the instrument.",
32+
name,
33+
)
34+
handle.close()
35+
2536
class VisaInstrument(Instrument):
2637

2738
"""
@@ -100,17 +111,26 @@ def __init__(
100111
f"file {pyvisa_sim_file} from module: {module}"
101112
)
102113
visalib = f"{str(sim_visalib_path)}@sim"
103-
visa_handle, visabackend = self._connect_and_handle_error(
104-
address, visalib
105-
)
114+
(
115+
visa_handle,
116+
visabackend,
117+
resource_manager,
118+
) = self._connect_and_handle_error(address, visalib)
106119
else:
107-
visa_handle, visabackend = self._connect_and_handle_error(address, visalib)
120+
visa_handle, visabackend, resource_manager = self._connect_and_handle_error(
121+
address, visalib
122+
)
123+
finalize(self, _close_visa_handle, visa_handle, str(self.name))
108124

109125
self.visabackend: str = visabackend
110126
self.visa_handle: pyvisa.resources.MessageBasedResource = visa_handle
111127
"""
112128
The VISA resource used by this instrument.
113129
"""
130+
self.resource_manager = resource_manager
131+
"""
132+
The VISA resource manager used by this instrument.
133+
"""
114134
self.visalib: str | None = visalib
115135
self._address = address
116136

@@ -122,18 +142,20 @@ def __init__(
122142

123143
def _connect_and_handle_error(
124144
self, address: str, visalib: str | None
125-
) -> tuple[pyvisa.resources.MessageBasedResource, str]:
145+
) -> tuple[pyvisa.resources.MessageBasedResource, str, pyvisa.ResourceManager]:
126146
try:
127-
visa_handle, visabackend = self._open_resource(address, visalib)
147+
visa_handle, visabackend, resource_manager = self._open_resource(
148+
address, visalib
149+
)
128150
except Exception as e:
129151
self.visa_log.exception(f"Could not connect at {address}")
130152
self.close()
131153
raise e
132-
return visa_handle, visabackend
154+
return visa_handle, visabackend, resource_manager
133155

134156
def _open_resource(
135157
self, address: str, visalib: str | None
136-
) -> tuple[pyvisa.resources.MessageBasedResource, str]:
158+
) -> tuple[pyvisa.resources.MessageBasedResource, str, pyvisa.ResourceManager]:
137159

138160
# in case we're changing the address - close the old handle first
139161
if getattr(self, "visa_handle", None):
@@ -156,7 +178,7 @@ def _open_resource(
156178
resource.close()
157179
raise TypeError("QCoDeS only support MessageBasedResource Visa resources")
158180

159-
return resource, visabackend
181+
return resource, visabackend, resource_manager
160182

161183
def set_address(self, address: str) -> None:
162184
"""
@@ -168,10 +190,13 @@ def set_address(self, address: str) -> None:
168190
change the backend for VISA, use the self.visalib attribute
169191
(and then call this function).
170192
"""
171-
resource, visabackend = self._open_resource(address, self.visalib)
193+
resource, visabackend, resource_manager = self._open_resource(
194+
address, self.visalib
195+
)
172196
self.visa_handle = resource
173197
self._address = address
174198
self.visabackend = visabackend
199+
self.resource_manager = resource_manager
175200

176201
def device_clear(self) -> None:
177202
"""Clear the buffers of the device"""
@@ -231,6 +256,13 @@ def close(self) -> None:
231256
"""Disconnect and irreversibly tear down the instrument."""
232257
if getattr(self, 'visa_handle', None):
233258
self.visa_handle.close()
259+
260+
if getattr(self, "visabackend", None) == "sim" and getattr(
261+
self, "resource_manager", None
262+
):
263+
# work around for https://github.com/QCoDeS/Qcodes/issues/5356 and
264+
# https://github.com/pyvisa/pyvisa-sim/issues/82
265+
self.resource_manager.close()
234266
super().close()
235267

236268
def write_raw(self, cmd: str) -> None:

qcodes/tests/test_instrument.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from __future__ import annotations
55

66
import contextlib
7+
import gc
78
import io
89
import re
910
import weakref
@@ -425,6 +426,19 @@ def test_instrument_label(cls, request: FixtureRequest) -> None:
425426
assert instrument.label == label
426427

427428

429+
def test_instrument_without_ref_is_gced():
430+
# When there are no active references to an instrument and it is
431+
# gced there should be no live references to the instrument
432+
433+
def use_some_instrument() -> None:
434+
_ = Instrument("SomeInstrument")
435+
assert list(Instrument._all_instruments.keys()) == ["SomeInstrument"]
436+
437+
assert len(Instrument._all_instruments) == 0
438+
use_some_instrument()
439+
gc.collect()
440+
assert len(Instrument._all_instruments) == 0
441+
428442
def test_snapshot_and_meta_attrs() -> None:
429443
"""Test snapshot of InstrumentBase contains _meta_attrs attributes"""
430444
instr = InstrumentBase("instr", label="Label")

qcodes/tests/test_visa.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import gc
2+
import logging
13
import re
24
from pathlib import Path
35

@@ -6,7 +8,8 @@
68
import pyvisa.constants
79
from pytest import FixtureRequest
810

9-
from qcodes.instrument.visa import VisaInstrument
11+
from qcodes.instrument import Instrument, VisaInstrument
12+
from qcodes.instrument_drivers.american_magnetics import AMIModel430
1013
from qcodes.validators import Numbers
1114

1215

@@ -19,7 +22,7 @@ def __init__(self, *args, **kwargs):
1922
vals=Numbers(-20, 20))
2023

2124
def _open_resource(self, address: str, visalib):
22-
return MockVisaHandle(), visalib
25+
return MockVisaHandle(), visalib, pyvisa.ResourceManager("@sim")
2326

2427

2528
class MockVisaHandle(pyvisa.resources.MessageBasedResource):
@@ -109,6 +112,37 @@ def _make_mock_visa():
109112
mv.close()
110113

111114

115+
def test_visa_gc_closes_connection(caplog) -> None:
116+
def use_magnet() -> pyvisa.ResourceManager:
117+
x = AMIModel430(
118+
"x",
119+
address="GPIB::1::INSTR",
120+
pyvisa_sim_file="AMI430.yaml",
121+
terminator="\n",
122+
)
123+
assert list(Instrument._all_instruments.keys()) == ["x"]
124+
assert len(x.resource_manager.list_opened_resources()) == 1
125+
assert x.resource_manager.list_opened_resources() == [x.visa_handle]
126+
return x.resource_manager
127+
128+
# ensure that any unused instruments that have not been gced are gced before running
129+
gc.collect()
130+
caplog.clear()
131+
with caplog.at_level(logging.INFO, logger="qcodes.instrument.visa"):
132+
rm = use_magnet()
133+
gc.collect()
134+
# at this stage the instrument created in use_magnet has gone out of scope
135+
# and we have triggered an explicit gc so the weakref.finalize function
136+
# has been triggered. We test this
137+
# and the instrument should no longer be in the instrument registry
138+
assert len(Instrument._all_instruments) == 0
139+
assert len(rm.list_opened_resources()) == 0
140+
assert (
141+
caplog.records[-1].message == "Closing VISA handle to x as there are no non "
142+
"weak references to the instrument."
143+
)
144+
145+
112146
def test_ask_write_local(mock_visa) -> None:
113147

114148
# test normal ask and write behavior

0 commit comments

Comments
 (0)