Repository navigation
Conversation
janiversen
left a comment
There was a problem hiding this comment.
The idea is quite ok, but the implementation have a side effect on other connected devices, which cannot be what you intended.
The PR also includes some general (but fundamental) changes, which for regression test reasons should be submitted as separate PRs.
Finally monkeypatch() is introduced, adding to the maintenance load, if really needed please explain why mock() is insufficient.
| from __future__ import annotations | ||
|
|
||
| import contextlib | ||
| import math |
There was a problem hiding this comment.
It seems very strange that the math is needed.
There was a problem hiding this comment.
Hi @janiversen thank you for reviewing the code.
For the moment, we need to check if the input is a finite number. We can strip out this check if you consider this level of input validation is out of scope. However, I would suggest the lower-bound check should remain.
| ) | ||
| if framer not in [FramerType.ASCII, FramerType.RTU]: | ||
| raise TypeError("Only FramerType RTU/ASCII allowed.") | ||
| if not math.isfinite(inter_request_delay) or inter_request_delay < 0: |
There was a problem hiding this comment.
inter_request_delay is defined as a float, so just check it is a float, which probably will cause problems with the type checker.
If it was defined as "float | None", this check would make sense.
"< 0" is comparing against an int not a float ?
There was a problem hiding this comment.
I will switch it out to float comparisons for consistency, no problem
There was a problem hiding this comment.
Both NaN and infinity are valid float types. So we have the finite check there for that.
| if framer not in [FramerType.ASCII, FramerType.RTU]: | ||
| raise TypeError("Only FramerType RTU/ASCII allowed.") | ||
| if not math.isfinite(inter_request_delay) or inter_request_delay < 0: | ||
| raise ValueError("inter_request_delay must be finite and non-negative") |
There was a problem hiding this comment.
No it must be a float (which includes int) according to the typing
| ) | ||
| if framer not in [FramerType.ASCII, FramerType.RTU]: | ||
| raise TypeError("Only RTU/ASCII allowed.") | ||
| if not math.isfinite(inter_request_delay) or inter_request_delay < 0: |
There was a problem hiding this comment.
inter_request_delay is defined as a float, so just check it is a float, which probably will cause problems with the type checker.
If it was defined as "float | None", this check would make sense.
"< 0" is comparing against an int not a float ?
| or self._last_transaction_end is None | ||
| ): | ||
| return 0.0 | ||
| return max( |
There was a problem hiding this comment.
It is a lot cheaper to calculate the difference (as you do) store it in a variable and add it the the if above)
| async def test_no_response_and_independent_clients(clock, monkeypatch, sync): | ||
| """No-response sends are paced without sharing deadlines between clients.""" | ||
| _, waits = clock | ||
| first, second = manager(sync, 0.02), manager(sync, 0.02) |
There was a problem hiding this comment.
this is not a bad test, now add a test with 1 serial device, with 2 devices connected.
| async def test_async_concurrent_callers(clock, monkeypatch): | ||
| """Queued callers cannot send during another caller's idle interval.""" | ||
| now, waits = clock | ||
| transact = manager(False, 0.02) |
There was a problem hiding this comment.
This test is wrong, different callers should be able to send to other devices.
| async def test_sync_concurrent_callers(clock, monkeypatch): | ||
| """Threads sharing a client retain spacing while queued on its lock.""" | ||
| now, waits = clock | ||
| transact = manager(True, 0.02) |
There was a problem hiding this comment.
sync pr definition cannot have concurrent callers !
| sleeping.set() | ||
| await asyncio.Future() | ||
|
|
||
| monkeypatch.setattr(transaction_module.asyncio, "sleep", sleep) |
There was a problem hiding this comment.
We do NOT, patch system libraries.
| monkeypatch.setattr(transaction_module.asyncio, "sleep", finish_sleep) | ||
| await execute(transact, False, True) | ||
| assert send.call_count == 2 | ||
| assert waits == pytest.approx([0.01]) |
There was a problem hiding this comment.
A general comment, all other tests uses mock(), not monkeypatch() so you need some convincing arguments why this is needed.
I am not against monkeypatch() but having 2 different mocks adds to the maintenance burden.
There was a problem hiding this comment.
Understood. I've made it similar to existing tests.
janiversen
left a comment
There was a problem hiding this comment.
Removed a non correct comment.
|
Tests are failing because I can migrate the rest of the code to |

Some serial devices respond successfully but become temporarily unresponsive to an immediate follow-up request. For example, writing a relay and immediately reading its state can cause intermittent timeouts, even when normal polling succeeds.
Add
inter_request_delaytoModbusSerialClientandAsyncModbusSerialClient. The value is in seconds and defaults to0.0, preserving existing timing.Here, readback waits until at least 20 ms after the write transaction finishes. The appropriate interval depends on the device.
Spacing is enforced under the transaction lock, including retries after timeouts. The first request is immediate, existing idle time counts toward the interval, and async waits yield to the event loop.
Tests cover both clients, default behavior, response timing, retries, failures, concurrent callers, cancellation, and parameter validation.