Add event handling for TCPIP instruments, allowing SRQ interrupts. Closes #544#577
Add event handling for TCPIP instruments, allowing SRQ interrupts. Closes #544#577SimonMerrett wants to merge 16 commits intopyvisa:mainfrom
Conversation
for more information, see https://pre-commit.ci
|
Thanks for the PR given the size of the changes ot will take me time to do a thorough review. Do not hesitate to ping me if I go silent for too long. |
|
BTW: I really like that SRQs support gets added. Was missing it! |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #577 +/- ##
==========================================
+ Coverage 30.72% 39.00% +8.27%
==========================================
Files 25 27 +2
Lines 4182 5053 +871
Branches 459 520 +61
==========================================
+ Hits 1285 1971 +686
- Misses 2882 3055 +173
- Partials 15 27 +12
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
MatthieuDartiailh
left a comment
There was a problem hiding this comment.
This is a first pass. I think one of the main thing that could be improved is the tracking of the type of events for which we handle events. Using an int flag internally could simplify many things I think.
| # vicp may not exist in older pyvisa versions | ||
| _vicp = getattr(constants.InterfaceType, "vicp", None) | ||
|
|
There was a problem hiding this comment.
We enforce a recent enough version of pyvisa (or we should I will have to double check). So this is not needed.
There was a problem hiding this comment.
This should be removed now.
| (constants.InterfaceType.vicp, "INSTR"), | ||
| *([(_vicp, "INSTR")] if _vicp is not None else []), |
| from .common import LOGGER | ||
|
|
||
|
|
||
| @dataclass |
There was a problem hiding this comment.
This should be marked as frozen since the intent is for instances to be immutable. Additionally we could have slot=True
There was a problem hiding this comment.
Done. Both frozen and slots.
| self._deque.extend(kept) | ||
|
|
||
|
|
||
| HandlerCallback = Callable[[Any, constants.EventType, int, Any], None] |
There was a problem hiding this comment.
A docstring explaining the meaning of each argument would be welcomed.
There was a problem hiding this comment.
Added docstring. Please confirm if formatting is acceptable or I need to look at this.
| def __init__(self) -> None: | ||
| self._lock = threading.RLock() | ||
| # event_type -> list of (handler, user_handle) | ||
| self._handlers: dict[ |
There was a problem hiding this comment.
Using a defaultdict here would simplify the code
| def __init__(self) -> None: | ||
| # {event_type: {mechanism_int}} | ||
| self._lock = threading.RLock() | ||
| self.enabled: dict[constants.EventType, set[int]] = {} |
There was a problem hiding this comment.
Using a Flag here rather than unnamed integer values in a set would be much more readable.
There was a problem hiding this comment.
flags implemented. Following pyvisa flag values.
|
|
||
| """ | ||
|
|
||
| from __future__ import annotations |
There was a problem hiding this comment.
If this does not bring anything I would rather not add it.
| sess = self.sessions[session] | ||
| except KeyError: | ||
| return self.handle_return_value(session, StatusCode.error_invalid_object) | ||
| if event_type not in sess._supported_event_types: |
There was a problem hiding this comment.
Some blank lines between logic block would improve the readability.
| if queue_enabled: | ||
| self._event_state.queue.put(ctx) | ||
| if handler_enabled: | ||
| session_handle = getattr(self, "_session_handle", self) |
There was a problem hiding this comment.
This looks a bit suspicious. What is the rational.
There was a problem hiding this comment.
I do not know but I asked for a review and this was the output
1. Design intent: why the getattr fallback exists
_session_handle is assigned only once in the entire codebase — in highlevel.py:201 immediately after an instrument session is created:
visa_session = self._register(sess)
sess._session_handle = visa_session
The getattr(self, "_session_handle", self) on sessions.py:387 is defensive coding: the author knew _session_handle is not part of the Session base class __init__ and added a fallback in case it was missing. However, the fallback self (the Python Session object) is never the correct value to pass to a VISA event handler.
2. The Resource Manager session
The RM session is created in highlevel.py:524 by registering the PyVisaLibrary object itself. _session_handle is never set on it.
Relevance: None. The RM session lacks _event_state, _supported_event_types, and any event monitor. If event methods were called on it they would AttributeError long before reaching _fire_event. _fire_event can only be invoked on instrument sessions, and those always receive _session_handle.
3. The test fixture
lib_and_session creates a MagicMock, manually attaches _event_state and _supported_event_types, registers it with the library, but forgets to set _session_handle. The existing test test_vxi11_fire_event_handler_mechanism passes only because its handler ignores the first argument, masking the fact that the mock object (not the integer handle) is being passed.
Verdict: This is a test bug, not a code bug. The fallback accommodates an incomplete test fixture.
4. VISA spec compliance
The handler contract (per the skill context and upstream pyvisa conventions) is:
handler(session: VISASession, event_type: EventType, event_ctx: int, user_handle: Any) -> None
VISASession is an integer handle. Passing self (a Python object) violates this contract.
---
Recommendation
✅ Recommended: Option A
Replace getattr(self, "_session_handle", self) with a direct attribute access:
session_handle = self._session_handle
Rationale:
- Every session that can legitimately fire events is an instrument session created through highlevel.open(), which unconditionally sets _session_handle.
- The fallback self is spec-violating and serves only to hide the incomplete test fixture.
- Direct attribute access makes the invariant obviously correct: "a session firing events must have a VISA handle".
- Option B documents incorrect behavior; Option C introduces unnecessary indirection for a problem that is really a missing test fixture line.
---
Required Test Changes
1. Fix lib_and_session fixture (test_events.py:~345)
Add one line so the mock session mirrors what real highlevel.open() does:
@pytest.fixture
def lib_and_session():
lib = PyVisaLibrary()
sess = MagicMock()
sess._event_state = EventState()
sess._supported_event_types = {constants.EventType.service_request}
sess._start_event_monitor.return_value = StatusCode.success
session_id = lib._register(sess)
sess._session_handle = session_id # <-- ADD THIS
return lib, sess, session_id
2. Strengthen test_vxi11_fire_event_handler_mechanism (test_events.py:~695)
Update the handler to capture the session argument and assert it is the integer handle (sid), not the mock object:
def test_vxi11_fire_event_handler_mechanism(self, lib_and_session):
lib, sess, sid = lib_and_session
calls = []
def my_handler(session, event_type, context_id, user_handle):
calls.append((session, event_type, context_id, user_handle))
# ... install and enable as before ...
Session._fire_event(sess, constants.EventType.service_request, ctx)
assert calls == [(sid, constants.EventType.service_request, 5555, "uh")]
No other tests require modification because they either use the queue mechanism (which does not touch _session_handle) or test EventState/highlevel directly without calling Session._fire_event.
So I implemented the recommendation. I will need more guidance if this is leading in the wrong direction.
| session_handle = getattr(self, "_session_handle", self) | ||
| self._event_state.registry.fire(event_type, session_handle, ctx.context_id) | ||
|
|
||
| def _start_srq_monitor(self) -> StatusCode: |
There was a problem hiding this comment.
The name seems a bit narrow since other types of event can be monitored
There was a problem hiding this comment.
Generalised the names to _start_event_monitor / _stop_event_monitor
…etween logic sections
…e access: session_handle = self._session_handle Rationale: - Every session that can legitimately fire events is an instrument session created through highlevel.open(), which unconditionally sets _session_handle. - The fallback self is spec-violating and serves only to hide the incomplete test fixture. - Direct attribute access makes the invariant obviously correct: 'a session firing events must have a VISA handle'.
for more information, see https://pre-commit.ci
|
@MatthieuDartiailh thanks for your review comments. I think I have addressed everything, although I cannot be confident I haven't introduced other mistakes. All pytest tests are passing but I haven't confirmed function on my test instrument yet. Please continue your review and I will confirm functionality asap. |
…'_session_handle' [attr-defined]
bring local in line with remote origin.
|
pyvisa-py_srq-test_script_log.txt |
My second attempt at providing the event handling I would like to see for instruments capable of sending SRQ service requests. @MatthieuDartiailh helpfully advised on the approach and reviewed my previous PR but I did not and do not have enough Python skill to take that review into action. I have done my best in this PR to provide the feature but actually was able to test it on hardware which took 5 measurements at 1Hz and returned SRQ. I have attached Wireshark pcap showing the communication of a successful test (measurement results are not retrieved during the test).
pyvisa-py_srq.zip
I used an agentic workflow in opencode to plan, code, test, review. Some of the wider-reaching changes were bound up in reviewer recommendations for robustness, such as compatibility "tweaks" in attributes.py. Everything is up for editing but with this PR I am happy that it should pass some degree of testing (the agents were testing against the built-in tests and added their own for the event aspects). I am also more confident because I was able to take my script for my instrument and test the functionality was there, that measurements were taken, SRQ was sent properly as far as I can tell (not a hidden python thread polling the instrument status registers, like one of the earlier agents tried to sneak in).
Please go easy on the feedback and feel free to edit! If you agree that this is closer to the mark than #547 I will close that PR.
black . && isort -c . && flake8with no errors