pyModbusTCP 0.4.0a1__tar.gz → 0.4.0a2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. pymodbustcp-0.4.0a2/.gitignore +26 -0
  2. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/CHANGES +73 -8
  3. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/PKG-INFO +10 -10
  4. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/README.md +7 -7
  5. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/client/_errors.py +1 -2
  6. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/client/_functions.py +33 -18
  7. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/client/_transport.py +10 -11
  8. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/client/_validators.py +5 -6
  9. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/client/device_id.py +8 -9
  10. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/client/modbus_client.py +24 -11
  11. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/constants.py +1 -1
  12. pymodbustcp-0.4.0a2/pyModbusTCP/server/_common.py +74 -0
  13. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/server/_functions.py +12 -11
  14. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/server/_transport.py +85 -57
  15. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/server/_validators.py +31 -15
  16. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/server/data_bank.py +16 -19
  17. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/server/data_handler.py +6 -6
  18. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/server/device_id.py +8 -9
  19. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/server/errors.py +0 -4
  20. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/server/frames.py +5 -5
  21. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/server/modbus_server.py +57 -29
  22. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/server/session.py +1 -1
  23. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/tools/__init__.py +1 -1
  24. pymodbustcp-0.4.0a2/pyModbusTCP/tools/_common.py +125 -0
  25. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/tools/_entry.py +9 -8
  26. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/tools/client.py +30 -22
  27. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/tools/rtu_to_tcp_gw.py +38 -27
  28. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/tools/server.py +10 -6
  29. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/tools/tcp_to_rtu_gw.py +204 -60
  30. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/utils.py +29 -45
  31. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyproject.toml +9 -4
  32. pymodbustcp-0.4.0a2/tests/test_client.py +468 -0
  33. pymodbustcp-0.4.0a2/tests/test_client_server.py +382 -0
  34. pymodbustcp-0.4.0a2/tests/test_server.py +1234 -0
  35. pymodbustcp-0.4.0a2/tests/test_tools.py +612 -0
  36. pymodbustcp-0.4.0a2/tests/test_utils.py +291 -0
  37. pymodbustcp-0.4.0a1/.gitignore +0 -14
  38. pymodbustcp-0.4.0a1/pyModbusTCP/server/_common.py +0 -13
  39. pymodbustcp-0.4.0a1/pyModbusTCP/tools/_common.py +0 -69
  40. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/LICENSE +0 -0
  41. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/__init__.py +0 -0
  42. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/client/__init__.py +0 -0
  43. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/client/_common.py +0 -0
  44. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/py.typed +0 -0
  45. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/server/__init__.py +0 -0
  46. {pymodbustcp-0.4.0a1 → pymodbustcp-0.4.0a2}/pyModbusTCP/tools/__main__.py +0 -0
@@ -0,0 +1,26 @@
1
+ # Also read by hatchling (the build backend): these files never go in the sdist or the wheel.
2
+
3
+ # bytecode
4
+ __pycache__/
5
+ *.py[cod]
6
+
7
+ # build outputs
8
+ build/
9
+ dist/
10
+ *.egg-info/
11
+
12
+ # virtual environments
13
+ /venv/
14
+ /.venv/
15
+
16
+ # editors and OS
17
+ /.idea/
18
+ /.vscode/
19
+ .DS_Store
20
+
21
+ # local experiments
22
+ /lab/
23
+
24
+ # Sphinx documentation (built pages and generated help of the tools)
25
+ /docs/_build/
26
+ /docs/_generated/
@@ -1,11 +1,76 @@
1
1
  Revision history for pyModbusTCP
2
2
 
3
- 0.4.0a1 xxxx-xx-xx (alpha: the command line tools, their options and output, may still change before 0.4.0)
3
+ 0.4.0a2 2026-10-17 (alpha: the command line tools, their options and output, may still change before 0.4.0)
4
+
5
+ Changes since 0.4.0a1 (see 0.4.0a1 below for the changes since 0.3.1).
6
+
7
+ Breaking changes:
8
+
9
+ - extras: "pyModbusTCP[tools]" is renamed "pyModbusTCP[cli]" (the mbtcp-* commands of pyModbusTCP-cli).
10
+ - ModbusServer: max_connections_per_host (16) is active by default: a client host (an IPv4 address, or a /64
11
+ network for IPv6) can't open more connections, the next ones are closed at once. Clients
12
+ behind a NAT share the same address: raise it, or set it to None to get the previous behavior.
13
+ - ModbusServer: the constructor checks host (a str) and port (an int from 0 to 65535): TypeError or ValueError,
14
+ a bad value was only seen by start().
15
+ - ModbusClient, DataBank: numpy booleans (numpy.bool_, like the items of a numpy bool array) are no longer
16
+ accepted as coil values (TypeError, like NumPy 2 that refuses them as integers): convert them
17
+ with bool() or tolist(). Numpy integers are still accepted.
18
+ - tools: mbtcp-client exits with status 130 when Ctrl-C interrupts a request (it was 0, a success). Ctrl-C is
19
+ still the normal end of a read repeated with -i (status 0).
20
+ - tools: the delays given on the command line (-t, -e, -i, --idle-timeout) must be greater than 0 and at most
21
+ 86400 s (one day), the serial rate (-b) greater than 0, and the slave address of mbtcp-rtu2tcp (-a)
22
+ from 1 to 247: invalid values are refused at startup (status 2).
23
+
24
+ Security:
25
+
26
+ - fix tools: mbtcp-tcp2rtu serves the waiting requests in turn by client host (round robin), instead of one
27
+ FIFO of 5 requests: 3 hosts (with 2 pending requests each) could fill it, every request of the
28
+ other hosts got exception 0x0A (denial of service). A request now waits for at most one request of
29
+ each other host. Up to 16 requests can wait for the serial line (the limit of 2 per host is kept).
30
+ - fix tools: mbtcp-tcp2rtu wrote the requests with an invalid function code (0, or 0x80 to 0xFF: the exception
31
+ flag of a reply) on the serial line (out of --read-only mode): they get exception 0x01 now.
32
+ - fix tools: mbtcp-tcp2rtu no longer sends on the serial line a request that its client session doesn't wait for
33
+ anymore (gateway timeout).
34
+ - fix ModbusServer: request_timeout is a limit for the whole request (MBAP header and PDU) from its first byte, as
35
+ documented: the delay restarted for the PDU, a client could hold a session twice as long.
36
+ - fix ModbusServer: on Windows, the listening port is reserved with SO_EXCLUSIVEADDRUSE instead of SO_REUSEADDR,
37
+ which allowed another local socket to bind the same port and take the connections of the
38
+ server (port hijacking). No change on the other systems.
39
+ - improve ModbusServer: add max_connections_per_host (default 16), a single host can no longer take every
40
+ connection of max_connections (denial of service).
41
+
42
+ Changes:
43
+
44
+ - tools: mbtcp-tcp2rtu limits the pending requests of a client host (at most 2, the next ones get exception
45
+ 0x0A). Its refused requests are logged once per 10 s at most (with their count) instead of one line per
46
+ request. New options --max-connections and --max-connections-per-host for mbtcp-tcp2rtu,
47
+ --max-connections-per-host for mbtcp-server.
48
+ - tools: mbtcp-tcp2rtu and mbtcp-rtu2tcp stop cleanly on SIGTERM (systemd, docker stop...), like mbtcp-server.
49
+ - fix tools: -b 0 made mbtcp-tcp2rtu and mbtcp-rtu2tcp crash (ZeroDivisionError), a negative or "nan" timeout
50
+ of mbtcp-tcp2rtu made every request fail, "-a 0" made mbtcp-rtu2tcp a silent slave, "-i inf" made
51
+ mbtcp-client crash (OverflowError).
52
+ - fix utils: valid_host() accepted some non-ASCII chars (like the Kelvin sign "\u212a", taken for a "k" by a
53
+ case-insensitive match), the resolver could then raise a UnicodeError at connect time.
54
+ - fix ModbusClient: a host name that the resolver can't encode (UnicodeError) is a connect error
55
+ (MB_CONNECT_ERR), the request raised the exception.
56
+ - fix ModbusServer: start() no longer leaves the listening socket open when bind() raises something else than
57
+ an OSError (like an OverflowError for a port changed to 70000 after the constructor).
58
+ - fix ModbusServer: a data handler that returns a value that can't be encoded in the response (like a register
59
+ value out of 0-65535, or an exception code over 255) gets a SLAVE_DEVICE_FAILURE (0x04)
60
+ exception, the session was closed.
61
+ - typing: type hints use the Python 3.10 syntax (X | None, list[int]...).
62
+ - build: tools/build_dist.sh -t (install test) ran "python -m pyModbusTCP.cli" (no such module) instead of
63
+ pyModbusTCP.tools, it also checks the release date of the CHANGES entry and the pre-release pins of the docs.
64
+ - build: tools/code_audit.sh runs the tools of the venv (python -m, never from the PATH), from any directory,
65
+ with a summary and an exit status (1 if a check fails), new options -a (with the tests), -s and -o.
66
+ - tests: type hints on the tests, "mypy --strict tests" passes.
67
+
68
+ 0.4.0a1 2026-10-15 (alpha: the command line tools, their options and output, may still change before 0.4.0)
4
69
 
5
70
  Breaking changes:
6
71
 
7
72
  - DataBank: remove deprecated class methods (get_bits, set_bits, get_words and set_words).
8
- - DataBank: set_*() methods raise a TypeError on non-integer items and a ValueError on registers values out of
73
+ - DataBank: set_*() methods raise a TypeError on non-integer items and a ValueError on register values out of
9
74
  the 0-65535 range (values were silently converted before, see below).
10
75
  - ModbusClient: non-integer arguments raise a TypeError, custom_request() checks its pdu argument
11
76
  (TypeError/ValueError) and auto_open, auto_close and no_delay only accept bool or int values.
@@ -33,20 +98,20 @@ Revision history for pyModbusTCP
33
98
  functions, TCP layer, argument checks...) with the same layout for both. No change for the users: same
34
99
  imports, same logger names, nested names kept (ModbusServer.PDU, ModbusServer.NetworkError...). Exceptions
35
100
  of the server are now shown as pyModbusTCP.server.errors.NetworkError (and so on) in tracebacks.
36
- - new module pyModbusTCP.tools: command line tools (Modbus/TCP server and client, TCP to RTU and RTU to TCP
101
+ - new package pyModbusTCP.tools: command line tools (Modbus/TCP server and client, TCP to RTU and RTU to TCP
37
102
  gateways), run them with "python -m pyModbusTCP.tools {server,client,tcp2rtu,rtu2tcp}". The gateways need
38
103
  pyserial, a new optional dependency: "pip install pyModbusTCP[serial]".
39
104
  - new package pyModbusTCP-cli (directory of the same name, no code): installs the tools as the commands
40
105
  mbtcp-server, mbtcp-client, mbtcp-tcp2rtu and mbtcp-rtu2tcp, with pyserial. Install it with
41
106
  "pip install pyModbusTCP[tools]" or "pipx install pyModbusTCP-cli". A plain "pip install pyModbusTCP" puts
42
107
  no command in the PATH and still has no dependency.
43
- - build: migrate from setup.py to pyproject.toml, with the hatchling build backend (no more MANIFEST.in, tests are
44
- no longer in the sdist).
108
+ - build: migrate from setup.py to pyproject.toml, with the hatchling build backend (no MANIFEST.in anymore, tests
109
+ are no longer in the sdist).
45
110
  - fix ModbusClient: is_open is now False only for a closed socket (file descriptor 0 is valid).
46
111
  - fix ModbusClient: non-integer arguments (float, str...) now raise a TypeError instead of an uncaught struct.error.
47
112
  - fix ModbusClient: treat socket.gaierror as MB_CONNECT_ERR (like any other connect error).
48
- - fix ModbusClient: timeout is now a limit for the whole request (send + receive) and no more for each socket I/O. A
49
- server sending its response byte by byte could hold the client much longer than timeout.
113
+ - fix ModbusClient: timeout is now a limit for the whole request (send + receive) and no longer for each socket I/O.
114
+ A server sending its response byte by byte could hold the client much longer than timeout.
50
115
  - fix ModbusClient: validate write_multiple_coils items to reject non-integer values (like 'a', None, [], 1.5),
51
116
  matching write_single_coil behavior.
52
117
  - fix ModbusClient: write_multiple_coils() and write_multiple_registers() now set last_error (MB_RECV_ERR) when the
@@ -71,7 +136,7 @@ Revision history for pyModbusTCP
71
136
  - fix ModbusServer: for functions 0x0F, 0x10 and 0x17 the byte count must exactly match the quantity.
72
137
  - fix ModbusServer: Read Device Identification (0x2B/0x0E) compliance with the spec (stream access restarts at the
73
138
  first object of the category, an unknown object in individual access gets exception 0x02).
74
- - fix ModbusServer: support MBAP header length from 2 to 253.
139
+ - fix ModbusServer: support MBAP header length from 2 to 254.
75
140
  - fix ModbusServer: limit client info to (address, port) for IPv6 peers.
76
141
  - fix DataBank: set_coils(), set_discrete_inputs(), set_holding_registers() and set_input_registers() now check
77
142
  their input. Non-integer items (float, str, None...) raise a TypeError (numpy integers and
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pyModbusTCP
3
- Version: 0.4.0a1
3
+ Version: 0.4.0a2
4
4
  Summary: A simple Modbus/TCP library for Python
5
5
  Project-URL: Documentation, https://pymodbustcp.readthedocs.io/
6
6
  Project-URL: Source, https://github.com/sourceperl/pyModbusTCP
@@ -24,13 +24,13 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
24
  Classifier: Topic :: System :: Networking
25
25
  Classifier: Typing :: Typed
26
26
  Requires-Python: >=3.10
27
+ Provides-Extra: cli
28
+ Requires-Dist: pymodbustcp-cli; extra == 'cli'
27
29
  Provides-Extra: serial
28
30
  Requires-Dist: pyserial<4,>=3.5; extra == 'serial'
29
- Provides-Extra: tools
30
- Requires-Dist: pymodbustcp-cli; extra == 'tools'
31
31
  Description-Content-Type: text/markdown
32
32
 
33
- # pyModbusTCP [![Tests](https://github.com/sourceperl/pyModbusTCP/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/sourceperl/pyModbusTCP/actions/workflows/tests.yml) [![Documentation](https://readthedocs.org/projects/pymodbustcp/badge/?version=latest)](https://pymodbustcp.readthedocs.io/)
33
+ # pyModbusTCP [![Tests](https://github.com/sourceperl/pyModbusTCP/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/sourceperl/pyModbusTCP/actions/workflows/tests.yml)
34
34
 
35
35
  A simple Modbus/TCP client and server library for Python.
36
36
 
@@ -42,7 +42,7 @@ The module is currently tested on Python 3.10, 3.11, 3.12, 3.13 and 3.14, on Lin
42
42
 
43
43
  ## Documentation
44
44
 
45
- Documentation of the last release is available online at https://pymodbustcp.readthedocs.io/.
45
+ Documentation of the latest release is available online at https://pymodbustcp.readthedocs.io/.
46
46
 
47
47
  ## Security
48
48
 
@@ -63,7 +63,7 @@ source venv/bin/activate
63
63
  From PyPI, the easy way:
64
64
 
65
65
  ```bash
66
- # install the last available release (stable)
66
+ # install the latest available release (stable)
67
67
  pip install pyModbusTCP
68
68
  ```
69
69
 
@@ -77,7 +77,7 @@ options and their output may still change. pip never installs a pre-release by d
77
77
 
78
78
  ```bash
79
79
  # a specific pre-release
80
- pip install pyModbusTCP==0.4.0a1
80
+ pip install pyModbusTCP==0.4.0a2
81
81
  # or the latest release, pre-releases included
82
82
  pip install --pre pyModbusTCP
83
83
  ```
@@ -165,8 +165,8 @@ server.start()
165
165
 
166
166
  ## Command line tools
167
167
 
168
- ***The tools are new in 0.4.0 (only pre-releases for now): without a version or `--pre`, `pip install "pyModbusTCP"`
169
- installs the stable 0.3.1, without the tools (pip only shows a warning).***
168
+ ***The tools are new in 0.4.0 (only pre-releases for now): without a version or `--pre`,
169
+ `pip install "pyModbusTCP[cli]"` installs the stable 0.3.1, without the tools (pip only shows a warning).***
170
170
 
171
171
  The library comes with command line tools: a Modbus/TCP server, a Modbus/TCP client, and two gateways between
172
172
  Modbus/TCP and Modbus/RTU (serial line). They run from the library itself:
@@ -186,7 +186,7 @@ install the optional package pyModbusTCP-cli. A plain `pip install pyModbusTCP`
186
186
 
187
187
  ```bash
188
188
  # in a virtual environment, with the library
189
- pip install "pyModbusTCP[tools]==0.4.0a1"
189
+ pip install "pyModbusTCP[cli]==0.4.0a2"
190
190
  # or as isolated command line tools
191
191
  pipx install pyModbusTCP-cli
192
192
  ```
@@ -1,4 +1,4 @@
1
- # pyModbusTCP [![Tests](https://github.com/sourceperl/pyModbusTCP/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/sourceperl/pyModbusTCP/actions/workflows/tests.yml) [![Documentation](https://readthedocs.org/projects/pymodbustcp/badge/?version=latest)](https://pymodbustcp.readthedocs.io/)
1
+ # pyModbusTCP [![Tests](https://github.com/sourceperl/pyModbusTCP/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/sourceperl/pyModbusTCP/actions/workflows/tests.yml)
2
2
 
3
3
  A simple Modbus/TCP client and server library for Python.
4
4
 
@@ -10,7 +10,7 @@ The module is currently tested on Python 3.10, 3.11, 3.12, 3.13 and 3.14, on Lin
10
10
 
11
11
  ## Documentation
12
12
 
13
- Documentation of the last release is available online at https://pymodbustcp.readthedocs.io/.
13
+ Documentation of the latest release is available online at https://pymodbustcp.readthedocs.io/.
14
14
 
15
15
  ## Security
16
16
 
@@ -31,7 +31,7 @@ source venv/bin/activate
31
31
  From PyPI, the easy way:
32
32
 
33
33
  ```bash
34
- # install the last available release (stable)
34
+ # install the latest available release (stable)
35
35
  pip install pyModbusTCP
36
36
  ```
37
37
 
@@ -45,7 +45,7 @@ options and their output may still change. pip never installs a pre-release by d
45
45
 
46
46
  ```bash
47
47
  # a specific pre-release
48
- pip install pyModbusTCP==0.4.0a1
48
+ pip install pyModbusTCP==0.4.0a2
49
49
  # or the latest release, pre-releases included
50
50
  pip install --pre pyModbusTCP
51
51
  ```
@@ -133,8 +133,8 @@ server.start()
133
133
 
134
134
  ## Command line tools
135
135
 
136
- ***The tools are new in 0.4.0 (only pre-releases for now): without a version or `--pre`, `pip install "pyModbusTCP"`
137
- installs the stable 0.3.1, without the tools (pip only shows a warning).***
136
+ ***The tools are new in 0.4.0 (only pre-releases for now): without a version or `--pre`,
137
+ `pip install "pyModbusTCP[cli]"` installs the stable 0.3.1, without the tools (pip only shows a warning).***
138
138
 
139
139
  The library comes with command line tools: a Modbus/TCP server, a Modbus/TCP client, and two gateways between
140
140
  Modbus/TCP and Modbus/RTU (serial line). They run from the library itself:
@@ -154,7 +154,7 @@ install the optional package pyModbusTCP-cli. A plain `pip install pyModbusTCP`
154
154
 
155
155
  ```bash
156
156
  # in a virtual environment, with the library
157
- pip install "pyModbusTCP[tools]==0.4.0a1"
157
+ pip install "pyModbusTCP[cli]==0.4.0a2"
158
158
  # or as isolated command line tools
159
159
  pipx install pyModbusTCP-cli
160
160
  ```
@@ -3,7 +3,6 @@
3
3
 
4
4
  class InternalError(Exception):
5
5
  """ Base of the internal errors of a request (turned into last_error by the client). """
6
- pass
7
6
 
8
7
 
9
8
  class NetworkError(InternalError):
@@ -19,5 +18,5 @@ class ModbusExcept(InternalError):
19
18
  """ Modbus exception returned by the server: code is an EXP_xxx constant. """
20
19
 
21
20
  def __init__(self, code: int):
22
- super().__init__('modbus exception 0x%02X' % code)
21
+ super().__init__(f'modbus exception 0x{code:02X}')
23
22
  self.code = code
@@ -1,7 +1,8 @@
1
1
  """ pyModbusTCP client: the Modbus functions (build the request PDU, decode the reply PDU). """
2
2
 
3
3
  import struct
4
- from typing import TYPE_CHECKING, Iterable, List, Optional, Union
4
+ from collections.abc import Iterable
5
+ from typing import TYPE_CHECKING
5
6
 
6
7
  from ..constants import (
7
8
  ENCAPSULATED_INTERFACE_TRANSPORT,
@@ -18,16 +19,30 @@ from ..constants import (
18
19
  WRITE_SINGLE_COIL,
19
20
  WRITE_SINGLE_REGISTER,
20
21
  )
21
- from ..utils import _pack_bits, _to_bool, _to_bool_list, _to_int, _to_int_list, _unpack_bits, byte_length
22
- from ._common import MAX_READ_BITS, MAX_READ_REGS, MAX_WRITE_BITS, MAX_WRITE_READ_REGS, MAX_WRITE_REGS
22
+ from ..utils import (
23
+ _pack_bits,
24
+ _to_bool,
25
+ _to_bool_list,
26
+ _to_int,
27
+ _to_int_list,
28
+ _unpack_bits,
29
+ byte_length,
30
+ )
31
+ from ._common import (
32
+ MAX_READ_BITS,
33
+ MAX_READ_REGS,
34
+ MAX_WRITE_BITS,
35
+ MAX_WRITE_READ_REGS,
36
+ MAX_WRITE_REGS,
37
+ )
23
38
  from ._errors import InternalError, NetworkError
24
39
  from ._validators import check_addr_nb, check_regs_values
25
40
  from .device_id import DeviceIdentificationResponse
26
41
 
27
42
 
28
- def _decode_regs(data: bytes, reg_nb: int) -> List[int]:
43
+ def _decode_regs(data: bytes, reg_nb: int) -> list[int]:
29
44
  """Decode the first reg_nb big-endian 16 bits registers of a modbus registers field."""
30
- return list(struct.unpack('>%dH' % reg_nb, data[:2 * reg_nb]))
45
+ return list(struct.unpack(f'>{reg_nb}H', data[:2 * reg_nb]))
31
46
 
32
47
 
33
48
  class ClientFunctionsMixin:
@@ -42,7 +57,7 @@ class ClientFunctionsMixin:
42
57
  def _req_pdu(self, tx_pdu: bytes, rx_min_len: int = 2) -> bytes: ...
43
58
  def _req_except_handler(self, _except: Exception) -> None: ...
44
59
 
45
- def custom_request(self, pdu: Union[bytes, bytearray]) -> Optional[bytes]:
60
+ def custom_request(self, pdu: bytes | bytearray) -> bytes | None:
46
61
  """Send a custom modbus request.
47
62
 
48
63
  The reply must have the same function code (or the same with the exception flag): otherwise it is an error.
@@ -59,7 +74,7 @@ class ClientFunctionsMixin:
59
74
  raise TypeError('pdu must be a bytes-like object')
60
75
  pdu = bytes(pdu)
61
76
  if not 1 <= len(pdu) <= MAX_PDU_SIZE:
62
- raise ValueError('pdu length out of range (valid from 1 to %d bytes)' % MAX_PDU_SIZE)
77
+ raise ValueError(f'pdu length out of range (valid from 1 to {MAX_PDU_SIZE} bytes)')
63
78
  # make request
64
79
  try:
65
80
  return self._req_pdu(pdu)
@@ -68,7 +83,7 @@ class ClientFunctionsMixin:
68
83
  self._req_except_handler(e)
69
84
  return None
70
85
 
71
- def _read_bits_req(self, func_code: int, bit_addr: int, bit_nb: int) -> Optional[List[bool]]:
86
+ def _read_bits_req(self, func_code: int, bit_addr: int, bit_nb: int) -> list[bool] | None:
72
87
  """Common part of READ_COILS and READ_DISCRETE_INPUTS (params are already checked)."""
73
88
  try:
74
89
  tx_pdu = struct.pack('>BHH', func_code, bit_addr, bit_nb)
@@ -87,7 +102,7 @@ class ClientFunctionsMixin:
87
102
  self._req_except_handler(e)
88
103
  return None
89
104
 
90
- def _read_regs_req(self, func_code: int, reg_addr: int, reg_nb: int) -> Optional[List[int]]:
105
+ def _read_regs_req(self, func_code: int, reg_addr: int, reg_nb: int) -> list[int] | None:
91
106
  """Common part of READ_HOLDING_REGISTERS and READ_INPUT_REGISTERS (params are already checked)."""
92
107
  try:
93
108
  tx_pdu = struct.pack('>BHH', func_code, reg_addr, reg_nb)
@@ -99,7 +114,7 @@ class ClientFunctionsMixin:
99
114
  return None
100
115
 
101
116
  @staticmethod
102
- def _decode_regs_reply(rx_pdu: bytes, reg_nb: int) -> List[int]:
117
+ def _decode_regs_reply(rx_pdu: bytes, reg_nb: int) -> list[int]:
103
118
  """Decode a [func code, byte count, registers...] reply PDU (functions 0x03, 0x04 and 0x17)."""
104
119
  # extract field "byte count"
105
120
  byte_count = rx_pdu[1]
@@ -124,7 +139,7 @@ class ClientFunctionsMixin:
124
139
  self._req_except_handler(e)
125
140
  return False
126
141
 
127
- def read_coils(self, bit_addr: int, bit_nb: int = 1) -> Optional[List[bool]]:
142
+ def read_coils(self, bit_addr: int, bit_nb: int = 1) -> list[bool] | None:
128
143
  """Modbus function READ_COILS (0x01).
129
144
 
130
145
  :param bit_addr: bit address (0 to 65535)
@@ -141,7 +156,7 @@ class ClientFunctionsMixin:
141
156
  # make request
142
157
  return self._read_bits_req(READ_COILS, bit_addr, bit_nb)
143
158
 
144
- def read_discrete_inputs(self, bit_addr: int, bit_nb: int = 1) -> Optional[List[bool]]:
159
+ def read_discrete_inputs(self, bit_addr: int, bit_nb: int = 1) -> list[bool] | None:
145
160
  """Modbus function READ_DISCRETE_INPUTS (0x02).
146
161
 
147
162
  :param bit_addr: bit address (0 to 65535)
@@ -158,7 +173,7 @@ class ClientFunctionsMixin:
158
173
  # make request
159
174
  return self._read_bits_req(READ_DISCRETE_INPUTS, bit_addr, bit_nb)
160
175
 
161
- def read_holding_registers(self, reg_addr: int, reg_nb: int = 1) -> Optional[List[int]]:
176
+ def read_holding_registers(self, reg_addr: int, reg_nb: int = 1) -> list[int] | None:
162
177
  """Modbus function READ_HOLDING_REGISTERS (0x03).
163
178
 
164
179
  :param reg_addr: register address (0 to 65535)
@@ -175,7 +190,7 @@ class ClientFunctionsMixin:
175
190
  # make request
176
191
  return self._read_regs_req(READ_HOLDING_REGISTERS, reg_addr, reg_nb)
177
192
 
178
- def read_input_registers(self, reg_addr: int, reg_nb: int = 1) -> Optional[List[int]]:
193
+ def read_input_registers(self, reg_addr: int, reg_nb: int = 1) -> list[int] | None:
179
194
  """Modbus function READ_INPUT_REGISTERS (0x04).
180
195
 
181
196
  :param reg_addr: register address (0 to 65535)
@@ -193,7 +208,7 @@ class ClientFunctionsMixin:
193
208
  return self._read_regs_req(READ_INPUT_REGISTERS, reg_addr, reg_nb)
194
209
 
195
210
  def read_device_identification(self, read_code: int = 1,
196
- object_id: int = 0) -> Optional[DeviceIdentificationResponse]:
211
+ object_id: int = 0) -> DeviceIdentificationResponse | None:
197
212
  """Modbus function Read Device Identification (0x2B/0x0E).
198
213
 
199
214
  :param read_code: read device id code, 1 to 3 for respectively: basic, regular and extended stream access,
@@ -326,14 +341,14 @@ class ClientFunctionsMixin:
326
341
  check_addr_nb(regs_addr, len(regs_value), MAX_WRITE_REGS, 'regs_addr', 'number of registers', 'write')
327
342
  check_regs_values(regs_value, 'regs_value')
328
343
  # build PDU: header + registers part (pack all registers in one call)
329
- pdu_regs_part = struct.pack('>%dH' % len(regs_value), *regs_value)
344
+ pdu_regs_part = struct.pack(f'>{len(regs_value)}H', *regs_value)
330
345
  tx_pdu = struct.pack('>BHHB', WRITE_MULTIPLE_REGISTERS, regs_addr, len(regs_value), len(pdu_regs_part))
331
346
  tx_pdu += pdu_regs_part
332
347
  # make request
333
348
  return self._write_req(tx_pdu, regs_addr, len(regs_value))
334
349
 
335
350
  def write_read_multiple_registers(self, write_addr: int, write_values: Iterable[int],
336
- read_addr: int, read_nb: int = 1) -> Optional[List[int]]:
351
+ read_addr: int, read_nb: int = 1) -> list[int] | None:
337
352
  """Modbus function WRITE_READ_MULTIPLE_REGISTERS (0x17).
338
353
 
339
354
  :param write_addr: write registers address (0 to 65535)
@@ -359,7 +374,7 @@ class ClientFunctionsMixin:
359
374
  # make request
360
375
  try:
361
376
  # build PDU: header + registers part (pack all registers in one call)
362
- pdu_regs_part = struct.pack('>%dH' % len(write_values), *write_values)
377
+ pdu_regs_part = struct.pack(f'>{len(write_values)}H', *write_values)
363
378
  tx_pdu = struct.pack('>BHHHHB', WRITE_READ_MULTIPLE_REGISTERS, read_addr, read_nb,
364
379
  write_addr, len(write_values), len(pdu_regs_part))
365
380
  tx_pdu += pdu_regs_part
@@ -6,7 +6,6 @@ import struct
6
6
  import time
7
7
  from binascii import hexlify
8
8
  from socket import AF_UNSPEC, IPPROTO_TCP, SOCK_STREAM, TCP_NODELAY
9
- from typing import Optional
10
9
 
11
10
  from ..constants import (
12
11
  MB_CONNECT_ERR,
@@ -36,7 +35,7 @@ class ClientTransportMixin:
36
35
  auto_open: bool
37
36
  auto_close: bool
38
37
  _no_delay: bool
39
- _sock: Optional[socket.socket]
38
+ _sock: socket.socket | None
40
39
  _deadline: float
41
40
  _transaction_id: int
42
41
 
@@ -60,11 +59,12 @@ class ClientTransportMixin:
60
59
  # AF_UNSPEC -> IPv6 (priority on some system) or 4
61
60
  try:
62
61
  addr_infos = socket.getaddrinfo(self.host, self.port, AF_UNSPEC, SOCK_STREAM)
63
- except socket.gaierror as e:
62
+ except (socket.gaierror, UnicodeError) as e:
64
63
  # name resolution failure: report it like any other connect error (no exception for the caller)
65
- raise NetworkError(MB_CONNECT_ERR, 'host name resolution error: %s' % e)
64
+ # (UnicodeError: a name that the IDNA codec can't encode, like a label too long once encoded)
65
+ raise NetworkError(MB_CONNECT_ERR, f'host name resolution error: {e}') from None
66
66
  # try each address in turn, keep the last failure reason for the error message
67
- last_err: Optional[OSError] = None
67
+ last_err: OSError | None = None
68
68
  for af, sock_type, proto, _canon_name, sa in addr_infos:
69
69
  try:
70
70
  self._sock = socket.socket(af, sock_type, proto)
@@ -84,7 +84,7 @@ class ClientTransportMixin:
84
84
  break
85
85
  # check connect status
86
86
  if not self.is_open:
87
- reason = 'connection refused' if last_err is None else 'connect error: %s' % last_err
87
+ reason = 'connection refused' if last_err is None else f'connect error: {last_err}'
88
88
  raise NetworkError(MB_CONNECT_ERR, reason)
89
89
 
90
90
  def close(self) -> None:
@@ -141,7 +141,7 @@ class ClientTransportMixin:
141
141
  # restore the full timeout (it's reduced by _recv() to the time remaining in the previous request)
142
142
  self._set_sock_timeout(self.timeout)
143
143
  self._sock.sendall(frame)
144
- except socket.timeout:
144
+ except TimeoutError:
145
145
  self.close()
146
146
  raise NetworkError(MB_TIMEOUT_ERR, 'timeout error')
147
147
  except OSError:
@@ -184,7 +184,7 @@ class ClientTransportMixin:
184
184
  try:
185
185
  self._set_sock_timeout(remaining)
186
186
  r_buffer = self._sock.recv(size)
187
- except socket.timeout:
187
+ except TimeoutError:
188
188
  self.close()
189
189
  raise NetworkError(MB_TIMEOUT_ERR, 'timeout error')
190
190
  except OSError:
@@ -246,8 +246,8 @@ class ClientTransportMixin:
246
246
  # extract function code: it must be the one of the request (with or without the exception flag)
247
247
  rx_fc = rx_pdu[0]
248
248
  if rx_fc & 0x7f != func_code:
249
- raise NetworkError(MB_RECV_ERR, 'function code mismatch (0x%02X for a 0x%02X request)'
250
- % (rx_fc, func_code))
249
+ raise NetworkError(MB_RECV_ERR,
250
+ f'function code mismatch (0x{rx_fc:02X} for a 0x{func_code:02X} request)')
251
251
  # check except status
252
252
  if rx_fc >= 0x80:
253
253
  exp_code = rx_pdu[1]
@@ -292,4 +292,3 @@ class ClientTransportMixin:
292
292
 
293
293
  def on_tx_rx(self, frame: bytes, is_tx: bool) -> None:
294
294
  """Call for each Tx/Rx (for user purposes)."""
295
- pass
@@ -1,7 +1,6 @@
1
1
  """ pyModbusTCP client: checks of the ModbusClient settings and of the request arguments. """
2
2
 
3
3
  import math
4
- from typing import List
5
4
 
6
5
  from ..utils import valid_host
7
6
 
@@ -12,20 +11,20 @@ def check_addr_nb(addr: int, nb: int, max_nb: int, addr_name: str, nb_name: str,
12
11
  :raises ValueError: if address, number of items or the end of the area is out of range
13
12
  """
14
13
  if not 0 <= addr <= 0xffff:
15
- raise ValueError('%s out of range (valid from 0 to 65535)' % addr_name)
14
+ raise ValueError(f'{addr_name} out of range (valid from 0 to 65535)')
16
15
  if not 1 <= nb <= max_nb:
17
- raise ValueError('%s out of range (valid from 1 to %d)' % (nb_name, max_nb))
16
+ raise ValueError(f'{nb_name} out of range (valid from 1 to {max_nb})')
18
17
  if addr + nb > 0x10000:
19
- raise ValueError('%s after end of modbus address space' % action)
18
+ raise ValueError(f'{action} after end of modbus address space')
20
19
 
21
20
 
22
- def check_regs_values(values: List[int], name: str) -> None:
21
+ def check_regs_values(values: list[int], name: str) -> None:
23
22
  """Check that every register value fit in 16 bits (values is a non-empty list of int).
24
23
 
25
24
  :raises ValueError: if a value is out of the 0 to 65535 range
26
25
  """
27
26
  if min(values) < 0 or max(values) > 0xffff:
28
- raise ValueError('%s list contains out of range values' % name)
27
+ raise ValueError(f'{name} list contains out of range values')
29
28
 
30
29
 
31
30
  def validate_host(value: object) -> str:
@@ -1,7 +1,6 @@
1
1
  """ pyModbusTCP client: DeviceIdentificationResponse, result of the read device identification function. """
2
2
 
3
3
  from dataclasses import dataclass, field
4
- from typing import Dict, Optional
5
4
 
6
5
 
7
6
  @dataclass
@@ -20,32 +19,32 @@ class DeviceIdentificationResponse:
20
19
  conformity_level: int = 0
21
20
  more_follows: int = 0
22
21
  next_object_id: int = 0
23
- objects_by_id: Dict[int, bytes] = field(default_factory=dict)
22
+ objects_by_id: dict[int, bytes] = field(default_factory=dict)
24
23
 
25
24
  @property
26
- def vendor_name(self) -> Optional[bytes]:
25
+ def vendor_name(self) -> bytes | None:
27
26
  return self.objects_by_id.get(0x00)
28
27
 
29
28
  @property
30
- def product_code(self) -> Optional[bytes]:
29
+ def product_code(self) -> bytes | None:
31
30
  return self.objects_by_id.get(0x01)
32
31
 
33
32
  @property
34
- def major_minor_revision(self) -> Optional[bytes]:
33
+ def major_minor_revision(self) -> bytes | None:
35
34
  return self.objects_by_id.get(0x02)
36
35
 
37
36
  @property
38
- def vendor_url(self) -> Optional[bytes]:
37
+ def vendor_url(self) -> bytes | None:
39
38
  return self.objects_by_id.get(0x03)
40
39
 
41
40
  @property
42
- def product_name(self) -> Optional[bytes]:
41
+ def product_name(self) -> bytes | None:
43
42
  return self.objects_by_id.get(0x04)
44
43
 
45
44
  @property
46
- def model_name(self) -> Optional[bytes]:
45
+ def model_name(self) -> bytes | None:
47
46
  return self.objects_by_id.get(0x05)
48
47
 
49
48
  @property
50
- def user_application_name(self) -> Optional[bytes]:
49
+ def user_application_name(self) -> bytes | None:
51
50
  return self.objects_by_id.get(0x06)