From 7203b65673675f4220bcfd9dec58b6c5b6cfacd2 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sun, 17 Mar 2024 09:01:53 +1300 Subject: [PATCH 001/127] =?UTF-8?q?=F0=9F=94=A7=20Update=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - update upload/download workflow actions - use OIDC credentials to release to PyPI --- .github/workflows/ci.yml | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e70f0f8cf..e1d26c7c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -155,7 +155,7 @@ jobs: - name: Run doctest run: | python -m sphinx -b doctest -W --keep-going doc build - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: sphinx-out path: ./build/ @@ -174,9 +174,9 @@ jobs: - name: Check build artifacts run: pipx run twine check --strict dist/* - name: Save artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: python-can-dist + name: "${{ matrix.os }}-${{ matrix.python-version }}" path: ./dist upload_pypi: @@ -186,12 +186,9 @@ jobs: # upload to PyPI only on release if: github.event.release && github.event.action == 'published' steps: - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: - name: python-can-dist path: dist + merge-multiple: true - - uses: pypa/gh-action-pypi-publish@v1.4.2 - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} + - uses: pypa/gh-action-pypi-publish@release/v1 From db1d16ad4647e238f0737c4eabb9b0eb886ce8ad Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sun, 17 Mar 2024 13:24:15 +1300 Subject: [PATCH 002/127] Draft release notes for v4.3.2 --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8798c19c7..548f14f0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,35 @@ +Version 4.3.2 +============= + +Features +-------- + +* TRC 1.3 Support: Added support for .trc log files as generated by PCAN Explorer v5 and other tools, expanding compatibility with common log file formats (#1753). +* ASCReader Performance: Significantly improved the read performance of ASCReader, optimizing log file processing and analysis (#1717). +* SYSTEC Interface Enhancements: Added the ability to pass an explicit DLC value to the send() method when using the SYSTEC interface, enhancing flexibility for message definitions (#1756). +* Socketcand Beacon Detection: Introduced a feature for detecting socketcand beacons, facilitating easier connection and configuration with socketcand servers (#1687). +* PCAN Driver Echo Frames: Enabled echo frames in the PCAN driver when receive_own_messages is set, improving feedback for message transmissions (#1723). +* CAN FD Bus Connection: Enabled connecting to CAN FD buses without specifying bus timings, simplifying the connection process for users (#1716). +* Neousys Configs Detection: Updated the detection mechanism for available Neousys configurations, ensuring more accurate and comprehensive configuration discovery (#1744). + + +Bug Fixes +--------- + +* Send Periodic Messages: Fixed an issue where fixed-duration periodic messages were sent one extra time beyond their intended count (#1713). +* Vector Interface on Windows 11: Addressed compatibility issues with the Vector interface on Windows 11, ensuring stable operation across the latest OS version (#1731). +* ASCWriter Millisecond Handling: Corrected the handling of milliseconds in ASCWriter, ensuring accurate time representation in log files (#1734). +* Various minor bug fixes: Addressed several minor bugs to improve overall stability and performance. + +Miscellaneous +------------- + +* Implemented various logging enhancements to provide more detailed and useful operational insights (#1703). +* Updated CI to use OIDC for connecting GitHub Actions to PyPi, improving security and access control for CI workflows. + +The release also includes various other minor enhancements and bug fixes aimed at improving the reliability and performance of the software. + + Version 4.3.1 ============= From f97ab8469ed7bacf44b4401ae6377c7f60661afc Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sun, 17 Mar 2024 13:27:30 +1300 Subject: [PATCH 003/127] Set version to 4.3.2-rc.1 --- can/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/__init__.py b/can/__init__.py index 9b6a26f58..454947192 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -8,7 +8,7 @@ import logging from typing import Any, Dict -__version__ = "4.3.1" +__version__ = "4.3.2-rc.1" __all__ = [ "ASCReader", "ASCWriter", From 651fe17f38af10e3973f0987518e53fc85797ee6 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sun, 17 Mar 2024 13:57:36 +1300 Subject: [PATCH 004/127] Fix CI - Add id-token permission to upload_pypi job --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e1d26c7c6..12e7b6f27 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -180,8 +180,10 @@ jobs: path: ./dist upload_pypi: - needs: [build] + needs: [build, test] runs-on: ubuntu-latest + permissions: + id-token: write # upload to PyPI only on release if: github.event.release && github.event.action == 'published' From 28e9061fb6fa78a905214f6af2f9fc68b1716c40 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sun, 17 Mar 2024 14:30:53 +1300 Subject: [PATCH 005/127] 4.3.2-rc.2 --- can/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/__init__.py b/can/__init__.py index 454947192..4fc703c72 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -8,7 +8,7 @@ import logging from typing import Any, Dict -__version__ = "4.3.2-rc.1" +__version__ = "4.3.2-rc.2" __all__ = [ "ASCReader", "ASCWriter", From c02a665bba7f864ed5a4f177a21f4bc24ab1cc77 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sun, 17 Mar 2024 14:54:05 +1300 Subject: [PATCH 006/127] Allow release without tests --- .github/workflows/ci.yml | 2 +- can/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12e7b6f27..7cf76e934 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -180,7 +180,7 @@ jobs: path: ./dist upload_pypi: - needs: [build, test] + needs: [build] runs-on: ubuntu-latest permissions: id-token: write diff --git a/can/__init__.py b/can/__init__.py index 4fc703c72..42cf2bc5d 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -8,7 +8,7 @@ import logging from typing import Any, Dict -__version__ = "4.3.2-rc.2" +__version__ = "4.3.2-rc.3" __all__ = [ "ASCReader", "ASCWriter", From c1b1152edeba7158dce6aef6b43bb6584727bb0b Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sun, 17 Mar 2024 18:56:50 +1300 Subject: [PATCH 007/127] Update CI.yml workflow --- .github/workflows/ci.yml | 24 +++++++++++------------- can/__init__.py | 2 +- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7cf76e934..5c06ee94f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,9 +42,9 @@ jobs: # python-version: "3.13.0-alpha - 3.13.0" fail-fast: false steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -139,9 +139,9 @@ jobs: docs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.10" - name: Install dependencies @@ -155,18 +155,14 @@ jobs: - name: Run doctest run: | python -m sphinx -b doctest -W --keep-going doc build - - uses: actions/upload-artifact@v4 - with: - name: sphinx-out - path: ./build/ - retention-days: 5 build: + name: Packaging runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.10" - name: Build wheel and sdist @@ -176,11 +172,12 @@ jobs: - name: Save artifacts uses: actions/upload-artifact@v4 with: - name: "${{ matrix.os }}-${{ matrix.python-version }}" + name: release path: ./dist upload_pypi: needs: [build] + name: Release to PyPi runs-on: ubuntu-latest permissions: id-token: write @@ -193,4 +190,5 @@ jobs: path: dist merge-multiple: true - - uses: pypa/gh-action-pypi-publish@release/v1 + - name: Publish release distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/can/__init__.py b/can/__init__.py index 42cf2bc5d..44c6032f9 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -8,7 +8,7 @@ import logging from typing import Any, Dict -__version__ = "4.3.2-rc.3" +__version__ = "4.3.2-rc.4" __all__ = [ "ASCReader", "ASCWriter", From 9c50c89e52a9d71850f70df3325d392ca79b822a Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Thu, 6 Jun 2024 19:51:01 +1200 Subject: [PATCH 008/127] Update changelog for v4.4.0 --- CHANGELOG.md | 10 ++++++---- can/__init__.py | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 548f14f0a..404b68c3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,15 @@ -Version 4.3.2 +Version 4.4.0 ============= Features -------- * TRC 1.3 Support: Added support for .trc log files as generated by PCAN Explorer v5 and other tools, expanding compatibility with common log file formats (#1753). -* ASCReader Performance: Significantly improved the read performance of ASCReader, optimizing log file processing and analysis (#1717). +* ASCReader refactor: improved the ASCReader code (#1717). * SYSTEC Interface Enhancements: Added the ability to pass an explicit DLC value to the send() method when using the SYSTEC interface, enhancing flexibility for message definitions (#1756). * Socketcand Beacon Detection: Introduced a feature for detecting socketcand beacons, facilitating easier connection and configuration with socketcand servers (#1687). * PCAN Driver Echo Frames: Enabled echo frames in the PCAN driver when receive_own_messages is set, improving feedback for message transmissions (#1723). -* CAN FD Bus Connection: Enabled connecting to CAN FD buses without specifying bus timings, simplifying the connection process for users (#1716). +* CAN FD Bus Connection for VectorBus: Enabled connecting to CAN FD buses without specifying bus timings, simplifying the connection process for users (#1716). * Neousys Configs Detection: Updated the detection mechanism for available Neousys configurations, ensuring more accurate and comprehensive configuration discovery (#1744). @@ -24,9 +24,11 @@ Bug Fixes Miscellaneous ------------- +* Invert default value logic for BusABC._is_shutdown. (#1774) * Implemented various logging enhancements to provide more detailed and useful operational insights (#1703). * Updated CI to use OIDC for connecting GitHub Actions to PyPi, improving security and access control for CI workflows. - +* Fix CI to work for MacOS (#1772). +* The release also includes various other minor enhancements and bug fixes aimed at improving the reliability and performance of the software. diff --git a/can/__init__.py b/can/__init__.py index 44c6032f9..d504aa16b 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -8,7 +8,7 @@ import logging from typing import Any, Dict -__version__ = "4.3.2-rc.4" +__version__ = "4.4.0-rc.1" __all__ = [ "ASCReader", "ASCWriter", From c8202f6a478013102c21ce1d1abd31cdfa38aea4 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Thu, 6 Jun 2024 20:14:57 +1200 Subject: [PATCH 009/127] Update release docs --- .github/workflows/ci.yml | 1 + can/__init__.py | 2 +- doc/development.rst | 14 ++++++++++---- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c06ee94f..742ccca03 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -181,6 +181,7 @@ jobs: runs-on: ubuntu-latest permissions: id-token: write + attestations: write # upload to PyPI only on release if: github.event.release && github.event.action == 'published' diff --git a/can/__init__.py b/can/__init__.py index d504aa16b..86e0f8a6c 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -8,7 +8,7 @@ import logging from typing import Any, Dict -__version__ = "4.4.0-rc.1" +__version__ = "4.4.0-rc.2" __all__ = [ "ASCReader", "ASCWriter", diff --git a/doc/development.rst b/doc/development.rst index ec7b7dc24..f635ffc06 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -117,13 +117,19 @@ Creating a new Release - Update ``CONTRIBUTORS.txt`` with any new contributors. - For larger changes update ``doc/history.rst``. - Sanity check that documentation has stayed inline with code. -- Create a temporary virtual environment. Run ``python setup.py install`` and ``tox``. -- Create and upload the distribution: ``python setup.py sdist bdist_wheel``. -- Sign the packages with gpg ``gpg --detach-sign -a dist/python_can-X.Y.Z-py3-none-any.whl``. -- Upload with twine ``twine upload dist/python-can-X.Y.Z*``. - In a new virtual env check that the package can be installed with pip: ``pip install python-can==X.Y.Z``. - Create a new tag in the repository. - Check the release on `PyPi `__, `Read the Docs `__ and `GitHub `__. + + +Manual release steps (deprecated) +--------------------------------- + +- Create a temporary virtual environment. +- Build with ``pipx run build`` +- Create and upload the distribution: ``python setup.py sdist bdist_wheel``. +- Sign the packages with gpg ``gpg --detach-sign -a dist/python_can-X.Y.Z-py3-none-any.whl``. +- Upload with twine ``twine upload dist/python-can-X.Y.Z*``. From 26a3f4bd4890436c5329d9fbd68c865e393bc821 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sat, 8 Jun 2024 08:18:00 +1200 Subject: [PATCH 010/127] Set version to 4.4.0 --- can/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/__init__.py b/can/__init__.py index 86e0f8a6c..d0c19f89b 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -8,7 +8,7 @@ import logging from typing import Any, Dict -__version__ = "4.4.0-rc.2" +__version__ = "4.4.0" __all__ = [ "ASCReader", "ASCWriter", From 034b400cd41588a154d7b69468ed43a74b5d9c4a Mon Sep 17 00:00:00 2001 From: Tian-Jionglu Date: Fri, 7 Jun 2024 13:20:03 +0800 Subject: [PATCH 011/127] doc update update installation doc since `setup.py` method removed. --- doc/installation.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/installation.rst b/doc/installation.rst index 6b2a2cfb2..ff72ae21b 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -131,5 +131,7 @@ reinstall. Download or clone the source repository then: :: - python setup.py develop + # install in editable mode + cd + python3 -m pip install -e . From fc6ff2235b240117ab1e4ae585f007578e21e57e Mon Sep 17 00:00:00 2001 From: Caleb Perkinson <60443297+cperkulator@users.noreply.github.com> Date: Fri, 7 Jun 2024 16:28:34 -0500 Subject: [PATCH 012/127] add silent mode support for vector (#1764) * add silent mode support for vector * fix: address comments and test works as expected * formatting --- can/interfaces/vector/canlib.py | 40 +++++++++++++++++++++++++++++---- test/test_vector.py | 39 ++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 3a25c99d8..adf7b6c5d 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -104,6 +104,7 @@ def __init__( sjw_dbr: int = 2, tseg1_dbr: int = 6, tseg2_dbr: int = 3, + listen_only: Optional[bool] = False, **kwargs: Any, ) -> None: """ @@ -157,6 +158,8 @@ def __init__( Bus timing value tseg1 (data) :param tseg2_dbr: Bus timing value tseg2 (data) + :param listen_only: + if the bus should be set to listen only mode. :raise ~can.exceptions.CanInterfaceNotImplementedError: If the current operating system is not supported or the driver could not be loaded. @@ -205,6 +208,8 @@ def __init__( self.index_to_channel: Dict[int, int] = {} self._can_protocol = CanProtocol.CAN_FD if is_fd else CanProtocol.CAN_20 + self._listen_only = listen_only + for channel in self.channels: if ( len(self.channels) == 1 @@ -232,7 +237,7 @@ def __init__( permission_mask = xlclass.XLaccess() # Set mask to request channel init permission if needed - if bitrate or fd or timing: + if bitrate or fd or timing or self._listen_only: permission_mask.value = self.mask interface_version = ( @@ -311,6 +316,9 @@ def __init__( if assert_timing: self._check_can_settings(channel_mask=self.mask, bitrate=bitrate) + if self._listen_only: + self._set_output_mode(channel_mask=self.mask, listen_only=True) + # Enable/disable TX receipts tx_receipts = 1 if receive_own_messages else 0 self.xldriver.xlCanSetChannelMode(self.port_handle, self.mask, tx_receipts, 0) @@ -445,6 +453,28 @@ def _read_bus_params( f"Channel configuration for channel {channel} not found." ) + def _set_output_mode(self, channel_mask: int, listen_only: bool) -> None: + # set parameters for channels with init access + channel_mask = channel_mask & self.permission_mask + + if channel_mask: + if listen_only: + self.xldriver.xlCanSetChannelOutput( + self.port_handle, + channel_mask, + xldefine.XL_OutputMode.XL_OUTPUT_MODE_SILENT, + ) + else: + self.xldriver.xlCanSetChannelOutput( + self.port_handle, + channel_mask, + xldefine.XL_OutputMode.XL_OUTPUT_MODE_NORMAL, + ) + + LOG.info("xlCanSetChannelOutput: listen_only=%u", listen_only) + else: + LOG.warning("No channels with init access to set listen only mode") + def _set_bitrate(self, channel_mask: int, bitrate: int) -> None: # set parameters for channels with init access channel_mask = channel_mask & self.permission_mask @@ -643,9 +673,11 @@ def _apply_filters(self, filters: Optional[CanFilters]) -> None: self.mask, can_filter["can_id"], can_filter["can_mask"], - xldefine.XL_AcceptanceFilter.XL_CAN_EXT - if can_filter.get("extended") - else xldefine.XL_AcceptanceFilter.XL_CAN_STD, + ( + xldefine.XL_AcceptanceFilter.XL_CAN_EXT + if can_filter.get("extended") + else xldefine.XL_AcceptanceFilter.XL_CAN_STD + ), ) except VectorOperationError as exception: LOG.warning("Could not set filters: %s", exception) diff --git a/test/test_vector.py b/test/test_vector.py index 99756c41d..ca46526fb 100644 --- a/test/test_vector.py +++ b/test/test_vector.py @@ -48,6 +48,7 @@ def mock_xldriver() -> None: xldriver_mock.xlCanSetChannelAcceptance = Mock(return_value=0) xldriver_mock.xlCanSetChannelBitrate = Mock(return_value=0) xldriver_mock.xlSetNotification = Mock(side_effect=xlSetNotification) + xldriver_mock.xlCanSetChannelOutput = Mock(return_value=0) # bus deactivation functions xldriver_mock.xlDeactivateChannel = Mock(return_value=0) @@ -78,6 +79,44 @@ def mock_xldriver() -> None: canlib.HAS_EVENTS = real_has_events +def test_listen_only_mocked(mock_xldriver) -> None: + bus = can.Bus(channel=0, interface="vector", listen_only=True, _testing=True) + assert isinstance(bus, canlib.VectorBus) + assert bus.protocol == can.CanProtocol.CAN_20 + + can.interfaces.vector.canlib.xldriver.xlCanSetChannelOutput.assert_called() + xlCanSetChannelOutput_args = ( + can.interfaces.vector.canlib.xldriver.xlCanSetChannelOutput.call_args[0] + ) + assert xlCanSetChannelOutput_args[2] == xldefine.XL_OutputMode.XL_OUTPUT_MODE_SILENT + + +@pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable") +def test_listen_only() -> None: + bus = can.Bus( + channel=0, + serial=_find_virtual_can_serial(), + interface="vector", + receive_own_messages=True, + listen_only=True, + ) + assert isinstance(bus, canlib.VectorBus) + assert bus.protocol == can.CanProtocol.CAN_20 + + msg = can.Message( + arbitration_id=0xC0FFEF, data=[1, 2, 3, 4, 5, 6, 7, 8], is_extended_id=True + ) + + bus.send(msg) + + received_msg = bus.recv() + + assert received_msg.arbitration_id == msg.arbitration_id + assert received_msg.data == msg.data + + bus.shutdown() + + def test_bus_creation_mocked(mock_xldriver) -> None: bus = can.Bus(channel=0, interface="vector", _testing=True) assert isinstance(bus, canlib.VectorBus) From 96754c1025bd1011d8957cd111ca38dd41414a70 Mon Sep 17 00:00:00 2001 From: Pierre-Luc Date: Mon, 10 Jun 2024 13:35:38 -0400 Subject: [PATCH 013/127] Return timestamps relative to epoch in neovi interface (#1789) --- can/interfaces/ics_neovi/neovi_bus.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py index 7863fcb65..815ed6fa0 100644 --- a/can/interfaces/ics_neovi/neovi_bus.py +++ b/can/interfaces/ics_neovi/neovi_bus.py @@ -13,6 +13,7 @@ import os import tempfile from collections import Counter, defaultdict, deque +from datetime import datetime from functools import partial from itertools import cycle from threading import Event @@ -68,6 +69,9 @@ def __exit__(self, exc_type, exc_val, exc_tb): open_lock = FileLock(os.path.join(tempfile.gettempdir(), "neovi.lock")) description_id = cycle(range(1, 0x8000)) +ICS_EPOCH = datetime.fromisoformat("2007-01-01") +ICS_EPOCH_DELTA = (ICS_EPOCH - datetime.fromisoformat("1970-01-01")).total_seconds() + class ICSApiError(CanError): """ @@ -384,7 +388,7 @@ def _get_timestamp_for_msg(self, ics_msg): return ics_msg.TimeSystem else: # This is the hardware time stamp. - return ics.get_timestamp_for_msg(self.dev, ics_msg) + return ics.get_timestamp_for_msg(self.dev, ics_msg) + ICS_EPOCH_DELTA def _ics_msg_to_message(self, ics_msg): is_fd = ics_msg.Protocol == ics.SPY_PROTOCOL_CANFD From 1ce550c8bf75790eadf23eae4a197588d764fe4f Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Wed, 12 Jun 2024 23:17:26 +0200 Subject: [PATCH 014/127] Update linters and gh actions (#1794) * update black * update ruff * update mypy and pylint * make pylint happy * update actions --- .github/workflows/ci.yml | 12 ++++++------ can/ctypesutil.py | 1 + can/interfaces/ixxat/structures.py | 22 +++++++++++----------- can/interfaces/kvaser/canlib.py | 1 - can/interfaces/kvaser/structures.py | 17 +++++++---------- can/interfaces/pcan/pcan.py | 1 + can/interfaces/socketcan/socketcan.py | 13 +++++++------ can/interfaces/socketcand/socketcand.py | 1 + can/interfaces/systec/exceptions.py | 3 +-- can/interfaces/systec/ucanbus.py | 10 +++++----- can/io/asc.py | 1 + can/io/generic.py | 1 + can/io/mf4.py | 1 + can/io/player.py | 1 + can/listener.py | 2 +- can/notifier.py | 10 ++++++---- can/typechecking.py | 1 + can/util.py | 1 + pyproject.toml | 16 +++++++++------- test/test_rotating_loggers.py | 3 +-- 20 files changed, 63 insertions(+), 55 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 742ccca03..4afb4c1ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,7 +64,7 @@ jobs: # See: https://foss.heptapod.net/pypy/pypy/-/issues/3809 TEST_SOCKETCAN: "${{ matrix.os == 'ubuntu-latest' && ! startsWith(matrix.python-version, 'pypy' ) }}" - name: Coveralls Parallel - uses: coverallsapp/github-action@master + uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.github_token }} flag-name: Unittests-${{ matrix.os }}-${{ matrix.python-version }} @@ -76,7 +76,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Coveralls Finished - uses: coverallsapp/github-action@master + uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.github_token }} parallel-finished: true @@ -84,9 +84,9 @@ jobs: static-code-analysis: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.10" - name: Install dependencies @@ -123,9 +123,9 @@ jobs: format: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.10" - name: Install dependencies diff --git a/can/ctypesutil.py b/can/ctypesutil.py index fa59255d1..0336b03d3 100644 --- a/can/ctypesutil.py +++ b/can/ctypesutil.py @@ -1,6 +1,7 @@ """ This module contains common `ctypes` utils. """ + import ctypes import logging import sys diff --git a/can/interfaces/ixxat/structures.py b/can/interfaces/ixxat/structures.py index 419a52973..611955db7 100644 --- a/can/interfaces/ixxat/structures.py +++ b/can/interfaces/ixxat/structures.py @@ -51,17 +51,17 @@ class UniqueHardwareId(ctypes.Union): ] def __str__(self): - return "Mfg: {}, Dev: {} HW: {}.{}.{}.{} Drv: {}.{}.{}.{}".format( - self.Manufacturer, - self.Description, - self.HardwareBranchVersion, - self.HardwareMajorVersion, - self.HardwareMinorVersion, - self.HardwareBuildVersion, - self.DriverReleaseVersion, - self.DriverMajorVersion, - self.DriverMinorVersion, - self.DriverBuildVersion, + return ( + f"Mfg: {self.Manufacturer}, " + f"Dev: {self.Description} " + f"HW: {self.HardwareBranchVersion}" + f".{self.HardwareMajorVersion}" + f".{self.HardwareMinorVersion}" + f".{self.HardwareBuildVersion} " + f"Drv: {self.DriverReleaseVersion}" + f".{self.DriverMajorVersion}" + f".{self.DriverMinorVersion}" + f".{self.DriverBuildVersion}" ) diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index 4c621ecf4..ccc03a696 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -64,7 +64,6 @@ def __get_canlib_function(func_name, argtypes=None, restype=None, errcheck=None) class CANLIBError(CanError): - """ Try to display errors that occur within the wrapped C library nicely. """ diff --git a/can/interfaces/kvaser/structures.py b/can/interfaces/kvaser/structures.py index 996f16c37..0229cc10a 100644 --- a/can/interfaces/kvaser/structures.py +++ b/can/interfaces/kvaser/structures.py @@ -23,16 +23,13 @@ class BusStatistics(ctypes.Structure): def __str__(self): return ( - "std_data: {}, std_remote: {}, ext_data: {}, ext_remote: {}, " - "err_frame: {}, bus_load: {:.1f}%, overruns: {}" - ).format( - self.std_data, - self.std_remote, - self.ext_data, - self.ext_remote, - self.err_frame, - self.bus_load / 100.0, - self.overruns, + f"std_data: {self.std_data}, " + f"std_remote: {self.std_remote}, " + f"ext_data: {self.ext_data}, " + f"ext_remote: {self.ext_remote}, " + f"err_frame: {self.err_frame}, " + f"bus_load: {self.bus_load / 100.0:.1f}%, " + f"overruns: {self.overruns}" ) @property diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index b1b089690..52dbb49b0 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -1,6 +1,7 @@ """ Enable basic CAN over a PCAN USB device. """ + import logging import platform import time diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index cdf4afac6..79a46ab2c 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -41,6 +41,13 @@ log.error("socket.CMSG_SPACE not available on this platform") +# Constants needed for precise handling of timestamps +RECEIVED_TIMESTAMP_STRUCT = struct.Struct("@ll") +RECEIVED_ANCILLARY_BUFFER_SIZE = ( + CMSG_SPACE(RECEIVED_TIMESTAMP_STRUCT.size) if CMSG_SPACE_available else 0 +) + + # Setup BCM struct def bcm_header_factory( fields: List[Tuple[str, Union[Type[ctypes.c_uint32], Type[ctypes.c_long]]]], @@ -597,12 +604,6 @@ def capture_message( return msg -# Constants needed for precise handling of timestamps -if CMSG_SPACE_available: - RECEIVED_TIMESTAMP_STRUCT = struct.Struct("@ll") - RECEIVED_ANCILLARY_BUFFER_SIZE = CMSG_SPACE(RECEIVED_TIMESTAMP_STRUCT.size) - - class SocketcanBus(BusABC): # pylint: disable=abstract-method """A SocketCAN interface to CAN. diff --git a/can/interfaces/socketcand/socketcand.py b/can/interfaces/socketcand/socketcand.py index e852c5e14..2cfdf54e5 100644 --- a/can/interfaces/socketcand/socketcand.py +++ b/can/interfaces/socketcand/socketcand.py @@ -7,6 +7,7 @@ Copyright (C) 2021 DOMOLOGIC GmbH http://www.domologic.de """ + import logging import os import select diff --git a/can/interfaces/systec/exceptions.py b/can/interfaces/systec/exceptions.py index 9f7d4e2e5..dcd94bdbf 100644 --- a/can/interfaces/systec/exceptions.py +++ b/can/interfaces/systec/exceptions.py @@ -22,8 +22,7 @@ def __init__(self, result, func, arguments): @property @abstractmethod - def _error_message_mapping(self) -> Dict[ReturnCode, str]: - ... + def _error_message_mapping(self) -> Dict[ReturnCode, str]: ... class UcanError(UcanException): diff --git a/can/interfaces/systec/ucanbus.py b/can/interfaces/systec/ucanbus.py index 3dff7fda5..00f101e4e 100644 --- a/can/interfaces/systec/ucanbus.py +++ b/can/interfaces/systec/ucanbus.py @@ -143,11 +143,11 @@ def __init__(self, channel, can_filters=None, **kwargs): self._ucan.init_hardware(device_number=device_number) self._ucan.init_can(self.channel, **self._params) hw_info_ex, _, _ = self._ucan.get_hardware_info() - self.channel_info = "{}, S/N {}, CH {}, BTR {}".format( - self._ucan.get_product_code_message(hw_info_ex.product_code), - hw_info_ex.serial, - self.channel, - self._ucan.get_baudrate_message(self.BITRATES[bitrate]), + self.channel_info = ( + f"{self._ucan.get_product_code_message(hw_info_ex.product_code)}, " + f"S/N {hw_info_ex.serial}, " + f"CH {self.channel}, " + f"BTR {self._ucan.get_baudrate_message(self.BITRATES[bitrate])}" ) except UcanException as exception: raise CanInitializationError() from exception diff --git a/can/io/asc.py b/can/io/asc.py index ceffaf5cb..2955ad7f9 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -5,6 +5,7 @@ - https://bitbucket.org/tobylorenz/vector_asc/src/master/src/Vector/ASC/tests/unittests/data/ - under `test/data/logfile.asc` """ + import logging import re from datetime import datetime diff --git a/can/io/generic.py b/can/io/generic.py index 4d877865e..55468ff16 100644 --- a/can/io/generic.py +++ b/can/io/generic.py @@ -1,4 +1,5 @@ """Contains generic base classes for file IO.""" + import gzip import locale from abc import ABCMeta diff --git a/can/io/mf4.py b/can/io/mf4.py index 7ab6ba8a7..042bf8765 100644 --- a/can/io/mf4.py +++ b/can/io/mf4.py @@ -4,6 +4,7 @@ MF4 files represent Measurement Data Format (MDF) version 4 as specified by the ASAM MDF standard (see https://www.asam.net/standards/detail/mdf/) """ + import logging from datetime import datetime from hashlib import md5 diff --git a/can/io/player.py b/can/io/player.py index 4cbd7ce16..214112164 100644 --- a/can/io/player.py +++ b/can/io/player.py @@ -3,6 +3,7 @@ well as :class:`MessageSync` which plays back messages in the recorded order and time intervals. """ + import gzip import pathlib import time diff --git a/can/listener.py b/can/listener.py index ff3672913..e0024c683 100644 --- a/can/listener.py +++ b/can/listener.py @@ -136,7 +136,7 @@ class AsyncBufferedReader( """ def __init__(self, **kwargs: Any) -> None: - self.buffer: "asyncio.Queue[Message]" + self.buffer: asyncio.Queue[Message] if "loop" in kwargs: warnings.warn( diff --git a/can/notifier.py b/can/notifier.py index 1c8b77c5d..34fcf74fa 100644 --- a/can/notifier.py +++ b/can/notifier.py @@ -112,10 +112,12 @@ def _rx_thread(self, bus: BusABC) -> None: # determine message handling callable early, not inside while loop handle_message = cast( Callable[[Message], None], - self._on_message_received - if self._loop is None - else functools.partial( - self._loop.call_soon_threadsafe, self._on_message_received + ( + self._on_message_received + if self._loop is None + else functools.partial( + self._loop.call_soon_threadsafe, self._on_message_received + ) ), ) diff --git a/can/typechecking.py b/can/typechecking.py index 29c760d31..89f978a1a 100644 --- a/can/typechecking.py +++ b/can/typechecking.py @@ -1,5 +1,6 @@ """Types for mypy type-checking """ + import gzip import typing diff --git a/can/util.py b/can/util.py index c1eeca8b1..23ab142fb 100644 --- a/can/util.py +++ b/can/util.py @@ -1,6 +1,7 @@ """ Utilities and configuration file parsing. """ + import copy import functools import json diff --git a/pyproject.toml b/pyproject.toml index ad59469da..08b501c60 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,10 +60,10 @@ changelog = "https://github.com/hardbyte/python-can/blob/develop/CHANGELOG.md" [project.optional-dependencies] lint = [ - "pylint==3.0.*", - "ruff==0.1.13", - "black==23.12.*", - "mypy==1.8.*", + "pylint==3.2.*", + "ruff==0.4.8", + "black==24.4.*", + "mypy==1.10.*", ] seeedstudio = ["pyserial>=3.0"] serial = ["pyserial~=3.0"] @@ -128,6 +128,9 @@ exclude = [ ] [tool.ruff] +line-length = 100 + +[tool.ruff.lint] select = [ "A", # flake8-builtins "B", # flake8-bugbear @@ -149,9 +152,8 @@ ignore = [ "B026", # star-arg-unpacking-after-keyword-arg "PLR", # pylint refactor ] -line-length = 100 -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "can/interfaces/*" = [ "E501", # Line too long "F403", # undefined-local-with-import-star @@ -163,7 +165,7 @@ line-length = 100 "can/logger.py" = ["T20"] # flake8-print "can/player.py" = ["T20"] # flake8-print -[tool.ruff.isort] +[tool.ruff.lint.isort] known-first-party = ["can"] [tool.pylint] diff --git a/test/test_rotating_loggers.py b/test/test_rotating_loggers.py index a6661280d..032106e5f 100644 --- a/test/test_rotating_loggers.py +++ b/test/test_rotating_loggers.py @@ -38,8 +38,7 @@ def writer(self) -> FileIOMessageWriter: def should_rollover(self, msg: can.Message) -> bool: return False - def do_rollover(self): - ... + def do_rollover(self): ... return SubClass(file=file) From 25d6999c20d6dee3595f586abc55b69c04fb2ea9 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Wed, 12 Jun 2024 23:43:02 +0200 Subject: [PATCH 015/127] remove abstractmethod (#1795) --- can/listener.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/can/listener.py b/can/listener.py index e0024c683..1f19c3acd 100644 --- a/can/listener.py +++ b/can/listener.py @@ -46,8 +46,7 @@ def on_error(self, exc: Exception) -> None: """ raise NotImplementedError() - @abstractmethod - def stop(self) -> None: + def stop(self) -> None: # noqa: B027 """ Stop handling new messages, carry out any final tasks to ensure data is persisted and cleanup any open resources. From 35e847f6cb799c4c4ae381f8c0e7cc1ccd791499 Mon Sep 17 00:00:00 2001 From: Federico Spada <160390475+FedericoSpada@users.noreply.github.com> Date: Thu, 13 Jun 2024 09:43:15 +0200 Subject: [PATCH 016/127] Fixed pcan Unpack error (#1767) * Fixed pcan Unpack error PCANBasic.GetValue() must always return a Tuple with 2 elements, otherwise PcanBus._detect_available_configs() raises a "not enough values to unpack" error at line 718. * Fixed typo --- can/interfaces/pcan/basic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/interfaces/pcan/basic.py b/can/interfaces/pcan/basic.py index b704ca9bd..4d175c645 100644 --- a/can/interfaces/pcan/basic.py +++ b/can/interfaces/pcan/basic.py @@ -967,7 +967,7 @@ def GetValue(self, Channel, Parameter): elif Parameter == PCAN_ATTACHED_CHANNELS: res = self.GetValue(Channel, PCAN_ATTACHED_CHANNELS_COUNT) if TPCANStatus(res[0]) != PCAN_ERROR_OK: - return (TPCANStatus(res[0]),) + return TPCANStatus(res[0]), () mybuffer = (TPCANChannelInformation * res[1])() elif ( From 046af70e7c8aeb377df61768251314c6b714cf42 Mon Sep 17 00:00:00 2001 From: Werner Wolfrum <42910294+wolfraven@users.noreply.github.com> Date: Sun, 16 Jun 2024 13:11:13 +0200 Subject: [PATCH 017/127] Extented slcan.py to use "L" command optional to "O" command. (#1496) * Update slcan.py Added additional parameter "listen_only" to open interface/channel with "L" command (in opposite to "O" command). * make listen_only attribute private --------- Co-authored-by: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> --- can/interfaces/slcan.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index 7851a0322..f32d82fd0 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -62,6 +62,7 @@ def __init__( btr: Optional[str] = None, sleep_after_open: float = _SLEEP_AFTER_SERIAL_OPEN, rtscts: bool = False, + listen_only: bool = False, timeout: float = 0.001, **kwargs: Any, ) -> None: @@ -81,12 +82,18 @@ def __init__( Time to wait in seconds after opening serial connection :param rtscts: turn hardware handshake (RTS/CTS) on and off + :param listen_only: + If True, open interface/channel in listen mode with ``L`` command. + Otherwise, the (default) ``O`` command is still used. See ``open`` method. :param timeout: Timeout for the serial or usb device in seconds (default 0.001) + :raise ValueError: if both ``bitrate`` and ``btr`` are set or the channel is invalid :raise CanInterfaceNotImplementedError: if the serial module is missing :raise CanInitializationError: if the underlying serial connection could not be established """ + self._listen_only = listen_only + if serial is None: raise CanInterfaceNotImplementedError("The serial module is not installed") @@ -188,7 +195,10 @@ def flush(self) -> None: self.serialPortOrig.reset_input_buffer() def open(self) -> None: - self._write("O") + if self._listen_only: + self._write("L") + else: + self._write("O") def close(self) -> None: self._write("C") From 4fe3881add06faca4062be197bfa82db78e624cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Colomb?= Date: Wed, 24 Apr 2024 12:46:19 +0200 Subject: [PATCH 018/127] Gracefully handle errors when binding SocketCAN fails. The parent class BusABC expects to be shutdown properly, checked via its self._is_shutdown flag during object deletion. But that flag is only set when the base class shutdown() is called, which doesn't happen if instantiation of the concrete class failed in can.Bus(). So there is no way to avoid the warning message "SocketcanBus was not properly shut down" if the constructor raised an exception. This change addresses that issue for the SocketCAN interface, by catching an OSError exception in bind_socket(), logging it, and calling self.shutdown(). --- can/interfaces/socketcan/socketcan.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index 79a46ab2c..3c1178ebf 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -706,14 +706,20 @@ def __init__( # so this is always supported by the kernel self.socket.setsockopt(socket.SOL_SOCKET, constants.SO_TIMESTAMPNS, 1) - bind_socket(self.socket, channel) - kwargs.update( - { - "receive_own_messages": receive_own_messages, - "fd": fd, - "local_loopback": local_loopback, - } - ) + try: + bind_socket(self.socket, channel) + kwargs.update( + { + "receive_own_messages": receive_own_messages, + "fd": fd, + "local_loopback": local_loopback, + } + ) + except OSError as error: + log.error("Could not access SocketCAN device %s (%s)", channel, error) + # Clean up so the parent class doesn't complain about not being shut down properly + self.shutdown() + raise super().__init__( channel=channel, can_filters=can_filters, From 2bc87c0155b19bef1364b5e50523e2d1ea5f3ae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Colomb?= Date: Tue, 30 Apr 2024 15:42:23 +0200 Subject: [PATCH 019/127] Remove shutdown call and obsolete comment. --- can/interfaces/socketcan/socketcan.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index 3c1178ebf..03bd8c3f8 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -717,8 +717,6 @@ def __init__( ) except OSError as error: log.error("Could not access SocketCAN device %s (%s)", channel, error) - # Clean up so the parent class doesn't complain about not being shut down properly - self.shutdown() raise super().__init__( channel=channel, From 8ea242593ea341caed03324543fd9c3ebc237d15 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Tue, 18 Jun 2024 13:20:33 +0200 Subject: [PATCH 020/127] Fix SizedRotatingLogger file format bug (#1793) --- can/io/logger.py | 10 ++++++---- test/test_rotating_loggers.py | 8 ++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/can/io/logger.py b/can/io/logger.py index ed7f15bd0..f54223741 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -259,9 +259,11 @@ def _get_new_writer(self, filename: StringPathLike) -> FileIOMessageWriter: :return: An instance of a writer class. """ - suffix = "".join(pathlib.Path(filename).suffixes[-2:]).lower() - - if suffix in self._supported_formats: + suffixes = pathlib.Path(filename).suffixes + for suffix_length in range(len(suffixes), 0, -1): + suffix = "".join(suffixes[-suffix_length:]).lower() + if suffix not in self._supported_formats: + continue logger = Logger(filename=filename, **self.writer_kwargs) if isinstance(logger, FileIOMessageWriter): return logger @@ -269,7 +271,7 @@ def _get_new_writer(self, filename: StringPathLike) -> FileIOMessageWriter: return cast(FileIOMessageWriter, logger) raise ValueError( - f'The log format "{suffix}" ' + f'The log format of "{pathlib.Path(filename).name}" ' f"is not supported by {self.__class__.__name__}. " f"{self.__class__.__name__} supports the following formats: " f"{', '.join(self._supported_formats)}" diff --git a/test/test_rotating_loggers.py b/test/test_rotating_loggers.py index 032106e5f..ab977e3ce 100644 --- a/test/test_rotating_loggers.py +++ b/test/test_rotating_loggers.py @@ -180,6 +180,14 @@ def test_on_message_received(self, tmp_path): do_rollover.assert_called() writers_on_message_received.assert_called_with(msg) + def test_issue_1792(self, tmp_path): + with self._get_instance(tmp_path / "__unused.log") as logger_instance: + writer = logger_instance._get_new_writer( + tmp_path / "2017_Jeep_Grand_Cherokee_3.6L_V6.log" + ) + assert isinstance(writer, can.CanutilsLogWriter) + writer.stop() + class TestSizedRotatingLogger: def test_import(self): From 25c6fe0d2a6daa2bb7520bc4913d0db41854aab3 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 21 Jun 2024 19:23:03 +0200 Subject: [PATCH 021/127] avoid logging socketcan exception on non-linux platforms (#1800) --- can/interfaces/socketcan/utils.py | 3 +++ test/test_socketcan_helpers.py | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/can/interfaces/socketcan/utils.py b/can/interfaces/socketcan/utils.py index 91878f9b6..679c9cefc 100644 --- a/can/interfaces/socketcan/utils.py +++ b/can/interfaces/socketcan/utils.py @@ -8,6 +8,7 @@ import os import struct import subprocess +import sys from typing import List, Optional, cast from can import typechecking @@ -46,6 +47,8 @@ def find_available_interfaces() -> List[str]: :return: The list of available and active CAN interfaces or an empty list of the command failed """ + if sys.platform != "linux": + return [] try: command = ["ip", "-json", "link", "list", "up"] diff --git a/test/test_socketcan_helpers.py b/test/test_socketcan_helpers.py index a1d0bc8af..710922290 100644 --- a/test/test_socketcan_helpers.py +++ b/test/test_socketcan_helpers.py @@ -45,9 +45,11 @@ def test_find_available_interfaces_w_patch(self): with mock.patch("subprocess.check_output") as check_output: check_output.return_value = ip_output - ifs = find_available_interfaces() - self.assertEqual(["vcan0", "mycustomCan123"], ifs) + with mock.patch("sys.platform", "linux"): + ifs = find_available_interfaces() + + self.assertEqual(["vcan0", "mycustomCan123"], ifs) def test_find_available_interfaces_exception(self): with mock.patch("subprocess.check_output") as check_output: From 7366c4289f7222b61b6d7d0481b7c66522c73930 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 21 Jun 2024 19:47:53 +0200 Subject: [PATCH 022/127] fix PCAN test DeprecationWarnings (#1801) --- test/test_pcan.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/test_pcan.py b/test/test_pcan.py index 9f4e36fc4..31c541f0a 100644 --- a/test/test_pcan.py +++ b/test/test_pcan.py @@ -54,7 +54,9 @@ def test_bus_creation(self) -> None: self.assertIsInstance(self.bus, PcanBus) self.assertEqual(self.bus.protocol, CanProtocol.CAN_20) - self.assertFalse(self.bus.fd) + + with pytest.deprecated_call(): + self.assertFalse(self.bus.fd) self.MockPCANBasic.assert_called_once() self.mock_pcan.Initialize.assert_called_once() @@ -83,7 +85,9 @@ def test_bus_creation_fd(self, clock_param: str, clock_val: int) -> None: self.assertIsInstance(self.bus, PcanBus) self.assertEqual(self.bus.protocol, CanProtocol.CAN_FD) - self.assertTrue(self.bus.fd) + + with pytest.deprecated_call(): + self.assertTrue(self.bus.fd) self.MockPCANBasic.assert_called_once() self.mock_pcan.Initialize.assert_not_called() From 621cd7ddc12400b4f53de84c19b5d04e5c1a9798 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 21 Jun 2024 20:02:09 +0200 Subject: [PATCH 023/127] Activate channel after CAN filters were applied (#1796) --- can/interfaces/kvaser/canlib.py | 11 +++++++---- can/interfaces/vector/canlib.py | 17 +++++++++-------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index ccc03a696..58baa1040 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -552,7 +552,6 @@ def __init__( else: flags_ = flags self._write_handle = canOpenChannel(channel, flags_) - canBusOn(self._read_handle) can_driver_mode = ( canstat.canDRIVER_SILENT @@ -560,8 +559,6 @@ def __init__( else canstat.canDRIVER_NORMAL ) canSetBusOutputControl(self._write_handle, can_driver_mode) - log.debug("Going bus on TX handle") - canBusOn(self._write_handle) timer = ctypes.c_uint(0) try: @@ -587,6 +584,12 @@ def __init__( **kwargs, ) + # activate channel after CAN filters were applied + log.debug("Go on bus") + if not self.single_handle: + canBusOn(self._read_handle) + canBusOn(self._write_handle) + def _apply_filters(self, filters): if filters and len(filters) == 1: can_id = filters[0]["can_id"] @@ -610,7 +613,7 @@ def _apply_filters(self, filters): for extended in (0, 1): canSetAcceptanceFilter(handle, 0, 0, extended) except (NotImplementedError, CANLIBError) as e: - log.error("An error occured while disabling filtering: %s", e) + log.error("An error occurred while disabling filtering: %s", e) def flush_tx_buffer(self): """Wipeout the transmit buffer on the Kvaser.""" diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index adf7b6c5d..d307d076f 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -329,14 +329,6 @@ def __init__( else: LOG.info("Install pywin32 to avoid polling") - try: - self.xldriver.xlActivateChannel( - self.port_handle, self.mask, xldefine.XL_BusTypes.XL_BUS_TYPE_CAN, 0 - ) - except VectorOperationError as error: - self.shutdown() - raise VectorInitializationError.from_generic(error) from None - # Calculate time offset for absolute timestamps offset = xlclass.XLuint64() try: @@ -366,6 +358,15 @@ def __init__( **kwargs, ) + # activate channels after CAN filters were applied + try: + self.xldriver.xlActivateChannel( + self.port_handle, self.mask, xldefine.XL_BusTypes.XL_BUS_TYPE_CAN, 0 + ) + except VectorOperationError as error: + self.shutdown() + raise VectorInitializationError.from_generic(error) from None + @property def fd(self) -> bool: class_name = self.__class__.__name__ From 867fd92cc367c1960a32972d1225263de6b8d950 Mon Sep 17 00:00:00 2001 From: Jacob Schaer Date: Sat, 22 Jun 2024 03:02:10 -0700 Subject: [PATCH 024/127] Add non-ISO CANFD support to kvaser (#1752) * Add basic non-iso support * Update test_kvaser.py Add unit test * Update test_kvaser.py Black to pass premerge checklist * Made code changes New enum for non-iso mode Documentation sections fixes Test update * Update bus.py Black cleanup --- can/bus.py | 3 ++- can/interfaces/kvaser/canlib.py | 16 ++++++++++++++-- doc/interfaces/pcan.rst | 7 +++++++ doc/interfaces/socketcan.rst | 5 +++++ test/test_kvaser.py | 27 +++++++++++++++++++++++++++ 5 files changed, 55 insertions(+), 3 deletions(-) diff --git a/can/bus.py b/can/bus.py index 0b5c9e762..954c78c5f 100644 --- a/can/bus.py +++ b/can/bus.py @@ -44,7 +44,8 @@ class CanProtocol(Enum): """The CAN protocol type supported by a :class:`can.BusABC` instance""" CAN_20 = auto() - CAN_FD = auto() + CAN_FD = auto() # ISO Mode + CAN_FD_NON_ISO = auto() CAN_XL = auto() diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index 58baa1040..e7409b668 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -428,6 +428,10 @@ def __init__( computer, set this to True or set single_handle to True. :param bool fd: If CAN-FD frames should be supported. + :param bool fd_non_iso: + Open the channel in Non-ISO (Bosch) FD mode. Only applies for FD buses. + This changes the handling of the stuff-bit counter and the CRC. Defaults + to False (ISO mode) :param bool exclusive: Don't allow sharing of this CANlib channel. :param bool override_exclusive: @@ -453,6 +457,7 @@ def __init__( accept_virtual = kwargs.get("accept_virtual", True) fd = isinstance(timing, BitTimingFd) if timing else kwargs.get("fd", False) data_bitrate = kwargs.get("data_bitrate", None) + fd_non_iso = kwargs.get("fd_non_iso", False) try: channel = int(channel) @@ -461,7 +466,11 @@ def __init__( self.channel = channel self.single_handle = single_handle - self._can_protocol = CanProtocol.CAN_FD if fd else CanProtocol.CAN_20 + self._can_protocol = CanProtocol.CAN_20 + if fd_non_iso: + self._can_protocol = CanProtocol.CAN_FD_NON_ISO + elif fd: + self._can_protocol = CanProtocol.CAN_FD log.debug("Initialising bus instance") num_channels = ctypes.c_int(0) @@ -482,7 +491,10 @@ def __init__( if accept_virtual: flags |= canstat.canOPEN_ACCEPT_VIRTUAL if fd: - flags |= canstat.canOPEN_CAN_FD + if fd_non_iso: + flags |= canstat.canOPEN_CAN_FD_NONISO + else: + flags |= canstat.canOPEN_CAN_FD log.debug("Creating read handle to bus channel: %s", channel) self._read_handle = canOpenChannel(channel, flags) diff --git a/doc/interfaces/pcan.rst b/doc/interfaces/pcan.rst index 790264627..88ab6838d 100644 --- a/doc/interfaces/pcan.rst +++ b/doc/interfaces/pcan.rst @@ -37,7 +37,14 @@ Here is an example configuration file for using `PCAN-USB Date: Sat, 22 Jun 2024 17:23:46 +0700 Subject: [PATCH 025/127] gs_usb: Use BitTiming internally to configure bitrate (#1748) * gs_usb: Use BitTiming to configure bitrate. * use kwargs and format black --------- Co-authored-by: Tuwuh S Wibowo Co-authored-by: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> --- can/interfaces/gs_usb.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/can/interfaces/gs_usb.py b/can/interfaces/gs_usb.py index 21e199a30..5efbd3da6 100644 --- a/can/interfaces/gs_usb.py +++ b/can/interfaces/gs_usb.py @@ -54,7 +54,19 @@ def __init__( self.channel_info = channel self._can_protocol = can.CanProtocol.CAN_20 - self.gs_usb.set_bitrate(bitrate) + bit_timing = can.BitTiming.from_sample_point( + f_clock=self.gs_usb.device_capability.fclk_can, + bitrate=bitrate, + sample_point=87.5, + ) + props_seg = 1 + self.gs_usb.set_timing( + prop_seg=props_seg, + phase_seg1=bit_timing.tseg1 - props_seg, + phase_seg2=bit_timing.tseg2, + sjw=bit_timing.sjw, + brp=bit_timing.brp, + ) self.gs_usb.start() super().__init__( From 0de8ca82f1c18afff438a7c0efba5f512ea6581e Mon Sep 17 00:00:00 2001 From: BroderickJack <39026705+BroderickJack@users.noreply.github.com> Date: Sat, 22 Jun 2024 07:03:11 -0400 Subject: [PATCH 026/127] Support CANdapter extended length arbitration ID (#1528) * Support CANdapter extended length arbitration ID * Update can/interfaces/slcan.py More efficient check of arbitration ID Co-authored-by: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> * Add description of x extended arbitration identifier * Add test for CANDapter extended arbitration ID * Update test/test_slcan.py Type fix in slcan test Co-authored-by: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> * format black --------- Co-authored-by: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> --- can/interfaces/slcan.py | 5 ++++- test/test_slcan.py | 10 ++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index f32d82fd0..f023e084c 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -215,7 +215,10 @@ def _recv_internal( if not string: pass - elif string[0] == "T": + elif string[0] in ( + "T", + "x", # x is an alternative extended message identifier for CANDapter + ): # extended frame canId = int(string[1:9], 16) dlc = int(string[9]) diff --git a/test/test_slcan.py b/test/test_slcan.py index f74207b9f..af7ae60c4 100644 --- a/test/test_slcan.py +++ b/test/test_slcan.py @@ -43,6 +43,16 @@ def test_recv_extended(self): self.assertEqual(msg.dlc, 2) self.assertSequenceEqual(msg.data, [0xAA, 0x55]) + # Ewert Energy Systems CANDapter specific + self.serial.write(b"x12ABCDEF2AA55\r") + msg = self.bus.recv(TIMEOUT) + self.assertIsNotNone(msg) + self.assertEqual(msg.arbitration_id, 0x12ABCDEF) + self.assertEqual(msg.is_extended_id, True) + self.assertEqual(msg.is_remote_frame, False) + self.assertEqual(msg.dlc, 2) + self.assertSequenceEqual(msg.data, [0xAA, 0x55]) + def test_send_extended(self): msg = can.Message( arbitration_id=0x12ABCDEF, is_extended_id=True, data=[0xAA, 0x55] From 5508f533ab8767fd3b0e6cbdef97d4633fe24bfb Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sun, 23 Jun 2024 14:01:15 +0200 Subject: [PATCH 027/127] Update Changelog for 4.4.1 (#1802) * undo unnecessary changes * update CHANGELOG.md for 4.4.1 --- CHANGELOG.md | 23 +++++++++++++++++++++++ doc/interfaces/pcan.rst | 8 -------- doc/interfaces/socketcan.rst | 4 ---- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 404b68c3e..0d371c104 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +Version 4.4.1 +============= + +Bug Fixes +--------- +* Remove `abstractmethod` decorator from `Listener.stop()` (#1770, #1795) +* Fix `SizedRotatingLogger` file suffix bug (#1792, #1793) +* gs_usb: Use `BitTiming` class internally to configure bitrate (#1747, #1748) +* pcan: Fix unpack error in `PcanBus._detect_available_configs()` (#1767) +* socketcan: Improve error handling in `SocketcanBus.__init__()` (#1771) +* socketcan: Do not log exception on non-linux platforms (#1800) +* vector, kvaser: Activate channels after CAN filters were applied (#1413, #1708, #1796) + +Features +-------- + +* kvaser: Add support for non-ISO CAN FD (#1752) +* neovi: Return timestamps relative to epoch (#1789) +* slcan: Support CANdapter extended length arbitration ID (#1506, #1528) +* slcan: Add support for `listen_only` mode (#1496) +* vector: Add support for `listen_only` mode (#1764) + + Version 4.4.0 ============= diff --git a/doc/interfaces/pcan.rst b/doc/interfaces/pcan.rst index 88ab6838d..2f73dd3a7 100644 --- a/doc/interfaces/pcan.rst +++ b/doc/interfaces/pcan.rst @@ -37,14 +37,6 @@ Here is an example configuration file for using `PCAN-USB Date: Sun, 23 Jun 2024 14:23:06 +0200 Subject: [PATCH 028/127] bump version to 4.4.1 --- can/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/__init__.py b/can/__init__.py index d0c19f89b..4499edfcc 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -8,7 +8,7 @@ import logging from typing import Any, Dict -__version__ = "4.4.0" +__version__ = "4.4.1" __all__ = [ "ASCReader", "ASCWriter", From 478f8b8749ce08906b0386d13db289cf98e3246e Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sun, 23 Jun 2024 14:41:14 +0200 Subject: [PATCH 029/127] set version to 4.4.2 (#1803) --- CHANGELOG.md | 2 +- can/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d371c104..1d2581d9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -Version 4.4.1 +Version 4.4.2 ============= Bug Fixes diff --git a/can/__init__.py b/can/__init__.py index 4499edfcc..53aec7160 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -8,7 +8,7 @@ import logging from typing import Any, Dict -__version__ = "4.4.1" +__version__ = "4.4.2" __all__ = [ "ASCReader", "ASCWriter", From 2c90f9f8e548a204e531e427864da6b6240f0b9c Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sun, 23 Jun 2024 16:46:12 +0200 Subject: [PATCH 030/127] improve TestBusConfig (#1804) --- can/util.py | 49 ++++++++++++++++++++-------------- test/test_util.py | 68 ++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 88 insertions(+), 29 deletions(-) diff --git a/can/util.py b/can/util.py index 23ab142fb..7f2f824db 100644 --- a/can/util.py +++ b/can/util.py @@ -2,6 +2,7 @@ Utilities and configuration file parsing. """ +import contextlib import copy import functools import json @@ -243,26 +244,9 @@ def _create_bus_config(config: Dict[str, Any]) -> typechecking.BusConfig: if not 0 < port < 65535: raise ValueError("Port config must be inside 0-65535 range!") - if config.get("timing", None) is None: - try: - if set(typechecking.BitTimingFdDict.__annotations__).issubset(config): - config["timing"] = can.BitTimingFd( - **{ - key: int(config[key]) - for key in typechecking.BitTimingFdDict.__annotations__ - }, - strict=False, - ) - elif set(typechecking.BitTimingDict.__annotations__).issubset(config): - config["timing"] = can.BitTiming( - **{ - key: int(config[key]) - for key in typechecking.BitTimingDict.__annotations__ - }, - strict=False, - ) - except (ValueError, TypeError): - pass + if "timing" not in config: + if timing := _dict2timing(config): + config["timing"] = timing if "fd" in config: config["fd"] = config["fd"] not in (0, False) @@ -270,6 +254,31 @@ def _create_bus_config(config: Dict[str, Any]) -> typechecking.BusConfig: return cast(typechecking.BusConfig, config) +def _dict2timing(data: Dict[str, Any]) -> Union[BitTiming, BitTimingFd, None]: + """Try to instantiate a :class:`~can.BitTiming` or :class:`~can.BitTimingFd` from + a dictionary. Return `None` if not possible.""" + + with contextlib.suppress(ValueError, TypeError): + if set(typechecking.BitTimingFdDict.__annotations__).issubset(data): + return BitTimingFd( + **{ + key: int(data[key]) + for key in typechecking.BitTimingFdDict.__annotations__ + }, + strict=False, + ) + elif set(typechecking.BitTimingDict.__annotations__).issubset(data): + return BitTiming( + **{ + key: int(data[key]) + for key in typechecking.BitTimingDict.__annotations__ + }, + strict=False, + ) + + return None + + def set_logging_level(level_name: str) -> None: """Set the logging level for the `"can"` logger. diff --git a/test/test_util.py b/test/test_util.py index ac8c87d9e..f3524468b 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -5,6 +5,7 @@ import pytest +import can from can import BitTiming, BitTimingFd from can.exceptions import CanInitializationError from can.util import ( @@ -132,10 +133,7 @@ def _test_func3(a): class TestBusConfig(unittest.TestCase): - base_config = dict(interface="socketcan", bitrate=500_000) - port_alpha_config = dict(interface="socketcan", bitrate=500_000, port="fail123") - port_to_high_config = dict(interface="socketcan", bitrate=500_000, port="999999") - port_wrong_type_config = dict(interface="socketcan", bitrate=500_000, port=(1234,)) + base_config = {"interface": "socketcan", "bitrate": 500_000} def test_timing_can_use_int(self): """ @@ -147,17 +145,69 @@ def test_timing_can_use_int(self): _create_bus_config({**self.base_config, **timing_conf}) except TypeError as e: self.fail(e) + + def test_port_datatype(self): self.assertRaises( - ValueError, _create_bus_config, {**self.port_alpha_config, **timing_conf} + ValueError, _create_bus_config, {**self.base_config, "port": "fail123"} ) self.assertRaises( - ValueError, _create_bus_config, {**self.port_to_high_config, **timing_conf} + ValueError, _create_bus_config, {**self.base_config, "port": "999999"} ) self.assertRaises( - TypeError, - _create_bus_config, - {**self.port_wrong_type_config, **timing_conf}, + TypeError, _create_bus_config, {**self.base_config, "port": (1234,)} + ) + + try: + _create_bus_config({**self.base_config, "port": "1234"}) + except TypeError as e: + self.fail(e) + + def test_bit_timing_cfg(self): + can_cfg = _create_bus_config( + { + **self.base_config, + "f_clock": "8000000", + "brp": "1", + "tseg1": "5", + "tseg2": "2", + "sjw": "1", + "nof_samples": "1", + } + ) + timing = can_cfg["timing"] + assert isinstance(timing, can.BitTiming) + assert timing.f_clock == 8_000_000 + assert timing.brp == 1 + assert timing.tseg1 == 5 + assert timing.tseg2 == 2 + assert timing.sjw == 1 + + def test_bit_timing_fd_cfg(self): + canfd_cfg = _create_bus_config( + { + **self.base_config, + "f_clock": "80000000", + "nom_brp": "1", + "nom_tseg1": "119", + "nom_tseg2": "40", + "nom_sjw": "40", + "data_brp": "1", + "data_tseg1": "29", + "data_tseg2": "10", + "data_sjw": "10", + } ) + timing = canfd_cfg["timing"] + assert isinstance(timing, can.BitTimingFd) + assert timing.f_clock == 80_000_000 + assert timing.nom_brp == 1 + assert timing.nom_tseg1 == 119 + assert timing.nom_tseg2 == 40 + assert timing.nom_sjw == 40 + assert timing.data_brp == 1 + assert timing.data_tseg1 == 29 + assert timing.data_tseg2 == 10 + assert timing.data_sjw == 10 class TestChannel2Int(unittest.TestCase): From b552f1dd64f4cd8a561f83b306a31203c8990c9e Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Tue, 25 Jun 2024 12:07:26 +0200 Subject: [PATCH 031/127] Refactor CLI filter parsing, add tests (#1805) * refactor filter parsing, add tests * change chapter title to "Command Line Tools" * upper case --- can/logger.py | 75 ++++++++++++++++++++++++++++++--------------- can/typechecking.py | 15 +++++++++ can/viewer.py | 25 ++++++--------- doc/other-tools.rst | 2 +- doc/scripts.rst | 4 +-- test/test_logger.py | 35 +++++++++++++++++++-- test/test_viewer.py | 75 ++++++++++++++++++++++----------------------- 7 files changed, 147 insertions(+), 84 deletions(-) diff --git a/can/logger.py b/can/logger.py index 7d1ae6f66..35c3db20b 100644 --- a/can/logger.py +++ b/can/logger.py @@ -3,17 +3,26 @@ import re import sys from datetime import datetime -from typing import TYPE_CHECKING, Any, Dict, List, Sequence, Tuple, Union +from typing import ( + TYPE_CHECKING, + Any, + Dict, + List, + Optional, + Sequence, + Tuple, + Union, +) import can +from can import Bus, BusState, Logger, SizedRotatingLogger +from can.typechecking import TAdditionalCliArgs from can.util import cast_from_string -from . import Bus, BusState, Logger, SizedRotatingLogger -from .typechecking import CanFilter, CanFilters - if TYPE_CHECKING: from can.io import BaseRotatingLogger from can.io.generic import MessageWriter + from can.typechecking import CanFilter def _create_base_argument_parser(parser: argparse.ArgumentParser) -> None: @@ -60,10 +69,7 @@ def _create_base_argument_parser(parser: argparse.ArgumentParser) -> None: def _append_filter_argument( - parser: Union[ - argparse.ArgumentParser, - argparse._ArgumentGroup, - ], + parser: Union[argparse.ArgumentParser, argparse._ArgumentGroup], *args: str, **kwargs: Any, ) -> None: @@ -78,16 +84,17 @@ def _append_filter_argument( "\n ~ (matches when & mask !=" " can_id & mask)" "\nFx to show only frames with ID 0x100 to 0x103 and 0x200 to 0x20F:" - "\n python -m can.viewer -f 100:7FC 200:7F0" + "\n python -m can.viewer --filter 100:7FC 200:7F0" "\nNote that the ID and mask are always interpreted as hex values", metavar="{:,~}", nargs=argparse.ONE_OR_MORE, - default="", + action=_CanFilterAction, + dest="can_filters", **kwargs, ) -def _create_bus(parsed_args: Any, **kwargs: Any) -> can.BusABC: +def _create_bus(parsed_args: argparse.Namespace, **kwargs: Any) -> can.BusABC: logging_level_names = ["critical", "error", "warning", "info", "debug", "subdebug"] can.set_logging_level(logging_level_names[min(5, parsed_args.verbosity)]) @@ -100,16 +107,27 @@ def _create_bus(parsed_args: Any, **kwargs: Any) -> can.BusABC: config["fd"] = True if parsed_args.data_bitrate: config["data_bitrate"] = parsed_args.data_bitrate + if getattr(parsed_args, "can_filters", None): + config["can_filters"] = parsed_args.can_filters return Bus(parsed_args.channel, **config) -def _parse_filters(parsed_args: Any) -> CanFilters: - can_filters: List[CanFilter] = [] +class _CanFilterAction(argparse.Action): + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: Union[str, Sequence[Any], None], + option_string: Optional[str] = None, + ) -> None: + if not isinstance(values, list): + raise argparse.ArgumentError(None, "Invalid filter argument") + + print(f"Adding filter(s): {values}") + can_filters: List[CanFilter] = [] - if parsed_args.filter: - print(f"Adding filter(s): {parsed_args.filter}") - for filt in parsed_args.filter: + for filt in values: if ":" in filt: parts = filt.split(":") can_id = int(parts[0], base=16) @@ -122,12 +140,10 @@ def _parse_filters(parsed_args: Any) -> CanFilters: raise argparse.ArgumentError(None, "Invalid filter argument") can_filters.append({"can_id": can_id, "can_mask": can_mask}) - return can_filters + setattr(namespace, self.dest, can_filters) -def _parse_additional_config( - unknown_args: Sequence[str], -) -> Dict[str, Union[str, int, float, bool]]: +def _parse_additional_config(unknown_args: Sequence[str]) -> TAdditionalCliArgs: for arg in unknown_args: if not re.match(r"^--[a-zA-Z\-]*?=\S*?$", arg): raise ValueError(f"Parsing argument {arg} failed") @@ -142,12 +158,18 @@ def _split_arg(_arg: str) -> Tuple[str, str]: return args -def main() -> None: +def _parse_logger_args( + args: List[str], +) -> Tuple[argparse.Namespace, TAdditionalCliArgs]: + """Parse command line arguments for logger script.""" + parser = argparse.ArgumentParser( description="Log CAN traffic, printing messages to stdout or to a " "given file.", ) + # Generate the standard arguments: + # Channel, bitrate, data_bitrate, interface, app_name, CAN-FD support _create_base_argument_parser(parser) parser.add_argument( @@ -200,13 +222,18 @@ def main() -> None: ) # print help message when no arguments were given - if len(sys.argv) < 2: + if not args: parser.print_help(sys.stderr) raise SystemExit(errno.EINVAL) - results, unknown_args = parser.parse_known_args() + results, unknown_args = parser.parse_known_args(args) additional_config = _parse_additional_config([*results.extra_args, *unknown_args]) - bus = _create_bus(results, can_filters=_parse_filters(results), **additional_config) + return results, additional_config + + +def main() -> None: + results, additional_config = _parse_logger_args(sys.argv[1:]) + bus = _create_bus(results, **additional_config) if results.active: bus.state = BusState.ACTIVE diff --git a/can/typechecking.py b/can/typechecking.py index 89f978a1a..284dd8aba 100644 --- a/can/typechecking.py +++ b/can/typechecking.py @@ -2,8 +2,16 @@ """ import gzip +import struct +import sys import typing +if sys.version_info >= (3, 10): + from typing import TypeAlias +else: + from typing_extensions import TypeAlias + + if typing.TYPE_CHECKING: import os @@ -40,6 +48,13 @@ class CanFilterExtended(typing.TypedDict): BusConfig = typing.NewType("BusConfig", typing.Dict[str, typing.Any]) +# Used by CLI scripts +TAdditionalCliArgs: TypeAlias = typing.Dict[str, typing.Union[str, int, float, bool]] +TDataStructs: TypeAlias = typing.Dict[ + typing.Union[int, typing.Tuple[int, ...]], + typing.Union[struct.Struct, typing.Tuple, None], +] + class AutoDetectedConfig(typing.TypedDict): interface: str diff --git a/can/viewer.py b/can/viewer.py index 07752327d..45c313b07 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -27,17 +27,16 @@ import struct import sys import time -from typing import Dict, List, Tuple, Union +from typing import Dict, List, Tuple from can import __version__ - -from .logger import ( +from can.logger import ( _append_filter_argument, _create_base_argument_parser, _create_bus, _parse_additional_config, - _parse_filters, ) +from can.typechecking import TAdditionalCliArgs, TDataStructs logger = logging.getLogger("can.viewer") @@ -391,7 +390,9 @@ def _fill_text(self, text, width, indent): return super()._fill_text(text, width, indent) -def parse_args(args: List[str]) -> Tuple: +def _parse_viewer_args( + args: List[str], +) -> Tuple[argparse.Namespace, TDataStructs, TAdditionalCliArgs]: # Parse command line arguments parser = argparse.ArgumentParser( "python -m can.viewer", @@ -489,8 +490,6 @@ def parse_args(args: List[str]) -> Tuple: parsed_args, unknown_args = parser.parse_known_args(args) - can_filters = _parse_filters(parsed_args) - # Dictionary used to convert between Python values and C structs represented as Python strings. # If the value is 'None' then the message does not contain any data package. # @@ -511,9 +510,7 @@ def parse_args(args: List[str]) -> Tuple: # similarly the values # are divided by the value in order to convert from real units to raw integer values. - data_structs: Dict[ - Union[int, Tuple[int, ...]], Union[struct.Struct, Tuple, None] - ] = {} + data_structs: TDataStructs = {} if parsed_args.decode: if os.path.isfile(parsed_args.decode[0]): with open(parsed_args.decode[0], encoding="utf-8") as f: @@ -544,16 +541,12 @@ def parse_args(args: List[str]) -> Tuple: additional_config = _parse_additional_config( [*parsed_args.extra_args, *unknown_args] ) - return parsed_args, can_filters, data_structs, additional_config + return parsed_args, data_structs, additional_config def main() -> None: - parsed_args, can_filters, data_structs, additional_config = parse_args(sys.argv[1:]) - - if can_filters: - additional_config.update({"can_filters": can_filters}) + parsed_args, data_structs, additional_config = _parse_viewer_args(sys.argv[1:]) bus = _create_bus(parsed_args, **additional_config) - curses.wrapper(CanViewer, bus, data_structs) # type: ignore[attr-defined,unused-ignore] diff --git a/doc/other-tools.rst b/doc/other-tools.rst index eab3c4f43..db06812ca 100644 --- a/doc/other-tools.rst +++ b/doc/other-tools.rst @@ -1,4 +1,4 @@ -Other CAN bus tools +Other CAN Bus Tools =================== In order to keep the project maintainable, the scope of the package is limited to providing common diff --git a/doc/scripts.rst b/doc/scripts.rst index e3a59a409..520b19177 100644 --- a/doc/scripts.rst +++ b/doc/scripts.rst @@ -1,5 +1,5 @@ -Scripts -======= +Command Line Tools +================== The following modules are callable from ``python-can``. diff --git a/test/test_logger.py b/test/test_logger.py index 083e4d19c..32fc987b4 100644 --- a/test/test_logger.py +++ b/test/test_logger.py @@ -16,8 +16,6 @@ import can import can.logger -from .config import * - class TestLoggerScriptModule(unittest.TestCase): def setUp(self) -> None: @@ -108,6 +106,39 @@ def test_log_virtual_sizedlogger(self): self.assertSuccessfullCleanup() self.mock_logger_sized.assert_called_once() + def test_parse_logger_args(self): + args = self.baseargs + [ + "--bitrate", + "250000", + "--fd", + "--data_bitrate", + "2000000", + "--receive-own-messages=True", + ] + results, additional_config = can.logger._parse_logger_args(args[1:]) + assert results.interface == "virtual" + assert results.bitrate == 250_000 + assert results.fd is True + assert results.data_bitrate == 2_000_000 + assert additional_config["receive_own_messages"] is True + + def test_parse_can_filters(self): + expected_can_filters = [{"can_id": 0x100, "can_mask": 0x7FC}] + results, additional_config = can.logger._parse_logger_args( + ["--filter", "100:7FC", "--bitrate", "250000"] + ) + assert results.can_filters == expected_can_filters + + def test_parse_can_filters_list(self): + expected_can_filters = [ + {"can_id": 0x100, "can_mask": 0x7FC}, + {"can_id": 0x200, "can_mask": 0x7F0}, + ] + results, additional_config = can.logger._parse_logger_args( + ["--filter", "100:7FC", "200:7F0", "--bitrate", "250000"] + ) + assert results.can_filters == expected_can_filters + def test_parse_additional_config(self): unknown_args = [ "--app-name=CANalyzer", diff --git a/test/test_viewer.py b/test/test_viewer.py index ecc594915..3bd32b25a 100644 --- a/test/test_viewer.py +++ b/test/test_viewer.py @@ -36,7 +36,7 @@ import pytest import can -from can.viewer import CanViewer, parse_args +from can.viewer import CanViewer, _parse_viewer_args # Allow the curses module to be missing (e.g. on PyPy on Windows) try: @@ -397,19 +397,19 @@ def test_pack_unpack(self): ) def test_parse_args(self): - parsed_args, _, _, _ = parse_args(["-b", "250000"]) + parsed_args, _, _ = _parse_viewer_args(["-b", "250000"]) self.assertEqual(parsed_args.bitrate, 250000) - parsed_args, _, _, _ = parse_args(["--bitrate", "500000"]) + parsed_args, _, _ = _parse_viewer_args(["--bitrate", "500000"]) self.assertEqual(parsed_args.bitrate, 500000) - parsed_args, _, _, _ = parse_args(["-c", "can0"]) + parsed_args, _, _ = _parse_viewer_args(["-c", "can0"]) self.assertEqual(parsed_args.channel, "can0") - parsed_args, _, _, _ = parse_args(["--channel", "PCAN_USBBUS1"]) + parsed_args, _, _ = _parse_viewer_args(["--channel", "PCAN_USBBUS1"]) self.assertEqual(parsed_args.channel, "PCAN_USBBUS1") - parsed_args, _, data_structs, _ = parse_args(["-d", "100: Date: Wed, 26 Jun 2024 23:00:12 -0700 Subject: [PATCH 032/127] Resolve AttributeError within NicanError (#1806) Co-authored-by: Vijayakumar Subbiah --- can/interfaces/nican.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/interfaces/nican.py b/can/interfaces/nican.py index f4b1a37f0..8a2efade7 100644 --- a/can/interfaces/nican.py +++ b/can/interfaces/nican.py @@ -89,7 +89,7 @@ class NicanError(CanError): def __init__(self, function, error_code: int, arguments) -> None: super().__init__( - message=f"{function} failed: {get_error_message(self.error_code)}", + message=f"{function} failed: {get_error_message(error_code)}", error_code=error_code, ) From 5cc25344e801b023f6155ef865f8c77f1abda446 Mon Sep 17 00:00:00 2001 From: Liam Kinne Date: Wed, 3 Jul 2024 22:06:07 +1000 Subject: [PATCH 033/127] socketcand: show actual result as well as expectation (#1807) --- can/interfaces/socketcand/socketcand.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/interfaces/socketcand/socketcand.py b/can/interfaces/socketcand/socketcand.py index 2cfdf54e5..7a2cc6fd0 100644 --- a/can/interfaces/socketcand/socketcand.py +++ b/can/interfaces/socketcand/socketcand.py @@ -323,7 +323,7 @@ def _expect_msg(self, msg): if self.__tcp_tune: self.__socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_QUICKACK, 1) if not ascii_msg == msg: - raise can.CanError(f"{msg} message expected!") + raise can.CanError(f"Expected '{msg}' got: '{ascii_msg}'") def send(self, msg, timeout=None): """Transmit a message to the CAN bus. From c87db06a456e0c391018fedf3f18bb67f5c9e27c Mon Sep 17 00:00:00 2001 From: Federico Spada <160390475+FedericoSpada@users.noreply.github.com> Date: Wed, 3 Jul 2024 14:40:56 +0200 Subject: [PATCH 034/127] Added extra info for Kvaser dongles (#1797) * Added extra info for Kvaser dongles Changed Kvaser auto-detection function to return additional info regarding the dongles found, such as Serial Number, Name and Dongle Channel number. In this way it's possible to discern between different physical dongles connected to the PC and if they are Virtual Channels or not. * Updated key value for dongle name Changed "name" into "device_name" as suggested by python-can owner. * Fixed retrocompatibility problem Changed " with ' for retrocompatibility problem with older Python versions. * Fixed failed Test I've updated the format using Black and modified test_kvaser.py\test_available_configs to consider the new _detect_available_configs output. * Fixed missed changes --- can/interfaces/kvaser/canlib.py | 23 +++++++++++++++-------- test/test_kvaser.py | 16 ++++++++++++++-- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index e7409b668..5501c311d 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -479,6 +479,7 @@ def __init__( log.info("Found %d available channels", num_channels) for idx in range(num_channels): channel_info = get_channel_info(idx) + channel_info = f'{channel_info["device_name"]}, S/N {channel_info["serial"]} (#{channel_info["dongle_channel"]})' log.info("%d: %s", idx, channel_info) if idx == channel: self.channel_info = channel_info @@ -766,16 +767,19 @@ def get_stats(self) -> structures.BusStatistics: @staticmethod def _detect_available_configs(): - num_channels = ctypes.c_int(0) + config_list = [] + try: + num_channels = ctypes.c_int(0) canGetNumberOfChannels(ctypes.byref(num_channels)) + + for channel in range(0, int(num_channels.value)): + info = get_channel_info(channel) + + config_list.append({"interface": "kvaser", "channel": channel, **info}) except (CANLIBError, NameError): pass - - return [ - {"interface": "kvaser", "channel": channel} - for channel in range(num_channels.value) - ] + return config_list def get_channel_info(channel): @@ -802,8 +806,11 @@ def get_channel_info(channel): ctypes.sizeof(number), ) - name_decoded = name.value.decode("ascii", errors="replace") - return f"{name_decoded}, S/N {serial.value} (#{number.value + 1})" + return { + "device_name": name.value.decode("ascii", errors="replace"), + "serial": serial.value, + "dongle_channel": number.value + 1, + } init_kvaser_library() diff --git a/test/test_kvaser.py b/test/test_kvaser.py index d7b43b55b..8d18976aa 100644 --- a/test/test_kvaser.py +++ b/test/test_kvaser.py @@ -163,8 +163,20 @@ def test_recv_standard(self): def test_available_configs(self): configs = canlib.KvaserBus._detect_available_configs() expected = [ - {"interface": "kvaser", "channel": 0}, - {"interface": "kvaser", "channel": 1}, + { + "interface": "kvaser", + "channel": 0, + "dongle_channel": 1, + "device_name": "", + "serial": 0, + }, + { + "interface": "kvaser", + "channel": 1, + "dongle_channel": 1, + "device_name": "", + "serial": 0, + }, ] self.assertListEqual(configs, expected) From acc90c6072d7ece0a4afbab2b9dd3d1ba2857275 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Thu, 11 Jul 2024 09:00:43 +0200 Subject: [PATCH 035/127] Stop notifier in examples (#1814) --- examples/print_notifier.py | 6 ++++-- examples/vcan_filtered.py | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/print_notifier.py b/examples/print_notifier.py index cb4a02799..8d55ca1dc 100755 --- a/examples/print_notifier.py +++ b/examples/print_notifier.py @@ -1,19 +1,21 @@ #!/usr/bin/env python import time + import can def main(): - with can.Bus(receive_own_messages=True) as bus: + with can.Bus(interface="virtual", receive_own_messages=True) as bus: print_listener = can.Printer() - can.Notifier(bus, [print_listener]) + notifier = can.Notifier(bus, [print_listener]) bus.send(can.Message(arbitration_id=1, is_extended_id=True)) bus.send(can.Message(arbitration_id=2, is_extended_id=True)) bus.send(can.Message(arbitration_id=1, is_extended_id=False)) time.sleep(1.0) + notifier.stop() if __name__ == "__main__": diff --git a/examples/vcan_filtered.py b/examples/vcan_filtered.py index f022759fa..9c67390ab 100755 --- a/examples/vcan_filtered.py +++ b/examples/vcan_filtered.py @@ -25,6 +25,7 @@ def main(): bus.send(can.Message(arbitration_id=1, is_extended_id=False)) time.sleep(1.0) + notifier.stop() if __name__ == "__main__": From 78e4f81f9a3bec1ce4e32b5e85e7b5545053a579 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Mon, 15 Jul 2024 09:16:03 +0200 Subject: [PATCH 036/127] use setuptools_scm (#1810) --- .github/workflows/ci.yml | 2 ++ can/__init__.py | 6 +++++- doc/conf.py | 7 ++++--- doc/development.rst | 2 +- pyproject.toml | 7 ++++--- 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4afb4c1ca..de5601ebf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -161,6 +161,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 # fetch tags for setuptools-scm - name: Set up Python uses: actions/setup-python@v5 with: diff --git a/can/__init__.py b/can/__init__.py index 53aec7160..324803b9e 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -5,10 +5,11 @@ messages on a can bus. """ +import contextlib import logging +from importlib.metadata import PackageNotFoundError, version from typing import Any, Dict -__version__ = "4.4.2" __all__ = [ "ASCReader", "ASCWriter", @@ -124,6 +125,9 @@ from .thread_safe_bus import ThreadSafeBus from .util import set_logging_level +with contextlib.suppress(PackageNotFoundError): + __version__ = version("python-can") + log = logging.getLogger("can") rc: Dict[str, Any] = {} diff --git a/doc/conf.py b/doc/conf.py index 54318883f..1322a2f83 100755 --- a/doc/conf.py +++ b/doc/conf.py @@ -9,6 +9,7 @@ import ctypes import os import sys +from importlib.metadata import version as get_version from unittest.mock import MagicMock # If extensions (or modules to document with autodoc) are in another directory, @@ -16,7 +17,6 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath("..")) -import can # pylint: disable=wrong-import-position from can import ctypesutil # pylint: disable=wrong-import-position # -- General configuration ----------------------------------------------------- @@ -27,9 +27,10 @@ # |version| and |release|, also used in various other places throughout the # built documents. # +# The full version, including alpha/beta/rc tags. +release: str = get_version("python-can") # The short X.Y version. -version = can.__version__.split("-", maxsplit=1)[0] -release = can.__version__ +version = ".".join(release.split(".")[:2]) # General information about the project. project = "python-can" diff --git a/doc/development.rst b/doc/development.rst index f635ffc06..c83f0f213 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -129,7 +129,7 @@ Manual release steps (deprecated) --------------------------------- - Create a temporary virtual environment. +- Create a new tag in the repository. Use `semantic versioning `__. - Build with ``pipx run build`` -- Create and upload the distribution: ``python setup.py sdist bdist_wheel``. - Sign the packages with gpg ``gpg --detach-sign -a dist/python_can-X.Y.Z-py3-none-any.whl``. - Upload with twine ``twine upload dist/python-can-X.Y.Z*``. diff --git a/pyproject.toml b/pyproject.toml index 08b501c60..99a54a5df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools >= 67.7"] +requires = ["setuptools >= 67.7", "setuptools_scm>=8"] build-backend = "setuptools.build_meta" [project] @@ -84,8 +84,6 @@ mf4 = ["asammdf>=6.0.0"] [tool.setuptools.dynamic] readme = { file = "README.rst" } -version = { attr = "can.__version__" } - [tool.setuptools.package-data] "*" = ["README.rst", "CONTRIBUTORS.txt", "LICENSE.txt", "CHANGELOG.md"] doc = ["*.*"] @@ -95,6 +93,9 @@ can = ["py.typed"] [tool.setuptools.packages.find] include = ["can*"] +[tool.setuptools_scm] +# can be empty if no extra settings are needed, presence enables setuptools_scm + [tool.mypy] warn_return_any = true warn_unused_configs = true From a598e22f4d2d8897c943a7ebb909c0c2892fef5d Mon Sep 17 00:00:00 2001 From: RitheeshBaradwaj Date: Mon, 8 Jul 2024 22:38:51 +0900 Subject: [PATCH 037/127] fix: handle "Start of measurement" line in ASCReader --- CONTRIBUTORS.txt | 3 ++- can/io/asc.py | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index ae7792e42..389d26412 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -81,4 +81,5 @@ Felix Nieuwenhuizen @fjburgos @pkess @felixn -@Tbruno25 \ No newline at end of file +@Tbruno25 +@RitheeshBaradwaj diff --git a/can/io/asc.py b/can/io/asc.py index 2955ad7f9..af9812c43 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -275,6 +275,11 @@ def __iter__(self) -> Generator[Message, None, None]: ) continue + # Handle the "Start of measurement" line + if line.startswith("0.000000") and "Start of measurement" in line: + # Skip this line as it's just an indicator + continue + if not ASC_MESSAGE_REGEX.match(line): # line might be a comment, chip status, # J1939 message or some other unsupported event From d34b2d62fe103d203967e8ce2d3481d89e9afc18 Mon Sep 17 00:00:00 2001 From: RitheeshBaradwaj Date: Tue, 9 Jul 2024 08:44:41 +0900 Subject: [PATCH 038/127] chore: use regex to identify start line --- can/io/asc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/io/asc.py b/can/io/asc.py index af9812c43..3a14f0a88 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -276,7 +276,7 @@ def __iter__(self) -> Generator[Message, None, None]: continue # Handle the "Start of measurement" line - if line.startswith("0.000000") and "Start of measurement" in line: + if re.match(r"^\d+\.\d+\s+Start of measurement", line): # Skip this line as it's just an indicator continue From aeff58dc9504c4a1b8d6cdb0a2a08f7a2e7384fa Mon Sep 17 00:00:00 2001 From: Ben Gardiner Date: Sat, 10 Aug 2024 05:44:59 -0400 Subject: [PATCH 039/127] gs_usb command-line support (and documentation updates and stability fixes) (#1790) * gs_usb, doc: correct docs to indicate libusbK not libusb-win32 * gs_usb, interface: fix bug on second .start() by repeating in shutdown() v2: updates as per review https://github.com/hardbyte/python-can/pull/1790#pullrequestreview-2121408123 by @zariiii9003 * gs_usb, interface: set a default bitrate of 500k (like many other interfaces) * gs_usb, interface: support this interface on command-line with e.g. can.logger by treating channel like index when all other arguments are missing * gs_usb: improve docs, don't use dev reference before assignment as requested in review https://github.com/hardbyte/python-can/pull/1790#pullrequestreview-2121408123 by @zariiii9003 * gs_usb: fix bug in transmit when frame timestamp is set (causes failure to pack 64bit host timestamp into gs_usb field) --- can/interfaces/gs_usb.py | 27 +++++++++++++++++++++++++-- doc/interfaces/gs_usb.rst | 7 +++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/can/interfaces/gs_usb.py b/can/interfaces/gs_usb.py index 5efbd3da6..6268350ee 100644 --- a/can/interfaces/gs_usb.py +++ b/can/interfaces/gs_usb.py @@ -17,7 +17,7 @@ class GsUsbBus(can.BusABC): def __init__( self, channel, - bitrate, + bitrate: int = 500_000, index=None, bus=None, address=None, @@ -33,11 +33,16 @@ def __init__( :param can_filters: not supported :param bitrate: CAN network bandwidth (bits/s) """ + self._is_shutdown = False if (index is not None) and ((bus or address) is not None): raise CanInitializationError( "index and bus/address cannot be used simultaneously" ) + if index is None and address is None and bus is None: + index = channel + + self._index = None if index is not None: devs = GsUsb.scan() if len(devs) <= index: @@ -45,6 +50,7 @@ def __init__( f"Cannot find device {index}. Devices found: {len(devs)}" ) gs_usb = devs[index] + self._index = index else: gs_usb = GsUsb.find(bus=bus, address=address) if not gs_usb: @@ -68,6 +74,7 @@ def __init__( brp=bit_timing.brp, ) self.gs_usb.start() + self._bitrate = bitrate super().__init__( channel=channel, @@ -102,7 +109,7 @@ def send(self, msg: can.Message, timeout: Optional[float] = None): frame = GsUsbFrame() frame.can_id = can_id frame.can_dlc = msg.dlc - frame.timestamp_us = int(msg.timestamp * 1000000) + frame.timestamp_us = 0 # timestamp frame field is only useful on receive frame.data = list(msg.data) try: @@ -154,5 +161,21 @@ def _recv_internal( return msg, False def shutdown(self): + if self._is_shutdown: + return + super().shutdown() self.gs_usb.stop() + if self._index is not None: + # Avoid errors on subsequent __init() by repeating the .scan() and .start() that would otherwise fail + # the next time the device is opened in __init__() + devs = GsUsb.scan() + if self._index < len(devs): + gs_usb = devs[self._index] + try: + gs_usb.set_bitrate(self._bitrate) + gs_usb.start() + gs_usb.stop() + except usb.core.USBError: + pass + self._is_shutdown = True diff --git a/doc/interfaces/gs_usb.rst b/doc/interfaces/gs_usb.rst index 3a869911c..e9c0131c5 100755 --- a/doc/interfaces/gs_usb.rst +++ b/doc/interfaces/gs_usb.rst @@ -8,13 +8,16 @@ and candleLight USB CAN interfaces. Install: ``pip install "python-can[gs_usb]"`` -Usage: pass device ``index`` (starting from 0) if using automatic device detection: +Usage: pass device ``index`` or ``channel`` (starting from 0) if using automatic device detection: :: import can + import usb + dev = usb.core.find(idVendor=0x1D50, idProduct=0x606F) bus = can.Bus(interface="gs_usb", channel=dev.product, index=0, bitrate=250000) + bus = can.Bus(interface="gs_usb", channel=0, bitrate=250000) # same Alternatively, pass ``bus`` and ``address`` to open a specific device. The parameters can be got by ``pyusb`` as shown below: @@ -50,7 +53,7 @@ Windows, Linux and Mac. ``libusb`` must be installed. On Windows a tool such as `Zadig `_ can be used to set the USB device driver to - ``libusb-win32``. + ``libusbK``. Supplementary Info From 971c3319f6543e461cb6b9205424226d3426222e Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 10 Aug 2024 16:26:45 +0200 Subject: [PATCH 040/127] Test on Python 3.13 (#1833) * update linters * test python 3.13 * make pywin32 optional, refactor broadcastmanager.py * fix pylint * update pytest to fix python3.12 CI * fix test * fix deprecation warning * make _Pywin32Event private * try to fix PyPy * add classifier for 3.13 * reduce scope of send_lock context manager --- .github/workflows/ci.yml | 8 +- can/broadcastmanager.py | 158 ++++++++++++++-------- can/interfaces/usb2can/serial_selector.py | 4 +- can/io/trc.py | 6 +- can/notifier.py | 19 ++- pyproject.toml | 10 +- test/network_test.py | 2 +- test/simplecyclic_test.py | 4 +- tox.ini | 5 +- 9 files changed, 133 insertions(+), 83 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de5601ebf..cd535d4e6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,7 @@ jobs: "3.10", "3.11", "3.12", + "3.13", "pypy-3.8", "pypy-3.9", ] @@ -34,12 +35,6 @@ jobs: include: - { python-version: "3.8", os: "macos-13", experimental: false } - { python-version: "3.9", os: "macos-13", experimental: false } - # uncomment when python 3.13.0 alpha is available - #include: - # # Only test on a single configuration while there are just pre-releases - # - os: ubuntu-latest - # experimental: true - # python-version: "3.13.0-alpha - 3.13.0" fail-fast: false steps: - uses: actions/checkout@v4 @@ -47,6 +42,7 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + allow-prereleases: true - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index a610b7a8a..6ca9c61b3 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -7,10 +7,21 @@ import abc import logging +import platform import sys import threading import time -from typing import TYPE_CHECKING, Callable, Final, Optional, Sequence, Tuple, Union +import warnings +from typing import ( + TYPE_CHECKING, + Callable, + Final, + Optional, + Sequence, + Tuple, + Union, + cast, +) from can import typechecking from can.message import Message @@ -19,22 +30,61 @@ from can.bus import BusABC -# try to import win32event for event-based cyclic send task (needs the pywin32 package) -USE_WINDOWS_EVENTS = False -try: - import pywintypes - import win32event +log = logging.getLogger("can.bcm") +NANOSECONDS_IN_SECOND: Final[int] = 1_000_000_000 - # Python 3.11 provides a more precise sleep implementation on Windows, so this is not necessary. - # Put version check here, so mypy does not complain about `win32event` not being defined. - if sys.version_info < (3, 11): - USE_WINDOWS_EVENTS = True -except ImportError: - pass -log = logging.getLogger("can.bcm") +class _Pywin32Event: + handle: int -NANOSECONDS_IN_SECOND: Final[int] = 1_000_000_000 + +class _Pywin32: + def __init__(self) -> None: + import pywintypes # pylint: disable=import-outside-toplevel,import-error + import win32event # pylint: disable=import-outside-toplevel,import-error + + self.pywintypes = pywintypes + self.win32event = win32event + + def create_timer(self) -> _Pywin32Event: + try: + event = self.win32event.CreateWaitableTimerEx( + None, + None, + self.win32event.CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, + self.win32event.TIMER_ALL_ACCESS, + ) + except ( + AttributeError, + OSError, + self.pywintypes.error, # pylint: disable=no-member + ): + event = self.win32event.CreateWaitableTimer(None, False, None) + + return cast(_Pywin32Event, event) + + def set_timer(self, event: _Pywin32Event, period_ms: int) -> None: + self.win32event.SetWaitableTimer(event.handle, 0, period_ms, None, None, False) + + def stop_timer(self, event: _Pywin32Event) -> None: + self.win32event.SetWaitableTimer(event.handle, 0, 0, None, None, False) + + def wait_0(self, event: _Pywin32Event) -> None: + self.win32event.WaitForSingleObject(event.handle, 0) + + def wait_inf(self, event: _Pywin32Event) -> None: + self.win32event.WaitForSingleObject( + event.handle, + self.win32event.INFINITE, + ) + + +PYWIN32: Optional[_Pywin32] = None +if sys.platform == "win32" and sys.version_info < (3, 11): + try: + PYWIN32 = _Pywin32() + except ImportError: + pass class CyclicTask(abc.ABC): @@ -254,25 +304,30 @@ def __init__( self.on_error = on_error self.modifier_callback = modifier_callback - if USE_WINDOWS_EVENTS: - self.period_ms = int(round(period * 1000, 0)) - try: - self.event = win32event.CreateWaitableTimerEx( - None, - None, - win32event.CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, - win32event.TIMER_ALL_ACCESS, - ) - except (AttributeError, OSError, pywintypes.error): - self.event = win32event.CreateWaitableTimer(None, False, None) + self.period_ms = int(round(period * 1000, 0)) + + self.event: Optional[_Pywin32Event] = None + if PYWIN32: + self.event = PYWIN32.create_timer() + elif ( + sys.platform == "win32" + and sys.version_info < (3, 11) + and platform.python_implementation() == "CPython" + ): + warnings.warn( + f"{self.__class__.__name__} may achieve better timing accuracy " + f"if the 'pywin32' package is installed.", + RuntimeWarning, + stacklevel=1, + ) self.start() def stop(self) -> None: self.stopped = True - if USE_WINDOWS_EVENTS: + if self.event and PYWIN32: # Reset and signal any pending wait by setting the timer to 0 - win32event.SetWaitableTimer(self.event.handle, 0, 0, None, None, False) + PYWIN32.stop_timer(self.event) def start(self) -> None: self.stopped = False @@ -281,10 +336,8 @@ def start(self) -> None: self.thread = threading.Thread(target=self._run, name=name) self.thread.daemon = True - if USE_WINDOWS_EVENTS: - win32event.SetWaitableTimer( - self.event.handle, 0, self.period_ms, None, None, False - ) + if self.event and PYWIN32: + PYWIN32.set_timer(self.event, self.period_ms) self.thread.start() @@ -292,43 +345,40 @@ def _run(self) -> None: msg_index = 0 msg_due_time_ns = time.perf_counter_ns() - if USE_WINDOWS_EVENTS: + if self.event and PYWIN32: # Make sure the timer is non-signaled before entering the loop - win32event.WaitForSingleObject(self.event.handle, 0) + PYWIN32.wait_0(self.event) while not self.stopped: if self.end_time is not None and time.perf_counter() >= self.end_time: break - # Prevent calling bus.send from multiple threads - with self.send_lock: - try: - if self.modifier_callback is not None: - self.modifier_callback(self.messages[msg_index]) + try: + if self.modifier_callback is not None: + self.modifier_callback(self.messages[msg_index]) + with self.send_lock: + # Prevent calling bus.send from multiple threads self.bus.send(self.messages[msg_index]) - except Exception as exc: # pylint: disable=broad-except - log.exception(exc) + except Exception as exc: # pylint: disable=broad-except + log.exception(exc) - # stop if `on_error` callback was not given - if self.on_error is None: - self.stop() - raise exc + # stop if `on_error` callback was not given + if self.on_error is None: + self.stop() + raise exc - # stop if `on_error` returns False - if not self.on_error(exc): - self.stop() - break + # stop if `on_error` returns False + if not self.on_error(exc): + self.stop() + break - if not USE_WINDOWS_EVENTS: + if not self.event: msg_due_time_ns += self.period_ns msg_index = (msg_index + 1) % len(self.messages) - if USE_WINDOWS_EVENTS: - win32event.WaitForSingleObject( - self.event.handle, - win32event.INFINITE, - ) + if self.event and PYWIN32: + PYWIN32.wait_inf(self.event) else: # Compensate for the time it takes to send the message delay_ns = msg_due_time_ns - time.perf_counter_ns() diff --git a/can/interfaces/usb2can/serial_selector.py b/can/interfaces/usb2can/serial_selector.py index c2e48ff97..92a3a07a2 100644 --- a/can/interfaces/usb2can/serial_selector.py +++ b/can/interfaces/usb2can/serial_selector.py @@ -9,7 +9,9 @@ try: import win32com.client except ImportError: - log.warning("win32com.client module required for usb2can") + log.warning( + "win32com.client module required for usb2can. Install the 'pywin32' package." + ) raise diff --git a/can/io/trc.py b/can/io/trc.py index f568f93a5..f0595c23e 100644 --- a/can/io/trc.py +++ b/can/io/trc.py @@ -343,7 +343,9 @@ def _write_header_v1_0(self, start_time: datetime) -> None: self.file.writelines(line + "\n" for line in lines) def _write_header_v2_1(self, start_time: datetime) -> None: - header_time = start_time - datetime(year=1899, month=12, day=30) + header_time = start_time - datetime( + year=1899, month=12, day=30, tzinfo=timezone.utc + ) lines = [ ";$FILEVERSION=2.1", f";$STARTTIME={header_time/timedelta(days=1)}", @@ -399,7 +401,7 @@ def _format_message_init(self, msg, channel): def write_header(self, timestamp: float) -> None: # write start of file header - start_time = datetime.utcfromtimestamp(timestamp) + start_time = datetime.fromtimestamp(timestamp, timezone.utc) if self.file_version == TRCFileVersion.V1_0: self._write_header_v1_0(start_time) diff --git a/can/notifier.py b/can/notifier.py index 34fcf74fa..088f0802e 100644 --- a/can/notifier.py +++ b/can/notifier.py @@ -7,7 +7,7 @@ import logging import threading import time -from typing import Awaitable, Callable, Iterable, List, Optional, Union, cast +from typing import Any, Awaitable, Callable, Iterable, List, Optional, Union from can.bus import BusABC from can.listener import Listener @@ -110,16 +110,13 @@ def stop(self, timeout: float = 5) -> None: def _rx_thread(self, bus: BusABC) -> None: # determine message handling callable early, not inside while loop - handle_message = cast( - Callable[[Message], None], - ( - self._on_message_received - if self._loop is None - else functools.partial( - self._loop.call_soon_threadsafe, self._on_message_received - ) - ), - ) + if self._loop: + handle_message: Callable[[Message], Any] = functools.partial( + self._loop.call_soon_threadsafe, + self._on_message_received, # type: ignore[arg-type] + ) + else: + handle_message = self._on_message_received while self._running: try: diff --git a/pyproject.toml b/pyproject.toml index 99a54a5df..8b76bc5e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,6 @@ dependencies = [ "packaging >= 23.1", "typing_extensions>=3.10.0.0", "msgpack~=1.0.0; platform_system != 'Windows'", - "pywin32>=305; platform_system == 'Windows' and platform_python_implementation == 'CPython'", ] requires-python = ">=3.8" license = { text = "LGPL v3" } @@ -35,6 +34,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Embedded Systems", @@ -61,10 +61,11 @@ changelog = "https://github.com/hardbyte/python-can/blob/develop/CHANGELOG.md" [project.optional-dependencies] lint = [ "pylint==3.2.*", - "ruff==0.4.8", - "black==24.4.*", - "mypy==1.10.*", + "ruff==0.5.7", + "black==24.8.*", + "mypy==1.11.*", ] +pywin32 = ["pywin32>=305"] seeedstudio = ["pyserial>=3.0"] serial = ["pyserial~=3.0"] neovi = ["filelock", "python-ics>=2.12"] @@ -171,6 +172,7 @@ known-first-party = ["can"] [tool.pylint] disable = [ + "c-extension-no-member", "cyclic-import", "duplicate-code", "fixme", diff --git a/test/network_test.py b/test/network_test.py index b0fcba37f..50070ef40 100644 --- a/test/network_test.py +++ b/test/network_test.py @@ -84,7 +84,7 @@ def testProducerConsumer(self): ready = threading.Event() msg_read = threading.Event() - self.server_bus = can.interface.Bus(channel=channel) + self.server_bus = can.interface.Bus(channel=channel, interface="virtual") t = threading.Thread(target=self.producer, args=(ready, msg_read)) t.start() diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py index 21e88e9f0..34d29b8b6 100644 --- a/test/simplecyclic_test.py +++ b/test/simplecyclic_test.py @@ -154,7 +154,7 @@ def test_stopping_perodic_tasks(self): def test_restart_perodic_tasks(self): period = 0.01 - safe_timeout = period * 5 + safe_timeout = period * 5 if not IS_PYPY else 1.0 msg = can.Message( is_extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7] @@ -241,7 +241,7 @@ def test_modifier_callback(self) -> None: msg_list: List[can.Message] = [] def increment_first_byte(msg: can.Message) -> None: - msg.data[0] += 1 + msg.data[0] = (msg.data[0] + 1) % 256 original_msg = can.Message( is_extended_id=False, arbitration_id=0x123, data=[0] * 8 diff --git a/tox.ini b/tox.ini index 477b1d4fc..1ca07a33f 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ isolated_build = true [testenv] deps = - pytest==7.3.* + pytest==8.3.* pytest-timeout==2.1.* coveralls==3.3.1 pytest-cov==4.0.0 @@ -11,7 +11,8 @@ deps = hypothesis~=6.35.0 pyserial~=3.5 parameterized~=0.8 - asammdf>=6.0;platform_python_implementation=="CPython" and python_version < "3.12" + asammdf>=6.0; platform_python_implementation=="CPython" and python_version<"3.13" + pywin32>=305; platform_system=="Windows" and platform_python_implementation=="CPython" and python_version<"3.13" commands = pytest {posargs} From 30601c70808f3feed533caf76da86b515a9a7d48 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 10 Aug 2024 18:08:40 +0200 Subject: [PATCH 041/127] fix slcan tests (#1834) --- test/test_slcan.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/test/test_slcan.py b/test/test_slcan.py index af7ae60c4..e1531e500 100644 --- a/test/test_slcan.py +++ b/test/test_slcan.py @@ -5,7 +5,7 @@ import serial -import can +import can.interfaces.slcan from .config import IS_PYPY @@ -58,9 +58,8 @@ def test_send_extended(self): arbitration_id=0x12ABCDEF, is_extended_id=True, data=[0xAA, 0x55] ) self.bus.send(msg) - expected = b"T12ABCDEF2AA55\r" - data = self.serial.read(len(expected)) - self.assertEqual(data, expected) + rx_msg = self.bus.recv(TIMEOUT) + self.assertTrue(msg.equals(rx_msg, timestamp_delta=None)) def test_recv_standard(self): self.serial.write(b"t4563112233\r") @@ -77,9 +76,8 @@ def test_send_standard(self): arbitration_id=0x456, is_extended_id=False, data=[0x11, 0x22, 0x33] ) self.bus.send(msg) - expected = b"t4563112233\r" - data = self.serial.read(len(expected)) - self.assertEqual(data, expected) + rx_msg = self.bus.recv(TIMEOUT) + self.assertTrue(msg.equals(rx_msg, timestamp_delta=None)) def test_recv_standard_remote(self): self.serial.write(b"r1238\r") @@ -95,9 +93,8 @@ def test_send_standard_remote(self): arbitration_id=0x123, is_extended_id=False, is_remote_frame=True, dlc=8 ) self.bus.send(msg) - expected = b"r1238\r" - data = self.serial.read(len(expected)) - self.assertEqual(data, expected) + rx_msg = self.bus.recv(TIMEOUT) + self.assertTrue(msg.equals(rx_msg, timestamp_delta=None)) def test_recv_extended_remote(self): self.serial.write(b"R12ABCDEF6\r") @@ -113,9 +110,8 @@ def test_send_extended_remote(self): arbitration_id=0x12ABCDEF, is_extended_id=True, is_remote_frame=True, dlc=6 ) self.bus.send(msg) - expected = b"R12ABCDEF6\r" - data = self.serial.read(len(expected)) - self.assertEqual(data, expected) + rx_msg = self.bus.recv(TIMEOUT) + self.assertTrue(msg.equals(rx_msg, timestamp_delta=None)) def test_partial_recv(self): self.serial.write(b"T12ABCDEF") From 80164c134d6545e9281f7069f1e695e0499785c0 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:51:32 +0200 Subject: [PATCH 042/127] Undo #1772 (#1837) --- .github/workflows/ci.yml | 8 -------- doc/conf.py | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd535d4e6..e71df9aa6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,14 +27,6 @@ jobs: "pypy-3.8", "pypy-3.9", ] - # Python 3.9 is on macos-13 but not macos-latest (macos-14-arm64) - # https://github.com/actions/setup-python/issues/696#issuecomment-1637587760 - exclude: - - { python-version: "3.8", os: "macos-latest", experimental: false } - - { python-version: "3.9", os: "macos-latest", experimental: false } - include: - - { python-version: "3.8", os: "macos-13", experimental: false } - - { python-version: "3.9", os: "macos-13", experimental: false } fail-fast: false steps: - uses: actions/checkout@v4 diff --git a/doc/conf.py b/doc/conf.py index 1322a2f83..34ce385cb 100755 --- a/doc/conf.py +++ b/doc/conf.py @@ -68,7 +68,7 @@ graphviz_output_format = "png" # 'svg' # The suffix of source filenames. -source_suffix = ".rst" +source_suffix = {".rst": "restructuredtext"} # The encoding of source files. # source_encoding = 'utf-8-sig' From 3738c4f24f2ed75c0100553a8f2be89b3e75a7fd Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 16 Aug 2024 09:41:32 +0200 Subject: [PATCH 043/127] Replace PyPy3.8 with PyPy3.10 (#1838) --- .github/workflows/ci.yml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e71df9aa6..b3e90b445 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,8 +24,8 @@ jobs: "3.11", "3.12", "3.13", - "pypy-3.8", "pypy-3.9", + "pypy-3.10", ] fail-fast: false steps: diff --git a/pyproject.toml b/pyproject.toml index 8b76bc5e6..ce2b52f2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ changelog = "https://github.com/hardbyte/python-can/blob/develop/CHANGELOG.md" [project.optional-dependencies] lint = [ "pylint==3.2.*", - "ruff==0.5.7", + "ruff==0.6.0", "black==24.8.*", "mypy==1.11.*", ] From e291874d9f1e17f6a206d2fa9460a34450c03299 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 16 Aug 2024 15:59:24 +0200 Subject: [PATCH 044/127] add zlgcan to docs (#1839) --- doc/plugin-interface.rst | 3 +++ pyproject.toml | 1 + 2 files changed, 4 insertions(+) diff --git a/doc/plugin-interface.rst b/doc/plugin-interface.rst index 4a08ee9a7..d3e6115fd 100644 --- a/doc/plugin-interface.rst +++ b/doc/plugin-interface.rst @@ -73,8 +73,11 @@ The table below lists interface drivers that can be added by installing addition +----------------------------+-------------------------------------------------------+ | `python-can-sontheim`_ | CAN Driver for Sontheim CAN interfaces (e.g. CANfox) | +----------------------------+-------------------------------------------------------+ +| `zlgcan-driver-py`_ | Python wrapper for zlgcan-driver-rs | ++----------------------------+-------------------------------------------------------+ .. _python-can-canine: https://github.com/tinymovr/python-can-canine .. _python-can-cvector: https://github.com/zariiii9003/python-can-cvector .. _python-can-remote: https://github.com/christiansandberg/python-can-remote .. _python-can-sontheim: https://github.com/MattWoodhead/python-can-sontheim +.. _zlgcan-driver-py: https://github.com/zhuyu4839/zlgcan-driver diff --git a/pyproject.toml b/pyproject.toml index ce2b52f2e..8beb99f5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,6 +78,7 @@ pcan = ["uptime~=3.0.1"] remote = ["python-can-remote"] sontheim = ["python-can-sontheim>=0.1.2"] canine = ["python-can-canine>=0.2.2"] +zlgcan = ["zlgcan-driver-py"] viewer = [ "windows-curses; platform_system == 'Windows' and platform_python_implementation=='CPython'" ] From c7121310253c42c6410a70bc8b4f27a51b8b4411 Mon Sep 17 00:00:00 2001 From: Pierre-Luc Date: Thu, 29 Aug 2024 13:03:38 -0400 Subject: [PATCH 045/127] ThreadBasedCyclicSendTask using WIN32 API cannot handle period smaller than 1 ms (#1847) Using a win32 SetWaitableTimer with a period of `0` means that the timer will only be signaled once. This will make the `ThreadBasedCyclicSendTask` to only send one message. --- can/broadcastmanager.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 6ca9c61b3..0fa94c82c 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -308,6 +308,9 @@ def __init__( self.event: Optional[_Pywin32Event] = None if PYWIN32: + if self.period_ms == 0: + # A period of 0 would mean that the timer is signaled only once + raise ValueError("The period cannot be smaller than 0.001 (1 ms)") self.event = PYWIN32.create_timer() elif ( sys.platform == "win32" From 7302127c88f157b837eae0633eb416250d07a850 Mon Sep 17 00:00:00 2001 From: Tomas Bures Date: Fri, 6 Sep 2024 13:05:45 +0200 Subject: [PATCH 046/127] Fix for #1849 (PCAN fails when PCAN_ERROR_ILLDATA is read via ReadFD) (#1850) * When there is an invalid frame on CAN bus (in our case CAN FD), PCAN first reports result PCAN_ERROR_ILLDATA and then it send the error frame. If the PCAN_ERROR_ILLDATA is not ignored, python-can throws an exception. This fix add the ignore on the PCAN_ERROR_ILLDATA. * Fix for ruff error `can/interfaces/pcan/pcan.py:5:1: I001 [*] Import block is un-sorted or un-formatted` Added comment explaining why to ignore the PCAN_ERROR_ILLDATA. --- can/interfaces/pcan/pcan.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index 52dbb49b0..d0372a83c 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -43,6 +43,7 @@ PCAN_DICT_STATUS, PCAN_ERROR_BUSHEAVY, PCAN_ERROR_BUSLIGHT, + PCAN_ERROR_ILLDATA, PCAN_ERROR_OK, PCAN_ERROR_QRCVEMPTY, PCAN_FD_PARAMETER_LIST, @@ -555,6 +556,12 @@ def _recv_internal( elif result & (PCAN_ERROR_BUSLIGHT | PCAN_ERROR_BUSHEAVY): log.warning(self._get_formatted_error(result)) + elif result == PCAN_ERROR_ILLDATA: + # When there is an invalid frame on CAN bus (in our case CAN FD), PCAN first reports result PCAN_ERROR_ILLDATA + # and then it sends the error frame. If the PCAN_ERROR_ILLDATA is not ignored, python-can throws an exception. + # So we ignore any PCAN_ERROR_ILLDATA results here. + pass + else: raise PcanCanOperationError(self._get_formatted_error(result)) From 757370d48104ee14c51867ab9e27c525f5302bb5 Mon Sep 17 00:00:00 2001 From: Pierre-Luc Date: Fri, 13 Sep 2024 08:58:58 -0400 Subject: [PATCH 047/127] Faster Message string representation (#1858) Improved the speed of data conversion to hex string --- can/message.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/can/message.py b/can/message.py index c26733087..6dc6a83bd 100644 --- a/can/message.py +++ b/can/message.py @@ -130,12 +130,11 @@ def __str__(self) -> str: field_strings.append(flag_string) field_strings.append(f"DL: {self.dlc:2d}") - data_strings = [] + data_strings = "" if self.data is not None: - for index in range(0, min(self.dlc, len(self.data))): - data_strings.append(f"{self.data[index]:02x}") + data_strings = self.data[: min(self.dlc, len(self.data))].hex(" ") if data_strings: # if not empty - field_strings.append(" ".join(data_strings).ljust(24, " ")) + field_strings.append(data_strings.ljust(24, " ")) else: field_strings.append(" " * 24) From d4f3954b726e857528d605ea88888aab322f1750 Mon Sep 17 00:00:00 2001 From: Pierre-Luc Date: Fri, 13 Sep 2024 11:14:05 -0400 Subject: [PATCH 048/127] ASCWriter speed improvement (#1856) * ASCWriter speed improvement Changed how the message data is converted to string. This results in and 100% speed improvement. On my PC, before the change, logging 10000 messages was taking ~16 seconds and this change drop it to ~8 seconds. With this change, the ASCWriter is still one of the slowest writer we have in Python-can. * Update asc.py * Update logformats_test.py * Create single_frame.asc * Update logformats_test.py * Update test/logformats_test.py Co-authored-by: Felix Divo <4403130+felixdivo@users.noreply.github.com> --------- Co-authored-by: Felix Divo <4403130+felixdivo@users.noreply.github.com> --- can/io/asc.py | 10 +++++----- test/data/single_frame.asc | 7 +++++++ test/logformats_test.py | 27 +++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 test/data/single_frame.asc diff --git a/can/io/asc.py b/can/io/asc.py index 3a14f0a88..deb7d429e 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -9,7 +9,7 @@ import logging import re from datetime import datetime -from typing import Any, Dict, Final, Generator, List, Optional, TextIO, Union +from typing import Any, Dict, Final, Generator, Optional, TextIO, Union from ..message import Message from ..typechecking import StringPathLike @@ -439,10 +439,10 @@ def on_message_received(self, msg: Message) -> None: return if msg.is_remote_frame: dtype = f"r {msg.dlc:x}" # New after v8.5 - data: List[str] = [] + data: str = "" else: dtype = f"d {msg.dlc:x}" - data = [f"{byte:02X}" for byte in msg.data] + data = msg.data.hex(" ").upper() arb_id = f"{msg.arbitration_id:X}" if msg.is_extended_id: arb_id += "x" @@ -462,7 +462,7 @@ def on_message_received(self, msg: Message) -> None: esi=1 if msg.error_state_indicator else 0, dlc=len2dlc(msg.dlc), data_length=len(msg.data), - data=" ".join(data), + data=data, message_duration=0, message_length=0, flags=flags, @@ -478,6 +478,6 @@ def on_message_received(self, msg: Message) -> None: id=arb_id, dir="Rx" if msg.is_rx else "Tx", dtype=dtype, - data=" ".join(data), + data=data, ) self.log_event(serialized, msg.timestamp) diff --git a/test/data/single_frame.asc b/test/data/single_frame.asc new file mode 100644 index 000000000..cae9d1b4d --- /dev/null +++ b/test/data/single_frame.asc @@ -0,0 +1,7 @@ +date Sat Sep 30 15:06:13.191 2017 +base hex timestamps absolute +internal events logged +Begin Triggerblock Sat Sep 30 15:06:13.191 2017 + 0.000000 Start of measurement + 0.000000 1 123x Rx d 40 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F 30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F +End TriggerBlock diff --git a/test/logformats_test.py b/test/logformats_test.py index 71d392aa8..0fbe065d2 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -651,6 +651,33 @@ def test_write_millisecond_handling(self): self.assertEqual(expected_file.read_text(), actual_file.read_text()) + def test_write(self): + now = datetime( + year=2017, month=9, day=30, hour=15, minute=6, second=13, microsecond=191456 + ) + + # We temporarily set the locale to C to ensure test reproducibility + with override_locale(category=locale.LC_TIME, locale_str="C"): + # We mock datetime.now during ASCWriter __init__ for reproducibility + # Unfortunately, now() is a readonly attribute, so we mock datetime + with patch("can.io.asc.datetime") as mock_datetime: + mock_datetime.now.return_value = now + writer = can.ASCWriter(self.test_file_name) + + msg = can.Message( + timestamp=now.timestamp(), + arbitration_id=0x123, + data=range(64), + ) + + with writer: + writer.on_message_received(msg) + + actual_file = Path(self.test_file_name) + expected_file = self._get_logfile_location("single_frame.asc") + + self.assertEqual(expected_file.read_text(), actual_file.read_text()) + class TestBlfFileFormat(ReaderWriterTest): """Tests can.BLFWriter and can.BLFReader. From c5c18dc275613c6af648fd24978b3119344375e7 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Mon, 7 Oct 2024 11:10:12 +0200 Subject: [PATCH 049/127] Fix regex in _parse_additional_config() (#1868) --- can/logger.py | 2 +- test/test_logger.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/can/logger.py b/can/logger.py index 35c3db20b..81e9527f0 100644 --- a/can/logger.py +++ b/can/logger.py @@ -145,7 +145,7 @@ def __call__( def _parse_additional_config(unknown_args: Sequence[str]) -> TAdditionalCliArgs: for arg in unknown_args: - if not re.match(r"^--[a-zA-Z\-]*?=\S*?$", arg): + if not re.match(r"^--[a-zA-Z][a-zA-Z0-9\-]*=\S*?$", arg): raise ValueError(f"Parsing argument {arg} failed") def _split_arg(_arg: str) -> Tuple[str, str]: diff --git a/test/test_logger.py b/test/test_logger.py index 32fc987b4..10df2557b 100644 --- a/test/test_logger.py +++ b/test/test_logger.py @@ -146,6 +146,7 @@ def test_parse_additional_config(self): "--receive-own-messages=True", "--false-boolean=False", "--offset=1.5", + "--tseg1-abr=127", ] parsed_args = can.logger._parse_additional_config(unknown_args) @@ -170,6 +171,9 @@ def test_parse_additional_config(self): assert "offset" in parsed_args assert parsed_args["offset"] == 1.5 + assert "tseg1_abr" in parsed_args + assert parsed_args["tseg1_abr"] == 127 + with pytest.raises(ValueError): can.logger._parse_additional_config(["--wrong-format"]) From 950e4b40854ee97e3b771b4049a074af1935f5bc Mon Sep 17 00:00:00 2001 From: Nick Cao Date: Tue, 27 Aug 2024 09:49:41 -0400 Subject: [PATCH 050/127] Use typing_extensions.TypedDict on python < 3.12 for pydantic support Reference: https://docs.pydantic.dev/2.8/errors/usage_errors/#typed-dict-version --- can/typechecking.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/can/typechecking.py b/can/typechecking.py index 284dd8aba..b0d1c22ac 100644 --- a/can/typechecking.py +++ b/can/typechecking.py @@ -11,17 +11,22 @@ else: from typing_extensions import TypeAlias +if sys.version_info >= (3, 12): + from typing import TypedDict +else: + from typing_extensions import TypedDict + if typing.TYPE_CHECKING: import os -class CanFilter(typing.TypedDict): +class CanFilter(TypedDict): can_id: int can_mask: int -class CanFilterExtended(typing.TypedDict): +class CanFilterExtended(TypedDict): can_id: int can_mask: int extended: bool @@ -56,7 +61,7 @@ class CanFilterExtended(typing.TypedDict): ] -class AutoDetectedConfig(typing.TypedDict): +class AutoDetectedConfig(TypedDict): interface: str channel: Channel @@ -64,7 +69,7 @@ class AutoDetectedConfig(typing.TypedDict): ReadableBytesLike = typing.Union[bytes, bytearray, memoryview] -class BitTimingDict(typing.TypedDict): +class BitTimingDict(TypedDict): f_clock: int brp: int tseg1: int @@ -73,7 +78,7 @@ class BitTimingDict(typing.TypedDict): nof_samples: int -class BitTimingFdDict(typing.TypedDict): +class BitTimingFdDict(TypedDict): f_clock: int nom_brp: int nom_tseg1: int From 7a3d23fa3ba114a6cd005c726e7815732c566f6e Mon Sep 17 00:00:00 2001 From: Sebastian Wolf Date: Tue, 8 Oct 2024 16:10:46 +0200 Subject: [PATCH 051/127] Add autostart option to BusABC.send_periodic() (#1853) * Add autostart option (kwarg) to BusABC.send_periodic() to fix issue #1848 * Fix for #1849 (PCAN fails when PCAN_ERROR_ILLDATA is read via ReadFD) (#1850) * When there is an invalid frame on CAN bus (in our case CAN FD), PCAN first reports result PCAN_ERROR_ILLDATA and then it send the error frame. If the PCAN_ERROR_ILLDATA is not ignored, python-can throws an exception. This fix add the ignore on the PCAN_ERROR_ILLDATA. * Fix for ruff error `can/interfaces/pcan/pcan.py:5:1: I001 [*] Import block is un-sorted or un-formatted` Added comment explaining why to ignore the PCAN_ERROR_ILLDATA. * Format with black to pass checks * Do not ignore autostart parameter for Bus.send_periodic() on IXXAT devices * Do not ignore autostart parameter for Bis.send_periodic() on socketcan devices * Fix double start socketcan periodic * Fix link methods in docstring for start() methods of the tasks can.broadcastmanager.CyclicTask.start * Change the behaviour of autostart parameter in socketcan implementation of CyclicSendTask to not call _tx_setup() method instead of adding a parameter to it. * Fix code style (max 100 chars per line) Fix wrong docstring reference. --------- Co-authored-by: Tomas Bures Co-authored-by: Sebastian Wolf --- can/broadcastmanager.py | 4 +++- can/bus.py | 15 ++++++++++++++- can/interfaces/ixxat/canlib.py | 3 ++- can/interfaces/ixxat/canlib_vcinpl.py | 22 +++++++++++++++++++--- can/interfaces/ixxat/canlib_vcinpl2.py | 22 +++++++++++++++++++--- can/interfaces/socketcan/socketcan.py | 20 ++++++++++++++++---- 6 files changed, 73 insertions(+), 13 deletions(-) diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 0fa94c82c..bc65d3a47 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -276,6 +276,7 @@ def __init__( period: float, duration: Optional[float] = None, on_error: Optional[Callable[[Exception], bool]] = None, + autostart: bool = True, modifier_callback: Optional[Callable[[Message], None]] = None, ) -> None: """Transmits `messages` with a `period` seconds for `duration` seconds on a `bus`. @@ -324,7 +325,8 @@ def __init__( stacklevel=1, ) - self.start() + if autostart: + self.start() def stop(self) -> None: self.stopped = True diff --git a/can/bus.py b/can/bus.py index 954c78c5f..a12808ab6 100644 --- a/can/bus.py +++ b/can/bus.py @@ -215,6 +215,7 @@ def send_periodic( period: float, duration: Optional[float] = None, store_task: bool = True, + autostart: bool = True, modifier_callback: Optional[Callable[[Message], None]] = None, ) -> can.broadcastmanager.CyclicSendTaskABC: """Start sending messages at a given period on this bus. @@ -237,6 +238,10 @@ def send_periodic( :param store_task: If True (the default) the task will be attached to this Bus instance. Disable to instead manage tasks manually. + :param autostart: + If True (the default) the sending task will immediately start after creation. + Otherwise, the task has to be started by calling the + tasks :meth:`~can.RestartableCyclicTaskABC.start` method on it. :param modifier_callback: Function which should be used to modify each message's data before sending. The callback modifies the :attr:`~can.Message.data` of the @@ -272,7 +277,9 @@ def send_periodic( # Create a backend specific task; will be patched to a _SelfRemovingCyclicTask later task = cast( _SelfRemovingCyclicTask, - self._send_periodic_internal(msgs, period, duration, modifier_callback), + self._send_periodic_internal( + msgs, period, duration, autostart, modifier_callback + ), ) # we wrap the task's stop method to also remove it from the Bus's list of tasks periodic_tasks = self._periodic_tasks @@ -299,6 +306,7 @@ def _send_periodic_internal( msgs: Union[Sequence[Message], Message], period: float, duration: Optional[float] = None, + autostart: bool = True, modifier_callback: Optional[Callable[[Message], None]] = None, ) -> can.broadcastmanager.CyclicSendTaskABC: """Default implementation of periodic message sending using threading. @@ -312,6 +320,10 @@ def _send_periodic_internal( :param duration: The duration between sending each message at the given rate. If no duration is provided, the task will continue indefinitely. + :param autostart: + If True (the default) the sending task will immediately start after creation. + Otherwise, the task has to be started by calling the + tasks :meth:`~can.RestartableCyclicTaskABC.start` method on it. :return: A started task instance. Note the task can be stopped (and depending on the backend modified) by calling the @@ -328,6 +340,7 @@ def _send_periodic_internal( messages=msgs, period=period, duration=duration, + autostart=autostart, modifier_callback=modifier_callback, ) return task diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 330ccdcd9..a1693aeed 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -155,10 +155,11 @@ def _send_periodic_internal( msgs: Union[Sequence[Message], Message], period: float, duration: Optional[float] = None, + autostart: bool = True, modifier_callback: Optional[Callable[[Message], None]] = None, ) -> CyclicSendTaskABC: return self.bus._send_periodic_internal( - msgs, period, duration, modifier_callback + msgs, period, duration, autostart, modifier_callback ) def shutdown(self) -> None: diff --git a/can/interfaces/ixxat/canlib_vcinpl.py b/can/interfaces/ixxat/canlib_vcinpl.py index 709780ceb..0579d0942 100644 --- a/can/interfaces/ixxat/canlib_vcinpl.py +++ b/can/interfaces/ixxat/canlib_vcinpl.py @@ -793,6 +793,7 @@ def _send_periodic_internal( msgs: Union[Sequence[Message], Message], period: float, duration: Optional[float] = None, + autostart: bool = True, modifier_callback: Optional[Callable[[Message], None]] = None, ) -> CyclicSendTaskABC: """Send a message using built-in cyclic transmit list functionality.""" @@ -807,7 +808,12 @@ def _send_periodic_internal( self._scheduler_resolution = caps.dwClockFreq / caps.dwCmsDivisor _canlib.canSchedulerActivate(self._scheduler, constants.TRUE) return CyclicSendTask( - self._scheduler, msgs, period, duration, self._scheduler_resolution + self._scheduler, + msgs, + period, + duration, + self._scheduler_resolution, + autostart=autostart, ) # fallback to thread based cyclic task @@ -821,6 +827,7 @@ def _send_periodic_internal( msgs=msgs, period=period, duration=duration, + autostart=autostart, modifier_callback=modifier_callback, ) @@ -863,7 +870,15 @@ def state(self) -> BusState: class CyclicSendTask(LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC): """A message in the cyclic transmit list.""" - def __init__(self, scheduler, msgs, period, duration, resolution): + def __init__( + self, + scheduler, + msgs, + period, + duration, + resolution, + autostart: bool = True, + ): super().__init__(msgs, period, duration) if len(self.messages) != 1: raise ValueError( @@ -883,7 +898,8 @@ def __init__(self, scheduler, msgs, period, duration, resolution): self._msg.uMsgInfo.Bits.dlc = self.messages[0].dlc for i, b in enumerate(self.messages[0].data): self._msg.abData[i] = b - self.start() + if autostart: + self.start() def start(self): """Start transmitting message (add to list if needed).""" diff --git a/can/interfaces/ixxat/canlib_vcinpl2.py b/can/interfaces/ixxat/canlib_vcinpl2.py index 18b3a1e57..5872f76b9 100644 --- a/can/interfaces/ixxat/canlib_vcinpl2.py +++ b/can/interfaces/ixxat/canlib_vcinpl2.py @@ -937,6 +937,7 @@ def _send_periodic_internal( msgs: Union[Sequence[Message], Message], period: float, duration: Optional[float] = None, + autostart: bool = True, modifier_callback: Optional[Callable[[Message], None]] = None, ) -> CyclicSendTaskABC: """Send a message using built-in cyclic transmit list functionality.""" @@ -953,7 +954,12 @@ def _send_periodic_internal( ) # TODO: confirm _canlib.canSchedulerActivate(self._scheduler, constants.TRUE) return CyclicSendTask( - self._scheduler, msgs, period, duration, self._scheduler_resolution + self._scheduler, + msgs, + period, + duration, + self._scheduler_resolution, + autostart=autostart, ) # fallback to thread based cyclic task @@ -967,6 +973,7 @@ def _send_periodic_internal( msgs=msgs, period=period, duration=duration, + autostart=autostart, modifier_callback=modifier_callback, ) @@ -983,7 +990,15 @@ def shutdown(self): class CyclicSendTask(LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC): """A message in the cyclic transmit list.""" - def __init__(self, scheduler, msgs, period, duration, resolution): + def __init__( + self, + scheduler, + msgs, + period, + duration, + resolution, + autostart: bool = True, + ): super().__init__(msgs, period, duration) if len(self.messages) != 1: raise ValueError( @@ -1003,7 +1018,8 @@ def __init__(self, scheduler, msgs, period, duration, resolution): self._msg.uMsgInfo.Bits.dlc = self.messages[0].dlc for i, b in enumerate(self.messages[0].data): self._msg.abData[i] = b - self.start() + if autostart: + self.start() def start(self): """Start transmitting message (add to list if needed).""" diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index 03bd8c3f8..40da0d094 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -327,6 +327,7 @@ def __init__( messages: Union[Sequence[Message], Message], period: float, duration: Optional[float] = None, + autostart: bool = True, ) -> None: """Construct and :meth:`~start` a task. @@ -349,10 +350,13 @@ def __init__( self.bcm_socket = bcm_socket self.task_id = task_id - self._tx_setup(self.messages) + if autostart: + self._tx_setup(self.messages) def _tx_setup( - self, messages: Sequence[Message], raise_if_task_exists: bool = True + self, + messages: Sequence[Message], + raise_if_task_exists: bool = True, ) -> None: # Create a low level packed frame to pass to the kernel body = bytearray() @@ -813,6 +817,7 @@ def _send_periodic_internal( msgs: Union[Sequence[Message], Message], period: float, duration: Optional[float] = None, + autostart: bool = True, modifier_callback: Optional[Callable[[Message], None]] = None, ) -> can.broadcastmanager.CyclicSendTaskABC: """Start sending messages at a given period on this bus. @@ -823,13 +828,17 @@ def _send_periodic_internal( :class:`CyclicSendTask` within BCM provides flexibility to schedule CAN messages sending with the same CAN ID, but different CAN data. - :param messages: + :param msgs: The message(s) to be sent periodically. :param period: The rate in seconds at which to send the messages. :param duration: Approximate duration in seconds to continue sending messages. If no duration is provided, the task will continue indefinitely. + :param autostart: + If True (the default) the sending task will immediately start after creation. + Otherwise, the task has to be started by calling the + tasks :meth:`~can.RestartableCyclicTaskABC.start` method on it. :raises ValueError: If task identifier passed to :class:`CyclicSendTask` can't be used @@ -854,7 +863,9 @@ def _send_periodic_internal( msgs_channel = str(msgs[0].channel) if msgs[0].channel else None bcm_socket = self._get_bcm_socket(msgs_channel or self.channel) task_id = self._get_next_task_id() - task = CyclicSendTask(bcm_socket, task_id, msgs, period, duration) + task = CyclicSendTask( + bcm_socket, task_id, msgs, period, duration, autostart=autostart + ) return task # fallback to thread based cyclic task @@ -868,6 +879,7 @@ def _send_periodic_internal( msgs=msgs, period=period, duration=duration, + autostart=autostart, modifier_callback=modifier_callback, ) From a39e63eac51ae7fbb872fa997aa4730113ba766c Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Wed, 9 Oct 2024 16:09:39 +0200 Subject: [PATCH 052/127] Add tox environment for doctest (#1870) * test * use py312 to build docs --- .github/workflows/ci.yml | 18 +++--------------- .readthedocs.yml | 2 +- doc/development.rst | 3 +-- doc/scripts.rst | 4 ++++ tox.ini | 15 ++++++++++++++- 5 files changed, 23 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3e90b445..6291994a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,10 +35,6 @@ jobs: with: python-version: ${{ matrix.python-version }} allow-prereleases: true - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install tox - name: Setup SocketCAN if: ${{ matrix.os == 'ubuntu-latest' }} run: | @@ -46,7 +42,7 @@ jobs: sudo ./test/open_vcan.sh - name: Test with pytest via tox run: | - tox -e gh + pipx run tox -e gh env: # SocketCAN tests currently fail with PyPy because it does not support raw CAN sockets # See: https://foss.heptapod.net/pypy/pypy/-/issues/3809 @@ -131,18 +127,10 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.10" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -e .[canalystii,gs_usb,mf4] - pip install -r doc/doc-requirements.txt + python-version: "3.12" - name: Build documentation run: | - python -m sphinx -Wan --keep-going doc build - - name: Run doctest - run: | - python -m sphinx -b doctest -W --keep-going doc build + pipx run tox -e docs build: name: Packaging diff --git a/.readthedocs.yml b/.readthedocs.yml index 32be9c7b5..dad8c28db 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -9,7 +9,7 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.10" + python: "3.12" # Build documentation in the docs/ directory with Sphinx sphinx: diff --git a/doc/development.rst b/doc/development.rst index c83f0f213..a8332eeb6 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -48,8 +48,7 @@ The unit tests can be run with:: The documentation can be built with:: - pip install -r doc/doc-requirements.txt - python -m sphinx -an doc build + pipx run tox -e docs The linters can be run with:: diff --git a/doc/scripts.rst b/doc/scripts.rst index 520b19177..2d59b7528 100644 --- a/doc/scripts.rst +++ b/doc/scripts.rst @@ -12,12 +12,14 @@ Command line help, called with ``--help``: .. command-output:: python -m can.logger -h + :shell: can.player ---------- .. command-output:: python -m can.player -h + :shell: can.viewer @@ -52,9 +54,11 @@ By default the ``can.viewer`` uses the :doc:`/interfaces/socketcan` interface. A The full usage page can be seen below: .. command-output:: python -m can.viewer -h + :shell: can.logconvert -------------- .. command-output:: python -m can.logconvert -h + :shell: diff --git a/tox.ini b/tox.ini index 1ca07a33f..1d4e88166 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,4 @@ [tox] -isolated_build = true [testenv] deps = @@ -30,6 +29,20 @@ passenv = PY_COLORS TEST_SOCKETCAN +[testenv:docs] +description = Build and test the documentation +basepython = py312 +deps = + -r doc/doc-requirements.txt + gs-usb + +extras = + canalystii + +commands = + python -m sphinx -b html -Wan --keep-going doc build + python -m sphinx -b doctest -W --keep-going doc build + [pytest] testpaths = test From abe0db58a2db6052f8d6f9cd4ebf2b2c9f2c1ba4 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 18 Oct 2024 10:21:44 +0200 Subject: [PATCH 053/127] Set end_time in ThreadBasedCyclicSendTask.start() (#1871) --- can/broadcastmanager.py | 9 ++++++--- test/simplecyclic_test.py | 32 ++++++++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index bc65d3a47..319fb0f53 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -182,6 +182,7 @@ def __init__( """ super().__init__(messages, period) self.duration = duration + self.end_time: Optional[float] = None class RestartableCyclicTaskABC(CyclicSendTaskABC, abc.ABC): @@ -299,9 +300,6 @@ def __init__( self.send_lock = lock self.stopped = True self.thread: Optional[threading.Thread] = None - self.end_time: Optional[float] = ( - time.perf_counter() + duration if duration else None - ) self.on_error = on_error self.modifier_callback = modifier_callback @@ -341,6 +339,10 @@ def start(self) -> None: self.thread = threading.Thread(target=self._run, name=name) self.thread.daemon = True + self.end_time: Optional[float] = ( + time.perf_counter() + self.duration if self.duration else None + ) + if self.event and PYWIN32: PYWIN32.set_timer(self.event, self.period_ms) @@ -356,6 +358,7 @@ def _run(self) -> None: while not self.stopped: if self.end_time is not None and time.perf_counter() >= self.end_time: + self.stop() break try: diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py index 34d29b8b6..7116efc9d 100644 --- a/test/simplecyclic_test.py +++ b/test/simplecyclic_test.py @@ -155,14 +155,21 @@ def test_stopping_perodic_tasks(self): def test_restart_perodic_tasks(self): period = 0.01 safe_timeout = period * 5 if not IS_PYPY else 1.0 + duration = 0.3 msg = can.Message( is_extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7] ) + def _read_all_messages(_bus: can.interfaces.virtual.VirtualBus) -> None: + sleep(safe_timeout) + while not _bus.queue.empty(): + _bus.recv(timeout=period) + sleep(safe_timeout) + with can.ThreadSafeBus(interface="virtual", receive_own_messages=True) as bus: task = bus.send_periodic(msg, period) - self.assertIsInstance(task, can.broadcastmanager.RestartableCyclicTaskABC) + self.assertIsInstance(task, can.broadcastmanager.ThreadBasedCyclicSendTask) # Test that the task is sending messages sleep(safe_timeout) @@ -170,10 +177,27 @@ def test_restart_perodic_tasks(self): # Stop the task and check that messages are no longer being sent bus.stop_all_periodic_tasks(remove_tasks=False) + _read_all_messages(bus) + assert bus.queue.empty(), "messages should not have been transmitted" + + # Restart the task and check that messages are being sent again + task.start() sleep(safe_timeout) - while not bus.queue.empty(): - bus.recv(timeout=period) - sleep(safe_timeout) + assert not bus.queue.empty(), "messages should have been transmitted" + + # Stop the task and check that messages are no longer being sent + bus.stop_all_periodic_tasks(remove_tasks=False) + _read_all_messages(bus) + assert bus.queue.empty(), "messages should not have been transmitted" + + # Restart the task with limited duration and wait until it stops + task.duration = duration + task.start() + sleep(duration + safe_timeout) + assert task.stopped + assert time.time() > task.end_time + assert not bus.queue.empty(), "messages should have been transmitted" + _read_all_messages(bus) assert bus.queue.empty(), "messages should not have been transmitted" # Restart the task and check that messages are being sent again From ff01a8b073f77c184f157e7060d03ce74891e2ba Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 18 Oct 2024 11:14:26 +0200 Subject: [PATCH 054/127] Update msgpack dependency (#1875) --- pyproject.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8beb99f5e..f2b6ac04f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ dependencies = [ "wrapt~=1.10", "packaging >= 23.1", "typing_extensions>=3.10.0.0", - "msgpack~=1.0.0; platform_system != 'Windows'", + "msgpack~=1.1.0; platform_system != 'Windows'", ] requires-python = ">=3.8" license = { text = "LGPL v3" } @@ -61,9 +61,9 @@ changelog = "https://github.com/hardbyte/python-can/blob/develop/CHANGELOG.md" [project.optional-dependencies] lint = [ "pylint==3.2.*", - "ruff==0.6.0", - "black==24.8.*", - "mypy==1.11.*", + "ruff==0.7.0", + "black==24.10.*", + "mypy==1.12.*", ] pywin32 = ["pywin32>=305"] seeedstudio = ["pyserial>=3.0"] From 5be89ecf2212c5ebc373ab230d86a686cf9e3911 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 25 Oct 2024 16:34:49 +0200 Subject: [PATCH 055/127] Fix Kvaser timestamp (#1878) * get timestamp offset after canBusOn * update comment --- can/interfaces/kvaser/canlib.py | 36 ++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index 5501c311d..51a77a567 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -573,6 +573,23 @@ def __init__( ) canSetBusOutputControl(self._write_handle, can_driver_mode) + self._is_filtered = False + super().__init__( + channel=channel, + can_filters=can_filters, + **kwargs, + ) + + # activate channel after CAN filters were applied + log.debug("Go on bus") + if not self.single_handle: + canBusOn(self._read_handle) + canBusOn(self._write_handle) + + # timestamp must be set after bus is online, otherwise kvReadTimer may return erroneous values + self._timestamp_offset = self._update_timestamp_offset() + + def _update_timestamp_offset(self) -> float: timer = ctypes.c_uint(0) try: if time.get_clock_info("time").resolution > 1e-5: @@ -580,28 +597,15 @@ def __init__( kvReadTimer(self._read_handle, ctypes.byref(timer)) current_perfcounter = time.perf_counter() now = ts + (current_perfcounter - perfcounter) - self._timestamp_offset = now - (timer.value * TIMESTAMP_FACTOR) + return now - (timer.value * TIMESTAMP_FACTOR) else: kvReadTimer(self._read_handle, ctypes.byref(timer)) - self._timestamp_offset = time.time() - (timer.value * TIMESTAMP_FACTOR) + return time.time() - (timer.value * TIMESTAMP_FACTOR) except Exception as exc: # timer is usually close to 0 log.info(str(exc)) - self._timestamp_offset = time.time() - (timer.value * TIMESTAMP_FACTOR) - - self._is_filtered = False - super().__init__( - channel=channel, - can_filters=can_filters, - **kwargs, - ) - - # activate channel after CAN filters were applied - log.debug("Go on bus") - if not self.single_handle: - canBusOn(self._read_handle) - canBusOn(self._write_handle) + return time.time() - (timer.value * TIMESTAMP_FACTOR) def _apply_filters(self, filters): if filters and len(filters) == 1: From 5ae913e8459cb87d7f45326707b16a850b0a3c99 Mon Sep 17 00:00:00 2001 From: Riccardo Belli <61554895+belliriccardo@users.noreply.github.com> Date: Tue, 5 Nov 2024 21:18:04 +0100 Subject: [PATCH 056/127] Added Netronic's CANdo and CANdoISO to plugin list (#1887) --- doc/plugin-interface.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/plugin-interface.rst b/doc/plugin-interface.rst index d3e6115fd..bfdedf3c6 100644 --- a/doc/plugin-interface.rst +++ b/doc/plugin-interface.rst @@ -75,9 +75,13 @@ The table below lists interface drivers that can be added by installing addition +----------------------------+-------------------------------------------------------+ | `zlgcan-driver-py`_ | Python wrapper for zlgcan-driver-rs | +----------------------------+-------------------------------------------------------+ +| `python-can-cando`_ | Python wrapper for Netronics' CANdo and CANdoISO | ++----------------------------+-------------------------------------------------------+ .. _python-can-canine: https://github.com/tinymovr/python-can-canine .. _python-can-cvector: https://github.com/zariiii9003/python-can-cvector .. _python-can-remote: https://github.com/christiansandberg/python-can-remote .. _python-can-sontheim: https://github.com/MattWoodhead/python-can-sontheim .. _zlgcan-driver-py: https://github.com/zhuyu4839/zlgcan-driver +.. _python-can-cando: https://github.com/belliriccardo/python-can-cando + From 9a766ce01575955341767b925ce937f7aa95d301 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Wed, 6 Nov 2024 17:06:45 +0100 Subject: [PATCH 057/127] Fix CI (#1889) --- .github/workflows/ci.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6291994a0..0ddb3028c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,10 @@ jobs: with: python-version: ${{ matrix.python-version }} allow-prereleases: true + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox - name: Setup SocketCAN if: ${{ matrix.os == 'ubuntu-latest' }} run: | @@ -42,7 +46,7 @@ jobs: sudo ./test/open_vcan.sh - name: Test with pytest via tox run: | - pipx run tox -e gh + tox -e gh env: # SocketCAN tests currently fail with PyPy because it does not support raw CAN sockets # See: https://foss.heptapod.net/pypy/pypy/-/issues/3809 @@ -128,9 +132,13 @@ jobs: uses: actions/setup-python@v5 with: python-version: "3.12" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox - name: Build documentation run: | - pipx run tox -e docs + tox -e docs build: name: Packaging From 805f3fb04e75e0590881d15d62bee5fbe86feb82 Mon Sep 17 00:00:00 2001 From: cssedev <187732701+cssedev@users.noreply.github.com> Date: Fri, 15 Nov 2024 03:46:58 +0100 Subject: [PATCH 058/127] MF4 reader updates (#1892) --- can/io/mf4.py | 416 +++++++++++++++++++++++++++----------------------- 1 file changed, 229 insertions(+), 187 deletions(-) diff --git a/can/io/mf4.py b/can/io/mf4.py index 042bf8765..4f5336b42 100644 --- a/can/io/mf4.py +++ b/can/io/mf4.py @@ -5,16 +5,18 @@ the ASAM MDF standard (see https://www.asam.net/standards/detail/mdf/) """ +import abc +import heapq import logging from datetime import datetime from hashlib import md5 from io import BufferedIOBase, BytesIO from pathlib import Path -from typing import Any, BinaryIO, Generator, Optional, Union, cast +from typing import Any, BinaryIO, Dict, Generator, Iterator, List, Optional, Union, cast from ..message import Message from ..typechecking import StringPathLike -from ..util import channel2int, dlc2len, len2dlc +from ..util import channel2int, len2dlc from .generic import BinaryIOMessageReader, BinaryIOMessageWriter logger = logging.getLogger("can.io.mf4") @@ -22,10 +24,10 @@ try: import asammdf import numpy as np - from asammdf import Signal + from asammdf import Signal, Source from asammdf.blocks.mdf_v4 import MDF4 - from asammdf.blocks.v4_blocks import SourceInformation - from asammdf.blocks.v4_constants import BUS_TYPE_CAN, SOURCE_BUS + from asammdf.blocks.v4_blocks import ChannelGroup, SourceInformation + from asammdf.blocks.v4_constants import BUS_TYPE_CAN, FLAG_CG_BUS_EVENT, SOURCE_BUS from asammdf.mdf import MDF STD_DTYPE = np.dtype( @@ -70,6 +72,8 @@ ) except ImportError: asammdf = None + MDF4 = None + Signal = None CAN_MSG_EXT = 0x80000000 @@ -266,13 +270,179 @@ def on_message_received(self, msg: Message) -> None: self._rtr_buffer = np.zeros(1, dtype=RTR_DTYPE) +class FrameIterator(metaclass=abc.ABCMeta): + """ + Iterator helper class for common handling among CAN DataFrames, ErrorFrames and RemoteFrames. + """ + + # Number of records to request for each asammdf call + _chunk_size = 1000 + + def __init__(self, mdf: MDF4, group_index: int, start_timestamp: float, name: str): + self._mdf = mdf + self._group_index = group_index + self._start_timestamp = start_timestamp + self._name = name + + # Extract names + channel_group: ChannelGroup = self._mdf.groups[self._group_index] + + self._channel_names = [] + + for channel in channel_group.channels: + if str(channel.name).startswith(f"{self._name}."): + self._channel_names.append(channel.name) + + def _get_data(self, current_offset: int) -> Signal: + # NOTE: asammdf suggests using select instead of get. Select seem to miss converting some + # channels which get does convert as expected. + data_raw = self._mdf.get( + self._name, + self._group_index, + record_offset=current_offset, + record_count=self._chunk_size, + raw=False, + ) + + return data_raw + + @abc.abstractmethod + def __iter__(self) -> Generator[Message, None, None]: + pass + + class MF4Reader(BinaryIOMessageReader): """ Iterator of CAN messages from a MF4 logging file. - The MF4Reader only supports MF4 files that were recorded with python-can. + The MF4Reader only supports MF4 files with CAN bus logging. """ + # NOTE: Readout based on the bus logging code from asammdf GUI + + class _CANDataFrameIterator(FrameIterator): + + def __init__(self, mdf: MDF4, group_index: int, start_timestamp: float): + super().__init__(mdf, group_index, start_timestamp, "CAN_DataFrame") + + def __iter__(self) -> Generator[Message, None, None]: + for current_offset in range( + 0, + self._mdf.groups[self._group_index].channel_group.cycles_nr, + self._chunk_size, + ): + data = self._get_data(current_offset) + names = data.samples[0].dtype.names + + for i in range(len(data)): + data_length = int(data["CAN_DataFrame.DataLength"][i]) + + kv: Dict[str, Any] = { + "timestamp": float(data.timestamps[i]) + self._start_timestamp, + "arbitration_id": int(data["CAN_DataFrame.ID"][i]) & 0x1FFFFFFF, + "data": data["CAN_DataFrame.DataBytes"][i][ + :data_length + ].tobytes(), + } + + if "CAN_DataFrame.BusChannel" in names: + kv["channel"] = int(data["CAN_DataFrame.BusChannel"][i]) + if "CAN_DataFrame.Dir" in names: + kv["is_rx"] = int(data["CAN_DataFrame.Dir"][i]) == 0 + if "CAN_DataFrame.IDE" in names: + kv["is_extended_id"] = bool(data["CAN_DataFrame.IDE"][i]) + if "CAN_DataFrame.EDL" in names: + kv["is_fd"] = bool(data["CAN_DataFrame.EDL"][i]) + if "CAN_DataFrame.BRS" in names: + kv["bitrate_switch"] = bool(data["CAN_DataFrame.BRS"][i]) + if "CAN_DataFrame.ESI" in names: + kv["error_state_indicator"] = bool(data["CAN_DataFrame.ESI"][i]) + + yield Message(**kv) + + class _CANErrorFrameIterator(FrameIterator): + + def __init__(self, mdf: MDF4, group_index: int, start_timestamp: float): + super().__init__(mdf, group_index, start_timestamp, "CAN_ErrorFrame") + + def __iter__(self) -> Generator[Message, None, None]: + for current_offset in range( + 0, + self._mdf.groups[self._group_index].channel_group.cycles_nr, + self._chunk_size, + ): + data = self._get_data(current_offset) + names = data.samples[0].dtype.names + + for i in range(len(data)): + kv: Dict[str, Any] = { + "timestamp": float(data.timestamps[i]) + self._start_timestamp, + "is_error_frame": True, + } + + if "CAN_ErrorFrame.BusChannel" in names: + kv["channel"] = int(data["CAN_ErrorFrame.BusChannel"][i]) + if "CAN_ErrorFrame.Dir" in names: + kv["is_rx"] = int(data["CAN_ErrorFrame.Dir"][i]) == 0 + if "CAN_ErrorFrame.ID" in names: + kv["arbitration_id"] = ( + int(data["CAN_ErrorFrame.ID"][i]) & 0x1FFFFFFF + ) + if "CAN_ErrorFrame.IDE" in names: + kv["is_extended_id"] = bool(data["CAN_ErrorFrame.IDE"][i]) + if "CAN_ErrorFrame.EDL" in names: + kv["is_fd"] = bool(data["CAN_ErrorFrame.EDL"][i]) + if "CAN_ErrorFrame.BRS" in names: + kv["bitrate_switch"] = bool(data["CAN_ErrorFrame.BRS"][i]) + if "CAN_ErrorFrame.ESI" in names: + kv["error_state_indicator"] = bool( + data["CAN_ErrorFrame.ESI"][i] + ) + if "CAN_ErrorFrame.RTR" in names: + kv["is_remote_frame"] = bool(data["CAN_ErrorFrame.RTR"][i]) + if ( + "CAN_ErrorFrame.DataLength" in names + and "CAN_ErrorFrame.DataBytes" in names + ): + data_length = int(data["CAN_ErrorFrame.DataLength"][i]) + kv["data"] = data["CAN_ErrorFrame.DataBytes"][i][ + :data_length + ].tobytes() + + yield Message(**kv) + + class _CANRemoteFrameIterator(FrameIterator): + + def __init__(self, mdf: MDF4, group_index: int, start_timestamp: float): + super().__init__(mdf, group_index, start_timestamp, "CAN_RemoteFrame") + + def __iter__(self) -> Generator[Message, None, None]: + for current_offset in range( + 0, + self._mdf.groups[self._group_index].channel_group.cycles_nr, + self._chunk_size, + ): + data = self._get_data(current_offset) + names = data.samples[0].dtype.names + + for i in range(len(data)): + kv: Dict[str, Any] = { + "timestamp": float(data.timestamps[i]) + self._start_timestamp, + "arbitration_id": int(data["CAN_RemoteFrame.ID"][i]) + & 0x1FFFFFFF, + "dlc": int(data["CAN_RemoteFrame.DLC"][i]), + "is_remote_frame": True, + } + + if "CAN_RemoteFrame.BusChannel" in names: + kv["channel"] = int(data["CAN_RemoteFrame.BusChannel"][i]) + if "CAN_RemoteFrame.Dir" in names: + kv["is_rx"] = int(data["CAN_RemoteFrame.Dir"][i]) == 0 + if "CAN_RemoteFrame.IDE" in names: + kv["is_extended_id"] = bool(data["CAN_RemoteFrame.IDE"][i]) + + yield Message(**kv) + def __init__( self, file: Union[StringPathLike, BinaryIO], @@ -293,193 +463,65 @@ def __init__( self._mdf: MDF4 if isinstance(file, BufferedIOBase): - self._mdf = MDF(BytesIO(file.read())) + self._mdf = cast(MDF4, MDF(BytesIO(file.read()))) else: - self._mdf = MDF(file) - - self.start_timestamp = self._mdf.header.start_time.timestamp() - - masters = [self._mdf.get_master(i) for i in range(3)] - - masters = [ - np.core.records.fromarrays((master, np.ones(len(master)) * i)) - for i, master in enumerate(masters) - ] - - self.masters = np.sort(np.concatenate(masters)) - - def __iter__(self) -> Generator[Message, None, None]: - standard_counter = 0 - error_counter = 0 - rtr_counter = 0 - - for timestamp, group_index in self.masters: - # standard frames - if group_index == 0: - sample = self._mdf.get( - "CAN_DataFrame", - group=group_index, - raw=True, - record_offset=standard_counter, - record_count=1, - ) - - try: - channel = int(sample["CAN_DataFrame.BusChannel"][0]) - except ValueError: - channel = None - - if sample["CAN_DataFrame.EDL"] == 0: - is_extended_id = bool(sample["CAN_DataFrame.IDE"][0]) - arbitration_id = int(sample["CAN_DataFrame.ID"][0]) - is_rx = int(sample["CAN_DataFrame.Dir"][0]) == 0 - size = int(sample["CAN_DataFrame.DataLength"][0]) - dlc = int(sample["CAN_DataFrame.DLC"][0]) - data = sample["CAN_DataFrame.DataBytes"][0, :size].tobytes() - - msg = Message( - timestamp=timestamp + self.start_timestamp, - is_error_frame=False, - is_remote_frame=False, - is_fd=False, - is_extended_id=is_extended_id, - channel=channel, - is_rx=is_rx, - arbitration_id=arbitration_id, - data=data, - dlc=dlc, - ) - - else: - is_extended_id = bool(sample["CAN_DataFrame.IDE"][0]) - arbitration_id = int(sample["CAN_DataFrame.ID"][0]) - is_rx = int(sample["CAN_DataFrame.Dir"][0]) == 0 - size = int(sample["CAN_DataFrame.DataLength"][0]) - dlc = dlc2len(sample["CAN_DataFrame.DLC"][0]) - data = sample["CAN_DataFrame.DataBytes"][0, :size].tobytes() - error_state_indicator = bool(sample["CAN_DataFrame.ESI"][0]) - bitrate_switch = bool(sample["CAN_DataFrame.BRS"][0]) - - msg = Message( - timestamp=timestamp + self.start_timestamp, - is_error_frame=False, - is_remote_frame=False, - is_fd=True, - is_extended_id=is_extended_id, - channel=channel, - arbitration_id=arbitration_id, - is_rx=is_rx, - data=data, - dlc=dlc, - bitrate_switch=bitrate_switch, - error_state_indicator=error_state_indicator, + self._mdf = cast(MDF4, MDF(file)) + + self._start_timestamp = self._mdf.header.start_time.timestamp() + + def __iter__(self) -> Iterator[Message]: + # To handle messages split over multiple channel groups, create a single iterator per + # channel group and merge these iterators into a single iterator using heapq. + iterators: List[FrameIterator] = [] + for group_index, group in enumerate(self._mdf.groups): + channel_group: ChannelGroup = group.channel_group + + if not channel_group.flags & FLAG_CG_BUS_EVENT: + # Not a bus event, skip + continue + + if channel_group.cycles_nr == 0: + # No data, skip + continue + + acquisition_source: Optional[Source] = channel_group.acq_source + + if acquisition_source is None: + # No source information, skip + continue + if not acquisition_source.source_type & Source.SOURCE_BUS: + # Not a bus type (likely already covered by the channel group flag), skip + continue + + channel_names = [channel.name for channel in group.channels] + + if acquisition_source.bus_type == Source.BUS_TYPE_CAN: + if "CAN_DataFrame" in channel_names: + iterators.append( + self._CANDataFrameIterator( + self._mdf, group_index, self._start_timestamp + ) ) - - yield msg - standard_counter += 1 - - # error frames - elif group_index == 1: - sample = self._mdf.get( - "CAN_ErrorFrame", - group=group_index, - raw=True, - record_offset=error_counter, - record_count=1, - ) - - try: - channel = int(sample["CAN_ErrorFrame.BusChannel"][0]) - except ValueError: - channel = None - - if sample["CAN_ErrorFrame.EDL"] == 0: - is_extended_id = bool(sample["CAN_ErrorFrame.IDE"][0]) - arbitration_id = int(sample["CAN_ErrorFrame.ID"][0]) - is_rx = int(sample["CAN_ErrorFrame.Dir"][0]) == 0 - size = int(sample["CAN_ErrorFrame.DataLength"][0]) - dlc = int(sample["CAN_ErrorFrame.DLC"][0]) - data = sample["CAN_ErrorFrame.DataBytes"][0, :size].tobytes() - - msg = Message( - timestamp=timestamp + self.start_timestamp, - is_error_frame=True, - is_remote_frame=False, - is_fd=False, - is_extended_id=is_extended_id, - channel=channel, - arbitration_id=arbitration_id, - is_rx=is_rx, - data=data, - dlc=dlc, + elif "CAN_ErrorFrame" in channel_names: + iterators.append( + self._CANErrorFrameIterator( + self._mdf, group_index, self._start_timestamp + ) ) - - else: - is_extended_id = bool(sample["CAN_ErrorFrame.IDE"][0]) - arbitration_id = int(sample["CAN_ErrorFrame.ID"][0]) - is_rx = int(sample["CAN_ErrorFrame.Dir"][0]) == 0 - size = int(sample["CAN_ErrorFrame.DataLength"][0]) - dlc = dlc2len(sample["CAN_ErrorFrame.DLC"][0]) - data = sample["CAN_ErrorFrame.DataBytes"][0, :size].tobytes() - error_state_indicator = bool(sample["CAN_ErrorFrame.ESI"][0]) - bitrate_switch = bool(sample["CAN_ErrorFrame.BRS"][0]) - - msg = Message( - timestamp=timestamp + self.start_timestamp, - is_error_frame=True, - is_remote_frame=False, - is_fd=True, - is_extended_id=is_extended_id, - channel=channel, - arbitration_id=arbitration_id, - is_rx=is_rx, - data=data, - dlc=dlc, - bitrate_switch=bitrate_switch, - error_state_indicator=error_state_indicator, + elif "CAN_RemoteFrame" in channel_names: + iterators.append( + self._CANRemoteFrameIterator( + self._mdf, group_index, self._start_timestamp + ) ) - - yield msg - error_counter += 1 - - # remote frames else: - sample = self._mdf.get( - "CAN_RemoteFrame", - group=group_index, - raw=True, - record_offset=rtr_counter, - record_count=1, - ) - - try: - channel = int(sample["CAN_RemoteFrame.BusChannel"][0]) - except ValueError: - channel = None - - is_extended_id = bool(sample["CAN_RemoteFrame.IDE"][0]) - arbitration_id = int(sample["CAN_RemoteFrame.ID"][0]) - is_rx = int(sample["CAN_RemoteFrame.Dir"][0]) == 0 - dlc = int(sample["CAN_RemoteFrame.DLC"][0]) - - msg = Message( - timestamp=timestamp + self.start_timestamp, - is_error_frame=False, - is_remote_frame=True, - is_fd=False, - is_extended_id=is_extended_id, - channel=channel, - arbitration_id=arbitration_id, - is_rx=is_rx, - dlc=dlc, - ) - - yield msg - - rtr_counter += 1 - - self.stop() + # Unknown bus type, skip + continue + + # Create merged iterator over all the groups, using the timestamps as comparison key + return iter(heapq.merge(*iterators, key=lambda x: x.timestamp)) def stop(self) -> None: self._mdf.close() + self._mdf = None super().stop() From 33a1ec740137345075390fe5252db69e27668294 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 23 Nov 2024 14:39:43 +0100 Subject: [PATCH 059/127] Fix CI failures (#1898) * implement join_threads helper method * simplify network_test.py * update tox.ini --- test/network_test.py | 103 +++++++++++++++----------------------- test/simplecyclic_test.py | 40 +++++++++++---- tox.ini | 4 +- 3 files changed, 71 insertions(+), 76 deletions(-) diff --git a/test/network_test.py b/test/network_test.py index 50070ef40..3a231dff7 100644 --- a/test/network_test.py +++ b/test/network_test.py @@ -6,16 +6,14 @@ import unittest import can +from test.config import IS_PYPY logging.getLogger(__file__).setLevel(logging.WARNING) # make a random bool: def rbool(): - return bool(round(random.random())) - - -channel = "vcan0" + return random.choice([False, True]) class ControllerAreaNetworkTestCase(unittest.TestCase): @@ -51,74 +49,51 @@ def tearDown(self): # Restore the defaults can.rc = self._can_rc - def producer(self, ready_event, msg_read): - self.client_bus = can.interface.Bus(channel=channel) - ready_event.wait() - for i in range(self.num_messages): - m = can.Message( - arbitration_id=self.ids[i], - is_remote_frame=self.remote_flags[i], - is_error_frame=self.error_flags[i], - is_extended_id=self.extended_flags[i], - data=self.data[i], - ) - # logging.debug("writing message: {}".format(m)) - if msg_read is not None: - # Don't send until the other thread is ready - msg_read.wait() - msg_read.clear() - - self.client_bus.send(m) + def producer(self, channel: str): + with can.interface.Bus(channel=channel) as client_bus: + for i in range(self.num_messages): + m = can.Message( + arbitration_id=self.ids[i], + is_remote_frame=self.remote_flags[i], + is_error_frame=self.error_flags[i], + is_extended_id=self.extended_flags[i], + data=self.data[i], + ) + client_bus.send(m) def testProducer(self): """Verify that we can send arbitrary messages on the bus""" logging.debug("testing producer alone") - ready = threading.Event() - ready.set() - self.producer(ready, None) - + self.producer(channel="testProducer") logging.debug("producer test complete") def testProducerConsumer(self): logging.debug("testing producer/consumer") - ready = threading.Event() - msg_read = threading.Event() - - self.server_bus = can.interface.Bus(channel=channel, interface="virtual") - - t = threading.Thread(target=self.producer, args=(ready, msg_read)) - t.start() - - # Ensure there are no messages on the bus - while True: - m = self.server_bus.recv(timeout=0.5) - if m is None: - print("No messages... lets go") - break - else: - self.fail("received messages before the test has started ...") - ready.set() - i = 0 - while i < self.num_messages: - msg_read.set() - msg = self.server_bus.recv(timeout=0.5) - self.assertIsNotNone(msg, "Didn't receive a message") - # logging.debug("Received message {} with data: {}".format(i, msg.data)) - - self.assertEqual(msg.is_extended_id, self.extended_flags[i]) - if not msg.is_remote_frame: - self.assertEqual(msg.data, self.data[i]) - self.assertEqual(msg.arbitration_id, self.ids[i]) - - self.assertEqual(msg.is_error_frame, self.error_flags[i]) - self.assertEqual(msg.is_remote_frame, self.remote_flags[i]) - - i += 1 - t.join() - - with contextlib.suppress(NotImplementedError): - self.server_bus.flush_tx_buffer() - self.server_bus.shutdown() + read_timeout = 2.0 if IS_PYPY else 0.5 + channel = "testProducerConsumer" + + with can.interface.Bus(channel=channel, interface="virtual") as server_bus: + t = threading.Thread(target=self.producer, args=(channel,)) + t.start() + + i = 0 + while i < self.num_messages: + msg = server_bus.recv(timeout=read_timeout) + self.assertIsNotNone(msg, "Didn't receive a message") + + self.assertEqual(msg.is_extended_id, self.extended_flags[i]) + if not msg.is_remote_frame: + self.assertEqual(msg.data, self.data[i]) + self.assertEqual(msg.arbitration_id, self.ids[i]) + + self.assertEqual(msg.is_error_frame, self.error_flags[i]) + self.assertEqual(msg.is_remote_frame, self.remote_flags[i]) + + i += 1 + t.join() + + with contextlib.suppress(NotImplementedError): + server_bus.flush_tx_buffer() if __name__ == "__main__": diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py index 7116efc9d..c4c1a2340 100644 --- a/test/simplecyclic_test.py +++ b/test/simplecyclic_test.py @@ -5,8 +5,11 @@ """ import gc +import sys import time +import traceback import unittest +from threading import Thread from time import sleep from typing import List from unittest.mock import MagicMock @@ -87,6 +90,8 @@ def test_removing_bus_tasks(self): # Note calling task.stop will remove the task from the Bus's internal task management list task.stop() + self.join_threads([task.thread for task in tasks], 5.0) + assert len(bus._periodic_tasks) == 0 bus.shutdown() @@ -115,8 +120,7 @@ def test_managed_tasks(self): for task in tasks: task.stop() - for task in tasks: - assert task.thread.join(5.0) is None, "Task didn't stop before timeout" + self.join_threads([task.thread for task in tasks], 5.0) bus.shutdown() @@ -142,9 +146,7 @@ def test_stopping_perodic_tasks(self): # stop the other half using the bus api bus.stop_all_periodic_tasks(remove_tasks=False) - - for task in tasks: - assert task.thread.join(5.0) is None, "Task didn't stop before timeout" + self.join_threads([task.thread for task in tasks], 5.0) # Tasks stopped via `stop_all_periodic_tasks` with remove_tasks=False should # still be associated with the bus (e.g. for restarting) @@ -161,7 +163,7 @@ def test_restart_perodic_tasks(self): is_extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7] ) - def _read_all_messages(_bus: can.interfaces.virtual.VirtualBus) -> None: + def _read_all_messages(_bus: "can.interfaces.virtual.VirtualBus") -> None: sleep(safe_timeout) while not _bus.queue.empty(): _bus.recv(timeout=period) @@ -207,9 +209,8 @@ def _read_all_messages(_bus: can.interfaces.virtual.VirtualBus) -> None: # Stop all tasks and wait for the thread to exit bus.stop_all_periodic_tasks() - if isinstance(task, can.broadcastmanager.ThreadBasedCyclicSendTask): - # Avoids issues where the thread is still running when the bus is shutdown - task.thread.join(safe_timeout) + # Avoids issues where the thread is still running when the bus is shutdown + self.join_threads([task.thread], 5.0) @unittest.skipIf(IS_CI, "fails randomly when run on CI server") def test_thread_based_cyclic_send_task(self): @@ -288,6 +289,27 @@ def increment_first_byte(msg: can.Message) -> None: self.assertEqual(b"\x06\x00\x00\x00\x00\x00\x00\x00", bytes(msg_list[5].data)) self.assertEqual(b"\x07\x00\x00\x00\x00\x00\x00\x00", bytes(msg_list[6].data)) + @staticmethod + def join_threads(threads: List[Thread], timeout: float) -> None: + stuck_threads: List[Thread] = [] + t0 = time.perf_counter() + for thread in threads: + time_left = timeout - (time.perf_counter() - t0) + if time_left > 0.0: + thread.join(time_left) + if thread.is_alive(): + if platform.python_implementation() == "CPython": + # print thread frame to help with debugging + frame = sys._current_frames()[thread.ident] + traceback.print_stack(frame, file=sys.stderr) + stuck_threads.append(thread) + if stuck_threads: + err_message = ( + f"Threads did not stop within {timeout:.1f} seconds: " + f"[{', '.join([str(t) for t in stuck_threads])}]" + ) + raise RuntimeError(err_message) + if __name__ == "__main__": unittest.main() diff --git a/tox.ini b/tox.ini index 1d4e88166..e112c22b4 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,7 @@ deps = pyserial~=3.5 parameterized~=0.8 asammdf>=6.0; platform_python_implementation=="CPython" and python_version<"3.13" - pywin32>=305; platform_system=="Windows" and platform_python_implementation=="CPython" and python_version<"3.13" + pywin32>=305; platform_system=="Windows" and platform_python_implementation=="CPython" and python_version<"3.14" commands = pytest {posargs} @@ -19,8 +19,6 @@ commands = extras = canalystii -recreate = True - [testenv:gh] passenv = CI From 856fd115da6672cc8821b88d5d3e601ded32c662 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 23 Nov 2024 15:48:55 +0100 Subject: [PATCH 060/127] Add BitTiming/BitTimingFd support to slcanBus (#1512) * add BitTiming parameter to slcanBus * remove btr argument, rename ttyBaudrate --- can/interfaces/slcan.py | 73 +++++++++++++++++++++++++++-------------- 1 file changed, 48 insertions(+), 25 deletions(-) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index f023e084c..4cf8b5e25 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -5,16 +5,17 @@ import io import logging import time -from typing import Any, Optional, Tuple +import warnings +from typing import Any, Optional, Tuple, Union -from can import BusABC, CanProtocol, Message, typechecking - -from ..exceptions import ( +from can import BitTiming, BitTimingFd, BusABC, CanProtocol, Message, typechecking +from can.exceptions import ( CanInitializationError, CanInterfaceNotImplementedError, CanOperationError, error_check, ) +from can.util import check_or_adjust_timing_clock, deprecated_args_alias logger = logging.getLogger(__name__) @@ -54,12 +55,17 @@ class slcanBus(BusABC): LINE_TERMINATOR = b"\r" + @deprecated_args_alias( + deprecation_start="4.5.0", + deprecation_end="5.0.0", + ttyBaudrate="tty_baudrate", + ) def __init__( self, channel: typechecking.ChannelStr, - ttyBaudrate: int = 115200, + tty_baudrate: int = 115200, bitrate: Optional[int] = None, - btr: Optional[str] = None, + timing: Optional[Union[BitTiming, BitTimingFd]] = None, sleep_after_open: float = _SLEEP_AFTER_SERIAL_OPEN, rtscts: bool = False, listen_only: bool = False, @@ -70,12 +76,16 @@ def __init__( :param str channel: port of underlying serial or usb device (e.g. ``/dev/ttyUSB0``, ``COM8``, ...) Must not be empty. Can also end with ``@115200`` (or similarly) to specify the baudrate. - :param int ttyBaudrate: + :param int tty_baudrate: baudrate of underlying serial or usb device (Ignored if set via the ``channel`` parameter) :param bitrate: Bitrate in bit/s - :param btr: - BTR register value to set custom can speed + :param timing: + Optional :class:`~can.BitTiming` instance to use for custom bit timing setting. + If this argument is set then it overrides the bitrate and btr arguments. The + `f_clock` value of the timing instance must be set to 8_000_000 (8MHz) + for standard CAN. + CAN FD and the :class:`~can.BitTimingFd` class are not supported. :param poll_interval: Poll interval in seconds when reading messages :param sleep_after_open: @@ -97,16 +107,26 @@ def __init__( if serial is None: raise CanInterfaceNotImplementedError("The serial module is not installed") + btr: Optional[str] = kwargs.get("btr", None) + if btr is not None: + warnings.warn( + "The 'btr' argument is deprecated since python-can v4.5.0 " + "and scheduled for removal in v5.0.0. " + "Use the 'timing' argument instead.", + DeprecationWarning, + stacklevel=1, + ) + if not channel: # if None or empty raise ValueError("Must specify a serial port.") if "@" in channel: (channel, baudrate) = channel.split("@") - ttyBaudrate = int(baudrate) + tty_baudrate = int(baudrate) with error_check(exception_type=CanInitializationError): self.serialPortOrig = serial.serial_for_url( channel, - baudrate=ttyBaudrate, + baudrate=tty_baudrate, rtscts=rtscts, timeout=timeout, ) @@ -117,21 +137,23 @@ def __init__( time.sleep(sleep_after_open) with error_check(exception_type=CanInitializationError): - if bitrate is not None and btr is not None: - raise ValueError("Bitrate and btr mutually exclusive.") - if bitrate is not None: - self.set_bitrate(bitrate) - if btr is not None: - self.set_bitrate_reg(btr) + if isinstance(timing, BitTiming): + timing = check_or_adjust_timing_clock(timing, valid_clocks=[8_000_000]) + self.set_bitrate_reg(f"{timing.btr0:02X}{timing.btr1:02X}") + elif isinstance(timing, BitTimingFd): + raise NotImplementedError( + f"CAN FD is not supported by {self.__class__.__name__}." + ) + else: + if bitrate is not None and btr is not None: + raise ValueError("Bitrate and btr mutually exclusive.") + if bitrate is not None: + self.set_bitrate(bitrate) + if btr is not None: + self.set_bitrate_reg(btr) self.open() - super().__init__( - channel, - ttyBaudrate=115200, - bitrate=None, - rtscts=False, - **kwargs, - ) + super().__init__(channel, **kwargs) def set_bitrate(self, bitrate: int) -> None: """ @@ -153,7 +175,8 @@ def set_bitrate(self, bitrate: int) -> None: def set_bitrate_reg(self, btr: str) -> None: """ :param btr: - BTR register value to set custom can speed + BTR register value to set custom can speed as a string `xxyy` where + xx is the BTR0 value in hex and yy is the BTR1 value in hex. """ self.close() self._write("s" + btr) From 2eb8f53d97faf14f3924023d26da2c540b5a5052 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 23 Nov 2024 15:49:29 +0100 Subject: [PATCH 061/127] Add BitTiming parameter to Usb2canBus (#1511) --- can/interfaces/usb2can/usb2canInterface.py | 49 ++++++++++++++++++---- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/can/interfaces/usb2can/usb2canInterface.py b/can/interfaces/usb2can/usb2canInterface.py index c89e394df..adc16e8b3 100644 --- a/can/interfaces/usb2can/usb2canInterface.py +++ b/can/interfaces/usb2can/usb2canInterface.py @@ -4,9 +4,18 @@ import logging from ctypes import byref -from typing import Optional - -from can import BusABC, CanInitializationError, CanOperationError, CanProtocol, Message +from typing import Optional, Union + +from can import ( + BitTiming, + BitTimingFd, + BusABC, + CanInitializationError, + CanOperationError, + CanProtocol, + Message, +) +from can.util import check_or_adjust_timing_clock from .serial_selector import find_serial_devices from .usb2canabstractionlayer import ( @@ -78,6 +87,13 @@ class Usb2canBus(BusABC): Bitrate of channel in bit/s. Values will be limited to a maximum of 1000 Kb/s. Default is 500 Kbs + :param timing: + Optional :class:`~can.BitTiming` instance to use for custom bit timing setting. + If this argument is set then it overrides the bitrate argument. The + `f_clock` value of the timing instance must be set to 32_000_000 (32MHz) + for standard CAN. + CAN FD and the :class:`~can.BitTimingFd` class are not supported. + :param flags: Flags to directly pass to open function of the usb2can abstraction layer. @@ -97,8 +113,8 @@ def __init__( channel: Optional[str] = None, dll: str = "usb2can.dll", flags: int = 0x00000008, - *_, bitrate: int = 500000, + timing: Optional[Union[BitTiming, BitTimingFd]] = None, serial: Optional[str] = None, **kwargs, ): @@ -114,13 +130,28 @@ def __init__( raise CanInitializationError("could not automatically find any device") device_id = devices[0] - # convert to kb/s and cap: max rate is 1000 kb/s - baudrate = min(int(bitrate // 1000), 1000) - self.channel_info = f"USB2CAN device {device_id}" - self._can_protocol = CanProtocol.CAN_20 - connector = f"{device_id}; {baudrate}" + if isinstance(timing, BitTiming): + timing = check_or_adjust_timing_clock(timing, valid_clocks=[32_000_000]) + connector = ( + f"{device_id};" + "0;" + f"{timing.tseg1};" + f"{timing.tseg2};" + f"{timing.sjw};" + f"{timing.brp}" + ) + elif isinstance(timing, BitTimingFd): + raise NotImplementedError( + f"CAN FD is not supported by {self.__class__.__name__}." + ) + else: + # convert to kb/s and cap: max rate is 1000 kb/s + baudrate = min(int(bitrate // 1000), 1000) + connector = f"{device_id};{baudrate}" + + self._can_protocol = CanProtocol.CAN_20 self.handle = self.can.open(connector, flags) super().__init__(channel=channel, **kwargs) From 7ba6ddd1af98145493035b1d74a6764c02ba1154 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 23 Nov 2024 21:15:11 +0100 Subject: [PATCH 062/127] Add timing parameter to CLI tools (#1869) --- can/logger.py | 47 ++++++++++++++++++++++++++++++++++++- doc/bit_timing.rst | 4 ---- test/test_logger.py | 57 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 5 deletions(-) diff --git a/can/logger.py b/can/logger.py index 81e9527f0..4167558d8 100644 --- a/can/logger.py +++ b/can/logger.py @@ -17,7 +17,7 @@ import can from can import Bus, BusState, Logger, SizedRotatingLogger from can.typechecking import TAdditionalCliArgs -from can.util import cast_from_string +from can.util import _dict2timing, cast_from_string if TYPE_CHECKING: from can.io import BaseRotatingLogger @@ -58,6 +58,19 @@ def _create_base_argument_parser(parser: argparse.ArgumentParser) -> None: help="Bitrate to use for the data phase in case of CAN-FD.", ) + parser.add_argument( + "--timing", + action=_BitTimingAction, + nargs=argparse.ONE_OR_MORE, + help="Configure bit rate and bit timing. For example, use " + "`--timing f_clock=8_000_000 tseg1=5 tseg2=2 sjw=2 brp=2 nof_samples=1` for classical CAN " + "or `--timing f_clock=80_000_000 nom_tseg1=119 nom_tseg2=40 nom_sjw=40 nom_brp=1 " + "data_tseg1=29 data_tseg2=10 data_sjw=10 data_brp=1` for CAN FD. " + "Check the python-can documentation to verify whether your " + "CAN interface supports the `timing` argument.", + metavar="TIMING_ARG", + ) + parser.add_argument( "extra_args", nargs=argparse.REMAINDER, @@ -109,6 +122,8 @@ def _create_bus(parsed_args: argparse.Namespace, **kwargs: Any) -> can.BusABC: config["data_bitrate"] = parsed_args.data_bitrate if getattr(parsed_args, "can_filters", None): config["can_filters"] = parsed_args.can_filters + if parsed_args.timing: + config["timing"] = parsed_args.timing return Bus(parsed_args.channel, **config) @@ -143,6 +158,36 @@ def __call__( setattr(namespace, self.dest, can_filters) +class _BitTimingAction(argparse.Action): + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: Union[str, Sequence[Any], None], + option_string: Optional[str] = None, + ) -> None: + if not isinstance(values, list): + raise argparse.ArgumentError(None, "Invalid --timing argument") + + timing_dict: Dict[str, int] = {} + for arg in values: + try: + key, value_string = arg.split("=") + value = int(value_string) + timing_dict[key] = value + except ValueError: + raise argparse.ArgumentError( + None, f"Invalid timing argument: {arg}" + ) from None + + if not (timing := _dict2timing(timing_dict)): + err_msg = "Invalid --timing argument. Incomplete parameters." + raise argparse.ArgumentError(None, err_msg) + + setattr(namespace, self.dest, timing) + print(timing) + + def _parse_additional_config(unknown_args: Sequence[str]) -> TAdditionalCliArgs: for arg in unknown_args: if not re.match(r"^--[a-zA-Z][a-zA-Z0-9\-]*=\S*?$", arg): diff --git a/doc/bit_timing.rst b/doc/bit_timing.rst index 73005a3c6..bf7aad486 100644 --- a/doc/bit_timing.rst +++ b/doc/bit_timing.rst @@ -1,10 +1,6 @@ Bit Timing Configuration ======================== -.. attention:: - This feature is experimental. The implementation might change in future - versions. - The CAN protocol, specified in ISO 11898, allows the bitrate, sample point and number of samples to be optimized for a given application. These parameters, known as bit timings, can be adjusted to meet the requirements diff --git a/test/test_logger.py b/test/test_logger.py index 10df2557b..d9f200e00 100644 --- a/test/test_logger.py +++ b/test/test_logger.py @@ -139,6 +139,63 @@ def test_parse_can_filters_list(self): ) assert results.can_filters == expected_can_filters + def test_parse_timing(self) -> None: + can20_args = self.baseargs + [ + "--timing", + "f_clock=8_000_000", + "tseg1=5", + "tseg2=2", + "sjw=2", + "brp=2", + "nof_samples=1", + "--app-name=CANalyzer", + ] + results, additional_config = can.logger._parse_logger_args(can20_args[1:]) + assert results.timing == can.BitTiming( + f_clock=8_000_000, brp=2, tseg1=5, tseg2=2, sjw=2, nof_samples=1 + ) + assert additional_config["app_name"] == "CANalyzer" + + canfd_args = self.baseargs + [ + "--timing", + "f_clock=80_000_000", + "nom_tseg1=119", + "nom_tseg2=40", + "nom_sjw=40", + "nom_brp=1", + "data_tseg1=29", + "data_tseg2=10", + "data_sjw=10", + "data_brp=1", + "--app-name=CANalyzer", + ] + results, additional_config = can.logger._parse_logger_args(canfd_args[1:]) + assert results.timing == can.BitTimingFd( + f_clock=80_000_000, + nom_brp=1, + nom_tseg1=119, + nom_tseg2=40, + nom_sjw=40, + data_brp=1, + data_tseg1=29, + data_tseg2=10, + data_sjw=10, + ) + assert additional_config["app_name"] == "CANalyzer" + + # remove f_clock parameter, parsing should fail + incomplete_args = self.baseargs + [ + "--timing", + "tseg1=5", + "tseg2=2", + "sjw=2", + "brp=2", + "nof_samples=1", + "--app-name=CANalyzer", + ] + with self.assertRaises(SystemExit): + can.logger._parse_logger_args(incomplete_args[1:]) + def test_parse_additional_config(self): unknown_args = [ "--app-name=CANalyzer", From 226e040ffa722593add5c763651d1e4504d8387b Mon Sep 17 00:00:00 2001 From: Adrian Immer <62163284+lebuni@users.noreply.github.com> Date: Tue, 26 Nov 2024 19:43:43 +0100 Subject: [PATCH 063/127] Improve speed of TRCReader (#1893) Rewrote some lines in the TRCReader to optimize for speed, mainly for TRC files v2.x. According to cProfile it's double as fast now. --- can/io/trc.py | 83 +++++++++++++++++++++++---------------------------- 1 file changed, 37 insertions(+), 46 deletions(-) diff --git a/can/io/trc.py b/can/io/trc.py index f0595c23e..2dbe3763c 100644 --- a/can/io/trc.py +++ b/can/io/trc.py @@ -11,15 +11,12 @@ import os from datetime import datetime, timedelta, timezone from enum import Enum -from typing import Any, Callable, Dict, Generator, List, Optional, TextIO, Union +from typing import Any, Callable, Dict, Generator, Optional, TextIO, Tuple, Union from ..message import Message from ..typechecking import StringPathLike -from ..util import channel2int, dlc2len, len2dlc -from .generic import ( - TextIOMessageReader, - TextIOMessageWriter, -) +from ..util import channel2int, len2dlc +from .generic import TextIOMessageReader, TextIOMessageWriter logger = logging.getLogger("can.io.trc") @@ -58,13 +55,22 @@ def __init__( """ super().__init__(file, mode="r") self.file_version = TRCFileVersion.UNKNOWN - self.start_time: Optional[datetime] = None + self._start_time: float = 0 self.columns: Dict[str, int] = {} + self._num_columns = -1 if not self.file: raise ValueError("The given file cannot be None") - self._parse_cols: Callable[[List[str]], Optional[Message]] = lambda x: None + self._parse_cols: Callable[[Tuple[str, ...]], Optional[Message]] = ( + lambda x: None + ) + + @property + def start_time(self) -> Optional[datetime]: + if self._start_time: + return datetime.fromtimestamp(self._start_time, timezone.utc) + return None def _extract_header(self): line = "" @@ -89,9 +95,10 @@ def _extract_header(self): elif line.startswith(";$STARTTIME"): logger.debug("TRCReader: Found start time '%s'", line) try: - self.start_time = datetime( - 1899, 12, 30, tzinfo=timezone.utc - ) + timedelta(days=float(line.split("=")[1])) + self._start_time = ( + datetime(1899, 12, 30, tzinfo=timezone.utc) + + timedelta(days=float(line.split("=")[1])) + ).timestamp() except IndexError: logger.debug("TRCReader: Failed to parse start time") elif line.startswith(";$COLUMNS"): @@ -99,6 +106,7 @@ def _extract_header(self): try: columns = line.split("=")[1].split(",") self.columns = {column: columns.index(column) for column in columns} + self._num_columns = len(columns) - 1 except IndexError: logger.debug("TRCReader: Failed to parse columns") elif line.startswith(";"): @@ -107,7 +115,7 @@ def _extract_header(self): break if self.file_version >= TRCFileVersion.V1_1: - if self.start_time is None: + if self._start_time is None: raise ValueError("File has no start time information") if self.file_version >= TRCFileVersion.V2_0: @@ -132,7 +140,7 @@ def _extract_header(self): return line - def _parse_msg_v1_0(self, cols: List[str]) -> Optional[Message]: + def _parse_msg_v1_0(self, cols: Tuple[str, ...]) -> Optional[Message]: arbit_id = cols[2] if arbit_id == "FFFFFFFF": logger.info("TRCReader: Dropping bus info line") @@ -147,16 +155,11 @@ def _parse_msg_v1_0(self, cols: List[str]) -> Optional[Message]: msg.data = bytearray([int(cols[i + 4], 16) for i in range(msg.dlc)]) return msg - def _parse_msg_v1_1(self, cols: List[str]) -> Optional[Message]: + def _parse_msg_v1_1(self, cols: Tuple[str, ...]) -> Optional[Message]: arbit_id = cols[3] msg = Message() - if isinstance(self.start_time, datetime): - msg.timestamp = ( - self.start_time + timedelta(milliseconds=float(cols[1])) - ).timestamp() - else: - msg.timestamp = float(cols[1]) / 1000 + msg.timestamp = float(cols[1]) / 1000 + self._start_time msg.arbitration_id = int(arbit_id, 16) msg.is_extended_id = len(arbit_id) > 4 msg.channel = 1 @@ -165,16 +168,11 @@ def _parse_msg_v1_1(self, cols: List[str]) -> Optional[Message]: msg.is_rx = cols[2] == "Rx" return msg - def _parse_msg_v1_3(self, cols: List[str]) -> Optional[Message]: + def _parse_msg_v1_3(self, cols: Tuple[str, ...]) -> Optional[Message]: arbit_id = cols[4] msg = Message() - if isinstance(self.start_time, datetime): - msg.timestamp = ( - self.start_time + timedelta(milliseconds=float(cols[1])) - ).timestamp() - else: - msg.timestamp = float(cols[1]) / 1000 + msg.timestamp = float(cols[1]) / 1000 + self._start_time msg.arbitration_id = int(arbit_id, 16) msg.is_extended_id = len(arbit_id) > 4 msg.channel = int(cols[2]) @@ -183,7 +181,7 @@ def _parse_msg_v1_3(self, cols: List[str]) -> Optional[Message]: msg.is_rx = cols[3] == "Rx" return msg - def _parse_msg_v2_x(self, cols: List[str]) -> Optional[Message]: + def _parse_msg_v2_x(self, cols: Tuple[str, ...]) -> Optional[Message]: type_ = cols[self.columns["T"]] bus = self.columns.get("B", None) @@ -192,32 +190,25 @@ def _parse_msg_v2_x(self, cols: List[str]) -> Optional[Message]: dlc = len2dlc(length) elif "L" in self.columns: dlc = int(cols[self.columns["L"]]) - length = dlc2len(dlc) else: raise ValueError("No length/dlc columns present.") msg = Message() - if isinstance(self.start_time, datetime): - msg.timestamp = ( - self.start_time + timedelta(milliseconds=float(cols[self.columns["O"]])) - ).timestamp() - else: - msg.timestamp = float(cols[1]) / 1000 + msg.timestamp = float(cols[self.columns["O"]]) / 1000 + self._start_time msg.arbitration_id = int(cols[self.columns["I"]], 16) msg.is_extended_id = len(cols[self.columns["I"]]) > 4 msg.channel = int(cols[bus]) if bus is not None else 1 msg.dlc = dlc - msg.data = bytearray( - [int(cols[i + self.columns["D"]], 16) for i in range(length)] - ) + if dlc: + msg.data = bytearray.fromhex(cols[self.columns["D"]]) msg.is_rx = cols[self.columns["d"]] == "Rx" - msg.is_fd = type_ in ["FD", "FB", "FE", "BI"] - msg.bitrate_switch = type_ in ["FB", " FE"] - msg.error_state_indicator = type_ in ["FE", "BI"] + msg.is_fd = type_ in {"FD", "FB", "FE", "BI"} + msg.bitrate_switch = type_ in {"FB", "FE"} + msg.error_state_indicator = type_ in {"FE", "BI"} return msg - def _parse_cols_v1_1(self, cols: List[str]) -> Optional[Message]: + def _parse_cols_v1_1(self, cols: Tuple[str, ...]) -> Optional[Message]: dtype = cols[2] if dtype in ("Tx", "Rx"): return self._parse_msg_v1_1(cols) @@ -225,7 +216,7 @@ def _parse_cols_v1_1(self, cols: List[str]) -> Optional[Message]: logger.info("TRCReader: Unsupported type '%s'", dtype) return None - def _parse_cols_v1_3(self, cols: List[str]) -> Optional[Message]: + def _parse_cols_v1_3(self, cols: Tuple[str, ...]) -> Optional[Message]: dtype = cols[3] if dtype in ("Tx", "Rx"): return self._parse_msg_v1_3(cols) @@ -233,9 +224,9 @@ def _parse_cols_v1_3(self, cols: List[str]) -> Optional[Message]: logger.info("TRCReader: Unsupported type '%s'", dtype) return None - def _parse_cols_v2_x(self, cols: List[str]) -> Optional[Message]: + def _parse_cols_v2_x(self, cols: Tuple[str, ...]) -> Optional[Message]: dtype = cols[self.columns["T"]] - if dtype in ["DT", "FD", "FB"]: + if dtype in {"DT", "FD", "FB", "FE", "BI"}: return self._parse_msg_v2_x(cols) else: logger.info("TRCReader: Unsupported type '%s'", dtype) @@ -244,7 +235,7 @@ def _parse_cols_v2_x(self, cols: List[str]) -> Optional[Message]: def _parse_line(self, line: str) -> Optional[Message]: logger.debug("TRCReader: Parse '%s'", line) try: - cols = line.split() + cols = tuple(line.split(maxsplit=self._num_columns)) return self._parse_cols(cols) except IndexError: logger.warning("TRCReader: Failed to parse message '%s'", line) From 654a02ae24bfc50bf1bb1fad7aab4aa88763d302 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Thu, 28 Nov 2024 18:59:18 +1300 Subject: [PATCH 064/127] Changelog for v4.5.0 (#1897) --- CHANGELOG.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ CONTRIBUTORS.txt | 9 +++++++++ doc/development.rst | 1 - 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d2581d9d..39cbaa716 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,48 @@ +Version 4.5.0 +============= + +Features +-------- + +* gs_usb command-line support (and documentation updates and stability fixes) by @BenGardiner in https://github.com/hardbyte/python-can/pull/1790 +* Faster and more general MF4 support by @cssedev in https://github.com/hardbyte/python-can/pull/1892 +* ASCWriter speed improvement by @pierreluctg in https://github.com/hardbyte/python-can/pull/1856 +* Faster Message string representation by @pierreluctg in https://github.com/hardbyte/python-can/pull/1858 +* Added Netronic's CANdo and CANdoISO adapters interface by @belliriccardo in https://github.com/hardbyte/python-can/pull/1887 +* Add autostart option to BusABC.send_periodic() to fix issue #1848 by @SWolfSchunk in https://github.com/hardbyte/python-can/pull/1853 +* Improve TestBusConfig by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1804 +* Improve speed of TRCReader by @lebuni in https://github.com/hardbyte/python-can/pull/1893 + +Bug Fixes +--------- + +* Fix Kvaser timestamp by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1878 +* Set end_time in ThreadBasedCyclicSendTask.start() by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1871 +* Fix regex in _parse_additional_config() by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1868 +* Fix for #1849 (PCAN fails when PCAN_ERROR_ILLDATA is read via ReadFD) by @bures in https://github.com/hardbyte/python-can/pull/1850 +* Period must be >= 1ms for BCM using Win32 API by @pierreluctg in https://github.com/hardbyte/python-can/pull/1847 +* Fix ASCReader Crash on "Start of Measurement" Line by @RitheeshBaradwaj in https://github.com/hardbyte/python-can/pull/1811 +* Resolve AttributeError within NicanError by @vijaysubbiah20 in https://github.com/hardbyte/python-can/pull/1806 + + +Miscellaneous +------------- + +* Fix CI by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1889 +* Update msgpack dependency by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1875 +* Add tox environment for doctest by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1870 +* Use typing_extensions.TypedDict on python < 3.12 for pydantic support by @NickCao in https://github.com/hardbyte/python-can/pull/1845 +* Replace PyPy3.8 with PyPy3.10 by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1838 +* Fix slcan tests by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1834 +* Test on Python 3.13 by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1833 +* Stop notifier in examples by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1814 +* Use setuptools_scm by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1810 +* Added extra info for Kvaser dongles by @FedericoSpada in https://github.com/hardbyte/python-can/pull/1797 +* Socketcand: show actual response as well as expected in error by @liamkinne in https://github.com/hardbyte/python-can/pull/1807 +* Refactor CLI filter parsing, add tests by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1805 +* Add zlgcan to docs by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1839 + + Version 4.4.2 ============= diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 389d26412..524908dfc 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -83,3 +83,12 @@ Felix Nieuwenhuizen @felixn @Tbruno25 @RitheeshBaradwaj +@vijaysubbiah20 +@liamkinne +@RitheeshBaradwaj +@BenGardiner +@bures +@NickCao +@SWolfSchunk +@belliriccardo +@cssedev diff --git a/doc/development.rst b/doc/development.rst index a8332eeb6..31a7ae077 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -110,7 +110,6 @@ Creating a new Release ---------------------- - Release from the ``main`` branch (except for pre-releases). -- Update the library version in ``__init__.py`` using `semantic versioning `__. - Check if any deprecations are pending. - Run all tests and examples against available hardware. - Update ``CONTRIBUTORS.txt`` with any new contributors. From 5526e32d718ae414ea8337128bd16f9b59c06f36 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sun, 2 Feb 2025 14:14:37 +0100 Subject: [PATCH 065/127] Fix slcanBus.get_version() and slcanBus.get_serial_number() (#1904) Co-authored-by: zariiii9003 --- can/interfaces/slcan.py | 53 ++++++++++++--------- pyproject.toml | 1 - test/test_slcan.py | 100 +++++++++++++++++++++++++++++++--------- 3 files changed, 110 insertions(+), 44 deletions(-) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index 4cf8b5e25..f0a04e305 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -6,7 +6,8 @@ import logging import time import warnings -from typing import Any, Optional, Tuple, Union +from queue import SimpleQueue +from typing import Any, Optional, Tuple, Union, cast from can import BitTiming, BitTimingFd, BusABC, CanProtocol, Message, typechecking from can.exceptions import ( @@ -131,6 +132,7 @@ def __init__( timeout=timeout, ) + self._queue: SimpleQueue[str] = SimpleQueue() self._buffer = bytearray() self._can_protocol = CanProtocol.CAN_20 @@ -196,7 +198,7 @@ def _read(self, timeout: Optional[float]) -> Optional[str]: # We read the `serialPortOrig.in_waiting` only once here. in_waiting = self.serialPortOrig.in_waiting for _ in range(max(1, in_waiting)): - new_byte = self.serialPortOrig.read(size=1) + new_byte = self.serialPortOrig.read(1) if new_byte: self._buffer.extend(new_byte) else: @@ -234,7 +236,10 @@ def _recv_internal( extended = False data = None - string = self._read(timeout) + if self._queue.qsize(): + string: Optional[str] = self._queue.get_nowait() + else: + string = self._read(timeout) if not string: pass @@ -300,7 +305,7 @@ def shutdown(self) -> None: def fileno(self) -> int: try: - return self.serialPortOrig.fileno() + return cast(int, self.serialPortOrig.fileno()) except io.UnsupportedOperation: raise NotImplementedError( "fileno is not implemented using current CAN bus on this platform" @@ -321,19 +326,21 @@ def get_version( int hw_version is the hardware version or None on timeout int sw_version is the software version or None on timeout """ + _timeout = serial.Timeout(timeout) cmd = "V" self._write(cmd) - string = self._read(timeout) - - if not string: - pass - elif string[0] == cmd and len(string) == 6: - # convert ASCII coded version - hw_version = int(string[1:3]) - sw_version = int(string[3:5]) - return hw_version, sw_version - + while True: + if string := self._read(_timeout.time_left()): + if string[0] == cmd: + # convert ASCII coded version + hw_version = int(string[1:3]) + sw_version = int(string[3:5]) + return hw_version, sw_version + else: + self._queue.put_nowait(string) + if _timeout.expired(): + break return None, None def get_serial_number(self, timeout: Optional[float]) -> Optional[str]: @@ -345,15 +352,17 @@ def get_serial_number(self, timeout: Optional[float]) -> Optional[str]: :return: :obj:`None` on timeout or a :class:`str` object. """ + _timeout = serial.Timeout(timeout) cmd = "N" self._write(cmd) - string = self._read(timeout) - - if not string: - pass - elif string[0] == cmd and len(string) == 6: - serial_number = string[1:-1] - return serial_number - + while True: + if string := self._read(_timeout.time_left()): + if string[0] == cmd: + serial_number = string[1:-1] + return serial_number + else: + self._queue.put_nowait(string) + if _timeout.expired(): + break return None diff --git a/pyproject.toml b/pyproject.toml index f2b6ac04f..4ee463c5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -122,7 +122,6 @@ exclude = [ "^can/interfaces/neousys", "^can/interfaces/pcan", "^can/interfaces/serial", - "^can/interfaces/slcan", "^can/interfaces/socketcan", "^can/interfaces/systec", "^can/interfaces/udp_multicast", diff --git a/test/test_slcan.py b/test/test_slcan.py index e1531e500..220a6d7e0 100644 --- a/test/test_slcan.py +++ b/test/test_slcan.py @@ -1,9 +1,9 @@ #!/usr/bin/env python -import unittest -from typing import cast +import unittest.mock +from typing import cast, Optional -import serial +from serial.serialutil import SerialBase import can.interfaces.slcan @@ -21,20 +21,69 @@ TIMEOUT = 0.5 if IS_PYPY else 0.01 # 0.001 is the default set in slcanBus +class SerialMock(SerialBase): + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + self._input_buffer = b"" + self._output_buffer = b"" + + def open(self) -> None: + self.is_open = True + + def close(self) -> None: + self.is_open = False + self._input_buffer = b"" + self._output_buffer = b"" + + def read(self, size: int = -1, /) -> bytes: + if size > 0: + data = self._input_buffer[:size] + self._input_buffer = self._input_buffer[size:] + return data + return b"" + + def write(self, b: bytes, /) -> Optional[int]: + self._output_buffer = b + if b == b"N\r": + self.set_input_buffer(b"NA123\r") + elif b == b"V\r": + self.set_input_buffer(b"V1013\r") + return len(b) + + def set_input_buffer(self, expected: bytes) -> None: + self._input_buffer = expected + + def get_output_buffer(self) -> bytes: + return self._output_buffer + + def reset_input_buffer(self) -> None: + self._input_buffer = b"" + + @property + def in_waiting(self) -> int: + return len(self._input_buffer) + + @classmethod + def serial_for_url(cls, *args, **kwargs) -> SerialBase: + return cls(*args, **kwargs) + + class slcanTestCase(unittest.TestCase): + @unittest.mock.patch("serial.serial_for_url", SerialMock.serial_for_url) def setUp(self): self.bus = cast( can.interfaces.slcan.slcanBus, can.Bus("loop://", interface="slcan", sleep_after_open=0, timeout=TIMEOUT), ) - self.serial = cast(serial.Serial, self.bus.serialPortOrig) + self.serial = cast(SerialMock, self.bus.serialPortOrig) self.serial.reset_input_buffer() def tearDown(self): self.bus.shutdown() def test_recv_extended(self): - self.serial.write(b"T12ABCDEF2AA55\r") + self.serial.set_input_buffer(b"T12ABCDEF2AA55\r") msg = self.bus.recv(TIMEOUT) self.assertIsNotNone(msg) self.assertEqual(msg.arbitration_id, 0x12ABCDEF) @@ -44,7 +93,7 @@ def test_recv_extended(self): self.assertSequenceEqual(msg.data, [0xAA, 0x55]) # Ewert Energy Systems CANDapter specific - self.serial.write(b"x12ABCDEF2AA55\r") + self.serial.set_input_buffer(b"x12ABCDEF2AA55\r") msg = self.bus.recv(TIMEOUT) self.assertIsNotNone(msg) self.assertEqual(msg.arbitration_id, 0x12ABCDEF) @@ -54,15 +103,19 @@ def test_recv_extended(self): self.assertSequenceEqual(msg.data, [0xAA, 0x55]) def test_send_extended(self): + payload = b"T12ABCDEF2AA55\r" msg = can.Message( arbitration_id=0x12ABCDEF, is_extended_id=True, data=[0xAA, 0x55] ) self.bus.send(msg) + self.assertEqual(payload, self.serial.get_output_buffer()) + + self.serial.set_input_buffer(payload) rx_msg = self.bus.recv(TIMEOUT) self.assertTrue(msg.equals(rx_msg, timestamp_delta=None)) def test_recv_standard(self): - self.serial.write(b"t4563112233\r") + self.serial.set_input_buffer(b"t4563112233\r") msg = self.bus.recv(TIMEOUT) self.assertIsNotNone(msg) self.assertEqual(msg.arbitration_id, 0x456) @@ -72,15 +125,19 @@ def test_recv_standard(self): self.assertSequenceEqual(msg.data, [0x11, 0x22, 0x33]) def test_send_standard(self): + payload = b"t4563112233\r" msg = can.Message( arbitration_id=0x456, is_extended_id=False, data=[0x11, 0x22, 0x33] ) self.bus.send(msg) + self.assertEqual(payload, self.serial.get_output_buffer()) + + self.serial.set_input_buffer(payload) rx_msg = self.bus.recv(TIMEOUT) self.assertTrue(msg.equals(rx_msg, timestamp_delta=None)) def test_recv_standard_remote(self): - self.serial.write(b"r1238\r") + self.serial.set_input_buffer(b"r1238\r") msg = self.bus.recv(TIMEOUT) self.assertIsNotNone(msg) self.assertEqual(msg.arbitration_id, 0x123) @@ -89,15 +146,19 @@ def test_recv_standard_remote(self): self.assertEqual(msg.dlc, 8) def test_send_standard_remote(self): + payload = b"r1238\r" msg = can.Message( arbitration_id=0x123, is_extended_id=False, is_remote_frame=True, dlc=8 ) self.bus.send(msg) + self.assertEqual(payload, self.serial.get_output_buffer()) + + self.serial.set_input_buffer(payload) rx_msg = self.bus.recv(TIMEOUT) self.assertTrue(msg.equals(rx_msg, timestamp_delta=None)) def test_recv_extended_remote(self): - self.serial.write(b"R12ABCDEF6\r") + self.serial.set_input_buffer(b"R12ABCDEF6\r") msg = self.bus.recv(TIMEOUT) self.assertIsNotNone(msg) self.assertEqual(msg.arbitration_id, 0x12ABCDEF) @@ -106,19 +167,23 @@ def test_recv_extended_remote(self): self.assertEqual(msg.dlc, 6) def test_send_extended_remote(self): + payload = b"R12ABCDEF6\r" msg = can.Message( arbitration_id=0x12ABCDEF, is_extended_id=True, is_remote_frame=True, dlc=6 ) self.bus.send(msg) + self.assertEqual(payload, self.serial.get_output_buffer()) + + self.serial.set_input_buffer(payload) rx_msg = self.bus.recv(TIMEOUT) self.assertTrue(msg.equals(rx_msg, timestamp_delta=None)) def test_partial_recv(self): - self.serial.write(b"T12ABCDEF") + self.serial.set_input_buffer(b"T12ABCDEF") msg = self.bus.recv(TIMEOUT) self.assertIsNone(msg) - self.serial.write(b"2AA55\rT12") + self.serial.set_input_buffer(b"2AA55\rT12") msg = self.bus.recv(TIMEOUT) self.assertIsNotNone(msg) self.assertEqual(msg.arbitration_id, 0x12ABCDEF) @@ -130,28 +195,21 @@ def test_partial_recv(self): msg = self.bus.recv(TIMEOUT) self.assertIsNone(msg) - self.serial.write(b"ABCDEF2AA55\r") + self.serial.set_input_buffer(b"ABCDEF2AA55\r") msg = self.bus.recv(TIMEOUT) self.assertIsNotNone(msg) def test_version(self): - self.serial.write(b"V1013\r") hw_ver, sw_ver = self.bus.get_version(0) + self.assertEqual(b"V\r", self.serial.get_output_buffer()) self.assertEqual(hw_ver, 10) self.assertEqual(sw_ver, 13) - hw_ver, sw_ver = self.bus.get_version(0) - self.assertIsNone(hw_ver) - self.assertIsNone(sw_ver) - def test_serial_number(self): - self.serial.write(b"NA123\r") sn = self.bus.get_serial_number(0) + self.assertEqual(b"N\r", self.serial.get_output_buffer()) self.assertEqual(sn, "A123") - sn = self.bus.get_serial_number(0) - self.assertIsNone(sn) - if __name__ == "__main__": unittest.main() From b323e7753ab95ce97ea08a7673177775e623f61d Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sun, 2 Feb 2025 14:15:14 +0100 Subject: [PATCH 066/127] BlfReader: Fix CAN_FD_MESSAGE_64 data padding (#1906) * add padding to CAN_FD_MESSAGE_64 data * set timezone to utc --------- Co-authored-by: zariiii9003 --- can/io/blf.py | 21 ++++++++++++++++----- test/data/issue_1905.blf | Bin 0 -> 524 bytes test/logformats_test.py | 24 ++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 test/data/issue_1905.blf diff --git a/can/io/blf.py b/can/io/blf.py index 81146233d..7d944a846 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -103,7 +103,7 @@ def timestamp_to_systemtime(timestamp: float) -> TSystemTime: if timestamp is None or timestamp < 631152000: # Probably not a Unix timestamp return 0, 0, 0, 0, 0, 0, 0, 0 - t = datetime.datetime.fromtimestamp(round(timestamp, 3)) + t = datetime.datetime.fromtimestamp(round(timestamp, 3), tz=datetime.timezone.utc) return ( t.year, t.month, @@ -126,6 +126,7 @@ def systemtime_to_timestamp(systemtime: TSystemTime) -> float: systemtime[5], systemtime[6], systemtime[7] * 1000, + tzinfo=datetime.timezone.utc, ) return t.timestamp() except ValueError: @@ -239,7 +240,7 @@ def _parse_data(self, data): raise BLFParseError("Could not find next object") from None header = unpack_obj_header_base(data, pos) # print(header) - signature, _, header_version, obj_size, obj_type = header + signature, header_size, header_version, obj_size, obj_type = header if signature != b"LOBJ": raise BLFParseError() @@ -334,10 +335,20 @@ def _parse_data(self, data): _, _, direction, - _, + ext_data_offset, _, ) = unpack_can_fd_64_msg(data, pos) - pos += can_fd_64_msg_size + + # :issue:`1905`: `valid_bytes` can be higher than the actually available data. + # Add zero-byte padding to mimic behavior of CANoe and binlog.dll. + data_field_length = min( + valid_bytes, + (ext_data_offset or obj_size) - header_size - can_fd_64_msg_size, + ) + msg_data_offset = pos + can_fd_64_msg_size + msg_data = data[msg_data_offset : msg_data_offset + data_field_length] + msg_data = msg_data.ljust(valid_bytes, b"\x00") + yield Message( timestamp=timestamp, arbitration_id=can_id & 0x1FFFFFFF, @@ -348,7 +359,7 @@ def _parse_data(self, data): bitrate_switch=bool(fd_flags & 0x2000), error_state_indicator=bool(fd_flags & 0x4000), dlc=dlc2len(dlc), - data=data[pos : pos + valid_bytes], + data=msg_data, channel=channel - 1, ) diff --git a/test/data/issue_1905.blf b/test/data/issue_1905.blf new file mode 100644 index 0000000000000000000000000000000000000000..a896a6d7ca36d8842a0958d26bcf4f66c0593916 GIT binary patch literal 524 zcmebAcXyw_z`*b)!Iy!JlaYak3CID0E!+@V3`j8o@e6hy1||l120jK(1`URLNPKPv z8HP8E9Uw(O5Cg;-U>13VkH3?b0MN#7KAtAwo zHA#()BVyyzYl;GA&%HmeVN!47LB=D72?sd;P3EX6xDh>}@ICKih8x-+{&Y%wX1MWs z;m=czsSIy?U;LhN)QaJoSxf!wLuL%$wB97&IB&=B&5t9Y_M{=hH!qJ)u43MXz4l*H za(A*Hh+}4JEt$u-!ThjWuF`vk8^4_nWtrb+xKaIJ?cLyij2p@s=WFkO;BX`QU9zT$ zz_)*&y~X4@zMPS|s%<3jwXX0<*~P{a?hgz3kFp)O$KQJMju5lpe#4*3CYfvzXK?#8 zrIdjI=I Date: Sun, 2 Feb 2025 13:15:59 +0000 Subject: [PATCH 067/127] Fix `is_rx` for `receive_own_messages` for Kvaser (#1908) * Enable LOCAL_TXACK to fix is_rx for Kvaser * Add missing constant definition * Update comment --- can/interfaces/kvaser/canlib.py | 11 +++++++++++ can/interfaces/kvaser/constants.py | 2 ++ 2 files changed, 13 insertions(+) diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index 51a77a567..9731a4415 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -554,6 +554,15 @@ def __init__( 1, ) + # enable canMSG_LOCAL_TXACK flag in received messages + + canIoCtlInit( + self._read_handle, + canstat.canIOCTL_SET_LOCAL_TXACK, + ctypes.byref(ctypes.c_byte(local_echo)), + 1, + ) + if self.single_handle: log.debug("We don't require separate handles to the bus") self._write_handle = self._read_handle @@ -671,6 +680,7 @@ def _recv_internal(self, timeout=None): is_remote_frame = bool(flags & canstat.canMSG_RTR) is_error_frame = bool(flags & canstat.canMSG_ERROR_FRAME) is_fd = bool(flags & canstat.canFDMSG_FDF) + is_rx = not bool(flags & canstat.canMSG_LOCAL_TXACK) bitrate_switch = bool(flags & canstat.canFDMSG_BRS) error_state_indicator = bool(flags & canstat.canFDMSG_ESI) msg_timestamp = timestamp.value * TIMESTAMP_FACTOR @@ -682,6 +692,7 @@ def _recv_internal(self, timeout=None): is_error_frame=is_error_frame, is_remote_frame=is_remote_frame, is_fd=is_fd, + is_rx=is_rx, bitrate_switch=bitrate_switch, error_state_indicator=error_state_indicator, channel=self.channel, diff --git a/can/interfaces/kvaser/constants.py b/can/interfaces/kvaser/constants.py index 3d01faa84..dc710648c 100644 --- a/can/interfaces/kvaser/constants.py +++ b/can/interfaces/kvaser/constants.py @@ -63,6 +63,7 @@ def CANSTATUS_SUCCESS(status): canMSG_ERROR_FRAME = 0x0020 canMSG_TXACK = 0x0040 canMSG_TXRQ = 0x0080 +canMSG_LOCAL_TXACK = 0x1000_0000 canFDMSG_FDF = 0x010000 canFDMSG_BRS = 0x020000 @@ -195,6 +196,7 @@ def CANSTATUS_SUCCESS(status): canIOCTL_GET_USB_THROTTLE = 29 canIOCTL_SET_BUSON_TIME_AUTO_RESET = 30 canIOCTL_SET_LOCAL_TXECHO = 32 +canIOCTL_SET_LOCAL_TXACK = 46 canIOCTL_PREFER_EXT = 1 canIOCTL_PREFER_STD = 2 canIOCTL_CLEAR_ERROR_COUNTERS = 5 From 8e685acc2599c9e1a93322a636301da53e0f8835 Mon Sep 17 00:00:00 2001 From: Pierre-Luc Date: Mon, 3 Feb 2025 09:04:51 -0500 Subject: [PATCH 068/127] Adding env marker to pywin32 requirements (extra) (#1916) As part of #1833, the `pywin32` requirements environment markers got dropped when moving the requirement to an extra. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4ee463c5d..67bf9ddd7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ lint = [ "black==24.10.*", "mypy==1.12.*", ] -pywin32 = ["pywin32>=305"] +pywin32 = ["pywin32>=305; platform_system == 'Windows' and platform_python_implementation == 'CPython'"] seeedstudio = ["pyserial>=3.0"] serial = ["pyserial~=3.0"] neovi = ["filelock", "python-ics>=2.12"] From 2bd4758038e1f727a23aa2379378db87e7e83aa9 Mon Sep 17 00:00:00 2001 From: Thomas Seiler Date: Sat, 15 Feb 2025 13:39:07 +0100 Subject: [PATCH 069/127] udp_multicast interface: support windows (#1914) * support windows Neither the SO_TIMESTAMPNS / via recvmsg() method, nor the SIOCGSTAMP / ioctl() method for obtaining the packet timestamp is supported on Windows. This code adds a fallback to datetime, and switches to recvfrom() so that the udp_multicast bus becomes usable on windows. * clean-up example on windows, the example would otherwise cause an _enter_buffered_busy fatal error, notifier shutdown and bus shutdown are racing eachother... * fix double inversion * remove unused import * datetime to time.time() * add msgpack dependency for windows * enable udp_multicast back2back tests on windows * handle WSAEINVAL like EINVAL * avoid potential AttributeError on Linux * simplify unittest skip condition * fix formatting issue --- can/interfaces/udp_multicast/bus.py | 61 ++++++++++++++++++++--------- doc/interfaces/udp_multicast.rst | 3 ++ pyproject.toml | 2 +- test/back2back_test.py | 13 +++--- 4 files changed, 54 insertions(+), 25 deletions(-) diff --git a/can/interfaces/udp_multicast/bus.py b/can/interfaces/udp_multicast/bus.py index 8ca2d516b..dd114278c 100644 --- a/can/interfaces/udp_multicast/bus.py +++ b/can/interfaces/udp_multicast/bus.py @@ -3,6 +3,7 @@ import select import socket import struct +import time import warnings from typing import List, Optional, Tuple, Union @@ -12,9 +13,12 @@ from .utils import check_msgpack_installed, pack_message, unpack_message +ioctl_supported = True + try: from fcntl import ioctl except ModuleNotFoundError: # Missing on Windows + ioctl_supported = False pass @@ -30,6 +34,9 @@ SO_TIMESTAMPNS = 35 SIOCGSTAMP = 0x8906 +# Additional constants for the interaction with the Winsock API +WSAEINVAL = 10022 + class UdpMulticastBus(BusABC): """A virtual interface for CAN communications between multiple processes using UDP over Multicast IP. @@ -272,7 +279,11 @@ def _create_socket(self, address_family: socket.AddressFamily) -> socket.socket: try: sock.setsockopt(socket.SOL_SOCKET, SO_TIMESTAMPNS, 1) except OSError as error: - if error.errno == errno.ENOPROTOOPT: # It is unavailable on macOS + if ( + error.errno == errno.ENOPROTOOPT + or error.errno == errno.EINVAL + or error.errno == WSAEINVAL + ): # It is unavailable on macOS (ENOPROTOOPT) or windows(EINVAL/WSAEINVAL) self.timestamp_nanosecond = False else: raise error @@ -353,18 +364,18 @@ def recv( ) from exc if ready_receive_sockets: # not empty - # fetch data & source address - ( - raw_message_data, - ancillary_data, - _, # flags - sender_address, - ) = self._socket.recvmsg( - self.max_buffer, self.received_ancillary_buffer_size - ) - # fetch timestamp; this is configured in _create_socket() if self.timestamp_nanosecond: + # fetch data, timestamp & source address + ( + raw_message_data, + ancillary_data, + _, # flags + sender_address, + ) = self._socket.recvmsg( + self.max_buffer, self.received_ancillary_buffer_size + ) + # Very similar to timestamp handling in can/interfaces/socketcan/socketcan.py -> capture_message() if len(ancillary_data) != 1: raise can.CanOperationError( @@ -385,14 +396,28 @@ def recv( ) timestamp = seconds + nanoseconds * 1.0e-9 else: - result_buffer = ioctl( - self._socket.fileno(), - SIOCGSTAMP, - bytes(self.received_timestamp_struct_size), - ) - seconds, microseconds = struct.unpack( - self.received_timestamp_struct, result_buffer + # fetch data & source address + (raw_message_data, sender_address) = self._socket.recvfrom( + self.max_buffer ) + + if ioctl_supported: + result_buffer = ioctl( + self._socket.fileno(), + SIOCGSTAMP, + bytes(self.received_timestamp_struct_size), + ) + seconds, microseconds = struct.unpack( + self.received_timestamp_struct, result_buffer + ) + else: + # fallback to time.time_ns + now = time.time() + + # Extract seconds and microseconds + seconds = int(now) + microseconds = int((now - seconds) * 1000000) + if microseconds >= 1e6: raise can.CanOperationError( f"Timestamp microseconds field was out of range: {microseconds} not less than 1e6" diff --git a/doc/interfaces/udp_multicast.rst b/doc/interfaces/udp_multicast.rst index 4f9745615..e41925cb3 100644 --- a/doc/interfaces/udp_multicast.rst +++ b/doc/interfaces/udp_multicast.rst @@ -53,6 +53,9 @@ from ``bus_1`` to ``bus_2``: # give the notifier enough time to get triggered by the second bus time.sleep(2.0) + # clean-up + notifier.stop() + Bus Class Documentation ----------------------- diff --git a/pyproject.toml b/pyproject.toml index 67bf9ddd7..d41bf6e22 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ dependencies = [ "wrapt~=1.10", "packaging >= 23.1", "typing_extensions>=3.10.0.0", - "msgpack~=1.1.0; platform_system != 'Windows'", + "msgpack~=1.1.0", ] requires-python = ">=3.8" license = { text = "LGPL v3" } diff --git a/test/back2back_test.py b/test/back2back_test.py index 90cf8a9bf..fc630fb65 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -21,6 +21,7 @@ IS_PYPY, IS_TRAVIS, IS_UNIX, + IS_WINDOWS, TEST_CAN_FD, TEST_INTERFACE_SOCKETCAN, ) @@ -302,9 +303,9 @@ class BasicTestSocketCan(Back2BackTestCase): # this doesn't even work on Travis CI for macOS; for example, see # https://travis-ci.org/github/hardbyte/python-can/jobs/745389871 -@unittest.skipUnless( - IS_UNIX and not (IS_CI and IS_OSX), - "only supported on Unix systems (but not on macOS at Travis CI and GitHub Actions)", +@unittest.skipIf( + IS_CI and IS_OSX, + "not supported for macOS CI", ) class BasicTestUdpMulticastBusIPv4(Back2BackTestCase): INTERFACE_1 = "udp_multicast" @@ -319,9 +320,9 @@ def test_unique_message_instances(self): # this doesn't even work for loopback multicast addresses on Travis CI; for example, see # https://travis-ci.org/github/hardbyte/python-can/builds/745065503 -@unittest.skipUnless( - IS_UNIX and not (IS_TRAVIS or (IS_CI and IS_OSX)), - "only supported on Unix systems (but not on Travis CI; and not on macOS at GitHub Actions)", +@unittest.skipIf( + IS_CI and IS_OSX, + "not supported for macOS CI", ) class BasicTestUdpMulticastBusIPv6(Back2BackTestCase): HOST_LOCAL_MCAST_GROUP_IPv6 = "ff11:7079:7468:6f6e:6465:6d6f:6d63:6173" From 6c9d59c2dedc571fa400b5774131cd8af6a7016f Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Mon, 24 Feb 2025 21:28:16 +0100 Subject: [PATCH 070/127] Fix typo busses to bus (#1925) --- can/exceptions.py | 2 +- doc/other-tools.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/can/exceptions.py b/can/exceptions.py index e1c970e27..ca2d6c5a3 100644 --- a/can/exceptions.py +++ b/can/exceptions.py @@ -1,6 +1,6 @@ """ There are several specific :class:`Exception` classes to allow user -code to react to specific scenarios related to CAN busses:: +code to react to specific scenarios related to CAN buses:: Exception (Python standard library) +-- ... diff --git a/doc/other-tools.rst b/doc/other-tools.rst index db06812ca..607db6c8a 100644 --- a/doc/other-tools.rst +++ b/doc/other-tools.rst @@ -47,7 +47,7 @@ CAN Frame Parsing tools etc. (implemented in Python) #. CAN Message / Database scripting * The `cantools`_ package provides multiple methods for interacting with can message database - files, and using these files to monitor live busses with a command line monitor tool. + files, and using these files to monitor live buses with a command line monitor tool. #. CAN Message / Log Decoding * The `canmatrix`_ module provides methods for converting between multiple popular message frame definition file formats (e.g. .DBC files, .KCD files, .ARXML files etc.). From 319a9f27ac57973b28580c65f4be2c23ac073ff7 Mon Sep 17 00:00:00 2001 From: Pierre-Luc Date: Tue, 25 Feb 2025 10:34:52 -0500 Subject: [PATCH 071/127] Fix timestamp offset bug in BLFWriter (#1921) Fix timestamp offset bug in BLFWriter In the BLF files, the frame timestamp are stored as a delta from the "start time". The "start time" stored in the file is the first frame timestamp rounded to 3 decimals. When we calculate the timestamp delta for each frame, we were previously calculating it using the non-rounded "start time". This new code, use the "start time" as the first frame timestamp truncated to 3 decimals. --- can/io/blf.py | 5 ++++- test/data/example_data.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/can/io/blf.py b/can/io/blf.py index 7d944a846..deefd4736 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -530,7 +530,10 @@ def _add_object(self, obj_type, data, timestamp=None): if timestamp is None: timestamp = self.stop_timestamp or time.time() if self.start_timestamp is None: - self.start_timestamp = timestamp + # Save start timestamp using the same precision as the BLF format + # Truncating to milliseconds to avoid rounding errors when calculating + # the timestamp difference + self.start_timestamp = int(timestamp * 1000) / 1000 self.stop_timestamp = timestamp timestamp = int((timestamp - self.start_timestamp) * 1e9) header_size = OBJ_HEADER_BASE_STRUCT.size + OBJ_HEADER_V1_STRUCT.size diff --git a/test/data/example_data.py b/test/data/example_data.py index 592556926..b78420e4e 100644 --- a/test/data/example_data.py +++ b/test/data/example_data.py @@ -160,7 +160,7 @@ def sort_messages(messages): TEST_MESSAGES_ERROR_FRAMES = sort_messages( [ - Message(is_error_frame=True), + Message(is_error_frame=True, timestamp=TEST_TIME), Message(is_error_frame=True, timestamp=TEST_TIME + 0.170), Message(is_error_frame=True, timestamp=TEST_TIME + 17.157), ] From 613c653f0f1ad5e911901734ab42d843284fd1bb Mon Sep 17 00:00:00 2001 From: eldadcool Date: Mon, 3 Mar 2025 20:43:26 +0200 Subject: [PATCH 072/127] =?UTF-8?q?CC-2174:=20modify=20CAN=20frame=20heade?= =?UTF-8?q?r=20structure=20to=20match=20updated=20struct=20ca=E2=80=A6=20(?= =?UTF-8?q?#1851)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * CC-2174: modify CAN frame header structure to match updated struct can_frame from kernel * allow only valid fd message lengths * add remote message special case for dlc handling * fix pylint issues * verify non FD frame before assigning the len8 DLC * Fix formatting --------- Co-authored-by: Gal Rahamim Co-authored-by: Eldad Sitbon Co-authored-by: rahagal <114643899+rahagal@users.noreply.github.com> --- can/interfaces/socketcan/constants.py | 14 +++- can/interfaces/socketcan/socketcan.py | 106 ++++++++++++++++++++++---- 2 files changed, 105 insertions(+), 15 deletions(-) diff --git a/can/interfaces/socketcan/constants.py b/can/interfaces/socketcan/constants.py index 3144a2cfa..941d52573 100644 --- a/can/interfaces/socketcan/constants.py +++ b/can/interfaces/socketcan/constants.py @@ -53,8 +53,18 @@ SIOCGSTAMP = 0x8906 EXTFLG = 0x0004 -CANFD_BRS = 0x01 -CANFD_ESI = 0x02 +CANFD_BRS = 0x01 # bit rate switch (second bitrate for payload data) +CANFD_ESI = 0x02 # error state indicator of the transmitting node +CANFD_FDF = 0x04 # mark CAN FD for dual use of struct canfd_frame + +# CAN payload length and DLC definitions according to ISO 11898-1 +CAN_MAX_DLC = 8 +CAN_MAX_RAW_DLC = 15 +CAN_MAX_DLEN = 8 + +# CAN FD payload length and DLC definitions according to ISO 11898-7 +CANFD_MAX_DLC = 15 +CANFD_MAX_DLEN = 64 CANFD_MTU = 72 diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index 40da0d094..a5819dcb5 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -139,23 +139,68 @@ def bcm_header_factory( # The 32bit can id is directly followed by the 8bit data link count # The data field is aligned on an 8 byte boundary, hence we add padding # which aligns the data field to an 8 byte boundary. -CAN_FRAME_HEADER_STRUCT = struct.Struct("=IBB2x") +CAN_FRAME_HEADER_STRUCT = struct.Struct("=IBB1xB") def build_can_frame(msg: Message) -> bytes: """CAN frame packing/unpacking (see 'struct can_frame' in ) /** - * struct can_frame - basic CAN frame structure - * @can_id: the CAN ID of the frame and CAN_*_FLAG flags, see above. - * @can_dlc: the data length field of the CAN frame - * @data: the CAN frame payload. - */ + * struct can_frame - Classical CAN frame structure (aka CAN 2.0B) + * @can_id: CAN ID of the frame and CAN_*_FLAG flags, see canid_t definition + * @len: CAN frame payload length in byte (0 .. 8) + * @can_dlc: deprecated name for CAN frame payload length in byte (0 .. 8) + * @__pad: padding + * @__res0: reserved / padding + * @len8_dlc: optional DLC value (9 .. 15) at 8 byte payload length + * len8_dlc contains values from 9 .. 15 when the payload length is + * 8 bytes but the DLC value (see ISO 11898-1) is greater then 8. + * CAN_CTRLMODE_CC_LEN8_DLC flag has to be enabled in CAN driver. + * @data: CAN frame payload (up to 8 byte) + */ struct can_frame { canid_t can_id; /* 32 bit CAN_ID + EFF/RTR/ERR flags */ - __u8 can_dlc; /* data length code: 0 .. 8 */ - __u8 data[8] __attribute__((aligned(8))); + union { + /* CAN frame payload length in byte (0 .. CAN_MAX_DLEN) + * was previously named can_dlc so we need to carry that + * name for legacy support + */ + __u8 len; + __u8 can_dlc; /* deprecated */ + } __attribute__((packed)); /* disable padding added in some ABIs */ + __u8 __pad; /* padding */ + __u8 __res0; /* reserved / padding */ + __u8 len8_dlc; /* optional DLC for 8 byte payload length (9 .. 15) */ + __u8 data[CAN_MAX_DLEN] __attribute__((aligned(8))); }; + /* + * defined bits for canfd_frame.flags + * + * The use of struct canfd_frame implies the FD Frame (FDF) bit to + * be set in the CAN frame bitstream on the wire. The FDF bit switch turns + * the CAN controllers bitstream processor into the CAN FD mode which creates + * two new options within the CAN FD frame specification: + * + * Bit Rate Switch - to indicate a second bitrate is/was used for the payload + * Error State Indicator - represents the error state of the transmitting node + * + * As the CANFD_ESI bit is internally generated by the transmitting CAN + * controller only the CANFD_BRS bit is relevant for real CAN controllers when + * building a CAN FD frame for transmission. Setting the CANFD_ESI bit can make + * sense for virtual CAN interfaces to test applications with echoed frames. + * + * The struct can_frame and struct canfd_frame intentionally share the same + * layout to be able to write CAN frame content into a CAN FD frame structure. + * When this is done the former differentiation via CAN_MTU / CANFD_MTU gets + * lost. CANFD_FDF allows programmers to mark CAN FD frames in the case of + * using struct canfd_frame for mixed CAN / CAN FD content (dual use). + * Since the introduction of CAN XL the CANFD_FDF flag is set in all CAN FD + * frame structures provided by the CAN subsystem of the Linux kernel. + */ + #define CANFD_BRS 0x01 /* bit rate switch (second bitrate for payload data) */ + #define CANFD_ESI 0x02 /* error state indicator of the transmitting node */ + #define CANFD_FDF 0x04 /* mark CAN FD for dual use of struct canfd_frame */ + /** * struct canfd_frame - CAN flexible data rate frame structure * @can_id: CAN ID of the frame and CAN_*_FLAG flags, see canid_t definition @@ -175,14 +220,30 @@ def build_can_frame(msg: Message) -> bytes: }; """ can_id = _compose_arbitration_id(msg) + flags = 0 + + # The socketcan code identify the received FD frame by the packet length. + # So, padding to the data length is performed according to the message type (Classic / FD) + if msg.is_fd: + flags |= constants.CANFD_FDF + max_len = constants.CANFD_MAX_DLEN + else: + max_len = constants.CAN_MAX_DLEN + if msg.bitrate_switch: flags |= constants.CANFD_BRS if msg.error_state_indicator: flags |= constants.CANFD_ESI - max_len = 64 if msg.is_fd else 8 + data = bytes(msg.data).ljust(max_len, b"\x00") - return CAN_FRAME_HEADER_STRUCT.pack(can_id, msg.dlc, flags) + data + + if msg.is_remote_frame: + data_len = msg.dlc + else: + data_len = min(i for i in can.util.CAN_FD_DLC if i >= len(msg.data)) + header = CAN_FRAME_HEADER_STRUCT.pack(can_id, data_len, flags, msg.dlc) + return header + data def build_bcm_header( @@ -259,12 +320,31 @@ def build_bcm_update_header(can_id: int, msg_flags: int, nframes: int = 1) -> by ) +def is_frame_fd(frame: bytes): + # According to the SocketCAN implementation the frame length + # should indicate if the message is FD or not (not the flag value) + return len(frame) == constants.CANFD_MTU + + def dissect_can_frame(frame: bytes) -> Tuple[int, int, int, bytes]: - can_id, can_dlc, flags = CAN_FRAME_HEADER_STRUCT.unpack_from(frame) - if len(frame) != constants.CANFD_MTU: + can_id, data_len, flags, len8_dlc = CAN_FRAME_HEADER_STRUCT.unpack_from(frame) + + if data_len not in can.util.CAN_FD_DLC: + data_len = min(i for i in can.util.CAN_FD_DLC if i >= data_len) + + can_dlc = data_len + + if not is_frame_fd(frame): # Flags not valid in non-FD frames flags = 0 - return can_id, can_dlc, flags, frame[8 : 8 + can_dlc] + + if ( + data_len == constants.CAN_MAX_DLEN + and constants.CAN_MAX_DLEN < len8_dlc <= constants.CAN_MAX_RAW_DLC + ): + can_dlc = len8_dlc + + return can_id, can_dlc, flags, frame[8 : 8 + data_len] def create_bcm_socket(channel: str) -> socket.socket: From 1a200c9acf44334eed070c7a6fd609052d53af43 Mon Sep 17 00:00:00 2001 From: Pierre-Luc Date: Wed, 5 Mar 2025 21:05:54 -0500 Subject: [PATCH 073/127] Fix float arithmetic in BLF reader (#1927) * Fix float arithmetic in BLF reader Using the Python decimal module for fast correctly rounded decimal floating-point arithmetic when applying the timestamp factor. * Remove the allowed_timestamp_delta from the BLF UT Now that the BLF float arithmetic issue is fixed we no longer need to tweek the `allowed_timestamp_delta` in the BLF unit tests --- can/io/blf.py | 5 +++-- test/logformats_test.py | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/can/io/blf.py b/can/io/blf.py index deefd4736..f6e1b6d4d 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -17,6 +17,7 @@ import struct import time import zlib +from decimal import Decimal from typing import Any, BinaryIO, Generator, List, Optional, Tuple, Union, cast from ..message import Message @@ -264,8 +265,8 @@ def _parse_data(self, data): continue # Calculate absolute timestamp in seconds - factor = 1e-5 if flags == 1 else 1e-9 - timestamp = timestamp * factor + start_timestamp + factor = Decimal("1e-5") if flags == 1 else Decimal("1e-9") + timestamp = float(Decimal(timestamp) * factor) + start_timestamp if obj_type in (CAN_MESSAGE, CAN_MESSAGE2): channel, flags, dlc, can_id, can_data = unpack_can_msg(data, pos) diff --git a/test/logformats_test.py b/test/logformats_test.py index a2fd0fe09..f3fe485b2 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -694,7 +694,6 @@ def _setup_instance(self): check_fd=True, check_comments=False, test_append=True, - allowed_timestamp_delta=1.0e-6, preserves_channel=False, adds_default_channel=0, ) From 6deca6e233cd01b18a7ea9bd2a008307ad622e36 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 14 Mar 2025 00:25:49 +0100 Subject: [PATCH 074/127] Update black, ruff & mypy (#1929) * update black and ruff * update mypy * fix pylint complaints --------- Co-authored-by: zariiii9003 --- can/__init__.py | 22 +-- can/bit_timing.py | 40 +++--- can/broadcastmanager.py | 6 +- can/bus.py | 4 +- can/ctypesutil.py | 2 +- can/interface.py | 4 +- can/interfaces/ics_neovi/__init__.py | 3 +- can/interfaces/ixxat/canlib_vcinpl.py | 8 +- can/interfaces/ixxat/canlib_vcinpl2.py | 8 +- can/interfaces/ixxat/exceptions.py | 6 +- can/interfaces/ixxat/structures.py | 14 +- can/interfaces/kvaser/__init__.py | 3 +- can/interfaces/neousys/__init__.py | 2 +- can/interfaces/neousys/neousys.py | 2 +- can/interfaces/pcan/__init__.py | 3 +- can/interfaces/seeedstudio/__init__.py | 3 - can/interfaces/serial/__init__.py | 3 +- can/interfaces/slcan.py | 2 +- can/interfaces/socketcan/utils.py | 2 +- can/interfaces/usb2can/__init__.py | 5 +- can/interfaces/usb2can/serial_selector.py | 3 +- can/interfaces/vector/__init__.py | 3 +- can/interfaces/vector/canlib.py | 4 +- can/io/__init__.py | 14 +- can/io/blf.py | 12 +- can/io/canutils.py | 2 +- can/io/generic.py | 8 +- can/io/logger.py | 2 +- can/io/mf4.py | 10 +- can/io/printer.py | 2 +- can/message.py | 16 +-- can/player.py | 11 +- can/typechecking.py | 3 +- can/util.py | 6 +- can/viewer.py | 3 +- pyproject.toml | 8 +- test/listener_test.py | 3 +- test/test_interface_canalystii.py | 3 +- test/test_kvaser.py | 3 +- test/test_neovi.py | 3 +- test/test_vector.py | 166 +++++++++++----------- test/zero_dlc_test.py | 3 +- 42 files changed, 212 insertions(+), 218 deletions(-) diff --git a/can/__init__.py b/can/__init__.py index 324803b9e..1d4b7f0cf 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -11,17 +11,20 @@ from typing import Any, Dict __all__ = [ + "VALID_INTERFACES", "ASCReader", "ASCWriter", "AsyncBufferedReader", - "BitTiming", - "BitTimingFd", "BLFReader", "BLFWriter", + "BitTiming", + "BitTimingFd", "BufferedReader", "Bus", "BusABC", "BusState", + "CSVReader", + "CSVWriter", "CanError", "CanInitializationError", "CanInterfaceNotImplementedError", @@ -30,18 +33,16 @@ "CanTimeoutError", "CanutilsLogReader", "CanutilsLogWriter", - "CSVReader", - "CSVWriter", "CyclicSendTaskABC", "LimitedDurationCyclicSendTaskABC", "Listener", - "Logger", "LogReader", - "ModifiableCyclicTaskABC", - "Message", - "MessageSync", + "Logger", "MF4Reader", "MF4Writer", + "Message", + "MessageSync", + "ModifiableCyclicTaskABC", "Notifier", "Printer", "RedirectReader", @@ -49,11 +50,10 @@ "SizedRotatingLogger", "SqliteReader", "SqliteWriter", - "ThreadSafeBus", "TRCFileVersion", "TRCReader", "TRCWriter", - "VALID_INTERFACES", + "ThreadSafeBus", "bit_timing", "broadcastmanager", "bus", @@ -64,8 +64,8 @@ "interfaces", "io", "listener", - "logconvert", "log", + "logconvert", "logger", "message", "notifier", diff --git a/can/bit_timing.py b/can/bit_timing.py index f5d50eac1..feba8b6d2 100644 --- a/can/bit_timing.py +++ b/can/bit_timing.py @@ -155,7 +155,7 @@ def from_bitrate_and_segments( if the arguments are invalid. """ try: - brp = int(round(f_clock / (bitrate * (1 + tseg1 + tseg2)))) + brp = round(f_clock / (bitrate * (1 + tseg1 + tseg2))) except ZeroDivisionError: raise ValueError("Invalid inputs") from None @@ -232,7 +232,7 @@ def iterate_from_sample_point( raise ValueError(f"sample_point (={sample_point}) must not be below 50%.") for brp in range(1, 65): - nbt = round(int(f_clock / (bitrate * brp))) + nbt = int(f_clock / (bitrate * brp)) if nbt < 8: break @@ -240,7 +240,7 @@ def iterate_from_sample_point( if abs(effective_bitrate - bitrate) > bitrate / 256: continue - tseg1 = int(round(sample_point / 100 * nbt)) - 1 + tseg1 = round(sample_point / 100 * nbt) - 1 # limit tseg1, so tseg2 is at least 1 TQ tseg1 = min(tseg1, nbt - 2) @@ -312,7 +312,7 @@ def f_clock(self) -> int: @property def bitrate(self) -> int: """Bitrate in bits/s.""" - return int(round(self.f_clock / (self.nbt * self.brp))) + return round(self.f_clock / (self.nbt * self.brp)) @property def brp(self) -> int: @@ -322,7 +322,7 @@ def brp(self) -> int: @property def tq(self) -> int: """Time quantum in nanoseconds""" - return int(round(self.brp / self.f_clock * 1e9)) + return round(self.brp / self.f_clock * 1e9) @property def nbt(self) -> int: @@ -433,7 +433,7 @@ def recreate_with_f_clock(self, f_clock: int) -> "BitTiming": "f_clock change failed because of sample point discrepancy." ) # adapt synchronization jump width, so it has the same size relative to bit time as self - sjw = int(round(self.sjw / self.nbt * bt.nbt)) + sjw = round(self.sjw / self.nbt * bt.nbt) sjw = max(1, min(4, bt.tseg2, sjw)) bt._data["sjw"] = sjw # pylint: disable=protected-access bt._data["nof_samples"] = self.nof_samples # pylint: disable=protected-access @@ -458,7 +458,7 @@ def __repr__(self) -> str: return f"can.{self.__class__.__name__}({args})" def __getitem__(self, key: str) -> int: - return cast(int, self._data.__getitem__(key)) + return cast("int", self._data.__getitem__(key)) def __len__(self) -> int: return self._data.__len__() @@ -716,10 +716,8 @@ def from_bitrate_and_segments( # pylint: disable=too-many-arguments if the arguments are invalid. """ try: - nom_brp = int(round(f_clock / (nom_bitrate * (1 + nom_tseg1 + nom_tseg2)))) - data_brp = int( - round(f_clock / (data_bitrate * (1 + data_tseg1 + data_tseg2))) - ) + nom_brp = round(f_clock / (nom_bitrate * (1 + nom_tseg1 + nom_tseg2))) + data_brp = round(f_clock / (data_bitrate * (1 + data_tseg1 + data_tseg2))) except ZeroDivisionError: raise ValueError("Invalid inputs.") from None @@ -787,7 +785,7 @@ def iterate_from_sample_point( sync_seg = 1 for nom_brp in range(1, 257): - nbt = round(int(f_clock / (nom_bitrate * nom_brp))) + nbt = int(f_clock / (nom_bitrate * nom_brp)) if nbt < 1: break @@ -795,7 +793,7 @@ def iterate_from_sample_point( if abs(effective_nom_bitrate - nom_bitrate) > nom_bitrate / 256: continue - nom_tseg1 = int(round(nom_sample_point / 100 * nbt)) - 1 + nom_tseg1 = round(nom_sample_point / 100 * nbt) - 1 # limit tseg1, so tseg2 is at least 2 TQ nom_tseg1 = min(nom_tseg1, nbt - sync_seg - 2) nom_tseg2 = nbt - nom_tseg1 - 1 @@ -811,7 +809,7 @@ def iterate_from_sample_point( if abs(effective_data_bitrate - data_bitrate) > data_bitrate / 256: continue - data_tseg1 = int(round(data_sample_point / 100 * dbt)) - 1 + data_tseg1 = round(data_sample_point / 100 * dbt) - 1 # limit tseg1, so tseg2 is at least 2 TQ data_tseg1 = min(data_tseg1, dbt - sync_seg - 2) data_tseg2 = dbt - data_tseg1 - 1 @@ -923,7 +921,7 @@ def f_clock(self) -> int: @property def nom_bitrate(self) -> int: """Nominal (arbitration phase) bitrate.""" - return int(round(self.f_clock / (self.nbt * self.nom_brp))) + return round(self.f_clock / (self.nbt * self.nom_brp)) @property def nom_brp(self) -> int: @@ -933,7 +931,7 @@ def nom_brp(self) -> int: @property def nom_tq(self) -> int: """Nominal time quantum in nanoseconds""" - return int(round(self.nom_brp / self.f_clock * 1e9)) + return round(self.nom_brp / self.f_clock * 1e9) @property def nbt(self) -> int: @@ -969,7 +967,7 @@ def nom_sample_point(self) -> float: @property def data_bitrate(self) -> int: """Bitrate of the data phase in bit/s.""" - return int(round(self.f_clock / (self.dbt * self.data_brp))) + return round(self.f_clock / (self.dbt * self.data_brp)) @property def data_brp(self) -> int: @@ -979,7 +977,7 @@ def data_brp(self) -> int: @property def data_tq(self) -> int: """Data time quantum in nanoseconds""" - return int(round(self.data_brp / self.f_clock * 1e9)) + return round(self.data_brp / self.f_clock * 1e9) @property def dbt(self) -> int: @@ -1106,10 +1104,10 @@ def recreate_with_f_clock(self, f_clock: int) -> "BitTimingFd": "f_clock change failed because of sample point discrepancy." ) # adapt synchronization jump width, so it has the same size relative to bit time as self - nom_sjw = int(round(self.nom_sjw / self.nbt * bt.nbt)) + nom_sjw = round(self.nom_sjw / self.nbt * bt.nbt) nom_sjw = max(1, min(bt.nom_tseg2, nom_sjw)) bt._data["nom_sjw"] = nom_sjw # pylint: disable=protected-access - data_sjw = int(round(self.data_sjw / self.dbt * bt.dbt)) + data_sjw = round(self.data_sjw / self.dbt * bt.dbt) data_sjw = max(1, min(bt.data_tseg2, data_sjw)) bt._data["data_sjw"] = data_sjw # pylint: disable=protected-access bt._validate() # pylint: disable=protected-access @@ -1138,7 +1136,7 @@ def __repr__(self) -> str: return f"can.{self.__class__.__name__}({args})" def __getitem__(self, key: str) -> int: - return cast(int, self._data.__getitem__(key)) + return cast("int", self._data.__getitem__(key)) def __len__(self) -> int: return self._data.__len__() diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 319fb0f53..19dca8fbc 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -61,7 +61,7 @@ def create_timer(self) -> _Pywin32Event: ): event = self.win32event.CreateWaitableTimer(None, False, None) - return cast(_Pywin32Event, event) + return cast("_Pywin32Event", event) def set_timer(self, event: _Pywin32Event, period_ms: int) -> None: self.win32event.SetWaitableTimer(event.handle, 0, period_ms, None, None, False) @@ -121,12 +121,12 @@ def __init__( # Take the Arbitration ID of the first element self.arbitration_id = messages[0].arbitration_id self.period = period - self.period_ns = int(round(period * 1e9)) + self.period_ns = round(period * 1e9) self.messages = messages @staticmethod def _check_and_convert_messages( - messages: Union[Sequence[Message], Message] + messages: Union[Sequence[Message], Message], ) -> Tuple[Message, ...]: """Helper function to convert a Message or Sequence of messages into a tuple, and raises an error when the given value is invalid. diff --git a/can/bus.py b/can/bus.py index a12808ab6..d186e2d05 100644 --- a/can/bus.py +++ b/can/bus.py @@ -276,7 +276,7 @@ def send_periodic( # Create a backend specific task; will be patched to a _SelfRemovingCyclicTask later task = cast( - _SelfRemovingCyclicTask, + "_SelfRemovingCyclicTask", self._send_periodic_internal( msgs, period, duration, autostart, modifier_callback ), @@ -452,7 +452,7 @@ def _matches_filters(self, msg: Message) -> bool: for _filter in self._filters: # check if this filter even applies to the message if "extended" in _filter: - _filter = cast(can.typechecking.CanFilterExtended, _filter) + _filter = cast("can.typechecking.CanFilterExtended", _filter) if _filter["extended"] != msg.is_extended_id: continue diff --git a/can/ctypesutil.py b/can/ctypesutil.py index 0336b03d3..a80dc3194 100644 --- a/can/ctypesutil.py +++ b/can/ctypesutil.py @@ -9,7 +9,7 @@ log = logging.getLogger("can.ctypesutil") -__all__ = ["CLibrary", "HANDLE", "PHANDLE", "HRESULT"] +__all__ = ["HANDLE", "HRESULT", "PHANDLE", "CLibrary"] if sys.platform == "win32": _LibBase = ctypes.WinDLL diff --git a/can/interface.py b/can/interface.py index 2b7e27f8d..f7a8ecc02 100644 --- a/can/interface.py +++ b/can/interface.py @@ -52,7 +52,7 @@ def _get_class_for_interface(interface: str) -> Type[BusABC]: f"'{interface}': {e}" ) from None - return cast(Type[BusABC], bus_class) + return cast("Type[BusABC]", bus_class) @util.deprecated_args_alias( @@ -138,7 +138,7 @@ def Bus( # noqa: N802 def detect_available_configs( - interfaces: Union[None, str, Iterable[str]] = None + interfaces: Union[None, str, Iterable[str]] = None, ) -> List[AutoDetectedConfig]: """Detect all configurations/channels that the interfaces could currently connect with. diff --git a/can/interfaces/ics_neovi/__init__.py b/can/interfaces/ics_neovi/__init__.py index 0252c7345..74cb43af4 100644 --- a/can/interfaces/ics_neovi/__init__.py +++ b/can/interfaces/ics_neovi/__init__.py @@ -1,5 +1,4 @@ -""" -""" +""" """ __all__ = [ "ICSApiError", diff --git a/can/interfaces/ixxat/canlib_vcinpl.py b/can/interfaces/ixxat/canlib_vcinpl.py index 0579d0942..567dd0cd9 100644 --- a/can/interfaces/ixxat/canlib_vcinpl.py +++ b/can/interfaces/ixxat/canlib_vcinpl.py @@ -35,11 +35,11 @@ from .exceptions import * __all__ = [ - "VCITimeout", - "VCIError", + "IXXATBus", "VCIBusOffError", "VCIDeviceNotFoundError", - "IXXATBus", + "VCIError", + "VCITimeout", "vciFormatError", ] @@ -890,7 +890,7 @@ def __init__( self._count = int(duration / period) if duration else 0 self._msg = structures.CANCYCLICTXMSG() - self._msg.wCycleTime = int(round(period * resolution)) + self._msg.wCycleTime = round(period * resolution) self._msg.dwMsgId = self.messages[0].arbitration_id self._msg.uMsgInfo.Bits.type = constants.CAN_MSGTYPE_DATA self._msg.uMsgInfo.Bits.ext = 1 if self.messages[0].is_extended_id else 0 diff --git a/can/interfaces/ixxat/canlib_vcinpl2.py b/can/interfaces/ixxat/canlib_vcinpl2.py index 5872f76b9..79ad34c4f 100644 --- a/can/interfaces/ixxat/canlib_vcinpl2.py +++ b/can/interfaces/ixxat/canlib_vcinpl2.py @@ -34,11 +34,11 @@ from .exceptions import * __all__ = [ - "VCITimeout", - "VCIError", + "IXXATBus", "VCIBusOffError", "VCIDeviceNotFoundError", - "IXXATBus", + "VCIError", + "VCITimeout", "vciFormatError", ] @@ -1010,7 +1010,7 @@ def __init__( self._count = int(duration / period) if duration else 0 self._msg = structures.CANCYCLICTXMSG2() - self._msg.wCycleTime = int(round(period * resolution)) + self._msg.wCycleTime = round(period * resolution) self._msg.dwMsgId = self.messages[0].arbitration_id self._msg.uMsgInfo.Bits.type = constants.CAN_MSGTYPE_DATA self._msg.uMsgInfo.Bits.ext = 1 if self.messages[0].is_extended_id else 0 diff --git a/can/interfaces/ixxat/exceptions.py b/can/interfaces/ixxat/exceptions.py index 50b84dfa4..771eec307 100644 --- a/can/interfaces/ixxat/exceptions.py +++ b/can/interfaces/ixxat/exceptions.py @@ -12,11 +12,11 @@ ) __all__ = [ - "VCITimeout", - "VCIError", - "VCIRxQueueEmptyError", "VCIBusOffError", "VCIDeviceNotFoundError", + "VCIError", + "VCIRxQueueEmptyError", + "VCITimeout", ] diff --git a/can/interfaces/ixxat/structures.py b/can/interfaces/ixxat/structures.py index 611955db7..680ed1ac6 100644 --- a/can/interfaces/ixxat/structures.py +++ b/can/interfaces/ixxat/structures.py @@ -201,13 +201,13 @@ class CANBTP(ctypes.Structure): ] def __str__(self): - return "dwMode=%d, dwBPS=%d, wTS1=%d, wTS2=%d, wSJW=%d, wTDO=%d" % ( - self.dwMode, - self.dwBPS, - self.wTS1, - self.wTS2, - self.wSJW, - self.wTDO, + return ( + f"dwMode={self.dwMode:d}, " + f"dwBPS={self.dwBPS:d}, " + f"wTS1={self.wTS1:d}, " + f"wTS2={self.wTS2:d}, " + f"wSJW={self.wSJW:d}, " + f"wTDO={self.wTDO:d}" ) diff --git a/can/interfaces/kvaser/__init__.py b/can/interfaces/kvaser/__init__.py index 8e7c3feb9..7cfb13d8d 100644 --- a/can/interfaces/kvaser/__init__.py +++ b/can/interfaces/kvaser/__init__.py @@ -1,5 +1,4 @@ -""" -""" +""" """ __all__ = [ "CANLIBInitializationError", diff --git a/can/interfaces/neousys/__init__.py b/can/interfaces/neousys/__init__.py index 44bd3127f..f3e0cb039 100644 --- a/can/interfaces/neousys/__init__.py +++ b/can/interfaces/neousys/__init__.py @@ -1,4 +1,4 @@ -""" Neousys CAN bus driver """ +"""Neousys CAN bus driver""" __all__ = [ "NeousysBus", diff --git a/can/interfaces/neousys/neousys.py b/can/interfaces/neousys/neousys.py index cb9d0174d..7e8c877b4 100644 --- a/can/interfaces/neousys/neousys.py +++ b/can/interfaces/neousys/neousys.py @@ -1,4 +1,4 @@ -""" Neousys CAN bus driver """ +"""Neousys CAN bus driver""" # # This kind of interface can be found for example on Neousys POC-551VTC diff --git a/can/interfaces/pcan/__init__.py b/can/interfaces/pcan/__init__.py index 73e351665..a3eafcb4d 100644 --- a/can/interfaces/pcan/__init__.py +++ b/can/interfaces/pcan/__init__.py @@ -1,5 +1,4 @@ -""" -""" +""" """ __all__ = [ "PcanBus", diff --git a/can/interfaces/seeedstudio/__init__.py b/can/interfaces/seeedstudio/__init__.py index 0b3c7b1c9..9466bde43 100644 --- a/can/interfaces/seeedstudio/__init__.py +++ b/can/interfaces/seeedstudio/__init__.py @@ -1,6 +1,3 @@ -""" -""" - __all__ = [ "SeeedBus", "seeedstudio", diff --git a/can/interfaces/serial/__init__.py b/can/interfaces/serial/__init__.py index beb6457bb..6327530d7 100644 --- a/can/interfaces/serial/__init__.py +++ b/can/interfaces/serial/__init__.py @@ -1,5 +1,4 @@ -""" -""" +""" """ __all__ = [ "SerialBus", diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index f0a04e305..ddd2bec23 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -305,7 +305,7 @@ def shutdown(self) -> None: def fileno(self) -> int: try: - return cast(int, self.serialPortOrig.fileno()) + return cast("int", self.serialPortOrig.fileno()) except io.UnsupportedOperation: raise NotImplementedError( "fileno is not implemented using current CAN bus on this platform" diff --git a/can/interfaces/socketcan/utils.py b/can/interfaces/socketcan/utils.py index 679c9cefc..1505c6cf8 100644 --- a/can/interfaces/socketcan/utils.py +++ b/can/interfaces/socketcan/utils.py @@ -28,7 +28,7 @@ def pack_filters(can_filters: Optional[typechecking.CanFilters] = None) -> bytes can_id = can_filter["can_id"] can_mask = can_filter["can_mask"] if "extended" in can_filter: - can_filter = cast(typechecking.CanFilterExtended, can_filter) + can_filter = cast("typechecking.CanFilterExtended", can_filter) # Match on either 11-bit OR 29-bit messages instead of both can_mask |= CAN_EFF_FLAG if can_filter["extended"]: diff --git a/can/interfaces/usb2can/__init__.py b/can/interfaces/usb2can/__init__.py index a2b587842..f818130ee 100644 --- a/can/interfaces/usb2can/__init__.py +++ b/can/interfaces/usb2can/__init__.py @@ -1,12 +1,11 @@ -""" -""" +""" """ __all__ = [ "Usb2CanAbstractionLayer", "Usb2canBus", "serial_selector", - "usb2canabstractionlayer", "usb2canInterface", + "usb2canabstractionlayer", ] from .usb2canabstractionlayer import Usb2CanAbstractionLayer diff --git a/can/interfaces/usb2can/serial_selector.py b/can/interfaces/usb2can/serial_selector.py index 92a3a07a2..de29f03fe 100644 --- a/can/interfaces/usb2can/serial_selector.py +++ b/can/interfaces/usb2can/serial_selector.py @@ -1,5 +1,4 @@ -""" -""" +""" """ import logging from typing import List diff --git a/can/interfaces/vector/__init__.py b/can/interfaces/vector/__init__.py index 3a5b13a1f..e78783f1f 100644 --- a/can/interfaces/vector/__init__.py +++ b/can/interfaces/vector/__init__.py @@ -1,5 +1,4 @@ -""" -""" +""" """ __all__ = [ "VectorBus", diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index d307d076f..d51d74529 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -220,7 +220,7 @@ def __init__( # at the same time. If the VectorBus is instantiated with a config, that was returned from # VectorBus._detect_available_configs(), then use the contained global channel_index # to avoid any ambiguities. - channel_index = cast(int, _channel_index) + channel_index = cast("int", _channel_index) else: channel_index = self._find_global_channel_idx( channel=channel, @@ -410,7 +410,7 @@ def _find_global_channel_idx( app_name, channel ) idx = cast( - int, self.xldriver.xlGetChannelIndex(hw_type, hw_index, hw_channel) + "int", self.xldriver.xlGetChannelIndex(hw_type, hw_index, hw_channel) ) if idx < 0: # Undocumented behavior! See issue #353. diff --git a/can/io/__init__.py b/can/io/__init__.py index 5601f2591..69894c3d0 100644 --- a/can/io/__init__.py +++ b/can/io/__init__.py @@ -4,22 +4,22 @@ """ __all__ = [ + "MESSAGE_READERS", + "MESSAGE_WRITERS", "ASCReader", "ASCWriter", - "BaseRotatingLogger", "BLFReader", "BLFWriter", - "CanutilsLogReader", - "CanutilsLogWriter", + "BaseRotatingLogger", "CSVReader", "CSVWriter", - "Logger", + "CanutilsLogReader", + "CanutilsLogWriter", "LogReader", - "MESSAGE_READERS", - "MESSAGE_WRITERS", - "MessageSync", + "Logger", "MF4Reader", "MF4Writer", + "MessageSync", "Printer", "SizedRotatingLogger", "SqliteReader", diff --git a/can/io/blf.py b/can/io/blf.py index f6e1b6d4d..139b44285 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -162,8 +162,12 @@ def __init__( self.file_size = header[10] self.uncompressed_size = header[11] self.object_count = header[12] - self.start_timestamp = systemtime_to_timestamp(cast(TSystemTime, header[14:22])) - self.stop_timestamp = systemtime_to_timestamp(cast(TSystemTime, header[22:30])) + self.start_timestamp = systemtime_to_timestamp( + cast("TSystemTime", header[14:22]) + ) + self.stop_timestamp = systemtime_to_timestamp( + cast("TSystemTime", header[22:30]) + ) # Read rest of header self.file.read(header[1] - FILE_HEADER_STRUCT.size) self._tail = b"" @@ -429,10 +433,10 @@ def __init__( self.uncompressed_size = header[11] self.object_count = header[12] self.start_timestamp: Optional[float] = systemtime_to_timestamp( - cast(TSystemTime, header[14:22]) + cast("TSystemTime", header[14:22]) ) self.stop_timestamp: Optional[float] = systemtime_to_timestamp( - cast(TSystemTime, header[22:30]) + cast("TSystemTime", header[22:30]) ) # Jump to the end of the file self.file.seek(0, 2) diff --git a/can/io/canutils.py b/can/io/canutils.py index d7ae99daf..cc978d4f2 100644 --- a/can/io/canutils.py +++ b/can/io/canutils.py @@ -165,7 +165,7 @@ def on_message_received(self, msg): timestamp = msg.timestamp channel = msg.channel if msg.channel is not None else self.channel - if isinstance(channel, int) or isinstance(channel, str) and channel.isdigit(): + if isinstance(channel, int) or (isinstance(channel, str) and channel.isdigit()): channel = f"can{channel}" framestr = f"({timestamp:f}) {channel}" diff --git a/can/io/generic.py b/can/io/generic.py index 55468ff16..93840f807 100644 --- a/can/io/generic.py +++ b/can/io/generic.py @@ -50,7 +50,7 @@ def __init__( """ if file is None or (hasattr(file, "read") and hasattr(file, "write")): # file is None or some file-like object - self.file = cast(Optional[typechecking.FileLike], file) + self.file = cast("Optional[typechecking.FileLike]", file) else: encoding: Optional[str] = ( None @@ -60,8 +60,10 @@ def __init__( # pylint: disable=consider-using-with # file is some path-like object self.file = cast( - typechecking.FileLike, - open(cast(typechecking.StringPathLike, file), mode, encoding=encoding), + "typechecking.FileLike", + open( + cast("typechecking.StringPathLike", file), mode, encoding=encoding + ), ) # for multiple inheritance diff --git a/can/io/logger.py b/can/io/logger.py index f54223741..359aae4ac 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -268,7 +268,7 @@ def _get_new_writer(self, filename: StringPathLike) -> FileIOMessageWriter: if isinstance(logger, FileIOMessageWriter): return logger elif isinstance(logger, Printer) and logger.file is not None: - return cast(FileIOMessageWriter, logger) + return cast("FileIOMessageWriter", logger) raise ValueError( f'The log format of "{pathlib.Path(filename).name}" ' diff --git a/can/io/mf4.py b/can/io/mf4.py index 4f5336b42..9efa1d83d 100644 --- a/can/io/mf4.py +++ b/can/io/mf4.py @@ -124,7 +124,7 @@ def __init__( super().__init__(file, mode="w+b") now = datetime.now() - self._mdf = cast(MDF4, MDF(version="4.10")) + self._mdf = cast("MDF4", MDF(version="4.10")) self._mdf.header.start_time = now self.last_timestamp = self._start_time = now.timestamp() @@ -184,7 +184,9 @@ def __init__( def file_size(self) -> int: """Return an estimate of the current file size in bytes.""" # TODO: find solution without accessing private attributes of asammdf - return cast(int, self._mdf._tempfile.tell()) # pylint: disable=protected-access + return cast( + "int", self._mdf._tempfile.tell() # pylint: disable=protected-access + ) def stop(self) -> None: self._mdf.save(self.file, compression=self._compression_level) @@ -463,9 +465,9 @@ def __init__( self._mdf: MDF4 if isinstance(file, BufferedIOBase): - self._mdf = cast(MDF4, MDF(BytesIO(file.read()))) + self._mdf = cast("MDF4", MDF(BytesIO(file.read()))) else: - self._mdf = cast(MDF4, MDF(file)) + self._mdf = cast("MDF4", MDF(file)) self._start_timestamp = self._mdf.header.start_time.timestamp() diff --git a/can/io/printer.py b/can/io/printer.py index 67c353cc6..30bc227ab 100644 --- a/can/io/printer.py +++ b/can/io/printer.py @@ -44,7 +44,7 @@ def __init__( def on_message_received(self, msg: Message) -> None: if self.write_to_file: - cast(TextIO, self.file).write(str(msg) + "\n") + cast("TextIO", self.file).write(str(msg) + "\n") else: print(msg) # noqa: T201 diff --git a/can/message.py b/can/message.py index 6dc6a83bd..d8d94ea84 100644 --- a/can/message.py +++ b/can/message.py @@ -32,19 +32,19 @@ class Message: # pylint: disable=too-many-instance-attributes; OK for a datacla """ __slots__ = ( - "timestamp", + "__weakref__", # support weak references to messages "arbitration_id", - "is_extended_id", - "is_remote_frame", - "is_error_frame", + "bitrate_switch", "channel", - "dlc", "data", + "dlc", + "error_state_indicator", + "is_error_frame", + "is_extended_id", "is_fd", + "is_remote_frame", "is_rx", - "bitrate_switch", - "error_state_indicator", - "__weakref__", # support weak references to messages + "timestamp", ) def __init__( # pylint: disable=too-many-locals, too-many-arguments diff --git a/can/player.py b/can/player.py index 40e4cc43a..9deb0c51f 100644 --- a/can/player.py +++ b/can/player.py @@ -9,12 +9,17 @@ import errno import sys from datetime import datetime -from typing import Iterable, cast +from typing import TYPE_CHECKING, cast -from can import LogReader, Message, MessageSync +from can import LogReader, MessageSync from .logger import _create_base_argument_parser, _create_bus, _parse_additional_config +if TYPE_CHECKING: + from typing import Iterable + + from can import Message + def main() -> None: parser = argparse.ArgumentParser(description="Replay CAN traffic.") @@ -88,7 +93,7 @@ def main() -> None: with _create_bus(results, **additional_config) as bus: with LogReader(results.infile, **additional_config) as reader: in_sync = MessageSync( - cast(Iterable[Message], reader), + cast("Iterable[Message]", reader), timestamps=results.timestamps, gap=results.gap, skip=results.skip, diff --git a/can/typechecking.py b/can/typechecking.py index b0d1c22ac..d993d6bd3 100644 --- a/can/typechecking.py +++ b/can/typechecking.py @@ -1,5 +1,4 @@ -"""Types for mypy type-checking -""" +"""Types for mypy type-checking""" import gzip import struct diff --git a/can/util.py b/can/util.py index 7f2f824db..bb835b846 100644 --- a/can/util.py +++ b/can/util.py @@ -178,7 +178,7 @@ def load_config( # Use the given dict for default values config_sources = cast( - Iterable[Union[Dict[str, Any], Callable[[Any], Dict[str, Any]]]], + "Iterable[Union[Dict[str, Any], Callable[[Any], Dict[str, Any]]]]", [ given_config, can.rc, @@ -251,7 +251,7 @@ def _create_bus_config(config: Dict[str, Any]) -> typechecking.BusConfig: if "fd" in config: config["fd"] = config["fd"] not in (0, False) - return cast(typechecking.BusConfig, config) + return cast("typechecking.BusConfig", config) def _dict2timing(data: Dict[str, Any]) -> Union[BitTiming, BitTimingFd, None]: @@ -396,7 +396,7 @@ def _rename_kwargs( func_name: str, start: str, end: Optional[str], - kwargs: P1.kwargs, + kwargs: Dict[str, Any], aliases: Dict[str, Optional[str]], ) -> None: """Helper function for `deprecated_args_alias`""" diff --git a/can/viewer.py b/can/viewer.py index 45c313b07..57b3d6f7d 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -166,9 +166,8 @@ def unpack_data(cmd: int, cmd_to_struct: Dict, data: bytes) -> List[float]: # These messages do not contain a data package return [] - for key in cmd_to_struct: + for key, value in cmd_to_struct.items(): if cmd == key if isinstance(key, int) else cmd in key: - value = cmd_to_struct[key] if isinstance(value, tuple): # The struct is given as the fist argument struct_t: struct.Struct = value[0] diff --git a/pyproject.toml b/pyproject.toml index d41bf6e22..c65275559 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,9 +61,9 @@ changelog = "https://github.com/hardbyte/python-can/blob/develop/CHANGELOG.md" [project.optional-dependencies] lint = [ "pylint==3.2.*", - "ruff==0.7.0", - "black==24.10.*", - "mypy==1.12.*", + "ruff==0.10.0", + "black==25.1.*", + "mypy==1.15.*", ] pywin32 = ["pywin32>=305; platform_system == 'Windows' and platform_python_implementation == 'CPython'"] seeedstudio = ["pyserial>=3.0"] @@ -133,7 +133,7 @@ exclude = [ line-length = 100 [tool.ruff.lint] -select = [ +extend-select = [ "A", # flake8-builtins "B", # flake8-bugbear "C4", # flake8-comprehensions diff --git a/test/listener_test.py b/test/listener_test.py index b530afa60..54496176b 100644 --- a/test/listener_test.py +++ b/test/listener_test.py @@ -1,7 +1,6 @@ #!/usr/bin/env python -""" -""" +""" """ import asyncio import logging import os diff --git a/test/test_interface_canalystii.py b/test/test_interface_canalystii.py index 65d9ee74b..3bdd281d2 100755 --- a/test/test_interface_canalystii.py +++ b/test/test_interface_canalystii.py @@ -1,7 +1,6 @@ #!/usr/bin/env python -""" -""" +""" """ import unittest from ctypes import c_ubyte diff --git a/test/test_kvaser.py b/test/test_kvaser.py index 8d18976aa..c18b3bc15 100644 --- a/test/test_kvaser.py +++ b/test/test_kvaser.py @@ -1,7 +1,6 @@ #!/usr/bin/env python -""" -""" +""" """ import ctypes import time diff --git a/test/test_neovi.py b/test/test_neovi.py index d8f54960a..8c816bef2 100644 --- a/test/test_neovi.py +++ b/test/test_neovi.py @@ -1,7 +1,6 @@ #!/usr/bin/env python -""" -""" +""" """ import pickle import unittest diff --git a/test/test_vector.py b/test/test_vector.py index ca46526fb..5d074f614 100644 --- a/test/test_vector.py +++ b/test/test_vector.py @@ -1042,142 +1042,142 @@ def _find_virtual_can_serial() -> int: XL_DRIVER_CONFIG_EXAMPLE = ( - b"\x0E\x00\x1E\x14\x0C\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x0e\x00\x1e\x14\x0c\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4E\x38\x39\x31\x34\x20\x43\x68\x61\x6E\x6E" - b"\x65\x6C\x20\x53\x74\x72\x65\x61\x6D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x2D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x04" - b"\x0A\x40\x00\x02\x00\x02\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4e\x38\x39\x31\x34\x20\x43\x68\x61\x6e\x6e" + b"\x65\x6c\x20\x53\x74\x72\x65\x61\x6d\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x2d\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x04" + b"\x0a\x40\x00\x02\x00\x02\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x0C\x00\x02\x0A\x04\x00\x00\x00\x00\x00\x00\x00\x8E" - b"\x00\x02\x0A\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xE9\x03\x00\x00\x08" - b"\x1C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x0a\x04\x00\x00\x00\x00\x00\x00\x00\x8e" + b"\x00\x02\x0a\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe9\x03\x00\x00\x08" + b"\x1c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x03\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4E\x38\x39\x31" - b"\x34\x20\x43\x68\x61\x6E\x6E\x65\x6C\x20\x31\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x2D\x00\x01\x03\x02\x00\x00\x00\x00\x01\x02\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4e\x38\x39\x31" + b"\x34\x20\x43\x68\x61\x6e\x6e\x65\x6c\x20\x31\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x2d\x00\x01\x03\x02\x00\x00\x00\x00\x01\x02\x00\x00" b"\x00\x00\x00\x00\x00\x02\x10\x00\x08\x07\x01\x04\x00\x00\x00\x00\x00\x00\x04\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0C\x00\x02\x0A\x04\x00" - b"\x00\x00\x00\x00\x00\x00\x8E\x00\x02\x0A\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x0a\x04\x00" + b"\x00\x00\x00\x00\x00\x00\x8e\x00\x02\x0a\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\xE9\x03\x00\x00\x08\x1C\x00\x00\x46\x52\x70\x69\x67\x67\x79\x20\x31\x30" - b"\x38\x30\x41\x6D\x61\x67\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\xe9\x03\x00\x00\x08\x1c\x00\x00\x46\x52\x70\x69\x67\x67\x79\x20\x31\x30" + b"\x38\x30\x41\x6d\x61\x67\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x05\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x56\x4E\x38\x39\x31\x34\x20\x43\x68\x61\x6E\x6E\x65\x6C\x20\x32\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x2D\x00\x02\x3C\x01\x00" - b"\x00\x00\x00\x02\x04\x00\x00\x00\x00\x00\x00\x00\x12\x00\x00\xA2\x03\x05\x01\x00" - b"\x00\x00\x04\x00\x00\x01\x00\x00\x00\x20\xA1\x07\x00\x01\x04\x03\x01\x01\x00\x00" + b"\x00\x00\x56\x4e\x38\x39\x31\x34\x20\x43\x68\x61\x6e\x6e\x65\x6c\x20\x32\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x2d\x00\x02\x3c\x01\x00" + b"\x00\x00\x00\x02\x04\x00\x00\x00\x00\x00\x00\x00\x12\x00\x00\xa2\x03\x05\x01\x00" + b"\x00\x00\x04\x00\x00\x01\x00\x00\x00\x20\xa1\x07\x00\x01\x04\x03\x01\x01\x00\x00" b"\x00\x00\x00\x00\x00\x01\x80\x00\x00\x00\x68\x89\x09\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x0C\x00\x02\x0A\x04\x00\x00\x00\x00\x00\x00\x00\x8E\x00\x02\x0A\x00\x00\x00" + b"\x00\x0c\x00\x02\x0a\x04\x00\x00\x00\x00\x00\x00\x00\x8e\x00\x02\x0a\x00\x00\x00" b"\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\xE9\x03\x00\x00\x08\x1C\x00\x00\x4F\x6E\x20" - b"\x62\x6F\x61\x72\x64\x20\x43\x41\x4E\x20\x31\x30\x35\x31\x63\x61\x70\x28\x48\x69" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe9\x03\x00\x00\x08\x1c\x00\x00\x4f\x6e\x20" + b"\x62\x6f\x61\x72\x64\x20\x43\x41\x4e\x20\x31\x30\x35\x31\x63\x61\x70\x28\x48\x69" b"\x67\x68\x73\x70\x65\x65\x64\x29\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03" b"\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4E\x38\x39\x31\x34\x20\x43\x68\x61\x6E" - b"\x6E\x65\x6C\x20\x33\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x2D\x00\x03\x3C\x01\x00\x00\x00\x00\x03\x08\x00\x00\x00\x00\x00\x00\x00\x12" - b"\x00\x00\xA2\x03\x09\x01\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x20\xA1\x07\x00" - b"\x01\x04\x03\x01\x01\x00\x00\x00\x00\x00\x00\x00\x01\x9B\x00\x00\x00\x68\x89\x09" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x0C\x00\x02\x0A\x04\x00\x00\x00\x00\x00\x00\x00" - b"\x8E\x00\x02\x0A\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xE9\x03\x00\x00" - b"\x08\x1C\x00\x00\x4F\x6E\x20\x62\x6F\x61\x72\x64\x20\x43\x41\x4E\x20\x31\x30\x35" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4e\x38\x39\x31\x34\x20\x43\x68\x61\x6e" + b"\x6e\x65\x6c\x20\x33\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x2d\x00\x03\x3c\x01\x00\x00\x00\x00\x03\x08\x00\x00\x00\x00\x00\x00\x00\x12" + b"\x00\x00\xa2\x03\x09\x01\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x20\xa1\x07\x00" + b"\x01\x04\x03\x01\x01\x00\x00\x00\x00\x00\x00\x00\x01\x9b\x00\x00\x00\x68\x89\x09" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x0a\x04\x00\x00\x00\x00\x00\x00\x00" + b"\x8e\x00\x02\x0a\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe9\x03\x00\x00" + b"\x08\x1c\x00\x00\x4f\x6e\x20\x62\x6f\x61\x72\x64\x20\x43\x41\x4e\x20\x31\x30\x35" b"\x31\x63\x61\x70\x28\x48\x69\x67\x68\x73\x70\x65\x65\x64\x29\x00\x04\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x03\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4E\x38\x39" - b"\x31\x34\x20\x43\x68\x61\x6E\x6E\x65\x6C\x20\x34\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x2D\x00\x04\x33\x01\x00\x00\x00\x00\x04\x10\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4e\x38\x39" + b"\x31\x34\x20\x43\x68\x61\x6e\x6e\x65\x6c\x20\x34\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x2d\x00\x04\x33\x01\x00\x00\x00\x00\x04\x10\x00" b"\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x03\x09\x02\x08\x00\x00\x00\x00\x00\x02" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0C\x00\x02\x0A\x03" - b"\x00\x00\x00\x00\x00\x00\x00\x8E\x00\x02\x0A\x00\x00\x00\x00\x00\x00\x00\x01\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x0a\x03" + b"\x00\x00\x00\x00\x00\x00\x00\x8e\x00\x02\x0a\x00\x00\x00\x00\x00\x00\x00\x01\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\xE9\x03\x00\x00\x08\x1C\x00\x00\x4C\x49\x4E\x70\x69\x67\x67\x79\x20" - b"\x37\x32\x36\x39\x6D\x61\x67\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x07\x00\x00\x00\x70\x17\x00\x00\x0C\x09\x03\x04\x58\x02\x10\x0E\x30" + b"\x00\x00\x00\xe9\x03\x00\x00\x08\x1c\x00\x00\x4c\x49\x4e\x70\x69\x67\x67\x79\x20" + b"\x37\x32\x36\x39\x6d\x61\x67\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x07\x00\x00\x00\x70\x17\x00\x00\x0c\x09\x03\x04\x58\x02\x10\x0e\x30" b"\x57\x05\x00\x00\x00\x00\x00\x88\x13\x88\x13\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x56\x4E\x38\x39\x31\x34\x20\x43\x68\x61\x6E\x6E\x65\x6C\x20\x35\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x2D\x00\x05\x00\x00" + b"\x00\x00\x00\x56\x4e\x38\x39\x31\x34\x20\x43\x68\x61\x6e\x6e\x65\x6c\x20\x35\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x2d\x00\x05\x00\x00" b"\x00\x00\x02\x00\x05\x20\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x0C\x00\x02\x0A\x00\x00\x00\x00\x00\x00\x00\x00\x8E\x00\x02\x0A\x00\x00" + b"\x00\x00\x0c\x00\x02\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x8e\x00\x02\x0a\x00\x00" b"\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xE9\x03\x00\x00\x08\x1C\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe9\x03\x00\x00\x08\x1c\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x03\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4E\x38\x39\x31\x34\x20\x43\x68\x61" - b"\x6E\x6E\x65\x6C\x20\x36\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x2D\x00\x06\x00\x00\x00\x00\x02\x00\x06\x40\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4e\x38\x39\x31\x34\x20\x43\x68\x61" + b"\x6e\x6e\x65\x6c\x20\x36\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x2d\x00\x06\x00\x00\x00\x00\x02\x00\x06\x40\x00\x00\x00\x00\x00\x00\x00" b"\x02\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0C\x00\x02\x0A\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x8E\x00\x02\x0A\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xE9\x03\x00" - b"\x00\x08\x1C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x0a\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x8e\x00\x02\x0a\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe9\x03\x00" + b"\x00\x08\x1c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x03\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4E\x38" - b"\x39\x31\x34\x20\x43\x68\x61\x6E\x6E\x65\x6C\x20\x37\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x2D\x00\x07\x00\x00\x00\x00\x02\x00\x07\x80" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4e\x38" + b"\x39\x31\x34\x20\x43\x68\x61\x6e\x6e\x65\x6c\x20\x37\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x2d\x00\x07\x00\x00\x00\x00\x02\x00\x07\x80" b"\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0C\x00\x02\x0A" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x8E\x00\x02\x0A\x00\x00\x00\x00\x00\x00\x00\x01" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x0a" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x8e\x00\x02\x0a\x00\x00\x00\x00\x00\x00\x00\x01" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\xE9\x03\x00\x00\x08\x1C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\xe9\x03\x00\x00\x08\x1c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x04\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x56\x4E\x38\x39\x31\x34\x20\x43\x68\x61\x6E\x6E\x65\x6C\x20\x38" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x2D\x00\x08\x3C" - b"\x01\x00\x00\x00\x00\x08\x00\x01\x00\x00\x00\x00\x00\x00\x12\x00\x00\xA2\x01\x00" - b"\x01\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x20\xA1\x07\x00\x01\x04\x03\x01\x01" + b"\x00\x00\x00\x00\x56\x4e\x38\x39\x31\x34\x20\x43\x68\x61\x6e\x6e\x65\x6c\x20\x38" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x2d\x00\x08\x3c" + b"\x01\x00\x00\x00\x00\x08\x00\x01\x00\x00\x00\x00\x00\x00\x12\x00\x00\xa2\x01\x00" + b"\x01\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x20\xa1\x07\x00\x01\x04\x03\x01\x01" b"\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x68\x89\x09\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x0C\x00\x02\x0A\x04\x00\x00\x00\x00\x00\x00\x00\x8E\x00\x02\x0A\x00" + b"\x00\x00\x00\x0c\x00\x02\x0a\x04\x00\x00\x00\x00\x00\x00\x00\x8e\x00\x02\x0a\x00" b"\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xE9\x03\x00\x00\x08\x1C\x00\x00\x4F" - b"\x6E\x20\x62\x6F\x61\x72\x64\x20\x43\x41\x4E\x20\x31\x30\x35\x31\x63\x61\x70\x28" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe9\x03\x00\x00\x08\x1c\x00\x00\x4f" + b"\x6e\x20\x62\x6f\x61\x72\x64\x20\x43\x41\x4e\x20\x31\x30\x35\x31\x63\x61\x70\x28" b"\x48\x69\x67\x68\x73\x70\x65\x65\x64\x29\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x03\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4E\x38\x39\x31\x34\x20\x43\x68" - b"\x61\x6E\x6E\x65\x6C\x20\x39\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x2D\x00\x09\x80\x02\x00\x00\x00\x00\x09\x00\x02\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4e\x38\x39\x31\x34\x20\x43\x68" + b"\x61\x6e\x6e\x65\x6c\x20\x39\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x2d\x00\x09\x80\x02\x00\x00\x00\x00\x09\x00\x02\x00\x00\x00\x00\x00" b"\x00\x02\x00\x00\x00\x40\x00\x40\x00\x00\x00\x00\x00\x00\x40\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0C\x00\x02\x0A\x03\x00\x00\x00\x00\x00" - b"\x00\x00\x8E\x00\x02\x0A\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xE9\x03" - b"\x00\x00\x08\x1C\x00\x00\x44\x2F\x41\x20\x49\x4F\x70\x69\x67\x67\x79\x20\x38\x36" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x0a\x03\x00\x00\x00\x00\x00" + b"\x00\x00\x8e\x00\x02\x0a\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe9\x03" + b"\x00\x00\x08\x1c\x00\x00\x44\x2f\x41\x20\x49\x4f\x70\x69\x67\x67\x79\x20\x38\x36" b"\x34\x32\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x03\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x69" - b"\x72\x74\x75\x61\x6C\x20\x43\x68\x61\x6E\x6E\x65\x6C\x20\x31\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x16\x00\x00\x00\x00\x00\x0A" - b"\x00\x04\x00\x00\x00\x00\x00\x00\x07\x00\x00\xA0\x01\x00\x01\x00\x00\x00\x00\x00" - b"\x00\x01\x00\x00\x00\x20\xA1\x07\x00\x01\x04\x03\x01\x01\x00\x00\x00\x00\x00\x00" - b"\x00\x01\x00\x00\x00\x00\x68\x89\x09\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x1E" + b"\x72\x74\x75\x61\x6c\x20\x43\x68\x61\x6e\x6e\x65\x6c\x20\x31\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x16\x00\x00\x00\x00\x00\x0a" + b"\x00\x04\x00\x00\x00\x00\x00\x00\x07\x00\x00\xa0\x01\x00\x01\x00\x00\x00\x00\x00" + b"\x00\x01\x00\x00\x00\x20\xa1\x07\x00\x01\x04\x03\x01\x01\x00\x00\x00\x00\x00\x00" + b"\x00\x01\x00\x00\x00\x00\x68\x89\x09\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x1e" b"\x14\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x69\x72\x74\x75\x61\x6C" - b"\x20\x43\x41\x4E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x69\x72\x74\x75\x61\x6c" + b"\x20\x43\x41\x4e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\x56\x69\x72\x74\x75\x61\x6C\x20\x43\x68\x61\x6E\x6E\x65\x6C" + b"\x00\x00\x00\x00\x00\x56\x69\x72\x74\x75\x61\x6c\x20\x43\x68\x61\x6e\x6e\x65\x6c" b"\x20\x32\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x01" - b"\x16\x00\x00\x00\x00\x00\x0B\x00\x08\x00\x00\x00\x00\x00\x00\x07\x00\x00\xA0\x01" - b"\x00\x01\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x20\xA1\x07\x00\x01\x04\x03\x01" + b"\x16\x00\x00\x00\x00\x00\x0b\x00\x08\x00\x00\x00\x00\x00\x00\x07\x00\x00\xa0\x01" + b"\x00\x01\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x20\xa1\x07\x00\x01\x04\x03\x01" b"\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x68\x89\x09\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x10\x00\x1E\x14\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x10\x00\x1e\x14\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x56\x69\x72\x74\x75\x61\x6C\x20\x43\x41\x4E\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x56\x69\x72\x74\x75\x61\x6c\x20\x43\x41\x4e\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x02" + 11832 * b"\x00" ) diff --git a/test/zero_dlc_test.py b/test/zero_dlc_test.py index 4e7596caf..d6693e294 100644 --- a/test/zero_dlc_test.py +++ b/test/zero_dlc_test.py @@ -1,7 +1,6 @@ #!/usr/bin/env python -""" -""" +""" """ import logging import unittest From 5d62394006f42d1bf98e159fe9bb08d10e47e6eb Mon Sep 17 00:00:00 2001 From: Pierre-Luc Date: Tue, 1 Apr 2025 11:32:10 -0400 Subject: [PATCH 075/127] Refactor timestamp factor calculations to use constants (#1933) --- can/io/blf.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/can/io/blf.py b/can/io/blf.py index 139b44285..e64a2247d 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -99,6 +99,9 @@ class BLFParseError(Exception): TIME_TEN_MICS = 0x00000001 TIME_ONE_NANS = 0x00000002 +TIME_TEN_MICS_FACTOR = Decimal("1e-5") +TIME_ONE_NANS_FACTOR = Decimal("1e-9") + def timestamp_to_systemtime(timestamp: float) -> TSystemTime: if timestamp is None or timestamp < 631152000: @@ -269,7 +272,7 @@ def _parse_data(self, data): continue # Calculate absolute timestamp in seconds - factor = Decimal("1e-5") if flags == 1 else Decimal("1e-9") + factor = TIME_TEN_MICS_FACTOR if flags == 1 else TIME_ONE_NANS_FACTOR timestamp = float(Decimal(timestamp) * factor) + start_timestamp if obj_type in (CAN_MESSAGE, CAN_MESSAGE2): From c46d3ea7bd631df633f2588877cfcd94ac0802e3 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 30 May 2025 11:14:53 +0200 Subject: [PATCH 076/127] Raise minimum python version to 3.9 (#1931) * require python>=3.9 * fix formatting * pin ruff==0.11.12, mypy==1.16.* --------- Co-authored-by: zariiii9003 --- .github/workflows/ci.yml | 4 ---- README.rst | 7 ++---- can/__init__.py | 4 ++-- can/_entry_points.py | 6 +++--- can/bit_timing.py | 7 +++--- can/broadcastmanager.py | 7 +++--- can/bus.py | 14 +++++------- can/ctypesutil.py | 4 ++-- can/exceptions.py | 11 +++------- can/interface.py | 9 ++++---- can/interfaces/__init__.py | 4 +--- can/interfaces/canalystii.py | 11 +++++----- can/interfaces/etas/__init__.py | 10 ++++----- can/interfaces/gs_usb.py | 4 ++-- can/interfaces/iscan.py | 4 ++-- can/interfaces/ixxat/canlib.py | 5 +++-- can/interfaces/ixxat/canlib_vcinpl.py | 7 +++--- can/interfaces/ixxat/canlib_vcinpl2.py | 5 +++-- can/interfaces/nican.py | 6 +++--- can/interfaces/nixnet.py | 6 +++--- can/interfaces/pcan/pcan.py | 6 +++--- can/interfaces/serial/serial_can.py | 8 +++---- can/interfaces/slcan.py | 6 +++--- can/interfaces/socketcan/socketcan.py | 21 +++++++++--------- can/interfaces/socketcan/utils.py | 4 ++-- can/interfaces/socketcand/socketcand.py | 5 ++--- can/interfaces/systec/exceptions.py | 9 ++++---- can/interfaces/udp_multicast/bus.py | 10 ++++----- can/interfaces/udp_multicast/utils.py | 4 ++-- can/interfaces/usb2can/serial_selector.py | 3 +-- can/interfaces/vector/canlib.py | 24 +++++++++------------ can/interfaces/virtual.py | 8 +++---- can/io/asc.py | 13 ++++++------ can/io/blf.py | 7 +++--- can/io/canutils.py | 3 ++- can/io/csv.py | 3 ++- can/io/generic.py | 14 +++++------- can/io/logger.py | 16 ++++++-------- can/io/mf4.py | 11 +++++----- can/io/player.py | 12 ++++------- can/io/sqlite.py | 3 ++- can/io/trc.py | 21 +++++++++--------- can/listener.py | 3 ++- can/logger.py | 19 +++++++---------- can/notifier.py | 9 ++++---- can/player.py | 2 +- can/typechecking.py | 10 ++++----- can/util.py | 26 +++++++++++------------ can/viewer.py | 9 ++++---- pyproject.toml | 7 +++--- test/contextmanager_test.py | 14 ++++++------ 51 files changed, 209 insertions(+), 236 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0ddb3028c..ab14b6b6a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,6 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] experimental: [false] python-version: [ - "3.8", "3.9", "3.10", "3.11", @@ -81,9 +80,6 @@ jobs: run: | python -m pip install --upgrade pip pip install -e .[lint] - - name: mypy 3.8 - run: | - mypy --python-version 3.8 . - name: mypy 3.9 run: | mypy --python-version 3.9 . diff --git a/README.rst b/README.rst index d2f05b2c1..3c185f6cb 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,7 @@ python-can |pypi| |conda| |python_implementation| |downloads| |downloads_monthly| -|docs| |github-actions| |coverage| |mergify| |formatter| +|docs| |github-actions| |coverage| |formatter| .. |pypi| image:: https://img.shields.io/pypi/v/python-can.svg :target: https://pypi.python.org/pypi/python-can/ @@ -41,10 +41,6 @@ python-can :target: https://coveralls.io/github/hardbyte/python-can?branch=develop :alt: Test coverage reports on Coveralls.io -.. |mergify| image:: https://img.shields.io/endpoint.svg?url=https://api.mergify.com/v1/badges/hardbyte/python-can&style=flat - :target: https://mergify.io - :alt: Mergify Status - The **C**\ ontroller **A**\ rea **N**\ etwork is a bus standard designed to allow microcontrollers and devices to communicate with each other. It has priority based bus arbitration and reliable deterministic @@ -64,6 +60,7 @@ Library Version Python 3.x 2.7+, 3.5+ 4.0+ 3.7+ 4.3+ 3.8+ + 4.6+ 3.9+ ============================== =========== diff --git a/can/__init__.py b/can/__init__.py index 1d4b7f0cf..b1bd636c1 100644 --- a/can/__init__.py +++ b/can/__init__.py @@ -8,7 +8,7 @@ import contextlib import logging from importlib.metadata import PackageNotFoundError, version -from typing import Any, Dict +from typing import Any __all__ = [ "VALID_INTERFACES", @@ -130,4 +130,4 @@ log = logging.getLogger("can") -rc: Dict[str, Any] = {} +rc: dict[str, Any] = {} diff --git a/can/_entry_points.py b/can/_entry_points.py index 6842e3c1a..e8ce92d7c 100644 --- a/can/_entry_points.py +++ b/can/_entry_points.py @@ -2,7 +2,7 @@ import sys from dataclasses import dataclass from importlib.metadata import entry_points -from typing import Any, List +from typing import Any @dataclass @@ -20,14 +20,14 @@ def load(self) -> Any: # "Compatibility Note". if sys.version_info >= (3, 10): - def read_entry_points(group: str) -> List[_EntryPoint]: + def read_entry_points(group: str) -> list[_EntryPoint]: return [ _EntryPoint(ep.name, ep.module, ep.attr) for ep in entry_points(group=group) ] else: - def read_entry_points(group: str) -> List[_EntryPoint]: + def read_entry_points(group: str) -> list[_EntryPoint]: return [ _EntryPoint(ep.name, *ep.value.split(":", maxsplit=1)) for ep in entry_points().get(group, []) diff --git a/can/bit_timing.py b/can/bit_timing.py index feba8b6d2..4b0074472 100644 --- a/can/bit_timing.py +++ b/can/bit_timing.py @@ -1,6 +1,7 @@ # pylint: disable=too-many-lines import math -from typing import TYPE_CHECKING, Iterator, List, Mapping, cast +from collections.abc import Iterator, Mapping +from typing import TYPE_CHECKING, cast if TYPE_CHECKING: from can.typechecking import BitTimingDict, BitTimingFdDict @@ -286,7 +287,7 @@ def from_sample_point( if sample_point < 50.0: raise ValueError(f"sample_point (={sample_point}) must not be below 50%.") - possible_solutions: List[BitTiming] = list( + possible_solutions: list[BitTiming] = list( cls.iterate_from_sample_point(f_clock, bitrate, sample_point) ) @@ -874,7 +875,7 @@ def from_sample_point( f"data_sample_point (={data_sample_point}) must not be below 50%." ) - possible_solutions: List[BitTimingFd] = list( + possible_solutions: list[BitTimingFd] = list( cls.iterate_from_sample_point( f_clock, nom_bitrate, diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 19dca8fbc..b2bc28e76 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -12,13 +12,12 @@ import threading import time import warnings +from collections.abc import Sequence from typing import ( TYPE_CHECKING, Callable, Final, Optional, - Sequence, - Tuple, Union, cast, ) @@ -127,7 +126,7 @@ def __init__( @staticmethod def _check_and_convert_messages( messages: Union[Sequence[Message], Message], - ) -> Tuple[Message, ...]: + ) -> tuple[Message, ...]: """Helper function to convert a Message or Sequence of messages into a tuple, and raises an error when the given value is invalid. @@ -194,7 +193,7 @@ def start(self) -> None: class ModifiableCyclicTaskABC(CyclicSendTaskABC, abc.ABC): - def _check_modified_messages(self, messages: Tuple[Message, ...]) -> None: + def _check_modified_messages(self, messages: tuple[Message, ...]) -> None: """Helper function to perform error checking when modifying the data in the cyclic task. diff --git a/can/bus.py b/can/bus.py index d186e2d05..0d031a18b 100644 --- a/can/bus.py +++ b/can/bus.py @@ -6,18 +6,14 @@ import logging import threading from abc import ABC, ABCMeta, abstractmethod +from collections.abc import Iterator, Sequence from enum import Enum, auto from time import time from types import TracebackType from typing import ( Any, Callable, - Iterator, - List, Optional, - Sequence, - Tuple, - Type, Union, cast, ) @@ -97,7 +93,7 @@ def __init__( :raises ~can.exceptions.CanInitializationError: If the bus cannot be initialized """ - self._periodic_tasks: List[_SelfRemovingCyclicTask] = [] + self._periodic_tasks: list[_SelfRemovingCyclicTask] = [] self.set_filters(can_filters) # Flip the class default value when the constructor finishes. That # usually means the derived class constructor was also successful, @@ -147,7 +143,7 @@ def recv(self, timeout: Optional[float] = None) -> Optional[Message]: def _recv_internal( self, timeout: Optional[float] - ) -> Tuple[Optional[Message], bool]: + ) -> tuple[Optional[Message], bool]: """ Read a message from the bus and tell whether it was filtered. This methods may be called by :meth:`~can.BusABC.recv` @@ -491,7 +487,7 @@ def __enter__(self) -> Self: def __exit__( self, - exc_type: Optional[Type[BaseException]], + exc_type: Optional[type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType], ) -> None: @@ -529,7 +525,7 @@ def protocol(self) -> CanProtocol: return self._can_protocol @staticmethod - def _detect_available_configs() -> List[can.typechecking.AutoDetectedConfig]: + def _detect_available_configs() -> list[can.typechecking.AutoDetectedConfig]: """Detect all configurations/channels that this interface could currently connect with. diff --git a/can/ctypesutil.py b/can/ctypesutil.py index a80dc3194..8336941be 100644 --- a/can/ctypesutil.py +++ b/can/ctypesutil.py @@ -5,7 +5,7 @@ import ctypes import logging import sys -from typing import Any, Callable, Optional, Tuple, Union +from typing import Any, Callable, Optional, Union log = logging.getLogger("can.ctypesutil") @@ -32,7 +32,7 @@ def map_symbol( self, func_name: str, restype: Any = None, - argtypes: Tuple[Any, ...] = (), + argtypes: tuple[Any, ...] = (), errcheck: Optional[Callable[..., Any]] = None, ) -> Any: """ diff --git a/can/exceptions.py b/can/exceptions.py index ca2d6c5a3..8abc75147 100644 --- a/can/exceptions.py +++ b/can/exceptions.py @@ -15,14 +15,9 @@ :class:`ValueError`. This should always be documented for the function at hand. """ -import sys +from collections.abc import Generator from contextlib import contextmanager -from typing import Optional, Type - -if sys.version_info >= (3, 9): - from collections.abc import Generator -else: - from typing import Generator +from typing import Optional class CanError(Exception): @@ -114,7 +109,7 @@ class CanTimeoutError(CanError, TimeoutError): @contextmanager def error_check( error_message: Optional[str] = None, - exception_type: Type[CanError] = CanOperationError, + exception_type: type[CanError] = CanOperationError, ) -> Generator[None, None, None]: """Catches any exceptions and turns them into the new type while preserving the stack trace.""" try: diff --git a/can/interface.py b/can/interface.py index f7a8ecc02..e017b9514 100644 --- a/can/interface.py +++ b/can/interface.py @@ -6,7 +6,8 @@ import importlib import logging -from typing import Any, Iterable, List, Optional, Type, Union, cast +from collections.abc import Iterable +from typing import Any, Optional, Union, cast from . import util from .bus import BusABC @@ -18,7 +19,7 @@ log_autodetect = log.getChild("detect_available_configs") -def _get_class_for_interface(interface: str) -> Type[BusABC]: +def _get_class_for_interface(interface: str) -> type[BusABC]: """ Returns the main bus class for the given interface. @@ -52,7 +53,7 @@ def _get_class_for_interface(interface: str) -> Type[BusABC]: f"'{interface}': {e}" ) from None - return cast("Type[BusABC]", bus_class) + return cast("type[BusABC]", bus_class) @util.deprecated_args_alias( @@ -139,7 +140,7 @@ def Bus( # noqa: N802 def detect_available_configs( interfaces: Union[None, str, Iterable[str]] = None, -) -> List[AutoDetectedConfig]: +) -> list[AutoDetectedConfig]: """Detect all configurations/channels that the interfaces could currently connect with. diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py index f220d28e5..1b401639a 100644 --- a/can/interfaces/__init__.py +++ b/can/interfaces/__init__.py @@ -2,8 +2,6 @@ Interfaces contain low level implementations that interact with CAN hardware. """ -from typing import Dict, Tuple - from can._entry_points import read_entry_points __all__ = [ @@ -35,7 +33,7 @@ ] # interface_name => (module, classname) -BACKENDS: Dict[str, Tuple[str, str]] = { +BACKENDS: dict[str, tuple[str, str]] = { "kvaser": ("can.interfaces.kvaser", "KvaserBus"), "socketcan": ("can.interfaces.socketcan", "SocketcanBus"), "serial": ("can.interfaces.serial.serial_can", "SerialBus"), diff --git a/can/interfaces/canalystii.py b/can/interfaces/canalystii.py index 2fef19497..d85211130 100644 --- a/can/interfaces/canalystii.py +++ b/can/interfaces/canalystii.py @@ -1,8 +1,9 @@ import logging import time from collections import deque +from collections.abc import Sequence from ctypes import c_ubyte -from typing import Any, Deque, Dict, Optional, Sequence, Tuple, Union +from typing import Any, Optional, Union import canalystii as driver @@ -26,7 +27,7 @@ def __init__( timing: Optional[Union[BitTiming, BitTimingFd]] = None, can_filters: Optional[CanFilters] = None, rx_queue_size: Optional[int] = None, - **kwargs: Dict[str, Any], + **kwargs: dict[str, Any], ): """ @@ -68,7 +69,7 @@ def __init__( self.channels = list(channel) self.channel_info = f"CANalyst-II: device {device}, channels {self.channels}" - self.rx_queue: Deque[Tuple[int, driver.Message]] = deque(maxlen=rx_queue_size) + self.rx_queue: deque[tuple[int, driver.Message]] = deque(maxlen=rx_queue_size) self.device = driver.CanalystDevice(device_index=device) self._can_protocol = CanProtocol.CAN_20 @@ -129,7 +130,7 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: if timeout is not None and not send_result: raise CanTimeoutError(f"Send timed out after {timeout} seconds") - def _recv_from_queue(self) -> Tuple[Message, bool]: + def _recv_from_queue(self) -> tuple[Message, bool]: """Return a message from the internal receive queue""" channel, raw_msg = self.rx_queue.popleft() @@ -166,7 +167,7 @@ def poll_received_messages(self) -> None: def _recv_internal( self, timeout: Optional[float] = None - ) -> Tuple[Optional[Message], bool]: + ) -> tuple[Optional[Message], bool]: """ :param timeout: float in seconds diff --git a/can/interfaces/etas/__init__.py b/can/interfaces/etas/__init__.py index 2bfcbf427..9d4d0bd2a 100644 --- a/can/interfaces/etas/__init__.py +++ b/can/interfaces/etas/__init__.py @@ -1,5 +1,5 @@ import time -from typing import Dict, List, Optional, Tuple +from typing import Optional import can from can.exceptions import CanInitializationError @@ -16,7 +16,7 @@ def __init__( bitrate: int = 1000000, fd: bool = True, data_bitrate: int = 2000000, - **kwargs: Dict[str, any], + **kwargs: dict[str, any], ): self.receive_own_messages = receive_own_messages self._can_protocol = can.CanProtocol.CAN_FD if fd else can.CanProtocol.CAN_20 @@ -122,7 +122,7 @@ def __init__( def _recv_internal( self, timeout: Optional[float] - ) -> Tuple[Optional[can.Message], bool]: + ) -> tuple[Optional[can.Message], bool]: ociMsgs = (ctypes.POINTER(OCI_CANMessageEx) * 1)() ociMsg = OCI_CANMessageEx() ociMsgs[0] = ctypes.pointer(ociMsg) @@ -295,12 +295,12 @@ def state(self, new_state: can.BusState) -> None: raise NotImplementedError("Setting state is not implemented.") @staticmethod - def _detect_available_configs() -> List[can.typechecking.AutoDetectedConfig]: + def _detect_available_configs() -> list[can.typechecking.AutoDetectedConfig]: nodeRange = CSI_NodeRange(CSI_NODE_MIN, CSI_NODE_MAX) tree = ctypes.POINTER(CSI_Tree)() CSI_CreateProtocolTree(ctypes.c_char_p(b""), nodeRange, ctypes.byref(tree)) - nodes: List[Dict[str, str]] = [] + nodes: list[dict[str, str]] = [] def _findNodes(tree, prefix): uri = f"{prefix}/{tree.contents.item.uriName.decode()}" diff --git a/can/interfaces/gs_usb.py b/can/interfaces/gs_usb.py index 6268350ee..4ab541f43 100644 --- a/can/interfaces/gs_usb.py +++ b/can/interfaces/gs_usb.py @@ -1,5 +1,5 @@ import logging -from typing import Optional, Tuple +from typing import Optional import usb from gs_usb.constants import CAN_EFF_FLAG, CAN_ERR_FLAG, CAN_MAX_DLC, CAN_RTR_FLAG @@ -119,7 +119,7 @@ def send(self, msg: can.Message, timeout: Optional[float] = None): def _recv_internal( self, timeout: Optional[float] - ) -> Tuple[Optional[can.Message], bool]: + ) -> tuple[Optional[can.Message], bool]: """ Read a message from the bus and tell whether it was filtered. This methods may be called by :meth:`~can.BusABC.recv` diff --git a/can/interfaces/iscan.py b/can/interfaces/iscan.py index be0b0dae8..79b4f754d 100644 --- a/can/interfaces/iscan.py +++ b/can/interfaces/iscan.py @@ -5,7 +5,7 @@ import ctypes import logging import time -from typing import Optional, Tuple, Union +from typing import Optional, Union from can import ( BusABC, @@ -117,7 +117,7 @@ def __init__( def _recv_internal( self, timeout: Optional[float] - ) -> Tuple[Optional[Message], bool]: + ) -> tuple[Optional[Message], bool]: raw_msg = MessageExStruct() end_time = time.time() + timeout if timeout is not None else None while True: diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index a1693aeed..e6ad25d57 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -1,4 +1,5 @@ -from typing import Callable, List, Optional, Sequence, Union +from collections.abc import Sequence +from typing import Callable, Optional, Union import can.interfaces.ixxat.canlib_vcinpl as vcinpl import can.interfaces.ixxat.canlib_vcinpl2 as vcinpl2 @@ -174,5 +175,5 @@ def state(self) -> BusState: return self.bus.state @staticmethod - def _detect_available_configs() -> List[AutoDetectedConfig]: + def _detect_available_configs() -> list[AutoDetectedConfig]: return vcinpl._detect_available_configs() diff --git a/can/interfaces/ixxat/canlib_vcinpl.py b/can/interfaces/ixxat/canlib_vcinpl.py index 567dd0cd9..ba8f1870b 100644 --- a/can/interfaces/ixxat/canlib_vcinpl.py +++ b/can/interfaces/ixxat/canlib_vcinpl.py @@ -14,7 +14,8 @@ import logging import sys import warnings -from typing import Callable, List, Optional, Sequence, Tuple, Union +from collections.abc import Sequence +from typing import Callable, Optional, Union from can import ( BusABC, @@ -64,7 +65,7 @@ def __vciFormatErrorExtended( - library_instance: CLibrary, function: Callable, vret: int, args: Tuple + library_instance: CLibrary, function: Callable, vret: int, args: tuple ): """Format a VCI error and attach failed function, decoded HRESULT and arguments :param CLibrary library_instance: @@ -961,7 +962,7 @@ def get_ixxat_hwids(): return hwids -def _detect_available_configs() -> List[AutoDetectedConfig]: +def _detect_available_configs() -> list[AutoDetectedConfig]: config_list = [] # list in wich to store the resulting bus kwargs # used to detect HWID diff --git a/can/interfaces/ixxat/canlib_vcinpl2.py b/can/interfaces/ixxat/canlib_vcinpl2.py index 79ad34c4f..f9ac5346b 100644 --- a/can/interfaces/ixxat/canlib_vcinpl2.py +++ b/can/interfaces/ixxat/canlib_vcinpl2.py @@ -15,7 +15,8 @@ import sys import time import warnings -from typing import Callable, Optional, Sequence, Tuple, Union +from collections.abc import Sequence +from typing import Callable, Optional, Union from can import ( BusABC, @@ -62,7 +63,7 @@ def __vciFormatErrorExtended( - library_instance: CLibrary, function: Callable, vret: int, args: Tuple + library_instance: CLibrary, function: Callable, vret: int, args: tuple ): """Format a VCI error and attach failed function, decoded HRESULT and arguments :param CLibrary library_instance: diff --git a/can/interfaces/nican.py b/can/interfaces/nican.py index 8a2efade7..1abf0b35f 100644 --- a/can/interfaces/nican.py +++ b/can/interfaces/nican.py @@ -16,7 +16,7 @@ import ctypes import logging import sys -from typing import Optional, Tuple, Type +from typing import Optional import can.typechecking from can import ( @@ -112,7 +112,7 @@ def check_status( result: int, function, arguments, - error_class: Type[NicanError] = NicanOperationError, + error_class: type[NicanError] = NicanOperationError, ) -> int: if result > 0: logger.warning(get_error_message(result)) @@ -281,7 +281,7 @@ def __init__( def _recv_internal( self, timeout: Optional[float] - ) -> Tuple[Optional[Message], bool]: + ) -> tuple[Optional[Message], bool]: """ Read a message from a NI-CAN bus. diff --git a/can/interfaces/nixnet.py b/can/interfaces/nixnet.py index 2b3cbd69a..c723d1f52 100644 --- a/can/interfaces/nixnet.py +++ b/can/interfaces/nixnet.py @@ -14,7 +14,7 @@ import warnings from queue import SimpleQueue from types import ModuleType -from typing import Any, List, Optional, Tuple, Union +from typing import Any, Optional, Union import can.typechecking from can import BitTiming, BitTimingFd, BusABC, CanProtocol, Message @@ -203,7 +203,7 @@ def fd(self) -> bool: def _recv_internal( self, timeout: Optional[float] - ) -> Tuple[Optional[Message], bool]: + ) -> tuple[Optional[Message], bool]: end_time = time.perf_counter() + timeout if timeout is not None else None while True: @@ -327,7 +327,7 @@ def shutdown(self) -> None: self._session_receive.close() @staticmethod - def _detect_available_configs() -> List[can.typechecking.AutoDetectedConfig]: + def _detect_available_configs() -> list[can.typechecking.AutoDetectedConfig]: configs = [] try: diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index d0372a83c..ef3b23e3b 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -6,7 +6,7 @@ import platform import time import warnings -from typing import Any, List, Optional, Tuple, Union +from typing import Any, Optional, Union from packaging import version @@ -502,7 +502,7 @@ def set_device_number(self, device_number): def _recv_internal( self, timeout: Optional[float] - ) -> Tuple[Optional[Message], bool]: + ) -> tuple[Optional[Message], bool]: end_time = time.time() + timeout if timeout is not None else None while True: @@ -726,7 +726,7 @@ def _detect_available_configs(): res, value = library_handle.GetValue(PCAN_NONEBUS, PCAN_ATTACHED_CHANNELS) if res != PCAN_ERROR_OK: return interfaces - channel_information: List[TPCANChannelInformation] = list(value) + channel_information: list[TPCANChannelInformation] = list(value) for channel in channel_information: # find channel name in PCAN_CHANNEL_NAMES by value channel_name = next( diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index 476cbd624..9fe715267 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -10,7 +10,7 @@ import io import logging import struct -from typing import Any, List, Optional, Tuple +from typing import Any, Optional from can import ( BusABC, @@ -38,7 +38,7 @@ from serial.tools.list_ports import comports as list_comports except ImportError: # If unavailable on some platform, just return nothing - def list_comports() -> List[Any]: + def list_comports() -> list[Any]: return [] @@ -160,7 +160,7 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: def _recv_internal( self, timeout: Optional[float] - ) -> Tuple[Optional[Message], bool]: + ) -> tuple[Optional[Message], bool]: """ Read a message from the serial device. @@ -229,7 +229,7 @@ def fileno(self) -> int: raise CanOperationError("Cannot fetch fileno") from exception @staticmethod - def _detect_available_configs() -> List[AutoDetectedConfig]: + def _detect_available_configs() -> list[AutoDetectedConfig]: return [ {"interface": "serial", "channel": port.device} for port in list_comports() ] diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index ddd2bec23..c51b298cc 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -7,7 +7,7 @@ import time import warnings from queue import SimpleQueue -from typing import Any, Optional, Tuple, Union, cast +from typing import Any, Optional, Union, cast from can import BitTiming, BitTimingFd, BusABC, CanProtocol, Message, typechecking from can.exceptions import ( @@ -230,7 +230,7 @@ def close(self) -> None: def _recv_internal( self, timeout: Optional[float] - ) -> Tuple[Optional[Message], bool]: + ) -> tuple[Optional[Message], bool]: canId = None remote = False extended = False @@ -315,7 +315,7 @@ def fileno(self) -> int: def get_version( self, timeout: Optional[float] - ) -> Tuple[Optional[int], Optional[int]]: + ) -> tuple[Optional[int], Optional[int]]: """Get HW and SW version of the slcan interface. :param timeout: diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index a5819dcb5..30b75108a 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -15,7 +15,8 @@ import threading import time import warnings -from typing import Callable, Dict, List, Optional, Sequence, Tuple, Type, Union +from collections.abc import Sequence +from typing import Callable, Optional, Union import can from can import BusABC, CanProtocol, Message @@ -50,13 +51,13 @@ # Setup BCM struct def bcm_header_factory( - fields: List[Tuple[str, Union[Type[ctypes.c_uint32], Type[ctypes.c_long]]]], + fields: list[tuple[str, Union[type[ctypes.c_uint32], type[ctypes.c_long]]]], alignment: int = 8, ): curr_stride = 0 - results: List[ - Tuple[ - str, Union[Type[ctypes.c_uint8], Type[ctypes.c_uint32], Type[ctypes.c_long]] + results: list[ + tuple[ + str, Union[type[ctypes.c_uint8], type[ctypes.c_uint32], type[ctypes.c_long]] ] ] = [] pad_index = 0 @@ -292,7 +293,7 @@ def build_bcm_transmit_header( # Note `TX_COUNTEVT` creates the message TX_EXPIRED when count expires flags |= constants.TX_COUNTEVT - def split_time(value: float) -> Tuple[int, int]: + def split_time(value: float) -> tuple[int, int]: """Given seconds as a float, return whole seconds and microseconds""" seconds = int(value) microseconds = int(1e6 * (value - seconds)) @@ -326,7 +327,7 @@ def is_frame_fd(frame: bytes): return len(frame) == constants.CANFD_MTU -def dissect_can_frame(frame: bytes) -> Tuple[int, int, int, bytes]: +def dissect_can_frame(frame: bytes) -> tuple[int, int, int, bytes]: can_id, data_len, flags, len8_dlc = CAN_FRAME_HEADER_STRUCT.unpack_from(frame) if data_len not in can.util.CAN_FD_DLC: @@ -739,7 +740,7 @@ def __init__( self.socket = create_socket() self.channel = channel self.channel_info = f"socketcan channel '{channel}'" - self._bcm_sockets: Dict[str, socket.socket] = {} + self._bcm_sockets: dict[str, socket.socket] = {} self._is_filtered = False self._task_id = 0 self._task_id_guard = threading.Lock() @@ -819,7 +820,7 @@ def shutdown(self) -> None: def _recv_internal( self, timeout: Optional[float] - ) -> Tuple[Optional[Message], bool]: + ) -> tuple[Optional[Message], bool]: try: # get all sockets that are ready (can be a list with a single value # being self.socket or an empty list if self.socket is not ready) @@ -992,7 +993,7 @@ def fileno(self) -> int: return self.socket.fileno() @staticmethod - def _detect_available_configs() -> List[can.typechecking.AutoDetectedConfig]: + def _detect_available_configs() -> list[can.typechecking.AutoDetectedConfig]: return [ {"interface": "socketcan", "channel": channel} for channel in find_available_interfaces() diff --git a/can/interfaces/socketcan/utils.py b/can/interfaces/socketcan/utils.py index 1505c6cf8..80dcb203f 100644 --- a/can/interfaces/socketcan/utils.py +++ b/can/interfaces/socketcan/utils.py @@ -9,7 +9,7 @@ import struct import subprocess import sys -from typing import List, Optional, cast +from typing import Optional, cast from can import typechecking from can.interfaces.socketcan.constants import CAN_EFF_FLAG @@ -39,7 +39,7 @@ def pack_filters(can_filters: Optional[typechecking.CanFilters] = None) -> bytes return struct.pack(can_filter_fmt, *filter_data) -def find_available_interfaces() -> List[str]: +def find_available_interfaces() -> list[str]: """Returns the names of all open can/vcan interfaces The function calls the ``ip link list`` command. If the lookup fails, an error diff --git a/can/interfaces/socketcand/socketcand.py b/can/interfaces/socketcand/socketcand.py index 7a2cc6fd0..3ecda082b 100644 --- a/can/interfaces/socketcand/socketcand.py +++ b/can/interfaces/socketcand/socketcand.py @@ -17,7 +17,6 @@ import urllib.parse as urlparselib import xml.etree.ElementTree as ET from collections import deque -from typing import List import can @@ -27,7 +26,7 @@ DEFAULT_SOCKETCAND_DISCOVERY_PORT = 42000 -def detect_beacon(timeout_ms: int = 3100) -> List[can.typechecking.AutoDetectedConfig]: +def detect_beacon(timeout_ms: int = 3100) -> list[can.typechecking.AutoDetectedConfig]: """ Detects socketcand servers @@ -340,7 +339,7 @@ def shutdown(self): self.__socket.close() @staticmethod - def _detect_available_configs() -> List[can.typechecking.AutoDetectedConfig]: + def _detect_available_configs() -> list[can.typechecking.AutoDetectedConfig]: try: return detect_beacon() except Exception as e: diff --git a/can/interfaces/systec/exceptions.py b/can/interfaces/systec/exceptions.py index dcd94bdbf..8768b412a 100644 --- a/can/interfaces/systec/exceptions.py +++ b/can/interfaces/systec/exceptions.py @@ -1,5 +1,4 @@ from abc import ABC, abstractmethod -from typing import Dict from can import CanError @@ -22,7 +21,7 @@ def __init__(self, result, func, arguments): @property @abstractmethod - def _error_message_mapping(self) -> Dict[ReturnCode, str]: ... + def _error_message_mapping(self) -> dict[ReturnCode, str]: ... class UcanError(UcanException): @@ -51,7 +50,7 @@ class UcanError(UcanException): } @property - def _error_message_mapping(self) -> Dict[ReturnCode, str]: + def _error_message_mapping(self) -> dict[ReturnCode, str]: return UcanError._ERROR_MESSAGES @@ -77,7 +76,7 @@ class UcanCmdError(UcanException): } @property - def _error_message_mapping(self) -> Dict[ReturnCode, str]: + def _error_message_mapping(self) -> dict[ReturnCode, str]: return UcanCmdError._ERROR_MESSAGES @@ -102,5 +101,5 @@ class UcanWarning(UcanException): } @property - def _error_message_mapping(self) -> Dict[ReturnCode, str]: + def _error_message_mapping(self) -> dict[ReturnCode, str]: return UcanWarning._ERROR_MESSAGES diff --git a/can/interfaces/udp_multicast/bus.py b/can/interfaces/udp_multicast/bus.py index dd114278c..31744c2b8 100644 --- a/can/interfaces/udp_multicast/bus.py +++ b/can/interfaces/udp_multicast/bus.py @@ -5,7 +5,7 @@ import struct import time import warnings -from typing import List, Optional, Tuple, Union +from typing import Optional, Union import can from can import BusABC, CanProtocol @@ -26,8 +26,8 @@ # see socket.getaddrinfo() -IPv4_ADDRESS_INFO = Tuple[str, int] # address, port -IPv6_ADDRESS_INFO = Tuple[str, int, int, int] # address, port, flowinfo, scope_id +IPv4_ADDRESS_INFO = tuple[str, int] # address, port +IPv6_ADDRESS_INFO = tuple[str, int, int, int] # address, port, flowinfo, scope_id IP_ADDRESS_INFO = Union[IPv4_ADDRESS_INFO, IPv6_ADDRESS_INFO] # Additional constants for the interaction with Unix kernels @@ -172,7 +172,7 @@ def shutdown(self) -> None: self._multicast.shutdown() @staticmethod - def _detect_available_configs() -> List[AutoDetectedConfig]: + def _detect_available_configs() -> list[AutoDetectedConfig]: if hasattr(socket, "CMSG_SPACE"): return [ { @@ -341,7 +341,7 @@ def send(self, data: bytes, timeout: Optional[float] = None) -> None: def recv( self, timeout: Optional[float] = None - ) -> Optional[Tuple[bytes, IP_ADDRESS_INFO, float]]: + ) -> Optional[tuple[bytes, IP_ADDRESS_INFO, float]]: """ Receive up to **max_buffer** bytes. diff --git a/can/interfaces/udp_multicast/utils.py b/can/interfaces/udp_multicast/utils.py index 35a0df185..5c9454ed4 100644 --- a/can/interfaces/udp_multicast/utils.py +++ b/can/interfaces/udp_multicast/utils.py @@ -2,7 +2,7 @@ Defines common functions. """ -from typing import Any, Dict, Optional +from typing import Any, Optional from can import CanInterfaceNotImplementedError, Message from can.typechecking import ReadableBytesLike @@ -44,7 +44,7 @@ def pack_message(message: Message) -> bytes: def unpack_message( data: ReadableBytesLike, - replace: Optional[Dict[str, Any]] = None, + replace: Optional[dict[str, Any]] = None, check: bool = False, ) -> Message: """Unpack a can.Message from a msgpack byte blob. diff --git a/can/interfaces/usb2can/serial_selector.py b/can/interfaces/usb2can/serial_selector.py index de29f03fe..9f3b7185e 100644 --- a/can/interfaces/usb2can/serial_selector.py +++ b/can/interfaces/usb2can/serial_selector.py @@ -1,7 +1,6 @@ """ """ import logging -from typing import List log = logging.getLogger("can.usb2can") @@ -43,7 +42,7 @@ def WMIDateStringToDate(dtmDate) -> str: return strDateTime -def find_serial_devices(serial_matcher: str = "") -> List[str]: +def find_serial_devices(serial_matcher: str = "") -> list[str]: """ Finds a list of USB devices where the serial number (partially) matches the given string. diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index d51d74529..986f52002 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -10,17 +10,13 @@ import os import time import warnings +from collections.abc import Iterator, Sequence from types import ModuleType from typing import ( Any, Callable, - Dict, - Iterator, - List, NamedTuple, Optional, - Sequence, - Tuple, Union, cast, ) @@ -204,8 +200,8 @@ def __init__( is_fd = isinstance(timing, BitTimingFd) if timing else fd self.mask = 0 - self.channel_masks: Dict[int, int] = {} - self.index_to_channel: Dict[int, int] = {} + self.channel_masks: dict[int, int] = {} + self.index_to_channel: dict[int, int] = {} self._can_protocol = CanProtocol.CAN_FD if is_fd else CanProtocol.CAN_20 self._listen_only = listen_only @@ -383,7 +379,7 @@ def _find_global_channel_idx( channel: int, serial: Optional[int], app_name: Optional[str], - channel_configs: List["VectorChannelConfig"], + channel_configs: list["VectorChannelConfig"], ) -> int: if serial is not None: serial_found = False @@ -439,7 +435,7 @@ def _has_init_access(self, channel: int) -> bool: return bool(self.permission_mask & self.channel_masks[channel]) def _read_bus_params( - self, channel_index: int, vcc_list: List["VectorChannelConfig"] + self, channel_index: int, vcc_list: list["VectorChannelConfig"] ) -> "VectorBusParams": for vcc in vcc_list: if vcc.channel_index == channel_index: @@ -712,7 +708,7 @@ def _apply_filters(self, filters: Optional[CanFilters]) -> None: def _recv_internal( self, timeout: Optional[float] - ) -> Tuple[Optional[Message], bool]: + ) -> tuple[Optional[Message], bool]: end_time = time.time() + timeout if timeout is not None else None while True: @@ -986,7 +982,7 @@ def reset(self) -> None: ) @staticmethod - def _detect_available_configs() -> List[AutoDetectedConfig]: + def _detect_available_configs() -> list[AutoDetectedConfig]: configs = [] channel_configs = get_channel_configs() LOG.info("Found %d channels", len(channel_configs)) @@ -1037,7 +1033,7 @@ def popup_vector_hw_configuration(wait_for_finish: int = 0) -> None: @staticmethod def get_application_config( app_name: str, app_channel: int - ) -> Tuple[Union[int, xldefine.XL_HardwareType], int, int]: + ) -> tuple[Union[int, xldefine.XL_HardwareType], int, int]: """Retrieve information for an application in Vector Hardware Configuration. :param app_name: @@ -1243,14 +1239,14 @@ def _read_bus_params_from_c_struct( ) -def get_channel_configs() -> List[VectorChannelConfig]: +def get_channel_configs() -> list[VectorChannelConfig]: """Read channel properties from Vector XL API.""" try: driver_config = _get_xl_driver_config() except VectorError: return [] - channel_list: List[VectorChannelConfig] = [] + channel_list: list[VectorChannelConfig] = [] for i in range(driver_config.channelCount): xlcc: xlclass.XLchannelConfig = driver_config.channel[i] vcc = VectorChannelConfig( diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index 62ad0cfe3..aa858913e 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -12,7 +12,7 @@ from copy import deepcopy from random import randint from threading import RLock -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple +from typing import TYPE_CHECKING, Any, Optional from can import CanOperationError from can.bus import BusABC, CanProtocol @@ -25,7 +25,7 @@ # Channels are lists of queues, one for each connection if TYPE_CHECKING: # https://mypy.readthedocs.io/en/stable/runtime_troubles.html#using-classes-that-are-generic-in-stubs-but-not-at-runtime - channels: Dict[Optional[Any], List[queue.Queue[Message]]] = {} + channels: dict[Optional[Any], list[queue.Queue[Message]]] = {} else: channels = {} channels_lock = RLock() @@ -125,7 +125,7 @@ def _check_if_open(self) -> None: def _recv_internal( self, timeout: Optional[float] - ) -> Tuple[Optional[Message], bool]: + ) -> tuple[Optional[Message], bool]: self._check_if_open() try: msg = self.queue.get(block=True, timeout=timeout) @@ -168,7 +168,7 @@ def shutdown(self) -> None: del channels[self.channel_id] @staticmethod - def _detect_available_configs() -> List[AutoDetectedConfig]: + def _detect_available_configs() -> list[AutoDetectedConfig]: """ Returns all currently used channels as well as one other currently unused channel. diff --git a/can/io/asc.py b/can/io/asc.py index deb7d429e..0bea823fd 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -8,8 +8,9 @@ import logging import re +from collections.abc import Generator from datetime import datetime -from typing import Any, Dict, Final, Generator, Optional, TextIO, Union +from typing import Any, Final, Optional, TextIO, Union from ..message import Message from ..typechecking import StringPathLike @@ -153,7 +154,7 @@ def _datetime_to_timestamp(datetime_string: str) -> float: raise ValueError(f"Incompatible datetime string {datetime_string}") - def _extract_can_id(self, str_can_id: str, msg_kwargs: Dict[str, Any]) -> None: + def _extract_can_id(self, str_can_id: str, msg_kwargs: dict[str, Any]) -> None: if str_can_id[-1:].lower() == "x": msg_kwargs["is_extended_id"] = True can_id = int(str_can_id[0:-1], self._converted_base) @@ -169,7 +170,7 @@ def _check_base(base: str) -> int: return BASE_DEC if base == "dec" else BASE_HEX def _process_data_string( - self, data_str: str, data_length: int, msg_kwargs: Dict[str, Any] + self, data_str: str, data_length: int, msg_kwargs: dict[str, Any] ) -> None: frame = bytearray() data = data_str.split() @@ -178,7 +179,7 @@ def _process_data_string( msg_kwargs["data"] = frame def _process_classic_can_frame( - self, line: str, msg_kwargs: Dict[str, Any] + self, line: str, msg_kwargs: dict[str, Any] ) -> Message: # CAN error frame if line.strip()[0:10].lower() == "errorframe": @@ -213,7 +214,7 @@ def _process_classic_can_frame( return Message(**msg_kwargs) - def _process_fd_can_frame(self, line: str, msg_kwargs: Dict[str, Any]) -> Message: + def _process_fd_can_frame(self, line: str, msg_kwargs: dict[str, Any]) -> Message: channel, direction, rest_of_message = line.split(None, 2) # See ASCWriter msg_kwargs["channel"] = int(channel) - 1 @@ -285,7 +286,7 @@ def __iter__(self) -> Generator[Message, None, None]: # J1939 message or some other unsupported event continue - msg_kwargs: Dict[str, Union[float, bool, int]] = {} + msg_kwargs: dict[str, Union[float, bool, int]] = {} try: _timestamp, channel, rest_of_message = line.split(None, 2) timestamp = float(_timestamp) + self.start_time diff --git a/can/io/blf.py b/can/io/blf.py index e64a2247d..6a1231fcc 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -17,15 +17,16 @@ import struct import time import zlib +from collections.abc import Generator from decimal import Decimal -from typing import Any, BinaryIO, Generator, List, Optional, Tuple, Union, cast +from typing import Any, BinaryIO, Optional, Union, cast from ..message import Message from ..typechecking import StringPathLike from ..util import channel2int, dlc2len, len2dlc from .generic import BinaryIOMessageReader, FileIOMessageWriter -TSystemTime = Tuple[int, int, int, int, int, int, int, int] +TSystemTime = tuple[int, int, int, int, int, int, int, int] class BLFParseError(Exception): @@ -422,7 +423,7 @@ def __init__( assert self.file is not None self.channel = channel self.compression_level = compression_level - self._buffer: List[bytes] = [] + self._buffer: list[bytes] = [] self._buffer_size = 0 # If max container size is located in kwargs, then update the instance if kwargs.get("max_container_size", False): diff --git a/can/io/canutils.py b/can/io/canutils.py index cc978d4f2..e83c21926 100644 --- a/can/io/canutils.py +++ b/can/io/canutils.py @@ -5,7 +5,8 @@ """ import logging -from typing import Any, Generator, TextIO, Union +from collections.abc import Generator +from typing import Any, TextIO, Union from can.message import Message diff --git a/can/io/csv.py b/can/io/csv.py index 2abaeb70e..dcc7996f7 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -10,7 +10,8 @@ """ from base64 import b64decode, b64encode -from typing import Any, Generator, TextIO, Union +from collections.abc import Generator +from typing import Any, TextIO, Union from can.message import Message diff --git a/can/io/generic.py b/can/io/generic.py index 93840f807..82523c3cd 100644 --- a/can/io/generic.py +++ b/can/io/generic.py @@ -3,16 +3,15 @@ import gzip import locale from abc import ABCMeta +from collections.abc import Iterable +from contextlib import AbstractContextManager from types import TracebackType from typing import ( Any, BinaryIO, - ContextManager, - Iterable, Literal, Optional, TextIO, - Type, Union, cast, ) @@ -24,7 +23,7 @@ from ..message import Message -class BaseIOHandler(ContextManager, metaclass=ABCMeta): +class BaseIOHandler(AbstractContextManager): """A generic file handler that can be used for reading and writing. Can be used as a context manager. @@ -60,10 +59,7 @@ def __init__( # pylint: disable=consider-using-with # file is some path-like object self.file = cast( - "typechecking.FileLike", - open( - cast("typechecking.StringPathLike", file), mode, encoding=encoding - ), + "typechecking.FileLike", open(file, mode, encoding=encoding) ) # for multiple inheritance @@ -74,7 +70,7 @@ def __enter__(self) -> Self: def __exit__( self, - exc_type: Optional[Type[BaseException]], + exc_type: Optional[type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> Literal[False]: diff --git a/can/io/logger.py b/can/io/logger.py index 359aae4ac..f9f029759 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -12,13 +12,9 @@ Any, Callable, ClassVar, - Dict, Final, Literal, Optional, - Set, - Tuple, - Type, cast, ) @@ -43,7 +39,7 @@ #: A map of file suffixes to their corresponding #: :class:`can.io.generic.MessageWriter` class -MESSAGE_WRITERS: Final[Dict[str, Type[MessageWriter]]] = { +MESSAGE_WRITERS: Final[dict[str, type[MessageWriter]]] = { ".asc": ASCWriter, ".blf": BLFWriter, ".csv": CSVWriter, @@ -66,7 +62,7 @@ def _update_writer_plugins() -> None: MESSAGE_WRITERS[entry_point.key] = writer_class -def _get_logger_for_suffix(suffix: str) -> Type[MessageWriter]: +def _get_logger_for_suffix(suffix: str) -> type[MessageWriter]: try: return MESSAGE_WRITERS[suffix] except KeyError: @@ -77,7 +73,7 @@ def _get_logger_for_suffix(suffix: str) -> Type[MessageWriter]: def _compress( filename: StringPathLike, **kwargs: Any -) -> Tuple[Type[MessageWriter], FileLike]: +) -> tuple[type[MessageWriter], FileLike]: """ Return the suffix and io object of the decompressed file. File will automatically recompress upon close. @@ -171,7 +167,7 @@ class BaseRotatingLogger(MessageWriter, ABC): Subclasses must set the `_writer` attribute upon initialization. """ - _supported_formats: ClassVar[Set[str]] = set() + _supported_formats: ClassVar[set[str]] = set() #: If this attribute is set to a callable, the :meth:`~BaseRotatingLogger.rotation_filename` #: method delegates to this callable. The parameters passed to the callable are @@ -290,7 +286,7 @@ def __enter__(self) -> Self: def __exit__( self, - exc_type: Optional[Type[BaseException]], + exc_type: Optional[type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> Literal[False]: @@ -347,7 +343,7 @@ class SizedRotatingLogger(BaseRotatingLogger): :meth:`~can.Listener.stop` is called. """ - _supported_formats: ClassVar[Set[str]] = {".asc", ".blf", ".csv", ".log", ".txt"} + _supported_formats: ClassVar[set[str]] = {".asc", ".blf", ".csv", ".log", ".txt"} def __init__( self, diff --git a/can/io/mf4.py b/can/io/mf4.py index 9efa1d83d..557d882e1 100644 --- a/can/io/mf4.py +++ b/can/io/mf4.py @@ -8,11 +8,12 @@ import abc import heapq import logging +from collections.abc import Generator, Iterator from datetime import datetime from hashlib import md5 from io import BufferedIOBase, BytesIO from pathlib import Path -from typing import Any, BinaryIO, Dict, Generator, Iterator, List, Optional, Union, cast +from typing import Any, BinaryIO, Optional, Union, cast from ..message import Message from ..typechecking import StringPathLike @@ -339,7 +340,7 @@ def __iter__(self) -> Generator[Message, None, None]: for i in range(len(data)): data_length = int(data["CAN_DataFrame.DataLength"][i]) - kv: Dict[str, Any] = { + kv: dict[str, Any] = { "timestamp": float(data.timestamps[i]) + self._start_timestamp, "arbitration_id": int(data["CAN_DataFrame.ID"][i]) & 0x1FFFFFFF, "data": data["CAN_DataFrame.DataBytes"][i][ @@ -377,7 +378,7 @@ def __iter__(self) -> Generator[Message, None, None]: names = data.samples[0].dtype.names for i in range(len(data)): - kv: Dict[str, Any] = { + kv: dict[str, Any] = { "timestamp": float(data.timestamps[i]) + self._start_timestamp, "is_error_frame": True, } @@ -428,7 +429,7 @@ def __iter__(self) -> Generator[Message, None, None]: names = data.samples[0].dtype.names for i in range(len(data)): - kv: Dict[str, Any] = { + kv: dict[str, Any] = { "timestamp": float(data.timestamps[i]) + self._start_timestamp, "arbitration_id": int(data["CAN_RemoteFrame.ID"][i]) & 0x1FFFFFFF, @@ -474,7 +475,7 @@ def __init__( def __iter__(self) -> Iterator[Message]: # To handle messages split over multiple channel groups, create a single iterator per # channel group and merge these iterators into a single iterator using heapq. - iterators: List[FrameIterator] = [] + iterators: list[FrameIterator] = [] for group_index, group in enumerate(self._mdf.groups): channel_group: ChannelGroup = group.channel_group diff --git a/can/io/player.py b/can/io/player.py index 214112164..2451eab41 100644 --- a/can/io/player.py +++ b/can/io/player.py @@ -7,14 +7,10 @@ import gzip import pathlib import time +from collections.abc import Generator, Iterable from typing import ( Any, - Dict, Final, - Generator, - Iterable, - Tuple, - Type, Union, ) @@ -32,7 +28,7 @@ #: A map of file suffixes to their corresponding #: :class:`can.io.generic.MessageReader` class -MESSAGE_READERS: Final[Dict[str, Type[MessageReader]]] = { +MESSAGE_READERS: Final[dict[str, type[MessageReader]]] = { ".asc": ASCReader, ".blf": BLFReader, ".csv": CSVReader, @@ -54,7 +50,7 @@ def _update_reader_plugins() -> None: MESSAGE_READERS[entry_point.key] = reader_class -def _get_logger_for_suffix(suffix: str) -> Type[MessageReader]: +def _get_logger_for_suffix(suffix: str) -> type[MessageReader]: """Find MessageReader class for given suffix.""" try: return MESSAGE_READERS[suffix] @@ -64,7 +60,7 @@ def _get_logger_for_suffix(suffix: str) -> Type[MessageReader]: def _decompress( filename: StringPathLike, -) -> Tuple[Type[MessageReader], Union[str, FileLike]]: +) -> tuple[type[MessageReader], Union[str, FileLike]]: """ Return the suffix and io object of the decompressed file. """ diff --git a/can/io/sqlite.py b/can/io/sqlite.py index 43fd761e9..686e2d038 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -8,7 +8,8 @@ import sqlite3 import threading import time -from typing import Any, Generator +from collections.abc import Generator +from typing import Any from can.listener import BufferedReader from can.message import Message diff --git a/can/io/trc.py b/can/io/trc.py index 2dbe3763c..a07a53a4d 100644 --- a/can/io/trc.py +++ b/can/io/trc.py @@ -9,9 +9,10 @@ import logging import os +from collections.abc import Generator from datetime import datetime, timedelta, timezone from enum import Enum -from typing import Any, Callable, Dict, Generator, Optional, TextIO, Tuple, Union +from typing import Any, Callable, Optional, TextIO, Union from ..message import Message from ..typechecking import StringPathLike @@ -56,13 +57,13 @@ def __init__( super().__init__(file, mode="r") self.file_version = TRCFileVersion.UNKNOWN self._start_time: float = 0 - self.columns: Dict[str, int] = {} + self.columns: dict[str, int] = {} self._num_columns = -1 if not self.file: raise ValueError("The given file cannot be None") - self._parse_cols: Callable[[Tuple[str, ...]], Optional[Message]] = ( + self._parse_cols: Callable[[tuple[str, ...]], Optional[Message]] = ( lambda x: None ) @@ -140,7 +141,7 @@ def _extract_header(self): return line - def _parse_msg_v1_0(self, cols: Tuple[str, ...]) -> Optional[Message]: + def _parse_msg_v1_0(self, cols: tuple[str, ...]) -> Optional[Message]: arbit_id = cols[2] if arbit_id == "FFFFFFFF": logger.info("TRCReader: Dropping bus info line") @@ -155,7 +156,7 @@ def _parse_msg_v1_0(self, cols: Tuple[str, ...]) -> Optional[Message]: msg.data = bytearray([int(cols[i + 4], 16) for i in range(msg.dlc)]) return msg - def _parse_msg_v1_1(self, cols: Tuple[str, ...]) -> Optional[Message]: + def _parse_msg_v1_1(self, cols: tuple[str, ...]) -> Optional[Message]: arbit_id = cols[3] msg = Message() @@ -168,7 +169,7 @@ def _parse_msg_v1_1(self, cols: Tuple[str, ...]) -> Optional[Message]: msg.is_rx = cols[2] == "Rx" return msg - def _parse_msg_v1_3(self, cols: Tuple[str, ...]) -> Optional[Message]: + def _parse_msg_v1_3(self, cols: tuple[str, ...]) -> Optional[Message]: arbit_id = cols[4] msg = Message() @@ -181,7 +182,7 @@ def _parse_msg_v1_3(self, cols: Tuple[str, ...]) -> Optional[Message]: msg.is_rx = cols[3] == "Rx" return msg - def _parse_msg_v2_x(self, cols: Tuple[str, ...]) -> Optional[Message]: + def _parse_msg_v2_x(self, cols: tuple[str, ...]) -> Optional[Message]: type_ = cols[self.columns["T"]] bus = self.columns.get("B", None) @@ -208,7 +209,7 @@ def _parse_msg_v2_x(self, cols: Tuple[str, ...]) -> Optional[Message]: return msg - def _parse_cols_v1_1(self, cols: Tuple[str, ...]) -> Optional[Message]: + def _parse_cols_v1_1(self, cols: tuple[str, ...]) -> Optional[Message]: dtype = cols[2] if dtype in ("Tx", "Rx"): return self._parse_msg_v1_1(cols) @@ -216,7 +217,7 @@ def _parse_cols_v1_1(self, cols: Tuple[str, ...]) -> Optional[Message]: logger.info("TRCReader: Unsupported type '%s'", dtype) return None - def _parse_cols_v1_3(self, cols: Tuple[str, ...]) -> Optional[Message]: + def _parse_cols_v1_3(self, cols: tuple[str, ...]) -> Optional[Message]: dtype = cols[3] if dtype in ("Tx", "Rx"): return self._parse_msg_v1_3(cols) @@ -224,7 +225,7 @@ def _parse_cols_v1_3(self, cols: Tuple[str, ...]) -> Optional[Message]: logger.info("TRCReader: Unsupported type '%s'", dtype) return None - def _parse_cols_v2_x(self, cols: Tuple[str, ...]) -> Optional[Message]: + def _parse_cols_v2_x(self, cols: tuple[str, ...]) -> Optional[Message]: dtype = cols[self.columns["T"]] if dtype in {"DT", "FD", "FB", "FE", "BI"}: return self._parse_msg_v2_x(cols) diff --git a/can/listener.py b/can/listener.py index 1f19c3acd..1e95fa990 100644 --- a/can/listener.py +++ b/can/listener.py @@ -6,8 +6,9 @@ import sys import warnings from abc import ABCMeta, abstractmethod +from collections.abc import AsyncIterator from queue import Empty, SimpleQueue -from typing import Any, AsyncIterator, Optional +from typing import Any, Optional from can.bus import BusABC from can.message import Message diff --git a/can/logger.py b/can/logger.py index 4167558d8..9c1134257 100644 --- a/can/logger.py +++ b/can/logger.py @@ -2,15 +2,12 @@ import errno import re import sys +from collections.abc import Sequence from datetime import datetime from typing import ( TYPE_CHECKING, Any, - Dict, - List, Optional, - Sequence, - Tuple, Union, ) @@ -111,7 +108,7 @@ def _create_bus(parsed_args: argparse.Namespace, **kwargs: Any) -> can.BusABC: logging_level_names = ["critical", "error", "warning", "info", "debug", "subdebug"] can.set_logging_level(logging_level_names[min(5, parsed_args.verbosity)]) - config: Dict[str, Any] = {"single_handle": True, **kwargs} + config: dict[str, Any] = {"single_handle": True, **kwargs} if parsed_args.interface: config["interface"] = parsed_args.interface if parsed_args.bitrate: @@ -140,7 +137,7 @@ def __call__( raise argparse.ArgumentError(None, "Invalid filter argument") print(f"Adding filter(s): {values}") - can_filters: List[CanFilter] = [] + can_filters: list[CanFilter] = [] for filt in values: if ":" in filt: @@ -169,7 +166,7 @@ def __call__( if not isinstance(values, list): raise argparse.ArgumentError(None, "Invalid --timing argument") - timing_dict: Dict[str, int] = {} + timing_dict: dict[str, int] = {} for arg in values: try: key, value_string = arg.split("=") @@ -193,19 +190,19 @@ def _parse_additional_config(unknown_args: Sequence[str]) -> TAdditionalCliArgs: if not re.match(r"^--[a-zA-Z][a-zA-Z0-9\-]*=\S*?$", arg): raise ValueError(f"Parsing argument {arg} failed") - def _split_arg(_arg: str) -> Tuple[str, str]: + def _split_arg(_arg: str) -> tuple[str, str]: left, right = _arg.split("=", 1) return left.lstrip("-").replace("-", "_"), right - args: Dict[str, Union[str, int, float, bool]] = {} + args: dict[str, Union[str, int, float, bool]] = {} for key, string_val in map(_split_arg, unknown_args): args[key] = cast_from_string(string_val) return args def _parse_logger_args( - args: List[str], -) -> Tuple[argparse.Namespace, TAdditionalCliArgs]: + args: list[str], +) -> tuple[argparse.Namespace, TAdditionalCliArgs]: """Parse command line arguments for logger script.""" parser = argparse.ArgumentParser( diff --git a/can/notifier.py b/can/notifier.py index 088f0802e..237c874da 100644 --- a/can/notifier.py +++ b/can/notifier.py @@ -7,7 +7,8 @@ import logging import threading import time -from typing import Any, Awaitable, Callable, Iterable, List, Optional, Union +from collections.abc import Awaitable, Iterable +from typing import Any, Callable, Optional, Union from can.bus import BusABC from can.listener import Listener @@ -21,7 +22,7 @@ class Notifier: def __init__( self, - bus: Union[BusABC, List[BusABC]], + bus: Union[BusABC, list[BusABC]], listeners: Iterable[MessageRecipient], timeout: float = 1.0, loop: Optional[asyncio.AbstractEventLoop] = None, @@ -43,7 +44,7 @@ def __init__( :param timeout: An optional maximum number of seconds to wait for any :class:`~can.Message`. :param loop: An :mod:`asyncio` event loop to schedule the ``listeners`` in. """ - self.listeners: List[MessageRecipient] = list(listeners) + self.listeners: list[MessageRecipient] = list(listeners) self.bus = bus self.timeout = timeout self._loop = loop @@ -54,7 +55,7 @@ def __init__( self._running = True self._lock = threading.Lock() - self._readers: List[Union[int, threading.Thread]] = [] + self._readers: list[Union[int, threading.Thread]] = [] buses = self.bus if isinstance(self.bus, list) else [self.bus] for each_bus in buses: self.add_bus(each_bus) diff --git a/can/player.py b/can/player.py index 9deb0c51f..38b76a331 100644 --- a/can/player.py +++ b/can/player.py @@ -16,7 +16,7 @@ from .logger import _create_base_argument_parser, _create_bus, _parse_additional_config if TYPE_CHECKING: - from typing import Iterable + from collections.abc import Iterable from can import Message diff --git a/can/typechecking.py b/can/typechecking.py index d993d6bd3..36343ddaa 100644 --- a/can/typechecking.py +++ b/can/typechecking.py @@ -50,13 +50,13 @@ class CanFilterExtended(TypedDict): StringPathLike = typing.Union[str, "os.PathLike[str]"] AcceptedIOType = typing.Union[FileLike, StringPathLike] -BusConfig = typing.NewType("BusConfig", typing.Dict[str, typing.Any]) +BusConfig = typing.NewType("BusConfig", dict[str, typing.Any]) # Used by CLI scripts -TAdditionalCliArgs: TypeAlias = typing.Dict[str, typing.Union[str, int, float, bool]] -TDataStructs: TypeAlias = typing.Dict[ - typing.Union[int, typing.Tuple[int, ...]], - typing.Union[struct.Struct, typing.Tuple, None], +TAdditionalCliArgs: TypeAlias = dict[str, typing.Union[str, int, float, bool]] +TDataStructs: TypeAlias = dict[ + typing.Union[int, tuple[int, ...]], + typing.Union[struct.Struct, tuple, None], ] diff --git a/can/util.py b/can/util.py index bb835b846..2f32dda8e 100644 --- a/can/util.py +++ b/can/util.py @@ -12,15 +12,13 @@ import platform import re import warnings +from collections.abc import Iterable from configparser import ConfigParser from time import get_clock_info, perf_counter, time from typing import ( Any, Callable, - Dict, - Iterable, Optional, - Tuple, TypeVar, Union, cast, @@ -53,7 +51,7 @@ def load_file_config( path: Optional[typechecking.AcceptedIOType] = None, section: str = "default" -) -> Dict[str, str]: +) -> dict[str, str]: """ Loads configuration from file with following content:: @@ -77,7 +75,7 @@ def load_file_config( else: config.read(path) - _config: Dict[str, str] = {} + _config: dict[str, str] = {} if config.has_section(section): _config.update(config.items(section)) @@ -85,7 +83,7 @@ def load_file_config( return _config -def load_environment_config(context: Optional[str] = None) -> Dict[str, str]: +def load_environment_config(context: Optional[str] = None) -> dict[str, str]: """ Loads config dict from environmental variables (if set): @@ -111,7 +109,7 @@ def load_environment_config(context: Optional[str] = None) -> Dict[str, str]: context_suffix = f"_{context}" if context else "" can_config_key = f"CAN_CONFIG{context_suffix}" - config: Dict[str, str] = json.loads(os.environ.get(can_config_key, "{}")) + config: dict[str, str] = json.loads(os.environ.get(can_config_key, "{}")) for key, val in mapper.items(): config_option = os.environ.get(val + context_suffix, None) @@ -123,7 +121,7 @@ def load_environment_config(context: Optional[str] = None) -> Dict[str, str]: def load_config( path: Optional[typechecking.AcceptedIOType] = None, - config: Optional[Dict[str, Any]] = None, + config: Optional[dict[str, Any]] = None, context: Optional[str] = None, ) -> typechecking.BusConfig: """ @@ -178,7 +176,7 @@ def load_config( # Use the given dict for default values config_sources = cast( - "Iterable[Union[Dict[str, Any], Callable[[Any], Dict[str, Any]]]]", + "Iterable[Union[dict[str, Any], Callable[[Any], dict[str, Any]]]]", [ given_config, can.rc, @@ -212,7 +210,7 @@ def load_config( return bus_config -def _create_bus_config(config: Dict[str, Any]) -> typechecking.BusConfig: +def _create_bus_config(config: dict[str, Any]) -> typechecking.BusConfig: """Validates some config values, performs compatibility mappings and creates specific structures (e.g. for bit timings). @@ -254,7 +252,7 @@ def _create_bus_config(config: Dict[str, Any]) -> typechecking.BusConfig: return cast("typechecking.BusConfig", config) -def _dict2timing(data: Dict[str, Any]) -> Union[BitTiming, BitTimingFd, None]: +def _dict2timing(data: dict[str, Any]) -> Union[BitTiming, BitTimingFd, None]: """Try to instantiate a :class:`~can.BitTiming` or :class:`~can.BitTimingFd` from a dictionary. Return `None` if not possible.""" @@ -396,8 +394,8 @@ def _rename_kwargs( func_name: str, start: str, end: Optional[str], - kwargs: Dict[str, Any], - aliases: Dict[str, Optional[str]], + kwargs: dict[str, Any], + aliases: dict[str, Optional[str]], ) -> None: """Helper function for `deprecated_args_alias`""" for alias, new in aliases.items(): @@ -468,7 +466,7 @@ def check_or_adjust_timing_clock(timing: T2, valid_clocks: Iterable[int]) -> T2: ) from None -def time_perfcounter_correlation() -> Tuple[float, float]: +def time_perfcounter_correlation() -> tuple[float, float]: """Get the `perf_counter` value nearest to when time.time() is updated Computed if the default timer used by `time.time` on this platform has a resolution diff --git a/can/viewer.py b/can/viewer.py index 57b3d6f7d..3eed727ab 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -27,7 +27,6 @@ import struct import sys import time -from typing import Dict, List, Tuple from can import __version__ from can.logger import ( @@ -161,7 +160,7 @@ def run(self): # Unpack the data and then convert it into SI-units @staticmethod - def unpack_data(cmd: int, cmd_to_struct: Dict, data: bytes) -> List[float]: + def unpack_data(cmd: int, cmd_to_struct: dict, data: bytes) -> list[float]: if not cmd_to_struct or not data: # These messages do not contain a data package return [] @@ -390,8 +389,8 @@ def _fill_text(self, text, width, indent): def _parse_viewer_args( - args: List[str], -) -> Tuple[argparse.Namespace, TDataStructs, TAdditionalCliArgs]: + args: list[str], +) -> tuple[argparse.Namespace, TDataStructs, TAdditionalCliArgs]: # Parse command line arguments parser = argparse.ArgumentParser( "python -m can.viewer", @@ -524,7 +523,7 @@ def _parse_viewer_args( key, fmt = int(tmp[0], base=16), tmp[1] # The scaling - scaling: List[float] = [] + scaling: list[float] = [] for t in tmp[2:]: # First try to convert to int, if that fails, then convert to a float try: diff --git a/pyproject.toml b/pyproject.toml index c65275559..2e8be3e2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ dependencies = [ "typing_extensions>=3.10.0.0", "msgpack~=1.1.0", ] -requires-python = ">=3.8" +requires-python = ">=3.9" license = { text = "LGPL v3" } classifiers = [ "Development Status :: 5 - Production/Stable", @@ -29,7 +29,6 @@ classifiers = [ "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", "Programming Language :: Python", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -61,9 +60,9 @@ changelog = "https://github.com/hardbyte/python-can/blob/develop/CHANGELOG.md" [project.optional-dependencies] lint = [ "pylint==3.2.*", - "ruff==0.10.0", + "ruff==0.11.12", "black==25.1.*", - "mypy==1.15.*", + "mypy==1.16.*", ] pywin32 = ["pywin32>=305; platform_system == 'Windows' and platform_python_implementation == 'CPython'"] seeedstudio = ["pyserial>=3.0"] diff --git a/test/contextmanager_test.py b/test/contextmanager_test.py index 3adb1e7c6..fe87f33b0 100644 --- a/test/contextmanager_test.py +++ b/test/contextmanager_test.py @@ -17,9 +17,10 @@ def setUp(self): ) def test_open_buses(self): - with can.Bus(interface="virtual") as bus_send, can.Bus( - interface="virtual" - ) as bus_recv: + with ( + can.Bus(interface="virtual") as bus_send, + can.Bus(interface="virtual") as bus_recv, + ): bus_send.send(self.msg_send) msg_recv = bus_recv.recv() @@ -27,9 +28,10 @@ def test_open_buses(self): self.assertTrue(msg_recv) def test_use_closed_bus(self): - with can.Bus(interface="virtual") as bus_send, can.Bus( - interface="virtual" - ) as bus_recv: + with ( + can.Bus(interface="virtual") as bus_send, + can.Bus(interface="virtual") as bus_recv, + ): bus_send.send(self.msg_send) # Receiving a frame after bus has been closed should raise a CanException From 6058ab9dfe8b3ba5d23cbff670c0fe767147390a Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 30 May 2025 15:14:24 +0200 Subject: [PATCH 077/127] Use dependency groups for docs/lint/test (#1945) * use dependency groups for docs/lint/test --------- Co-authored-by: zariiii9003 --- .github/workflows/ci.yml | 7 ++++-- .readthedocs.yml | 8 ++++-- can/interfaces/udp_multicast/bus.py | 4 +-- can/interfaces/udp_multicast/utils.py | 22 +++++++++++++---- can/listener.py | 2 +- doc/development.rst | 2 +- doc/doc-requirements.txt | 5 ---- doc/interfaces/udp_multicast.rst | 9 +++++++ pyproject.toml | 35 +++++++++++++++++++++------ test/back2back_test.py | 12 ++++++--- test/listener_test.py | 7 +++++- tox.ini | 27 +++++++++------------ 12 files changed, 94 insertions(+), 46 deletions(-) delete mode 100644 doc/doc-requirements.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab14b6b6a..f04654f0d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,7 +79,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -e .[lint] + pip install --group lint -e . - name: mypy 3.9 run: | mypy --python-version 3.9 . @@ -92,6 +92,9 @@ jobs: - name: mypy 3.12 run: | mypy --python-version 3.12 . + - name: mypy 3.13 + run: | + mypy --python-version 3.13 . - name: ruff run: | ruff check can @@ -115,7 +118,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -e .[lint] + pip install --group lint - name: Code Format Check with Black run: | black --check --verbose . diff --git a/.readthedocs.yml b/.readthedocs.yml index dad8c28db..6fe4009e4 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -10,6 +10,9 @@ build: os: ubuntu-22.04 tools: python: "3.12" + jobs: + post_install: + - pip install --group docs # Build documentation in the docs/ directory with Sphinx sphinx: @@ -23,10 +26,11 @@ formats: # Optionally declare the Python requirements required to build your docs python: install: - - requirements: doc/doc-requirements.txt - method: pip path: . extra_requirements: - canalystii - - gs_usb + - gs-usb - mf4 + - remote + - serial diff --git a/can/interfaces/udp_multicast/bus.py b/can/interfaces/udp_multicast/bus.py index 31744c2b8..ec94e22b5 100644 --- a/can/interfaces/udp_multicast/bus.py +++ b/can/interfaces/udp_multicast/bus.py @@ -11,7 +11,7 @@ from can import BusABC, CanProtocol from can.typechecking import AutoDetectedConfig -from .utils import check_msgpack_installed, pack_message, unpack_message +from .utils import is_msgpack_installed, pack_message, unpack_message ioctl_supported = True @@ -104,7 +104,7 @@ def __init__( fd: bool = True, **kwargs, ) -> None: - check_msgpack_installed() + is_msgpack_installed() if receive_own_messages: raise can.CanInterfaceNotImplementedError( diff --git a/can/interfaces/udp_multicast/utils.py b/can/interfaces/udp_multicast/utils.py index 5c9454ed4..c6b2630a5 100644 --- a/can/interfaces/udp_multicast/utils.py +++ b/can/interfaces/udp_multicast/utils.py @@ -13,10 +13,22 @@ msgpack = None -def check_msgpack_installed() -> None: - """Raises a :class:`can.CanInterfaceNotImplementedError` if `msgpack` is not installed.""" +def is_msgpack_installed(raise_exception: bool = True) -> bool: + """Check whether the ``msgpack`` module is installed. + + :param raise_exception: + If True, raise a :class:`can.CanInterfaceNotImplementedError` when ``msgpack`` is not installed. + If False, return False instead. + :return: + True if ``msgpack`` is installed, False otherwise. + :raises can.CanInterfaceNotImplementedError: + If ``msgpack`` is not installed and ``raise_exception`` is True. + """ if msgpack is None: - raise CanInterfaceNotImplementedError("msgpack not installed") + if raise_exception: + raise CanInterfaceNotImplementedError("msgpack not installed") + return False + return True def pack_message(message: Message) -> bytes: @@ -25,7 +37,7 @@ def pack_message(message: Message) -> bytes: :param message: the message to be packed """ - check_msgpack_installed() + is_msgpack_installed() as_dict = { "timestamp": message.timestamp, "arbitration_id": message.arbitration_id, @@ -58,7 +70,7 @@ def unpack_message( :raise ValueError: if `check` is true and the message metadata is invalid in some way :raise Exception: if there was another problem while unpacking """ - check_msgpack_installed() + is_msgpack_installed() as_dict = msgpack.unpackb(data, raw=False) if replace is not None: as_dict.update(replace) diff --git a/can/listener.py b/can/listener.py index 1e95fa990..b450cf36d 100644 --- a/can/listener.py +++ b/can/listener.py @@ -136,6 +136,7 @@ class AsyncBufferedReader( """ def __init__(self, **kwargs: Any) -> None: + self._is_stopped: bool = False self.buffer: asyncio.Queue[Message] if "loop" in kwargs: @@ -150,7 +151,6 @@ def __init__(self, **kwargs: Any) -> None: return self.buffer = asyncio.Queue() - self._is_stopped: bool = False def on_message_received(self, msg: Message) -> None: """Append a message to the buffer. diff --git a/doc/development.rst b/doc/development.rst index 31a7ae077..074c1318d 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -52,7 +52,7 @@ The documentation can be built with:: The linters can be run with:: - pip install -e .[lint] + pip install --group lint -e . black --check can mypy can ruff check can diff --git a/doc/doc-requirements.txt b/doc/doc-requirements.txt deleted file mode 100644 index 9a01cf589..000000000 --- a/doc/doc-requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -sphinx>=5.2.3 -sphinxcontrib-programoutput -sphinx-inline-tabs -sphinx-copybutton -furo diff --git a/doc/interfaces/udp_multicast.rst b/doc/interfaces/udp_multicast.rst index e41925cb3..b2a049d83 100644 --- a/doc/interfaces/udp_multicast.rst +++ b/doc/interfaces/udp_multicast.rst @@ -22,6 +22,15 @@ sufficiently reliable for this interface to function properly. Please refer to the `Bus class documentation`_ below for configuration options and useful resources for specifying multicast IP addresses. +Installation +------------------- + +The Multicast IP Interface depends on the **msgpack** python library, +which is automatically installed with the `multicast` extra keyword:: + + $ pip install python-can[multicast] + + Supported Platforms ------------------- diff --git a/pyproject.toml b/pyproject.toml index 2e8be3e2a..37abec266 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,6 @@ dependencies = [ "wrapt~=1.10", "packaging >= 23.1", "typing_extensions>=3.10.0.0", - "msgpack~=1.1.0", ] requires-python = ">=3.9" license = { text = "LGPL v3" } @@ -58,12 +57,6 @@ repository = "https://github.com/hardbyte/python-can" changelog = "https://github.com/hardbyte/python-can/blob/develop/CHANGELOG.md" [project.optional-dependencies] -lint = [ - "pylint==3.2.*", - "ruff==0.11.12", - "black==25.1.*", - "mypy==1.16.*", -] pywin32 = ["pywin32>=305; platform_system == 'Windows' and platform_python_implementation == 'CPython'"] seeedstudio = ["pyserial>=3.0"] serial = ["pyserial~=3.0"] @@ -71,7 +64,7 @@ neovi = ["filelock", "python-ics>=2.12"] canalystii = ["canalystii>=0.1.0"] cantact = ["cantact>=0.0.7"] cvector = ["python-can-cvector"] -gs_usb = ["gs_usb>=0.2.1"] +gs-usb = ["gs-usb>=0.2.1"] nixnet = ["nixnet>=0.3.2"] pcan = ["uptime~=3.0.1"] remote = ["python-can-remote"] @@ -82,6 +75,32 @@ viewer = [ "windows-curses; platform_system == 'Windows' and platform_python_implementation=='CPython'" ] mf4 = ["asammdf>=6.0.0"] +multicast = ["msgpack~=1.1.0"] + +[dependency-groups] +docs = [ + "sphinx>=5.2.3", + "sphinxcontrib-programoutput", + "sphinx-inline-tabs", + "sphinx-copybutton", + "furo", +] +lint = [ + "pylint==3.2.*", + "ruff==0.11.12", + "black==25.1.*", + "mypy==1.16.*", +] +test = [ + "pytest==8.3.*", + "pytest-timeout==2.1.*", + "coveralls==3.3.1", + "pytest-cov==4.0.0", + "coverage==6.5.0", + "hypothesis~=6.35.0", + "pyserial~=3.5", + "parameterized~=0.8", +] [tool.setuptools.dynamic] readme = { file = "README.rst" } diff --git a/test/back2back_test.py b/test/back2back_test.py index fc630fb65..a46597ef4 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -14,14 +14,12 @@ import can from can import CanInterfaceNotImplementedError from can.interfaces.udp_multicast import UdpMulticastBus +from can.interfaces.udp_multicast.utils import is_msgpack_installed from .config import ( IS_CI, IS_OSX, IS_PYPY, - IS_TRAVIS, - IS_UNIX, - IS_WINDOWS, TEST_CAN_FD, TEST_INTERFACE_SOCKETCAN, ) @@ -307,6 +305,10 @@ class BasicTestSocketCan(Back2BackTestCase): IS_CI and IS_OSX, "not supported for macOS CI", ) +@unittest.skipUnless( + is_msgpack_installed(raise_exception=False), + "msgpack not installed", +) class BasicTestUdpMulticastBusIPv4(Back2BackTestCase): INTERFACE_1 = "udp_multicast" CHANNEL_1 = UdpMulticastBus.DEFAULT_GROUP_IPv4 @@ -324,6 +326,10 @@ def test_unique_message_instances(self): IS_CI and IS_OSX, "not supported for macOS CI", ) +@unittest.skipUnless( + is_msgpack_installed(raise_exception=False), + "msgpack not installed", +) class BasicTestUdpMulticastBusIPv6(Back2BackTestCase): HOST_LOCAL_MCAST_GROUP_IPv6 = "ff11:7079:7468:6f6e:6465:6d6f:6d63:6173" diff --git a/test/listener_test.py b/test/listener_test.py index 54496176b..bbcbed56e 100644 --- a/test/listener_test.py +++ b/test/listener_test.py @@ -159,8 +159,13 @@ def testBufferedListenerReceives(self): def test_deprecated_loop_arg(recwarn): + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + warnings.simplefilter("always") - can.AsyncBufferedReader(loop=asyncio.get_event_loop()) + can.AsyncBufferedReader(loop=loop) assert len(recwarn) > 0 assert recwarn.pop(DeprecationWarning) recwarn.clear() diff --git a/tox.ini b/tox.ini index e112c22b4..c69f541f4 100644 --- a/tox.ini +++ b/tox.ini @@ -1,21 +1,15 @@ [tox] +min_version = 4.22 [testenv] +dependency_groups = + test deps = - pytest==8.3.* - pytest-timeout==2.1.* - coveralls==3.3.1 - pytest-cov==4.0.0 - coverage==6.5.0 - hypothesis~=6.35.0 - pyserial~=3.5 - parameterized~=0.8 - asammdf>=6.0; platform_python_implementation=="CPython" and python_version<"3.13" + asammdf>=6.0; platform_python_implementation=="CPython" and python_version<"3.14" + msgpack~=1.1.0; python_version<"3.14" pywin32>=305; platform_system=="Windows" and platform_python_implementation=="CPython" and python_version<"3.14" - commands = pytest {posargs} - extras = canalystii @@ -30,13 +24,14 @@ passenv = [testenv:docs] description = Build and test the documentation basepython = py312 -deps = - -r doc/doc-requirements.txt - gs-usb - +dependency_groups = + docs extras = canalystii - + gs-usb + mf4 + remote + serial commands = python -m sphinx -b html -Wan --keep-going doc build python -m sphinx -b doctest -W --keep-going doc build From c46492b55a9613ef3e6df77b6f59a38e85416d5b Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 30 May 2025 16:53:21 +0200 Subject: [PATCH 078/127] rename zlgcan-driver-py to zlgcan (#1946) Co-authored-by: zariiii9003 --- doc/plugin-interface.rst | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/plugin-interface.rst b/doc/plugin-interface.rst index bfdedf3c6..8e60c50c2 100644 --- a/doc/plugin-interface.rst +++ b/doc/plugin-interface.rst @@ -73,7 +73,7 @@ The table below lists interface drivers that can be added by installing addition +----------------------------+-------------------------------------------------------+ | `python-can-sontheim`_ | CAN Driver for Sontheim CAN interfaces (e.g. CANfox) | +----------------------------+-------------------------------------------------------+ -| `zlgcan-driver-py`_ | Python wrapper for zlgcan-driver-rs | +| `zlgcan`_ | Python wrapper for zlgcan-driver-rs | +----------------------------+-------------------------------------------------------+ | `python-can-cando`_ | Python wrapper for Netronics' CANdo and CANdoISO | +----------------------------+-------------------------------------------------------+ @@ -82,6 +82,6 @@ The table below lists interface drivers that can be added by installing addition .. _python-can-cvector: https://github.com/zariiii9003/python-can-cvector .. _python-can-remote: https://github.com/christiansandberg/python-can-remote .. _python-can-sontheim: https://github.com/MattWoodhead/python-can-sontheim -.. _zlgcan-driver-py: https://github.com/zhuyu4839/zlgcan-driver +.. _zlgcan: https://github.com/jesses2025smith/zlgcan-driver .. _python-can-cando: https://github.com/belliriccardo/python-can-cando diff --git a/pyproject.toml b/pyproject.toml index 37abec266..6ad9ee7b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,7 +70,7 @@ pcan = ["uptime~=3.0.1"] remote = ["python-can-remote"] sontheim = ["python-can-sontheim>=0.1.2"] canine = ["python-can-canine>=0.2.2"] -zlgcan = ["zlgcan-driver-py"] +zlgcan = ["zlgcan"] viewer = [ "windows-curses; platform_system == 'Windows' and platform_python_implementation=='CPython'" ] From b075a68824b0063d6d1747af187a88e3a4fea768 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 30 May 2025 19:17:07 +0200 Subject: [PATCH 079/127] use ThreadPoolExecutor in detect_available_configs(), add timeout param (#1947) Co-authored-by: zariiii9003 --- can/interface.py | 95 +++++++++++++++-------- can/interfaces/usb2can/serial_selector.py | 2 + doc/conf.py | 2 +- doc/interfaces/gs_usb.rst | 2 +- pyproject.toml | 3 +- 5 files changed, 67 insertions(+), 37 deletions(-) diff --git a/can/interface.py b/can/interface.py index e017b9514..eee58ff41 100644 --- a/can/interface.py +++ b/can/interface.py @@ -4,9 +4,10 @@ CyclicSendTasks. """ +import concurrent.futures.thread import importlib import logging -from collections.abc import Iterable +from collections.abc import Callable, Iterable from typing import Any, Optional, Union, cast from . import util @@ -140,6 +141,7 @@ def Bus( # noqa: N802 def detect_available_configs( interfaces: Union[None, str, Iterable[str]] = None, + timeout: float = 5.0, ) -> list[AutoDetectedConfig]: """Detect all configurations/channels that the interfaces could currently connect with. @@ -148,59 +150,84 @@ def detect_available_configs( Automated configuration detection may not be implemented by every interface on every platform. This method will not raise - an error in that case, but with rather return an empty list + an error in that case, but will rather return an empty list for that interface. :param interfaces: either - the name of an interface to be searched in as a string, - an iterable of interface names to search in, or - `None` to search in all known interfaces. + :param timeout: maximum number of seconds to wait for all interface + detection tasks to complete. If exceeded, any pending tasks + will be cancelled, a warning will be logged, and the method + will return results gathered so far. :rtype: list[dict] :return: an iterable of dicts, each suitable for usage in - the constructor of :class:`can.BusABC`. + the constructor of :class:`can.BusABC`. Interfaces that + timed out will be logged as warnings and excluded. """ - # Figure out where to search + # Determine which interfaces to search if interfaces is None: interfaces = BACKENDS elif isinstance(interfaces, str): interfaces = (interfaces,) - # else it is supposed to be an iterable of strings + # otherwise assume iterable of strings - result = [] - for interface in interfaces: + # Collect detection callbacks + callbacks: dict[str, Callable[[], list[AutoDetectedConfig]]] = {} + for interface_keyword in interfaces: try: - bus_class = _get_class_for_interface(interface) + bus_class = _get_class_for_interface(interface_keyword) + callbacks[interface_keyword] = ( + bus_class._detect_available_configs # pylint: disable=protected-access + ) except CanInterfaceNotImplementedError: log_autodetect.debug( 'interface "%s" cannot be loaded for detection of available configurations', - interface, + interface_keyword, ) - continue - # get available channels - try: - available = list( - bus_class._detect_available_configs() # pylint: disable=protected-access - ) - except NotImplementedError: - log_autodetect.debug( - 'interface "%s" does not support detection of available configurations', - interface, - ) - else: - log_autodetect.debug( - 'interface "%s" detected %i available configurations', - interface, - len(available), - ) - - # add the interface name to the configs if it is not already present - for config in available: - if "interface" not in config: - config["interface"] = interface - - # append to result - result += available + result: list[AutoDetectedConfig] = [] + # Use manual executor to allow shutdown without waiting + executor = concurrent.futures.ThreadPoolExecutor() + try: + futures_to_keyword = { + executor.submit(func): kw for kw, func in callbacks.items() + } + done, not_done = concurrent.futures.wait( + futures_to_keyword, + timeout=timeout, + return_when=concurrent.futures.ALL_COMPLETED, + ) + # Log timed-out tasks + if not_done: + log_autodetect.warning( + "Timeout (%.2fs) reached for interfaces: %s", + timeout, + ", ".join(sorted(futures_to_keyword[fut] for fut in not_done)), + ) + # Process completed futures + for future in done: + keyword = futures_to_keyword[future] + try: + available = future.result() + except NotImplementedError: + log_autodetect.debug( + 'interface "%s" does not support detection of available configurations', + keyword, + ) + else: + log_autodetect.debug( + 'interface "%s" detected %i available configurations', + keyword, + len(available), + ) + for config in available: + config.setdefault("interface", keyword) + result.extend(available) + finally: + # shutdown immediately, do not wait for pending threads + executor.shutdown(wait=False, cancel_futures=True) return result diff --git a/can/interfaces/usb2can/serial_selector.py b/can/interfaces/usb2can/serial_selector.py index 9f3b7185e..18ad3f873 100644 --- a/can/interfaces/usb2can/serial_selector.py +++ b/can/interfaces/usb2can/serial_selector.py @@ -5,6 +5,7 @@ log = logging.getLogger("can.usb2can") try: + import pythoncom import win32com.client except ImportError: log.warning( @@ -50,6 +51,7 @@ def find_serial_devices(serial_matcher: str = "") -> list[str]: only device IDs starting with this string are returned """ serial_numbers = [] + pythoncom.CoInitialize() wmi = win32com.client.GetObject("winmgmts:") for usb_controller in wmi.InstancesOf("Win32_USBControllerDevice"): usb_device = wmi.Get(usb_controller.Dependent) diff --git a/doc/conf.py b/doc/conf.py index 34ce385cb..f4a9ab95f 100755 --- a/doc/conf.py +++ b/doc/conf.py @@ -136,7 +136,7 @@ ] # mock windows specific attributes -autodoc_mock_imports = ["win32com"] +autodoc_mock_imports = ["win32com", "pythoncom"] ctypes.windll = MagicMock() ctypesutil.HRESULT = ctypes.c_long diff --git a/doc/interfaces/gs_usb.rst b/doc/interfaces/gs_usb.rst index e9c0131c5..8bab07c6f 100755 --- a/doc/interfaces/gs_usb.rst +++ b/doc/interfaces/gs_usb.rst @@ -6,7 +6,7 @@ Geschwister Schneider and candleLight Windows/Linux/Mac CAN driver based on usbfs or WinUSB WCID for Geschwister Schneider USB/CAN devices and candleLight USB CAN interfaces. -Install: ``pip install "python-can[gs_usb]"`` +Install: ``pip install "python-can[gs-usb]"`` Usage: pass device ``index`` or ``channel`` (starting from 0) if using automatic device detection: diff --git a/pyproject.toml b/pyproject.toml index 6ad9ee7b2..a9f3fcbb1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,7 +86,7 @@ docs = [ "furo", ] lint = [ - "pylint==3.2.*", + "pylint==3.3.*", "ruff==0.11.12", "black==25.1.*", "mypy==1.16.*", @@ -205,6 +205,7 @@ disable = [ "too-many-branches", "too-many-instance-attributes", "too-many-locals", + "too-many-positional-arguments", "too-many-public-methods", "too-many-statements", ] From 9fe17e4ce9eb05b63aa4e0ada0a8002d7ceb1de0 Mon Sep 17 00:00:00 2001 From: Nathan Pennie Date: Sat, 31 May 2025 17:48:26 +0000 Subject: [PATCH 080/127] Support 11-bit identifiers in the serial interface (#1758) --- can/interfaces/serial/serial_can.py | 14 +++++++++++--- doc/interfaces/serial.rst | 22 +++++++++++++++++++++- test/serial_test.py | 22 ++++++++++++++++++++-- 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index 9fe715267..e87a32063 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -135,7 +135,8 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: # Pack arbitration ID try: - arbitration_id = struct.pack("= 0x20000000: + is_extended_id = False if arbitration_id & 0x20000000 else True + arbitration_id -= 0 if is_extended_id else 0x20000000 + if is_extended_id and arbitration_id >= 0x20000000: raise ValueError( - "received arbitration id may not exceed 2^29 (0x20000000)" + "received arbitration id may not exceed or equal 2^29 (0x20000000) if extended" + ) + if not is_extended_id and arbitration_id >= 0x800: + raise ValueError( + "received arbitration id may not exceed or equal 2^11 (0x800) if not extended" ) data = self._ser.read(dlc) @@ -204,6 +211,7 @@ def _recv_internal( arbitration_id=arbitration_id, dlc=dlc, data=data, + is_extended_id=is_extended_id, ) return msg, False diff --git a/doc/interfaces/serial.rst b/doc/interfaces/serial.rst index 99ee54df6..316fc143b 100644 --- a/doc/interfaces/serial.rst +++ b/doc/interfaces/serial.rst @@ -30,7 +30,9 @@ six parts. The start and the stop byte for the frame, the timestamp, DLC, arbitration ID and the payload. The payload has a variable length of between 0 and 8 bytes, the other parts are fixed. Both, the timestamp and the arbitration ID will be interpreted as 4 byte unsigned integers. The DLC is -also an unsigned integer with a length of 1 byte. +also an unsigned integer with a length of 1 byte. Non-extended (11-bit) +identifiers are encoded by adding 0x20000000 to the 11-bit ID. For example, an +11-bit CAN ID of 0x123 is encoded with an arbitration ID of 0x20000123. Serial frame format ^^^^^^^^^^^^^^^^^^^ @@ -102,3 +104,21 @@ Examples of serial frames +================+=====================+======+=====================+==============+ | 0xAA | 0x66 0x73 0x00 0x00 | 0x00 | 0x01 0x00 0x00 0x00 | 0xBB | +----------------+---------------------+------+---------------------+--------------+ + +.. rubric:: CAN message with 0 byte payload with an 11-bit CAN ID + ++----------------+---------+ +| CAN message | ++----------------+---------+ +| Arbitration ID | Payload | ++================+=========+ +| 0x20000001 (1) | None | ++----------------+---------+ + ++----------------+---------------------+------+---------------------+--------------+ +| Serial frame | ++----------------+---------------------+------+---------------------+--------------+ +| Start of frame | Timestamp | DLC | Arbitration ID | End of frame | ++================+=====================+======+=====================+==============+ +| 0xAA | 0x66 0x73 0x00 0x00 | 0x00 | 0x01 0x00 0x00 0x20 | 0xBB | ++----------------+---------------------+------+---------------------+--------------+ diff --git a/test/serial_test.py b/test/serial_test.py index 5fa90704b..e183e8408 100644 --- a/test/serial_test.py +++ b/test/serial_test.py @@ -86,7 +86,7 @@ def test_rx_tx_data_none(self): def test_rx_tx_min_id(self): """ - Tests the transfer with the lowest arbitration id + Tests the transfer with the lowest extended arbitration id """ msg = can.Message(arbitration_id=0) self.bus.send(msg) @@ -95,13 +95,31 @@ def test_rx_tx_min_id(self): def test_rx_tx_max_id(self): """ - Tests the transfer with the highest arbitration id + Tests the transfer with the highest extended arbitration id """ msg = can.Message(arbitration_id=536870911) self.bus.send(msg) msg_receive = self.bus.recv() self.assertMessageEqual(msg, msg_receive) + def test_rx_tx_min_nonext_id(self): + """ + Tests the transfer with the lowest non-extended arbitration id + """ + msg = can.Message(arbitration_id=0x000, is_extended_id=False) + self.bus.send(msg) + msg_receive = self.bus.recv() + self.assertMessageEqual(msg, msg_receive) + + def test_rx_tx_max_nonext_id(self): + """ + Tests the transfer with the highest non-extended arbitration id + """ + msg = can.Message(arbitration_id=0x7FF, is_extended_id=False) + self.bus.send(msg) + msg_receive = self.bus.recv() + self.assertMessageEqual(msg, msg_receive) + def test_rx_tx_max_timestamp(self): """ Tests the transfer with the highest possible timestamp From 2b1f6f6d04fe3585dbd5b093e2fe52637378b624 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 31 May 2025 20:31:24 +0200 Subject: [PATCH 081/127] Add Support for Remote and Error Frames to SerialBus (#1948) --- .github/workflows/ci.yml | 1 + can/interfaces/serial/serial_can.py | 56 +++++++++++++++-------------- doc/interfaces/serial.rst | 12 +++---- test/serial_test.py | 46 +++++++++++++++++------- 4 files changed, 71 insertions(+), 44 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f04654f0d..588d9a96b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,7 @@ jobs: - name: Setup SocketCAN if: ${{ matrix.os == 'ubuntu-latest' }} run: | + sudo apt-get update sudo apt-get -y install linux-modules-extra-$(uname -r) sudo ./test/open_vcan.sh - name: Test with pytest via tox diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index e87a32063..12ce5aff1 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -42,6 +42,13 @@ def list_comports() -> list[Any]: return [] +CAN_ERR_FLAG = 0x20000000 +CAN_RTR_FLAG = 0x40000000 +CAN_EFF_FLAG = 0x80000000 +CAN_ID_MASK_EXT = 0x1FFFFFFF +CAN_ID_MASK_STD = 0x7FF + + class SerialBus(BusABC): """ Enable basic can communication over a serial device. @@ -116,9 +123,6 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: :param msg: Message to send. - .. note:: Flags like ``extended_id``, ``is_remote_frame`` and - ``is_error_frame`` will be ignored. - .. note:: If the timestamp is a float value it will be converted to an integer. @@ -134,20 +138,25 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: raise ValueError(f"Timestamp is out of range: {msg.timestamp}") from None # Pack arbitration ID - try: - arbitration_id = msg.arbitration_id + (0 if msg.is_extended_id else 0x20000000) - arbitration_id = struct.pack("= 0x20000000: - raise ValueError( - "received arbitration id may not exceed or equal 2^29 (0x20000000) if extended" - ) - if not is_extended_id and arbitration_id >= 0x800: - raise ValueError( - "received arbitration id may not exceed or equal 2^11 (0x800) if not extended" - ) + is_extended_id = bool(arbitration_id & CAN_EFF_FLAG) + is_error_frame = bool(arbitration_id & CAN_ERR_FLAG) + is_remote_frame = bool(arbitration_id & CAN_RTR_FLAG) + + if is_extended_id: + arbitration_id = arbitration_id & CAN_ID_MASK_EXT + else: + arbitration_id = arbitration_id & CAN_ID_MASK_STD data = self._ser.read(dlc) @@ -212,6 +214,8 @@ def _recv_internal( dlc=dlc, data=data, is_extended_id=is_extended_id, + is_error_frame=is_error_frame, + is_remote_frame=is_remote_frame, ) return msg, False diff --git a/doc/interfaces/serial.rst b/doc/interfaces/serial.rst index 316fc143b..566ec7755 100644 --- a/doc/interfaces/serial.rst +++ b/doc/interfaces/serial.rst @@ -30,9 +30,9 @@ six parts. The start and the stop byte for the frame, the timestamp, DLC, arbitration ID and the payload. The payload has a variable length of between 0 and 8 bytes, the other parts are fixed. Both, the timestamp and the arbitration ID will be interpreted as 4 byte unsigned integers. The DLC is -also an unsigned integer with a length of 1 byte. Non-extended (11-bit) -identifiers are encoded by adding 0x20000000 to the 11-bit ID. For example, an -11-bit CAN ID of 0x123 is encoded with an arbitration ID of 0x20000123. +also an unsigned integer with a length of 1 byte. Extended (29-bit) +identifiers are encoded by adding 0x80000000 to the ID. For example, a +29-bit CAN ID of 0x123 is encoded with an arbitration ID of 0x80000123. Serial frame format ^^^^^^^^^^^^^^^^^^^ @@ -105,14 +105,14 @@ Examples of serial frames | 0xAA | 0x66 0x73 0x00 0x00 | 0x00 | 0x01 0x00 0x00 0x00 | 0xBB | +----------------+---------------------+------+---------------------+--------------+ -.. rubric:: CAN message with 0 byte payload with an 11-bit CAN ID +.. rubric:: Extended Frame CAN message with 0 byte payload with an 29-bit CAN ID +----------------+---------+ | CAN message | +----------------+---------+ | Arbitration ID | Payload | +================+=========+ -| 0x20000001 (1) | None | +| 0x80000001 (1) | None | +----------------+---------+ +----------------+---------------------+------+---------------------+--------------+ @@ -120,5 +120,5 @@ Examples of serial frames +----------------+---------------------+------+---------------------+--------------+ | Start of frame | Timestamp | DLC | Arbitration ID | End of frame | +================+=====================+======+=====================+==============+ -| 0xAA | 0x66 0x73 0x00 0x00 | 0x00 | 0x01 0x00 0x00 0x20 | 0xBB | +| 0xAA | 0x66 0x73 0x00 0x00 | 0x00 | 0x01 0x00 0x00 0x80 | 0xBB | +----------------+---------------------+------+---------------------+--------------+ diff --git a/test/serial_test.py b/test/serial_test.py index e183e8408..409485112 100644 --- a/test/serial_test.py +++ b/test/serial_test.py @@ -84,38 +84,38 @@ def test_rx_tx_data_none(self): msg_receive = self.bus.recv() self.assertMessageEqual(msg, msg_receive) - def test_rx_tx_min_id(self): + def test_rx_tx_min_std_id(self): """ - Tests the transfer with the lowest extended arbitration id + Tests the transfer with the lowest standard arbitration id """ - msg = can.Message(arbitration_id=0) + msg = can.Message(arbitration_id=0, is_extended_id=False) self.bus.send(msg) msg_receive = self.bus.recv() self.assertMessageEqual(msg, msg_receive) - def test_rx_tx_max_id(self): + def test_rx_tx_max_std_id(self): """ - Tests the transfer with the highest extended arbitration id + Tests the transfer with the highest standard arbitration id """ - msg = can.Message(arbitration_id=536870911) + msg = can.Message(arbitration_id=0x7FF, is_extended_id=False) self.bus.send(msg) msg_receive = self.bus.recv() self.assertMessageEqual(msg, msg_receive) - def test_rx_tx_min_nonext_id(self): + def test_rx_tx_min_ext_id(self): """ - Tests the transfer with the lowest non-extended arbitration id + Tests the transfer with the lowest extended arbitration id """ - msg = can.Message(arbitration_id=0x000, is_extended_id=False) + msg = can.Message(arbitration_id=0x000, is_extended_id=True) self.bus.send(msg) msg_receive = self.bus.recv() self.assertMessageEqual(msg, msg_receive) - def test_rx_tx_max_nonext_id(self): + def test_rx_tx_max_ext_id(self): """ - Tests the transfer with the highest non-extended arbitration id + Tests the transfer with the highest extended arbitration id """ - msg = can.Message(arbitration_id=0x7FF, is_extended_id=False) + msg = can.Message(arbitration_id=0x1FFFFFFF, is_extended_id=True) self.bus.send(msg) msg_receive = self.bus.recv() self.assertMessageEqual(msg, msg_receive) @@ -155,6 +155,28 @@ def test_rx_tx_min_timestamp_error(self): msg = can.Message(timestamp=-1) self.assertRaises(ValueError, self.bus.send, msg) + def test_rx_tx_err_frame(self): + """ + Test the transfer of error frames. + """ + msg = can.Message( + is_extended_id=False, is_error_frame=True, is_remote_frame=False + ) + self.bus.send(msg) + msg_receive = self.bus.recv() + self.assertMessageEqual(msg, msg_receive) + + def test_rx_tx_rtr_frame(self): + """ + Test the transfer of remote frames. + """ + msg = can.Message( + is_extended_id=False, is_error_frame=False, is_remote_frame=True + ) + self.bus.send(msg) + msg_receive = self.bus.recv() + self.assertMessageEqual(msg, msg_receive) + def test_when_no_fileno(self): """ Tests for the fileno method catching the missing pyserial implementeation on the Windows platform From dcc33a7027ef491893305dce9f60e28e7a2364ad Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 31 May 2025 21:40:43 +0200 Subject: [PATCH 082/127] Keep Track of Active Notifiers, Make Notifier usable as ContextManager (#1890) --- can/notifier.py | 179 ++++++++++++++++++++++++++++++++---- examples/asyncio_demo.py | 44 +++++---- examples/cyclic_checksum.py | 5 +- examples/print_notifier.py | 15 ++- examples/send_multiple.py | 2 +- examples/serial_com.py | 2 +- examples/vcan_filtered.py | 13 +-- pyproject.toml | 1 + test/notifier_test.py | 36 ++++++++ 9 files changed, 234 insertions(+), 63 deletions(-) diff --git a/can/notifier.py b/can/notifier.py index 237c874da..2b9944450 100644 --- a/can/notifier.py +++ b/can/notifier.py @@ -8,7 +8,16 @@ import threading import time from collections.abc import Awaitable, Iterable -from typing import Any, Callable, Optional, Union +from contextlib import AbstractContextManager +from types import TracebackType +from typing import ( + Any, + Callable, + Final, + NamedTuple, + Optional, + Union, +) from can.bus import BusABC from can.listener import Listener @@ -19,7 +28,85 @@ MessageRecipient = Union[Listener, Callable[[Message], Union[Awaitable[None], None]]] -class Notifier: +class _BusNotifierPair(NamedTuple): + bus: "BusABC" + notifier: "Notifier" + + +class _NotifierRegistry: + """A registry to manage the association between CAN buses and Notifiers. + + This class ensures that a bus is not added to multiple active Notifiers. + """ + + def __init__(self) -> None: + """Initialize the registry with an empty list of bus-notifier pairs and a threading lock.""" + self.pairs: list[_BusNotifierPair] = [] + self.lock = threading.Lock() + + def register(self, bus: BusABC, notifier: "Notifier") -> None: + """Register a bus and its associated notifier. + + Ensures that a bus is not added to multiple active :class:`~can.Notifier` instances. + + :param bus: + The CAN bus to register. + :param notifier: + The :class:`~can.Notifier` instance associated with the bus. + :raises ValueError: + If the bus is already assigned to an active Notifier. + """ + with self.lock: + for pair in self.pairs: + if bus is pair.bus and not pair.notifier.stopped: + raise ValueError( + "A bus can not be added to multiple active Notifier instances." + ) + self.pairs.append(_BusNotifierPair(bus, notifier)) + + def unregister(self, bus: BusABC, notifier: "Notifier") -> None: + """Unregister a bus and its associated notifier. + + Removes the bus-notifier pair from the registry. + + :param bus: + The CAN bus to unregister. + :param notifier: + The :class:`~can.Notifier` instance associated with the bus. + """ + with self.lock: + registered_pairs_to_remove: list[_BusNotifierPair] = [] + for pair in self.pairs: + if pair.bus is bus and pair.notifier is notifier: + registered_pairs_to_remove.append(pair) + for pair in registered_pairs_to_remove: + self.pairs.remove(pair) + + def find_instances(self, bus: BusABC) -> tuple["Notifier", ...]: + """Find the :class:`~can.Notifier` instances associated with a given CAN bus. + + This method searches the registry for the :class:`~can.Notifier` + that is linked to the specified bus. If the bus is found, the + corresponding :class:`~can.Notifier` instances are returned. If the bus is not + found in the registry, an empty tuple is returned. + + :param bus: + The CAN bus for which to find the associated :class:`~can.Notifier` . + :return: + A tuple of :class:`~can.Notifier` instances associated with the given bus. + """ + instance_list = [] + with self.lock: + for pair in self.pairs: + if bus is pair.bus: + instance_list.append(pair.notifier) + return tuple(instance_list) + + +class Notifier(AbstractContextManager): + + _registry: Final = _NotifierRegistry() + def __init__( self, bus: Union[BusABC, list[BusABC]], @@ -33,61 +120,81 @@ def __init__( .. Note:: - Remember to call `stop()` after all messages are received as + Remember to call :meth:`~can.Notifier.stop` after all messages are received as many listeners carry out flush operations to persist data. - :param bus: A :ref:`bus` or a list of buses to listen to. + :param bus: + A :ref:`bus` or a list of buses to consume messages from. :param listeners: An iterable of :class:`~can.Listener` or callables that receive a :class:`~can.Message` and return nothing. - :param timeout: An optional maximum number of seconds to wait for any :class:`~can.Message`. - :param loop: An :mod:`asyncio` event loop to schedule the ``listeners`` in. + :param timeout: + An optional maximum number of seconds to wait for any :class:`~can.Message`. + :param loop: + An :mod:`asyncio` event loop to schedule the ``listeners`` in. + :raises ValueError: + If a passed in *bus* is already assigned to an active :class:`~can.Notifier`. """ self.listeners: list[MessageRecipient] = list(listeners) - self.bus = bus + self._bus_list: list[BusABC] = [] self.timeout = timeout self._loop = loop #: Exception raised in thread self.exception: Optional[Exception] = None - self._running = True + self._stopped = False self._lock = threading.Lock() self._readers: list[Union[int, threading.Thread]] = [] - buses = self.bus if isinstance(self.bus, list) else [self.bus] - for each_bus in buses: + _bus_list: list[BusABC] = bus if isinstance(bus, list) else [bus] + for each_bus in _bus_list: self.add_bus(each_bus) + @property + def bus(self) -> Union[BusABC, tuple["BusABC", ...]]: + """Return the associated bus or a tuple of buses.""" + if len(self._bus_list) == 1: + return self._bus_list[0] + return tuple(self._bus_list) + def add_bus(self, bus: BusABC) -> None: """Add a bus for notification. :param bus: CAN bus instance. + :raises ValueError: + If the *bus* is already assigned to an active :class:`~can.Notifier`. """ - reader: int = -1 + # add bus to notifier registry + Notifier._registry.register(bus, self) + + # add bus to internal bus list + self._bus_list.append(bus) + + file_descriptor: int = -1 try: - reader = bus.fileno() + file_descriptor = bus.fileno() except NotImplementedError: # Bus doesn't support fileno, we fall back to thread based reader pass - if self._loop is not None and reader >= 0: + if self._loop is not None and file_descriptor >= 0: # Use bus file descriptor to watch for messages - self._loop.add_reader(reader, self._on_message_available, bus) - self._readers.append(reader) + self._loop.add_reader(file_descriptor, self._on_message_available, bus) + self._readers.append(file_descriptor) else: reader_thread = threading.Thread( target=self._rx_thread, args=(bus,), - name=f'can.notifier for bus "{bus.channel_info}"', + name=f'{self.__class__.__qualname__} for bus "{bus.channel_info}"', ) reader_thread.daemon = True reader_thread.start() self._readers.append(reader_thread) - def stop(self, timeout: float = 5) -> None: + def stop(self, timeout: float = 5.0) -> None: """Stop notifying Listeners when new :class:`~can.Message` objects arrive and call :meth:`~can.Listener.stop` on each Listener. @@ -95,7 +202,7 @@ def stop(self, timeout: float = 5) -> None: Max time in seconds to wait for receive threads to finish. Should be longer than timeout given at instantiation. """ - self._running = False + self._stopped = True end_time = time.time() + timeout for reader in self._readers: if isinstance(reader, threading.Thread): @@ -109,6 +216,10 @@ def stop(self, timeout: float = 5) -> None: if hasattr(listener, "stop"): listener.stop() + # remove bus from registry + for bus in self._bus_list: + Notifier._registry.unregister(bus, self) + def _rx_thread(self, bus: BusABC) -> None: # determine message handling callable early, not inside while loop if self._loop: @@ -119,7 +230,7 @@ def _rx_thread(self, bus: BusABC) -> None: else: handle_message = self._on_message_received - while self._running: + while not self._stopped: try: if msg := bus.recv(self.timeout): with self._lock: @@ -184,3 +295,33 @@ def remove_listener(self, listener: MessageRecipient) -> None: :raises ValueError: if `listener` was never added to this notifier """ self.listeners.remove(listener) + + @property + def stopped(self) -> bool: + """Return ``True``, if Notifier was properly shut down with :meth:`~can.Notifier.stop`.""" + return self._stopped + + @staticmethod + def find_instances(bus: BusABC) -> tuple["Notifier", ...]: + """Find :class:`~can.Notifier` instances associated with a given CAN bus. + + This method searches the registry for the :class:`~can.Notifier` + that is linked to the specified bus. If the bus is found, the + corresponding :class:`~can.Notifier` instances are returned. If the bus is not + found in the registry, an empty tuple is returned. + + :param bus: + The CAN bus for which to find the associated :class:`~can.Notifier` . + :return: + A tuple of :class:`~can.Notifier` instances associated with the given bus. + """ + return Notifier._registry.find_instances(bus) + + def __exit__( + self, + exc_type: Optional[type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: + if not self._stopped: + self.stop() diff --git a/examples/asyncio_demo.py b/examples/asyncio_demo.py index d29f03bc5..6befbe7a9 100755 --- a/examples/asyncio_demo.py +++ b/examples/asyncio_demo.py @@ -5,10 +5,12 @@ """ import asyncio -from typing import List +from typing import TYPE_CHECKING import can -from can.notifier import MessageRecipient + +if TYPE_CHECKING: + from can.notifier import MessageRecipient def print_message(msg: can.Message) -> None: @@ -25,32 +27,28 @@ async def main() -> None: reader = can.AsyncBufferedReader() logger = can.Logger("logfile.asc") - listeners: List[MessageRecipient] = [ + listeners: list[MessageRecipient] = [ print_message, # Callback function reader, # AsyncBufferedReader() listener logger, # Regular Listener object ] # Create Notifier with an explicit loop to use for scheduling of callbacks - loop = asyncio.get_running_loop() - notifier = can.Notifier(bus, listeners, loop=loop) - # Start sending first message - bus.send(can.Message(arbitration_id=0)) - - print("Bouncing 10 messages...") - for _ in range(10): - # Wait for next message from AsyncBufferedReader - msg = await reader.get_message() - # Delay response - await asyncio.sleep(0.5) - msg.arbitration_id += 1 - bus.send(msg) - - # Wait for last message to arrive - await reader.get_message() - print("Done!") - - # Clean-up - notifier.stop() + with can.Notifier(bus, listeners, loop=asyncio.get_running_loop()): + # Start sending first message + bus.send(can.Message(arbitration_id=0)) + + print("Bouncing 10 messages...") + for _ in range(10): + # Wait for next message from AsyncBufferedReader + msg = await reader.get_message() + # Delay response + await asyncio.sleep(0.5) + msg.arbitration_id += 1 + bus.send(msg) + + # Wait for last message to arrive + await reader.get_message() + print("Done!") if __name__ == "__main__": diff --git a/examples/cyclic_checksum.py b/examples/cyclic_checksum.py index 3ab6c78ac..763fcd72b 100644 --- a/examples/cyclic_checksum.py +++ b/examples/cyclic_checksum.py @@ -59,6 +59,5 @@ def compute_xbr_checksum(message: can.Message, counter: int) -> int: if __name__ == "__main__": with can.Bus(channel=0, interface="virtual", receive_own_messages=True) as _bus: - notifier = can.Notifier(bus=_bus, listeners=[print]) - cyclic_checksum_send(_bus) - notifier.stop() + with can.Notifier(bus=_bus, listeners=[print]): + cyclic_checksum_send(_bus) diff --git a/examples/print_notifier.py b/examples/print_notifier.py index 8d55ca1dc..e6e11dbec 100755 --- a/examples/print_notifier.py +++ b/examples/print_notifier.py @@ -8,14 +8,13 @@ def main(): with can.Bus(interface="virtual", receive_own_messages=True) as bus: print_listener = can.Printer() - notifier = can.Notifier(bus, [print_listener]) - - bus.send(can.Message(arbitration_id=1, is_extended_id=True)) - bus.send(can.Message(arbitration_id=2, is_extended_id=True)) - bus.send(can.Message(arbitration_id=1, is_extended_id=False)) - - time.sleep(1.0) - notifier.stop() + with can.Notifier(bus, listeners=[print_listener]): + # using Notifier as a context manager automatically calls `Notifier.stop()` + # at the end of the `with` block + bus.send(can.Message(arbitration_id=1, is_extended_id=True)) + bus.send(can.Message(arbitration_id=2, is_extended_id=True)) + bus.send(can.Message(arbitration_id=1, is_extended_id=False)) + time.sleep(1.0) if __name__ == "__main__": diff --git a/examples/send_multiple.py b/examples/send_multiple.py index fdcaa5b59..9123e1bc8 100755 --- a/examples/send_multiple.py +++ b/examples/send_multiple.py @@ -4,8 +4,8 @@ This demo creates multiple processes of producers to spam a socketcan bus. """ -from time import sleep from concurrent.futures import ProcessPoolExecutor +from time import sleep import can diff --git a/examples/serial_com.py b/examples/serial_com.py index 538c8d12f..9f203b2e0 100755 --- a/examples/serial_com.py +++ b/examples/serial_com.py @@ -18,8 +18,8 @@ com0com: http://com0com.sourceforge.net/ """ -import time import threading +import time import can diff --git a/examples/vcan_filtered.py b/examples/vcan_filtered.py index 9c67390ab..22bca706c 100755 --- a/examples/vcan_filtered.py +++ b/examples/vcan_filtered.py @@ -18,14 +18,11 @@ def main(): # print all incoming messages, which includes the ones sent, # since we set receive_own_messages to True # assign to some variable so it does not garbage collected - notifier = can.Notifier(bus, [can.Printer()]) # pylint: disable=unused-variable - - bus.send(can.Message(arbitration_id=1, is_extended_id=True)) - bus.send(can.Message(arbitration_id=2, is_extended_id=True)) - bus.send(can.Message(arbitration_id=1, is_extended_id=False)) - - time.sleep(1.0) - notifier.stop() + with can.Notifier(bus, [can.Printer()]): # pylint: disable=unused-variable + bus.send(can.Message(arbitration_id=1, is_extended_id=True)) + bus.send(can.Message(arbitration_id=2, is_extended_id=True)) + bus.send(can.Message(arbitration_id=1, is_extended_id=False)) + time.sleep(1.0) if __name__ == "__main__": diff --git a/pyproject.toml b/pyproject.toml index a9f3fcbb1..2a5735598 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -184,6 +184,7 @@ ignore = [ ] "can/logger.py" = ["T20"] # flake8-print "can/player.py" = ["T20"] # flake8-print +"examples/*" = ["T20"] # flake8-print [tool.ruff.lint.isort] known-first-party = ["can"] diff --git a/test/notifier_test.py b/test/notifier_test.py index 6982130cf..c21d51f04 100644 --- a/test/notifier_test.py +++ b/test/notifier_test.py @@ -12,16 +12,19 @@ def test_single_bus(self): with can.Bus("test", interface="virtual", receive_own_messages=True) as bus: reader = can.BufferedReader() notifier = can.Notifier(bus, [reader], 0.1) + self.assertFalse(notifier.stopped) msg = can.Message() bus.send(msg) self.assertIsNotNone(reader.get_message(1)) notifier.stop() + self.assertTrue(notifier.stopped) def test_multiple_bus(self): with can.Bus(0, interface="virtual", receive_own_messages=True) as bus1: with can.Bus(1, interface="virtual", receive_own_messages=True) as bus2: reader = can.BufferedReader() notifier = can.Notifier([bus1, bus2], [reader], 0.1) + self.assertFalse(notifier.stopped) msg = can.Message() bus1.send(msg) time.sleep(0.1) @@ -33,6 +36,39 @@ def test_multiple_bus(self): self.assertIsNotNone(recv_msg) self.assertEqual(recv_msg.channel, 1) notifier.stop() + self.assertTrue(notifier.stopped) + + def test_context_manager(self): + with can.Bus("test", interface="virtual", receive_own_messages=True) as bus: + reader = can.BufferedReader() + with can.Notifier(bus, [reader], 0.1) as notifier: + self.assertFalse(notifier.stopped) + msg = can.Message() + bus.send(msg) + self.assertIsNotNone(reader.get_message(1)) + notifier.stop() + self.assertTrue(notifier.stopped) + + def test_registry(self): + with can.Bus("test", interface="virtual", receive_own_messages=True) as bus: + reader = can.BufferedReader() + with can.Notifier(bus, [reader], 0.1) as notifier: + # creating a second notifier for the same bus must fail + self.assertRaises(ValueError, can.Notifier, bus, [reader], 0.1) + + # find_instance must return the existing instance + self.assertEqual(can.Notifier.find_instances(bus), (notifier,)) + + # Notifier is stopped, find_instances() must return an empty tuple + self.assertEqual(can.Notifier.find_instances(bus), ()) + + # now the first notifier is stopped, a new notifier can be created without error: + with can.Notifier(bus, [reader], 0.1) as notifier: + # the next notifier call should fail again since there is an active notifier already + self.assertRaises(ValueError, can.Notifier, bus, [reader], 0.1) + + # find_instance must return the existing instance + self.assertEqual(can.Notifier.find_instances(bus), (notifier,)) class AsyncNotifierTest(unittest.TestCase): From f43bedbc68777162132251bad91aee6ed77d6b8b Mon Sep 17 00:00:00 2001 From: Joachim Stolberg <106076945+HMS-jost@users.noreply.github.com> Date: Sat, 31 May 2025 21:55:54 +0200 Subject: [PATCH 083/127] Handle timer overflow message and build timestamp according to the epoch in IXXATBus (#1934) * Handle timer overflow message and build timestamp according to the epoch * Revert "Handle timer overflow message and build timestamp according to the epoch" This reverts commit e0ccfb0d4b3fef9fbed24c9a6e673e5199e35c52. * Handle timer overflow message and build timestamp according to the epoch * Fix formating issues * Format with black --- can/interfaces/ixxat/canlib_vcinpl.py | 24 +++++++++++++++++++----- can/interfaces/ixxat/canlib_vcinpl2.py | 24 ++++++++++++++++++------ 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/can/interfaces/ixxat/canlib_vcinpl.py b/can/interfaces/ixxat/canlib_vcinpl.py index ba8f1870b..098b022bb 100644 --- a/can/interfaces/ixxat/canlib_vcinpl.py +++ b/can/interfaces/ixxat/canlib_vcinpl.py @@ -13,6 +13,7 @@ import functools import logging import sys +import time import warnings from collections.abc import Sequence from typing import Callable, Optional, Union @@ -620,7 +621,15 @@ def __init__( log.info("Accepting ID: 0x%X MASK: 0x%X", code, mask) # Start the CAN controller. Messages will be forwarded to the channel + start_begin = time.time() _canlib.canControlStart(self._control_handle, constants.TRUE) + start_end = time.time() + + # Calculate an offset to make them relative to epoch + # Assume that the time offset is in the middle of the start command + self._timeoffset = start_begin + (start_end - start_begin / 2) + self._overrunticks = 0 + self._starttickoffset = 0 # For cyclic transmit list. Set when .send_periodic() is first called self._scheduler = None @@ -693,6 +702,9 @@ def _recv_internal(self, timeout): f"Unknown CAN info message code {self._message.abData[0]}", ) ) + # Handle CAN start info message + if self._message.abData[0] == constants.CAN_INFO_START: + self._starttickoffset = self._message.dwTime elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR: if self._message.uMsgInfo.Bytes.bFlags & constants.CAN_MSGFLAGS_OVR: log.warning("CAN error: data overrun") @@ -709,7 +721,8 @@ def _recv_internal(self, timeout): self._message.uMsgInfo.Bytes.bFlags, ) elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_TIMEOVR: - pass + # Add the number of timestamp overruns to the high word + self._overrunticks += self._message.dwMsgId << 32 else: log.warning( "Unexpected message info type 0x%X", @@ -741,11 +754,12 @@ def _recv_internal(self, timeout): # Timed out / can message type is not DATA return None, True - # The _message.dwTime is a 32bit tick value and will overrun, - # so expect to see the value restarting from 0 rx_msg = Message( - timestamp=self._message.dwTime - / self._tick_resolution, # Relative time in s + timestamp=( + (self._message.dwTime + self._overrunticks - self._starttickoffset) + / self._tick_resolution + ) + + self._timeoffset, is_remote_frame=bool(self._message.uMsgInfo.Bits.rtr), is_extended_id=bool(self._message.uMsgInfo.Bits.ext), arbitration_id=self._message.dwMsgId, diff --git a/can/interfaces/ixxat/canlib_vcinpl2.py b/can/interfaces/ixxat/canlib_vcinpl2.py index f9ac5346b..b7698277f 100644 --- a/can/interfaces/ixxat/canlib_vcinpl2.py +++ b/can/interfaces/ixxat/canlib_vcinpl2.py @@ -727,7 +727,15 @@ def __init__( log.info("Accepting ID: 0x%X MASK: 0x%X", code, mask) # Start the CAN controller. Messages will be forwarded to the channel + start_begin = time.time() _canlib.canControlStart(self._control_handle, constants.TRUE) + start_end = time.time() + + # Calculate an offset to make them relative to epoch + # Assume that the time offset is in the middle of the start command + self._timeoffset = start_begin + (start_end - start_begin / 2) + self._overrunticks = 0 + self._starttickoffset = 0 # For cyclic transmit list. Set when .send_periodic() is first called self._scheduler = None @@ -832,7 +840,9 @@ def _recv_internal(self, timeout): f"Unknown CAN info message code {self._message.abData[0]}", ) ) - + # Handle CAN start info message + elif self._message.abData[0] == constants.CAN_INFO_START: + self._starttickoffset = self._message.dwTime elif ( self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR ): @@ -854,7 +864,8 @@ def _recv_internal(self, timeout): self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_TIMEOVR ): - pass + # Add the number of timestamp overruns to the high word + self._overrunticks += self._message.dwMsgId << 32 else: log.warning("Unexpected message info type") @@ -868,11 +879,12 @@ def _recv_internal(self, timeout): return None, True data_len = dlc2len(self._message.uMsgInfo.Bits.dlc) - # The _message.dwTime is a 32bit tick value and will overrun, - # so expect to see the value restarting from 0 rx_msg = Message( - timestamp=self._message.dwTime - / self._tick_resolution, # Relative time in s + timestamp=( + (self._message.dwTime + self._overrunticks - self._starttickoffset) + / self._tick_resolution + ) + + self._timeoffset, is_remote_frame=bool(self._message.uMsgInfo.Bits.rtr), is_fd=bool(self._message.uMsgInfo.Bits.edl), is_rx=True, From 3b75c338f4bd6e7eaa006e2b6483c059ac9bd475 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 6 Jun 2025 18:29:22 +0200 Subject: [PATCH 084/127] Add public functions for creating bus command lines options (#1949) --- can/cli.py | 320 ++++++++++++++++++++++++++++++++++++++++++++ can/logger.py | 219 +++--------------------------- can/player.py | 34 +++-- can/viewer.py | 32 ++--- doc/utils.rst | 3 + pyproject.toml | 2 + test/test_cli.py | 154 +++++++++++++++++++++ test/test_logger.py | 15 ++- test/test_viewer.py | 26 ++-- 9 files changed, 559 insertions(+), 246 deletions(-) create mode 100644 can/cli.py create mode 100644 test/test_cli.py diff --git a/can/cli.py b/can/cli.py new file mode 100644 index 000000000..6e3850354 --- /dev/null +++ b/can/cli.py @@ -0,0 +1,320 @@ +import argparse +import re +from collections.abc import Sequence +from typing import Any, Optional, Union + +import can +from can.typechecking import CanFilter, TAdditionalCliArgs +from can.util import _dict2timing, cast_from_string + + +def add_bus_arguments( + parser: argparse.ArgumentParser, + *, + filter_arg: bool = False, + prefix: Optional[str] = None, + group_title: Optional[str] = None, +) -> None: + """Adds CAN bus configuration options to an argument parser. + + :param parser: + The argument parser to which the options will be added. + :param filter_arg: + Whether to include the filter argument. + :param prefix: + An optional prefix for the argument names, allowing configuration of multiple buses. + :param group_title: + The title of the argument group. If not provided, a default title will be generated + based on the prefix. For example, "bus arguments (prefix)" if a prefix is specified, + or "bus arguments" otherwise. + """ + if group_title is None: + group_title = f"bus arguments ({prefix})" if prefix else "bus arguments" + + group = parser.add_argument_group(group_title) + + flags = [f"--{prefix}-channel"] if prefix else ["-c", "--channel"] + dest = f"{prefix}_channel" if prefix else "channel" + group.add_argument( + *flags, + dest=dest, + default=argparse.SUPPRESS, + metavar="CHANNEL", + help=r"Most backend interfaces require some sort of channel. For " + r"example with the serial interface the channel might be a rfcomm" + r' device: "/dev/rfcomm0". With the socketcan interface valid ' + r'channel examples include: "can0", "vcan0".', + ) + + flags = [f"--{prefix}-interface"] if prefix else ["-i", "--interface"] + dest = f"{prefix}_interface" if prefix else "interface" + group.add_argument( + *flags, + dest=dest, + default=argparse.SUPPRESS, + choices=sorted(can.VALID_INTERFACES), + help="""Specify the backend CAN interface to use. If left blank, + fall back to reading from configuration files.""", + ) + + flags = [f"--{prefix}-bitrate"] if prefix else ["-b", "--bitrate"] + dest = f"{prefix}_bitrate" if prefix else "bitrate" + group.add_argument( + *flags, + dest=dest, + type=int, + default=argparse.SUPPRESS, + metavar="BITRATE", + help="Bitrate to use for the CAN bus.", + ) + + flags = [f"--{prefix}-fd"] if prefix else ["--fd"] + dest = f"{prefix}_fd" if prefix else "fd" + group.add_argument( + *flags, + dest=dest, + default=argparse.SUPPRESS, + action="store_true", + help="Activate CAN-FD support", + ) + + flags = [f"--{prefix}-data-bitrate"] if prefix else ["--data-bitrate"] + dest = f"{prefix}_data_bitrate" if prefix else "data_bitrate" + group.add_argument( + *flags, + dest=dest, + type=int, + default=argparse.SUPPRESS, + metavar="DATA_BITRATE", + help="Bitrate to use for the data phase in case of CAN-FD.", + ) + + flags = [f"--{prefix}-timing"] if prefix else ["--timing"] + dest = f"{prefix}_timing" if prefix else "timing" + group.add_argument( + *flags, + dest=dest, + action=_BitTimingAction, + nargs=argparse.ONE_OR_MORE, + default=argparse.SUPPRESS, + metavar="TIMING_ARG", + help="Configure bit rate and bit timing. For example, use " + "`--timing f_clock=8_000_000 tseg1=5 tseg2=2 sjw=2 brp=2 nof_samples=1` for classical CAN " + "or `--timing f_clock=80_000_000 nom_tseg1=119 nom_tseg2=40 nom_sjw=40 nom_brp=1 " + "data_tseg1=29 data_tseg2=10 data_sjw=10 data_brp=1` for CAN FD. " + "Check the python-can documentation to verify whether your " + "CAN interface supports the `timing` argument.", + ) + + if filter_arg: + flags = [f"--{prefix}-filter"] if prefix else ["--filter"] + dest = f"{prefix}_can_filters" if prefix else "can_filters" + group.add_argument( + *flags, + dest=dest, + nargs=argparse.ONE_OR_MORE, + action=_CanFilterAction, + default=argparse.SUPPRESS, + metavar="{:,~}", + help="R|Space separated CAN filters for the given CAN interface:" + "\n : (matches when & mask ==" + " can_id & mask)" + "\n ~ (matches when & mask !=" + " can_id & mask)" + "\nFx to show only frames with ID 0x100 to 0x103 and 0x200 to 0x20F:" + "\n python -m can.viewer --filter 100:7FC 200:7F0" + "\nNote that the ID and mask are always interpreted as hex values", + ) + + flags = [f"--{prefix}-bus-kwargs"] if prefix else ["--bus-kwargs"] + dest = f"{prefix}_bus_kwargs" if prefix else "bus_kwargs" + group.add_argument( + *flags, + dest=dest, + action=_BusKwargsAction, + nargs=argparse.ONE_OR_MORE, + default=argparse.SUPPRESS, + metavar="BUS_KWARG", + help="Pass keyword arguments down to the instantiation of the bus class. " + "For example, `-i vector -c 1 --bus-kwargs app_name=MyCanApp serial=1234` is equivalent " + "to opening the bus with `can.Bus('vector', channel=1, app_name='MyCanApp', serial=1234)", + ) + + +def create_bus_from_namespace( + namespace: argparse.Namespace, + *, + prefix: Optional[str] = None, + **kwargs: Any, +) -> can.BusABC: + """Creates and returns a CAN bus instance based on the provided namespace and arguments. + + :param namespace: + The namespace containing parsed arguments. + :param prefix: + An optional prefix for the argument names, enabling support for multiple buses. + :param kwargs: + Additional keyword arguments to configure the bus. + :return: + A CAN bus instance. + """ + config: dict[str, Any] = {"single_handle": True, **kwargs} + + for keyword in ( + "channel", + "interface", + "bitrate", + "fd", + "data_bitrate", + "can_filters", + "timing", + "bus_kwargs", + ): + prefixed_keyword = f"{prefix}_{keyword}" if prefix else keyword + + if prefixed_keyword in namespace: + value = getattr(namespace, prefixed_keyword) + + if keyword == "bus_kwargs": + config.update(value) + else: + config[keyword] = value + + try: + return can.Bus(**config) + except Exception as exc: + err_msg = f"Unable to instantiate bus from arguments {vars(namespace)}." + raise argparse.ArgumentError(None, err_msg) from exc + + +class _CanFilterAction(argparse.Action): + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: Union[str, Sequence[Any], None], + option_string: Optional[str] = None, + ) -> None: + if not isinstance(values, list): + raise argparse.ArgumentError(self, "Invalid filter argument") + + print(f"Adding filter(s): {values}") + can_filters: list[CanFilter] = [] + + for filt in values: + if ":" in filt: + parts = filt.split(":") + can_id = int(parts[0], base=16) + can_mask = int(parts[1], base=16) + elif "~" in filt: + parts = filt.split("~") + can_id = int(parts[0], base=16) | 0x20000000 # CAN_INV_FILTER + can_mask = int(parts[1], base=16) & 0x20000000 # socket.CAN_ERR_FLAG + else: + raise argparse.ArgumentError(self, "Invalid filter argument") + can_filters.append({"can_id": can_id, "can_mask": can_mask}) + + setattr(namespace, self.dest, can_filters) + + +class _BitTimingAction(argparse.Action): + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: Union[str, Sequence[Any], None], + option_string: Optional[str] = None, + ) -> None: + if not isinstance(values, list): + raise argparse.ArgumentError(self, "Invalid --timing argument") + + timing_dict: dict[str, int] = {} + for arg in values: + try: + key, value_string = arg.split("=") + value = int(value_string) + timing_dict[key] = value + except ValueError: + raise argparse.ArgumentError( + self, f"Invalid timing argument: {arg}" + ) from None + + if not (timing := _dict2timing(timing_dict)): + err_msg = "Invalid --timing argument. Incomplete parameters." + raise argparse.ArgumentError(self, err_msg) + + setattr(namespace, self.dest, timing) + print(timing) + + +class _BusKwargsAction(argparse.Action): + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: Union[str, Sequence[Any], None], + option_string: Optional[str] = None, + ) -> None: + if not isinstance(values, list): + raise argparse.ArgumentError(self, "Invalid --bus-kwargs argument") + + bus_kwargs: dict[str, Union[str, int, float, bool]] = {} + + for arg in values: + try: + match = re.match( + r"^(?P[_a-zA-Z][_a-zA-Z0-9]*)=(?P\S*?)$", + arg, + ) + if not match: + raise ValueError + key = match["name"].replace("-", "_") + string_val = match["value"] + bus_kwargs[key] = cast_from_string(string_val) + except ValueError: + raise argparse.ArgumentError( + self, + f"Unable to parse bus keyword argument '{arg}'", + ) from None + + setattr(namespace, self.dest, bus_kwargs) + + +def _add_extra_args( + parser: Union[argparse.ArgumentParser, argparse._ArgumentGroup], +) -> None: + parser.add_argument( + "extra_args", + nargs=argparse.REMAINDER, + help="The remaining arguments will be used for logger/player initialisation. " + "For example, `can_logger -i virtual -c test -f logfile.blf --compression-level=9` " + "passes the keyword argument `compression_level=9` to the BlfWriter.", + ) + + +def _parse_additional_config(unknown_args: Sequence[str]) -> TAdditionalCliArgs: + for arg in unknown_args: + if not re.match(r"^--[a-zA-Z][a-zA-Z0-9\-]*=\S*?$", arg): + raise ValueError(f"Parsing argument {arg} failed") + + def _split_arg(_arg: str) -> tuple[str, str]: + left, right = _arg.split("=", 1) + return left.lstrip("-").replace("-", "_"), right + + args: dict[str, Union[str, int, float, bool]] = {} + for key, string_val in map(_split_arg, unknown_args): + args[key] = cast_from_string(string_val) + return args + + +def _set_logging_level_from_namespace(namespace: argparse.Namespace) -> None: + if "verbosity" in namespace: + logging_level_names = [ + "critical", + "error", + "warning", + "info", + "debug", + "subdebug", + ] + can.set_logging_level(logging_level_names[min(5, namespace.verbosity)]) diff --git a/can/logger.py b/can/logger.py index 9c1134257..8274d6668 100644 --- a/can/logger.py +++ b/can/logger.py @@ -1,203 +1,25 @@ import argparse import errno -import re import sys -from collections.abc import Sequence from datetime import datetime from typing import ( TYPE_CHECKING, - Any, - Optional, Union, ) -import can -from can import Bus, BusState, Logger, SizedRotatingLogger +from can import BusState, Logger, SizedRotatingLogger +from can.cli import ( + _add_extra_args, + _parse_additional_config, + _set_logging_level_from_namespace, + add_bus_arguments, + create_bus_from_namespace, +) from can.typechecking import TAdditionalCliArgs -from can.util import _dict2timing, cast_from_string if TYPE_CHECKING: from can.io import BaseRotatingLogger from can.io.generic import MessageWriter - from can.typechecking import CanFilter - - -def _create_base_argument_parser(parser: argparse.ArgumentParser) -> None: - """Adds common options to an argument parser.""" - - parser.add_argument( - "-c", - "--channel", - help=r"Most backend interfaces require some sort of channel. For " - r"example with the serial interface the channel might be a rfcomm" - r' device: "/dev/rfcomm0". With the socketcan interface valid ' - r'channel examples include: "can0", "vcan0".', - ) - - parser.add_argument( - "-i", - "--interface", - dest="interface", - help="""Specify the backend CAN interface to use. If left blank, - fall back to reading from configuration files.""", - choices=sorted(can.VALID_INTERFACES), - ) - - parser.add_argument( - "-b", "--bitrate", type=int, help="Bitrate to use for the CAN bus." - ) - - parser.add_argument("--fd", help="Activate CAN-FD support", action="store_true") - - parser.add_argument( - "--data_bitrate", - type=int, - help="Bitrate to use for the data phase in case of CAN-FD.", - ) - - parser.add_argument( - "--timing", - action=_BitTimingAction, - nargs=argparse.ONE_OR_MORE, - help="Configure bit rate and bit timing. For example, use " - "`--timing f_clock=8_000_000 tseg1=5 tseg2=2 sjw=2 brp=2 nof_samples=1` for classical CAN " - "or `--timing f_clock=80_000_000 nom_tseg1=119 nom_tseg2=40 nom_sjw=40 nom_brp=1 " - "data_tseg1=29 data_tseg2=10 data_sjw=10 data_brp=1` for CAN FD. " - "Check the python-can documentation to verify whether your " - "CAN interface supports the `timing` argument.", - metavar="TIMING_ARG", - ) - - parser.add_argument( - "extra_args", - nargs=argparse.REMAINDER, - help="The remaining arguments will be used for the interface and " - "logger/player initialisation. " - "For example, `-i vector -c 1 --app-name=MyCanApp` is the equivalent " - "to opening the bus with `Bus('vector', channel=1, app_name='MyCanApp')", - ) - - -def _append_filter_argument( - parser: Union[argparse.ArgumentParser, argparse._ArgumentGroup], - *args: str, - **kwargs: Any, -) -> None: - """Adds the ``filter`` option to an argument parser.""" - - parser.add_argument( - *args, - "--filter", - help="R|Space separated CAN filters for the given CAN interface:" - "\n : (matches when & mask ==" - " can_id & mask)" - "\n ~ (matches when & mask !=" - " can_id & mask)" - "\nFx to show only frames with ID 0x100 to 0x103 and 0x200 to 0x20F:" - "\n python -m can.viewer --filter 100:7FC 200:7F0" - "\nNote that the ID and mask are always interpreted as hex values", - metavar="{:,~}", - nargs=argparse.ONE_OR_MORE, - action=_CanFilterAction, - dest="can_filters", - **kwargs, - ) - - -def _create_bus(parsed_args: argparse.Namespace, **kwargs: Any) -> can.BusABC: - logging_level_names = ["critical", "error", "warning", "info", "debug", "subdebug"] - can.set_logging_level(logging_level_names[min(5, parsed_args.verbosity)]) - - config: dict[str, Any] = {"single_handle": True, **kwargs} - if parsed_args.interface: - config["interface"] = parsed_args.interface - if parsed_args.bitrate: - config["bitrate"] = parsed_args.bitrate - if parsed_args.fd: - config["fd"] = True - if parsed_args.data_bitrate: - config["data_bitrate"] = parsed_args.data_bitrate - if getattr(parsed_args, "can_filters", None): - config["can_filters"] = parsed_args.can_filters - if parsed_args.timing: - config["timing"] = parsed_args.timing - - return Bus(parsed_args.channel, **config) - - -class _CanFilterAction(argparse.Action): - def __call__( - self, - parser: argparse.ArgumentParser, - namespace: argparse.Namespace, - values: Union[str, Sequence[Any], None], - option_string: Optional[str] = None, - ) -> None: - if not isinstance(values, list): - raise argparse.ArgumentError(None, "Invalid filter argument") - - print(f"Adding filter(s): {values}") - can_filters: list[CanFilter] = [] - - for filt in values: - if ":" in filt: - parts = filt.split(":") - can_id = int(parts[0], base=16) - can_mask = int(parts[1], base=16) - elif "~" in filt: - parts = filt.split("~") - can_id = int(parts[0], base=16) | 0x20000000 # CAN_INV_FILTER - can_mask = int(parts[1], base=16) & 0x20000000 # socket.CAN_ERR_FLAG - else: - raise argparse.ArgumentError(None, "Invalid filter argument") - can_filters.append({"can_id": can_id, "can_mask": can_mask}) - - setattr(namespace, self.dest, can_filters) - - -class _BitTimingAction(argparse.Action): - def __call__( - self, - parser: argparse.ArgumentParser, - namespace: argparse.Namespace, - values: Union[str, Sequence[Any], None], - option_string: Optional[str] = None, - ) -> None: - if not isinstance(values, list): - raise argparse.ArgumentError(None, "Invalid --timing argument") - - timing_dict: dict[str, int] = {} - for arg in values: - try: - key, value_string = arg.split("=") - value = int(value_string) - timing_dict[key] = value - except ValueError: - raise argparse.ArgumentError( - None, f"Invalid timing argument: {arg}" - ) from None - - if not (timing := _dict2timing(timing_dict)): - err_msg = "Invalid --timing argument. Incomplete parameters." - raise argparse.ArgumentError(None, err_msg) - - setattr(namespace, self.dest, timing) - print(timing) - - -def _parse_additional_config(unknown_args: Sequence[str]) -> TAdditionalCliArgs: - for arg in unknown_args: - if not re.match(r"^--[a-zA-Z][a-zA-Z0-9\-]*=\S*?$", arg): - raise ValueError(f"Parsing argument {arg} failed") - - def _split_arg(_arg: str) -> tuple[str, str]: - left, right = _arg.split("=", 1) - return left.lstrip("-").replace("-", "_"), right - - args: dict[str, Union[str, int, float, bool]] = {} - for key, string_val in map(_split_arg, unknown_args): - args[key] = cast_from_string(string_val) - return args def _parse_logger_args( @@ -210,11 +32,9 @@ def _parse_logger_args( "given file.", ) - # Generate the standard arguments: - # Channel, bitrate, data_bitrate, interface, app_name, CAN-FD support - _create_base_argument_parser(parser) + logger_group = parser.add_argument_group("logger arguments") - parser.add_argument( + logger_group.add_argument( "-f", "--file_name", dest="log_file", @@ -222,7 +42,7 @@ def _parse_logger_args( default=None, ) - parser.add_argument( + logger_group.add_argument( "-a", "--append", dest="append", @@ -230,7 +50,7 @@ def _parse_logger_args( action="store_true", ) - parser.add_argument( + logger_group.add_argument( "-s", "--file_size", dest="file_size", @@ -242,7 +62,7 @@ def _parse_logger_args( default=None, ) - parser.add_argument( + logger_group.add_argument( "-v", action="count", dest="verbosity", @@ -251,9 +71,7 @@ def _parse_logger_args( default=2, ) - _append_filter_argument(parser) - - state_group = parser.add_mutually_exclusive_group(required=False) + state_group = logger_group.add_mutually_exclusive_group(required=False) state_group.add_argument( "--active", help="Start the bus as active, this is applied by default.", @@ -263,6 +81,12 @@ def _parse_logger_args( "--passive", help="Start the bus as passive.", action="store_true" ) + # handle remaining arguments + _add_extra_args(logger_group) + + # add bus options + add_bus_arguments(parser, filter_arg=True) + # print help message when no arguments were given if not args: parser.print_help(sys.stderr) @@ -275,7 +99,8 @@ def _parse_logger_args( def main() -> None: results, additional_config = _parse_logger_args(sys.argv[1:]) - bus = _create_bus(results, **additional_config) + bus = create_bus_from_namespace(results) + _set_logging_level_from_namespace(results) if results.active: bus.state = BusState.ACTIVE diff --git a/can/player.py b/can/player.py index 38b76a331..a92cccc3d 100644 --- a/can/player.py +++ b/can/player.py @@ -12,8 +12,13 @@ from typing import TYPE_CHECKING, cast from can import LogReader, MessageSync - -from .logger import _create_base_argument_parser, _create_bus, _parse_additional_config +from can.cli import ( + _add_extra_args, + _parse_additional_config, + _set_logging_level_from_namespace, + add_bus_arguments, + create_bus_from_namespace, +) if TYPE_CHECKING: from collections.abc import Iterable @@ -24,9 +29,9 @@ def main() -> None: parser = argparse.ArgumentParser(description="Replay CAN traffic.") - _create_base_argument_parser(parser) + player_group = parser.add_argument_group("Player arguments") - parser.add_argument( + player_group.add_argument( "-f", "--file_name", dest="log_file", @@ -34,7 +39,7 @@ def main() -> None: default=None, ) - parser.add_argument( + player_group.add_argument( "-v", action="count", dest="verbosity", @@ -43,27 +48,27 @@ def main() -> None: default=2, ) - parser.add_argument( + player_group.add_argument( "--ignore-timestamps", dest="timestamps", help="""Ignore timestamps (send all frames immediately with minimum gap between frames)""", action="store_false", ) - parser.add_argument( + player_group.add_argument( "--error-frames", help="Also send error frames to the interface.", action="store_true", ) - parser.add_argument( + player_group.add_argument( "-g", "--gap", type=float, help=" minimum time between replayed frames", default=0.0001, ) - parser.add_argument( + player_group.add_argument( "-s", "--skip", type=float, @@ -71,13 +76,19 @@ def main() -> None: help=" skip gaps greater than 's' seconds", ) - parser.add_argument( + player_group.add_argument( "infile", metavar="input-file", type=str, help="The file to replay. For supported types see can.LogReader.", ) + # handle remaining arguments + _add_extra_args(player_group) + + # add bus options + add_bus_arguments(parser) + # print help message when no arguments were given if len(sys.argv) < 2: parser.print_help(sys.stderr) @@ -86,11 +97,12 @@ def main() -> None: results, unknown_args = parser.parse_known_args() additional_config = _parse_additional_config([*results.extra_args, *unknown_args]) + _set_logging_level_from_namespace(results) verbosity = results.verbosity error_frames = results.error_frames - with _create_bus(results, **additional_config) as bus: + with create_bus_from_namespace(results) as bus: with LogReader(results.infile, **additional_config) as reader: in_sync = MessageSync( cast("Iterable[Message]", reader), diff --git a/can/viewer.py b/can/viewer.py index 3eed727ab..81e8942a4 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -29,13 +29,12 @@ import time from can import __version__ -from can.logger import ( - _append_filter_argument, - _create_base_argument_parser, - _create_bus, - _parse_additional_config, +from can.cli import ( + _set_logging_level_from_namespace, + add_bus_arguments, + create_bus_from_namespace, ) -from can.typechecking import TAdditionalCliArgs, TDataStructs +from can.typechecking import TDataStructs logger = logging.getLogger("can.viewer") @@ -390,7 +389,7 @@ def _fill_text(self, text, width, indent): def _parse_viewer_args( args: list[str], -) -> tuple[argparse.Namespace, TDataStructs, TAdditionalCliArgs]: +) -> tuple[argparse.Namespace, TDataStructs]: # Parse command line arguments parser = argparse.ArgumentParser( "python -m can.viewer", @@ -411,9 +410,8 @@ def _parse_viewer_args( allow_abbrev=False, ) - # Generate the standard arguments: - # Channel, bitrate, data_bitrate, interface, app_name, CAN-FD support - _create_base_argument_parser(parser) + # add bus options group + add_bus_arguments(parser, filter_arg=True, group_title="Bus arguments") optional = parser.add_argument_group("Optional arguments") @@ -470,8 +468,6 @@ def _parse_viewer_args( default="", ) - _append_filter_argument(optional, "-f") - optional.add_argument( "-v", action="count", @@ -487,6 +483,8 @@ def _parse_viewer_args( raise SystemExit(errno.EINVAL) parsed_args, unknown_args = parser.parse_known_args(args) + if unknown_args: + print("Unknown arguments:", unknown_args) # Dictionary used to convert between Python values and C structs represented as Python strings. # If the value is 'None' then the message does not contain any data package. @@ -536,15 +534,13 @@ def _parse_viewer_args( else: data_structs[key] = struct.Struct(fmt) - additional_config = _parse_additional_config( - [*parsed_args.extra_args, *unknown_args] - ) - return parsed_args, data_structs, additional_config + return parsed_args, data_structs def main() -> None: - parsed_args, data_structs, additional_config = _parse_viewer_args(sys.argv[1:]) - bus = _create_bus(parsed_args, **additional_config) + parsed_args, data_structs = _parse_viewer_args(sys.argv[1:]) + bus = create_bus_from_namespace(parsed_args) + _set_logging_level_from_namespace(parsed_args) curses.wrapper(CanViewer, bus, data_structs) # type: ignore[attr-defined,unused-ignore] diff --git a/doc/utils.rst b/doc/utils.rst index a87d411a9..9c742e2fb 100644 --- a/doc/utils.rst +++ b/doc/utils.rst @@ -4,4 +4,7 @@ Utilities .. autofunction:: can.detect_available_configs +.. autofunction:: can.cli.add_bus_arguments + +.. autofunction:: can.cli.create_bus_from_namespace diff --git a/pyproject.toml b/pyproject.toml index 2a5735598..ee98fec24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -182,8 +182,10 @@ ignore = [ "PGH003", # blanket-type-ignore "RUF012", # mutable-class-default ] +"can/cli.py" = ["T20"] # flake8-print "can/logger.py" = ["T20"] # flake8-print "can/player.py" = ["T20"] # flake8-print +"can/viewer.py" = ["T20"] # flake8-print "examples/*" = ["T20"] # flake8-print [tool.ruff.lint.isort] diff --git a/test/test_cli.py b/test/test_cli.py new file mode 100644 index 000000000..ecc662832 --- /dev/null +++ b/test/test_cli.py @@ -0,0 +1,154 @@ +import argparse +import unittest +from unittest.mock import patch + +from can.cli import add_bus_arguments, create_bus_from_namespace + + +class TestCliUtils(unittest.TestCase): + def test_add_bus_arguments(self): + parser = argparse.ArgumentParser() + add_bus_arguments(parser, filter_arg=True, prefix="test") + + parsed_args = parser.parse_args( + [ + "--test-channel", + "0", + "--test-interface", + "vector", + "--test-timing", + "f_clock=8000000", + "brp=4", + "tseg1=11", + "tseg2=4", + "sjw=2", + "nof_samples=3", + "--test-filter", + "100:7FF", + "200~7F0", + "--test-bus-kwargs", + "app_name=MyApp", + "serial=1234", + ] + ) + + self.assertNotIn("channel", parsed_args) + self.assertNotIn("test_bitrate", parsed_args) + self.assertNotIn("test_data_bitrate", parsed_args) + self.assertNotIn("test_fd", parsed_args) + + self.assertEqual(parsed_args.test_channel, "0") + self.assertEqual(parsed_args.test_interface, "vector") + self.assertEqual(parsed_args.test_timing.f_clock, 8000000) + self.assertEqual(parsed_args.test_timing.brp, 4) + self.assertEqual(parsed_args.test_timing.tseg1, 11) + self.assertEqual(parsed_args.test_timing.tseg2, 4) + self.assertEqual(parsed_args.test_timing.sjw, 2) + self.assertEqual(parsed_args.test_timing.nof_samples, 3) + self.assertEqual(len(parsed_args.test_can_filters), 2) + self.assertEqual(parsed_args.test_can_filters[0]["can_id"], 0x100) + self.assertEqual(parsed_args.test_can_filters[0]["can_mask"], 0x7FF) + self.assertEqual(parsed_args.test_can_filters[1]["can_id"], 0x200 | 0x20000000) + self.assertEqual( + parsed_args.test_can_filters[1]["can_mask"], 0x7F0 & 0x20000000 + ) + self.assertEqual(parsed_args.test_bus_kwargs["app_name"], "MyApp") + self.assertEqual(parsed_args.test_bus_kwargs["serial"], 1234) + + def test_add_bus_arguments_no_prefix(self): + parser = argparse.ArgumentParser() + add_bus_arguments(parser, filter_arg=True) + + parsed_args = parser.parse_args( + [ + "--channel", + "0", + "--interface", + "vector", + "--timing", + "f_clock=8000000", + "brp=4", + "tseg1=11", + "tseg2=4", + "sjw=2", + "nof_samples=3", + "--filter", + "100:7FF", + "200~7F0", + "--bus-kwargs", + "app_name=MyApp", + "serial=1234", + ] + ) + + self.assertEqual(parsed_args.channel, "0") + self.assertEqual(parsed_args.interface, "vector") + self.assertEqual(parsed_args.timing.f_clock, 8000000) + self.assertEqual(parsed_args.timing.brp, 4) + self.assertEqual(parsed_args.timing.tseg1, 11) + self.assertEqual(parsed_args.timing.tseg2, 4) + self.assertEqual(parsed_args.timing.sjw, 2) + self.assertEqual(parsed_args.timing.nof_samples, 3) + self.assertEqual(len(parsed_args.can_filters), 2) + self.assertEqual(parsed_args.can_filters[0]["can_id"], 0x100) + self.assertEqual(parsed_args.can_filters[0]["can_mask"], 0x7FF) + self.assertEqual(parsed_args.can_filters[1]["can_id"], 0x200 | 0x20000000) + self.assertEqual(parsed_args.can_filters[1]["can_mask"], 0x7F0 & 0x20000000) + self.assertEqual(parsed_args.bus_kwargs["app_name"], "MyApp") + self.assertEqual(parsed_args.bus_kwargs["serial"], 1234) + + @patch("can.Bus") + def test_create_bus_from_namespace(self, mock_bus): + namespace = argparse.Namespace( + test_channel="vcan0", + test_interface="virtual", + test_bitrate=500000, + test_data_bitrate=2000000, + test_fd=True, + test_can_filters=[{"can_id": 0x100, "can_mask": 0x7FF}], + test_bus_kwargs={"app_name": "MyApp", "serial": 1234}, + ) + + create_bus_from_namespace(namespace, prefix="test") + + mock_bus.assert_called_once_with( + channel="vcan0", + interface="virtual", + bitrate=500000, + data_bitrate=2000000, + fd=True, + can_filters=[{"can_id": 0x100, "can_mask": 0x7FF}], + app_name="MyApp", + serial=1234, + single_handle=True, + ) + + @patch("can.Bus") + def test_create_bus_from_namespace_no_prefix(self, mock_bus): + namespace = argparse.Namespace( + channel="vcan0", + interface="virtual", + bitrate=500000, + data_bitrate=2000000, + fd=True, + can_filters=[{"can_id": 0x100, "can_mask": 0x7FF}], + bus_kwargs={"app_name": "MyApp", "serial": 1234}, + ) + + create_bus_from_namespace(namespace) + + mock_bus.assert_called_once_with( + channel="vcan0", + interface="virtual", + bitrate=500000, + data_bitrate=2000000, + fd=True, + can_filters=[{"can_id": 0x100, "can_mask": 0x7FF}], + app_name="MyApp", + serial=1234, + single_handle=True, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_logger.py b/test/test_logger.py index d9f200e00..41778ab6a 100644 --- a/test/test_logger.py +++ b/test/test_logger.py @@ -14,6 +14,7 @@ import pytest import can +import can.cli import can.logger @@ -89,7 +90,7 @@ def test_log_virtual_with_config(self): "--bitrate", "250000", "--fd", - "--data_bitrate", + "--data-bitrate", "2000000", ] can.logger.main() @@ -111,7 +112,7 @@ def test_parse_logger_args(self): "--bitrate", "250000", "--fd", - "--data_bitrate", + "--data-bitrate", "2000000", "--receive-own-messages=True", ] @@ -205,7 +206,7 @@ def test_parse_additional_config(self): "--offset=1.5", "--tseg1-abr=127", ] - parsed_args = can.logger._parse_additional_config(unknown_args) + parsed_args = can.cli._parse_additional_config(unknown_args) assert "app_name" in parsed_args assert parsed_args["app_name"] == "CANalyzer" @@ -232,16 +233,16 @@ def test_parse_additional_config(self): assert parsed_args["tseg1_abr"] == 127 with pytest.raises(ValueError): - can.logger._parse_additional_config(["--wrong-format"]) + can.cli._parse_additional_config(["--wrong-format"]) with pytest.raises(ValueError): - can.logger._parse_additional_config(["-wrongformat=value"]) + can.cli._parse_additional_config(["-wrongformat=value"]) with pytest.raises(ValueError): - can.logger._parse_additional_config(["--wrongformat=value1 value2"]) + can.cli._parse_additional_config(["--wrongformat=value1 value2"]) with pytest.raises(ValueError): - can.logger._parse_additional_config(["wrongformat="]) + can.cli._parse_additional_config(["wrongformat="]) class TestLoggerCompressedFile(unittest.TestCase): diff --git a/test/test_viewer.py b/test/test_viewer.py index 3bd32b25a..e71d06dc8 100644 --- a/test/test_viewer.py +++ b/test/test_viewer.py @@ -397,19 +397,19 @@ def test_pack_unpack(self): ) def test_parse_args(self): - parsed_args, _, _ = _parse_viewer_args(["-b", "250000"]) + parsed_args, _ = _parse_viewer_args(["-b", "250000"]) self.assertEqual(parsed_args.bitrate, 250000) - parsed_args, _, _ = _parse_viewer_args(["--bitrate", "500000"]) + parsed_args, _ = _parse_viewer_args(["--bitrate", "500000"]) self.assertEqual(parsed_args.bitrate, 500000) - parsed_args, _, _ = _parse_viewer_args(["-c", "can0"]) + parsed_args, _ = _parse_viewer_args(["-c", "can0"]) self.assertEqual(parsed_args.channel, "can0") - parsed_args, _, _ = _parse_viewer_args(["--channel", "PCAN_USBBUS1"]) + parsed_args, _ = _parse_viewer_args(["--channel", "PCAN_USBBUS1"]) self.assertEqual(parsed_args.channel, "PCAN_USBBUS1") - parsed_args, data_structs, _ = _parse_viewer_args(["-d", "100: Date: Fri, 13 Jun 2025 00:04:43 +0200 Subject: [PATCH 085/127] Parse socketcand error messages to create a CAN error frame (#1941) * parse socketcand error messages to create a CAN error frame * Add test for socketcand convert_ascii_message_to_can_message --- can/interfaces/socketcand/socketcand.py | 32 +++++++++++++++-- test/test_socketcand.py | 46 +++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 test/test_socketcand.py diff --git a/can/interfaces/socketcand/socketcand.py b/can/interfaces/socketcand/socketcand.py index 3ecda082b..d401102f7 100644 --- a/can/interfaces/socketcand/socketcand.py +++ b/can/interfaces/socketcand/socketcand.py @@ -124,10 +124,11 @@ def detect_beacon(timeout_ms: int = 3100) -> list[can.typechecking.AutoDetectedC def convert_ascii_message_to_can_message(ascii_msg: str) -> can.Message: - if not ascii_msg.startswith("< frame ") or not ascii_msg.endswith(" >"): - log.warning(f"Could not parse ascii message: {ascii_msg}") + if not ascii_msg.endswith(" >"): + log.warning(f"Missing ending character in ascii message: {ascii_msg}") return None - else: + + if ascii_msg.startswith("< frame "): # frame_string = ascii_msg.removeprefix("< frame ").removesuffix(" >") frame_string = ascii_msg[8:-2] parts = frame_string.split(" ", 3) @@ -146,6 +147,31 @@ def convert_ascii_message_to_can_message(ascii_msg: str) -> can.Message: ) return can_message + if ascii_msg.startswith("< error "): + frame_string = ascii_msg[8:-2] + parts = frame_string.split(" ", 3) + can_id, timestamp = int(parts[0], 16), float(parts[1]) + is_ext = len(parts[0]) != 3 + + # socketcand sends no data in the error message so we don't have information + # about the error details, therefore the can frame is created with one + # data byte set to zero + data = bytearray([0]) + can_dlc = len(data) + can_message = can.Message( + timestamp=timestamp, + arbitration_id=can_id & 0x1FFFFFFF, + is_error_frame=True, + data=data, + dlc=can_dlc, + is_extended_id=True, + is_rx=True, + ) + return can_message + + log.warning(f"Could not parse ascii message: {ascii_msg}") + return None + def convert_can_message_to_ascii_message(can_message: can.Message) -> str: # Note: socketcan bus adds extended flag, remote_frame_flag & error_flag to id diff --git a/test/test_socketcand.py b/test/test_socketcand.py new file mode 100644 index 000000000..7050b9f20 --- /dev/null +++ b/test/test_socketcand.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python + +import unittest +import can +from can.interfaces.socketcand import socketcand + + +class TestConvertAsciiMessageToCanMessage(unittest.TestCase): + def test_valid_frame_message(self): + # Example: < frame 123 1680000000.0 01020304 > + ascii_msg = "< frame 123 1680000000.0 01020304 >" + msg = socketcand.convert_ascii_message_to_can_message(ascii_msg) + self.assertIsInstance(msg, can.Message) + self.assertEqual(msg.arbitration_id, 0x123) + self.assertEqual(msg.timestamp, 1680000000.0) + self.assertEqual(msg.data, bytearray([1, 2, 3, 4])) + self.assertEqual(msg.dlc, 4) + self.assertFalse(msg.is_extended_id) + self.assertTrue(msg.is_rx) + + def test_valid_error_message(self): + # Example: < error 1ABCDEF0 1680000001.0 > + ascii_msg = "< error 1ABCDEF0 1680000001.0 >" + msg = socketcand.convert_ascii_message_to_can_message(ascii_msg) + self.assertIsInstance(msg, can.Message) + self.assertEqual(msg.arbitration_id, 0x1ABCDEF0) + self.assertEqual(msg.timestamp, 1680000001.0) + self.assertEqual(msg.data, bytearray([0])) + self.assertEqual(msg.dlc, 1) + self.assertTrue(msg.is_extended_id) + self.assertTrue(msg.is_error_frame) + self.assertTrue(msg.is_rx) + + def test_invalid_message(self): + ascii_msg = "< unknown 123 0.0 >" + msg = socketcand.convert_ascii_message_to_can_message(ascii_msg) + self.assertIsNone(msg) + + def test_missing_ending_character(self): + ascii_msg = "< frame 123 1680000000.0 01020304" + msg = socketcand.convert_ascii_message_to_can_message(ascii_msg) + self.assertIsNone(msg) + + +if __name__ == "__main__": + unittest.main() From 958fc64ed504d6c655e92e66c48e0b49ee8f8aca Mon Sep 17 00:00:00 2001 From: Dennis Johnson <23355179+Dennis-Johnson@users.noreply.github.com> Date: Fri, 13 Jun 2025 21:40:41 +0530 Subject: [PATCH 086/127] Fix UDP multicast interface on MacOS (#1940) * Add sock option SO_REUSEPORT to allow udp multicast on macos * macos doesn't support ioctl SIOCGSTAMP * REUSE PORT option not supported on windows * only import ioctl on linux --- can/interfaces/udp_multicast/bus.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/can/interfaces/udp_multicast/bus.py b/can/interfaces/udp_multicast/bus.py index ec94e22b5..45882ec07 100644 --- a/can/interfaces/udp_multicast/bus.py +++ b/can/interfaces/udp_multicast/bus.py @@ -1,5 +1,6 @@ import errno import logging +import platform import select import socket import struct @@ -13,14 +14,9 @@ from .utils import is_msgpack_installed, pack_message, unpack_message -ioctl_supported = True - -try: +is_linux = platform.system() == "Linux" +if is_linux: from fcntl import ioctl -except ModuleNotFoundError: # Missing on Windows - ioctl_supported = False - pass - log = logging.getLogger(__name__) @@ -275,6 +271,10 @@ def _create_socket(self, address_family: socket.AddressFamily) -> socket.socket: # Allow multiple programs to access that address + port sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + # Option not supported on Windows. + if hasattr(socket, "SO_REUSEPORT"): + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + # set how to receive timestamps try: sock.setsockopt(socket.SOL_SOCKET, SO_TIMESTAMPNS, 1) @@ -401,7 +401,8 @@ def recv( self.max_buffer ) - if ioctl_supported: + if is_linux: + # This ioctl isn't supported on Darwin & Windows. result_buffer = ioctl( self._socket.fileno(), SIOCGSTAMP, From 82d52fcd5b231fa05f5164beb26108d0db8a1bda Mon Sep 17 00:00:00 2001 From: Denis Jullien Date: Mon, 21 Jul 2025 10:55:32 +0200 Subject: [PATCH 087/127] Add TRCReader RTR frames support (#1953) * New trc test files with RTR frame * trc reader support for RTR frames parsing * black format --- can/io/trc.py | 20 ++++++++++---- test/data/test_CanMessage_V1_0_BUS1.trc | 7 ++--- test/data/test_CanMessage_V1_1.trc | 3 ++- test/data/test_CanMessage_V1_3.trc | 35 ++++++++++++++----------- test/data/test_CanMessage_V2_0_BUS1.trc | 11 ++++---- test/data/test_CanMessage_V2_1.trc | 11 ++++---- test/logformats_test.py | 16 +++++++++++ 7 files changed, 68 insertions(+), 35 deletions(-) diff --git a/can/io/trc.py b/can/io/trc.py index a07a53a4d..e1eaa077c 100644 --- a/can/io/trc.py +++ b/can/io/trc.py @@ -153,7 +153,10 @@ def _parse_msg_v1_0(self, cols: tuple[str, ...]) -> Optional[Message]: msg.is_extended_id = len(arbit_id) > 4 msg.channel = 1 msg.dlc = int(cols[3]) - msg.data = bytearray([int(cols[i + 4], 16) for i in range(msg.dlc)]) + if len(cols) > 4 and cols[4] == "RTR": + msg.is_remote_frame = True + else: + msg.data = bytearray([int(cols[i + 4], 16) for i in range(msg.dlc)]) return msg def _parse_msg_v1_1(self, cols: tuple[str, ...]) -> Optional[Message]: @@ -165,7 +168,10 @@ def _parse_msg_v1_1(self, cols: tuple[str, ...]) -> Optional[Message]: msg.is_extended_id = len(arbit_id) > 4 msg.channel = 1 msg.dlc = int(cols[4]) - msg.data = bytearray([int(cols[i + 5], 16) for i in range(msg.dlc)]) + if len(cols) > 5 and cols[5] == "RTR": + msg.is_remote_frame = True + else: + msg.data = bytearray([int(cols[i + 5], 16) for i in range(msg.dlc)]) msg.is_rx = cols[2] == "Rx" return msg @@ -178,7 +184,10 @@ def _parse_msg_v1_3(self, cols: tuple[str, ...]) -> Optional[Message]: msg.is_extended_id = len(arbit_id) > 4 msg.channel = int(cols[2]) msg.dlc = int(cols[6]) - msg.data = bytearray([int(cols[i + 7], 16) for i in range(msg.dlc)]) + if len(cols) > 7 and cols[7] == "RTR": + msg.is_remote_frame = True + else: + msg.data = bytearray([int(cols[i + 7], 16) for i in range(msg.dlc)]) msg.is_rx = cols[3] == "Rx" return msg @@ -200,7 +209,8 @@ def _parse_msg_v2_x(self, cols: tuple[str, ...]) -> Optional[Message]: msg.is_extended_id = len(cols[self.columns["I"]]) > 4 msg.channel = int(cols[bus]) if bus is not None else 1 msg.dlc = dlc - if dlc: + msg.is_remote_frame = type_ in {"RR"} + if dlc and not msg.is_remote_frame: msg.data = bytearray.fromhex(cols[self.columns["D"]]) msg.is_rx = cols[self.columns["d"]] == "Rx" msg.is_fd = type_ in {"FD", "FB", "FE", "BI"} @@ -227,7 +237,7 @@ def _parse_cols_v1_3(self, cols: tuple[str, ...]) -> Optional[Message]: def _parse_cols_v2_x(self, cols: tuple[str, ...]) -> Optional[Message]: dtype = cols[self.columns["T"]] - if dtype in {"DT", "FD", "FB", "FE", "BI"}: + if dtype in {"DT", "FD", "FB", "FE", "BI", "RR"}: return self._parse_msg_v2_x(cols) else: logger.info("TRCReader: Unsupported type '%s'", dtype) diff --git a/test/data/test_CanMessage_V1_0_BUS1.trc b/test/data/test_CanMessage_V1_0_BUS1.trc index 8985db188..c3905ae47 100644 --- a/test/data/test_CanMessage_V1_0_BUS1.trc +++ b/test/data/test_CanMessage_V1_0_BUS1.trc @@ -1,10 +1,10 @@ ;########################################################################## -; C:\Users\User\Desktop\python-can\test\data\test_CanMessage_V1_0_BUS1.trc +; C:\NewFileName_BUS1.trc ; -; CAN activities imported from C:\Users\User\Desktop\python-can\test\data\test_CanMessage_V1_1.trc +; CAN activities imported from C:\test_CanMessage_V1_1.trc ; Start time: 18.12.2021 14:28:07.062 ; PCAN-Net: N/A -; Generated by PEAK-Converter Version 2.2.4.136 +; Generated by PEAK-Converter Version 3.0.4.594 ; ; Columns description: ; ~~~~~~~~~~~~~~~~~~~~~ @@ -26,3 +26,4 @@ 9) 20798 00000100 8 00 00 00 00 00 00 00 00 10) 20956 00000100 8 00 00 00 00 00 00 00 00 11) 21097 00000100 8 00 00 00 00 00 00 00 00 + 12) 48937 0704 1 RTR \ No newline at end of file diff --git a/test/data/test_CanMessage_V1_1.trc b/test/data/test_CanMessage_V1_1.trc index 5a02cd59b..9a9cc7574 100644 --- a/test/data/test_CanMessage_V1_1.trc +++ b/test/data/test_CanMessage_V1_1.trc @@ -22,4 +22,5 @@ 8) 20592.7 Tx 00000100 8 00 00 00 00 00 00 00 00 9) 20798.6 Tx 00000100 8 00 00 00 00 00 00 00 00 10) 20956.0 Tx 00000100 8 00 00 00 00 00 00 00 00 - 11) 21097.1 Tx 00000100 8 00 00 00 00 00 00 00 00 + 11) 21097.1 Tx 00000100 8 00 00 00 00 00 00 00 00 + 12) 48937.6 Rx 0704 1 RTR diff --git a/test/data/test_CanMessage_V1_3.trc b/test/data/test_CanMessage_V1_3.trc index 5b0bf060a..96db1748c 100644 --- a/test/data/test_CanMessage_V1_3.trc +++ b/test/data/test_CanMessage_V1_3.trc @@ -1,13 +1,14 @@ ;$FILEVERSION=1.3 ;$STARTTIME=44548.6028595139 +; C:\NewFileName_V1_3.trc ; -; C:\test.trc -; Start time: 18.12.2021 14:28:07.062.0 -; Generated by PCAN-Explorer v5.4.0 +; Start time: 18.12.2021 14:28:07.062.1 +; +; Generated by PEAK-Converter Version 3.0.4.594 +; Data imported from C:\test_CanMessage_V1_1.trc ;------------------------------------------------------------------------------- -; Bus Name Connection Protocol Bit rate -; 1 PCAN Untitled@pcan_usb CAN 500 kbit/s -; 2 PTCAN PCANLight_USB_16@pcan_usb CAN +; Bus Name Connection Protocol +; N/A N/A N/A CAN ;------------------------------------------------------------------------------- ; Message Number ; | Time Offset (ms) @@ -20,13 +21,15 @@ ; | | | | | | | | ; | | | | | | | | ;---+-- ------+------ +- --+-- ----+--- +- -+-- -+ -- -- -- -- -- -- -- - 1) 17535.4 1 Tx 00000100 - 8 00 00 00 00 00 00 00 00 - 2) 17700.3 1 Tx 00000100 - 8 00 00 00 00 00 00 00 00 - 3) 17873.8 1 Tx 00000100 - 8 00 00 00 00 00 00 00 00 - 4) 19295.4 1 Tx 0000 - 8 00 00 00 00 00 00 00 00 - 5) 19500.6 1 Tx 0000 - 8 00 00 00 00 00 00 00 00 - 6) 19705.2 1 Tx 0000 - 8 00 00 00 00 00 00 00 00 - 7) 20592.7 1 Tx 00000100 - 8 00 00 00 00 00 00 00 00 - 8) 20798.6 1 Tx 00000100 - 8 00 00 00 00 00 00 00 00 - 9) 20956.0 1 Tx 00000100 - 8 00 00 00 00 00 00 00 00 - 10) 21097.1 1 Tx 00000100 - 8 00 00 00 00 00 00 00 00 + 1) 17535.400 1 Tx 00000100 - 8 00 00 00 00 00 00 00 00 + 2) 17540.300 1 Warng FFFFFFFF - 4 00 00 00 08 BUSHEAVY + 3) 17700.300 1 Tx 00000100 - 8 00 00 00 00 00 00 00 00 + 4) 17873.800 1 Tx 00000100 - 8 00 00 00 00 00 00 00 00 + 5) 19295.400 1 Tx 0000 - 8 00 00 00 00 00 00 00 00 + 6) 19500.600 1 Tx 0000 - 8 00 00 00 00 00 00 00 00 + 7) 19705.200 1 Tx 0000 - 8 00 00 00 00 00 00 00 00 + 8) 20592.700 1 Tx 00000100 - 8 00 00 00 00 00 00 00 00 + 9) 20798.600 1 Tx 00000100 - 8 00 00 00 00 00 00 00 00 + 10) 20956.000 1 Tx 00000100 - 8 00 00 00 00 00 00 00 00 + 11) 21097.100 1 Tx 00000100 - 8 00 00 00 00 00 00 00 00 + 12) 48937.600 1 Rx 0704 - 1 RTR diff --git a/test/data/test_CanMessage_V2_0_BUS1.trc b/test/data/test_CanMessage_V2_0_BUS1.trc index cf2384df0..c1af8abc1 100644 --- a/test/data/test_CanMessage_V2_0_BUS1.trc +++ b/test/data/test_CanMessage_V2_0_BUS1.trc @@ -2,18 +2,18 @@ ;$STARTTIME=44548.6028595139 ;$COLUMNS=N,O,T,I,d,l,D ; -; C:\Users\User\Desktop\python-can\test\data\test_CanMessage_V2_0_BUS1.trc +; C:\test_CanMessage_V2_0_BUS1.trc ; Start time: 18.12.2021 14:28:07.062.001 -; Generated by PEAK-Converter Version 2.2.4.136 -; Data imported from C:\Users\User\Desktop\python-can\test\data\test_CanMessage_V1_1.trc +; Generated by PEAK-Converter Version 3.0.4.594 +; Data imported from C:\test_CanMessage_V1_1.trc ;------------------------------------------------------------------------------- ; Connection Bit rate -; N/A N/A +; N/A N/A ;------------------------------------------------------------------------------- ; Message Time Type ID Rx/Tx ; Number Offset | [hex] | Data Length ; | [ms] | | | | Data [hex] ... -; | | | | | | | +; | | | | | | | ;---+-- ------+------ +- --+----- +- +- +- -- -- -- -- -- -- -- 1 17535.400 DT 00000100 Tx 8 00 00 00 00 00 00 00 00 2 17540.300 ST Rx 00 00 00 08 @@ -26,3 +26,4 @@ 9 20798.600 DT 00000100 Tx 8 00 00 00 00 00 00 00 00 10 20956.000 DT 00000100 Tx 8 00 00 00 00 00 00 00 00 11 21097.100 DT 00000100 Tx 8 00 00 00 00 00 00 00 00 + 12 48937.600 RR 0704 Rx 1 \ No newline at end of file diff --git a/test/data/test_CanMessage_V2_1.trc b/test/data/test_CanMessage_V2_1.trc index 55ceefaf1..0d259f084 100644 --- a/test/data/test_CanMessage_V2_1.trc +++ b/test/data/test_CanMessage_V2_1.trc @@ -2,19 +2,19 @@ ;$STARTTIME=44548.6028595139 ;$COLUMNS=N,O,T,B,I,d,R,L,D ; -; C:\Users\User\Desktop\python-can\test\data\test_CanMessage_V2_1.trc +; C:\test_CanMessage_V2_1.trc ; Start time: 18.12.2021 14:28:07.062.001 -; Generated by PEAK-Converter Version 2.2.4.136 -; Data imported from C:\Users\User\Desktop\python-can\test\data\test_CanMessage_V1_1.trc +; Generated by PEAK-Converter Version 3.0.4.594 +; Data imported from C:\test_CanMessage_V1_1.trc ;------------------------------------------------------------------------------- ; Bus Name Connection Protocol -; N/A N/A N/A N/A +; N/A N/A N/A N/A ;------------------------------------------------------------------------------- ; Message Time Type ID Rx/Tx ; Number Offset | Bus [hex] | Reserved ; | [ms] | | | | | Data Length Code ; | | | | | | | | Data [hex] ... -; | | | | | | | | | +; | | | | | | | | | ;---+-- ------+------ +- +- --+----- +- +- +--- +- -- -- -- -- -- -- -- 1 17535.400 DT 1 00000100 Tx - 8 00 00 00 00 00 00 00 00 2 17540.300 ST 1 - Rx - 4 00 00 00 08 @@ -27,3 +27,4 @@ 9 20798.600 DT 1 00000100 Tx - 8 00 00 00 00 00 00 00 00 10 20956.000 DT 1 00000100 Tx - 8 00 00 00 00 00 00 00 00 11 21097.100 DT 1 00000100 Tx - 8 00 00 00 00 00 00 00 00 + 12 48937.600 RR 1 0704 Rx - 1 \ No newline at end of file diff --git a/test/logformats_test.py b/test/logformats_test.py index f3fe485b2..e9db47af3 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -990,6 +990,7 @@ def test_can_message(self): ("V1_0", "test_CanMessage_V1_0_BUS1.trc", False), ("V1_1", "test_CanMessage_V1_1.trc", True), ("V1_3", "test_CanMessage_V1_3.trc", True), + ("V2_0", "test_CanMessage_V2_0_BUS1.trc", True), ("V2_1", "test_CanMessage_V2_1.trc", True), ] ) @@ -1029,6 +1030,20 @@ def msg_ext(timestamp): msg.is_rx = False return msg + def msg_rtr(timestamp): + msg = can.Message( + timestamp=timestamp + start_time, + arbitration_id=0x704, + is_extended_id=False, + is_remote_frame=True, + channel=1, + dlc=1, + data=[], + ) + if is_rx_support: + msg.is_rx = True + return msg + expected_messages = [ msg_ext(17.5354), msg_ext(17.7003), @@ -1040,6 +1055,7 @@ def msg_ext(timestamp): msg_ext(20.7986), msg_ext(20.9560), msg_ext(21.0971), + msg_rtr(48.9376), ] actual = self._read_log_file(filename) self.assertMessagesEqual(actual, expected_messages) From 141223a47aad9d7663609a4713be0f47d217100c Mon Sep 17 00:00:00 2001 From: Arclight <59406481+chinaheyu@users.noreply.github.com> Date: Mon, 21 Jul 2025 17:06:47 +0800 Subject: [PATCH 088/127] Add an alternative plugin interface for gs_usb (#1954) --- doc/plugin-interface.rst | 3 +++ pyproject.toml | 1 + 2 files changed, 4 insertions(+) diff --git a/doc/plugin-interface.rst b/doc/plugin-interface.rst index 8e60c50c2..d841281e8 100644 --- a/doc/plugin-interface.rst +++ b/doc/plugin-interface.rst @@ -77,6 +77,8 @@ The table below lists interface drivers that can be added by installing addition +----------------------------+-------------------------------------------------------+ | `python-can-cando`_ | Python wrapper for Netronics' CANdo and CANdoISO | +----------------------------+-------------------------------------------------------+ +| `python-can-candle`_ | A full-featured driver for candleLight | ++----------------------------+-------------------------------------------------------+ .. _python-can-canine: https://github.com/tinymovr/python-can-canine .. _python-can-cvector: https://github.com/zariiii9003/python-can-cvector @@ -84,4 +86,5 @@ The table below lists interface drivers that can be added by installing addition .. _python-can-sontheim: https://github.com/MattWoodhead/python-can-sontheim .. _zlgcan: https://github.com/jesses2025smith/zlgcan-driver .. _python-can-cando: https://github.com/belliriccardo/python-can-cando +.. _python-can-candle: https://github.com/BIRLab/python-can-candle diff --git a/pyproject.toml b/pyproject.toml index ee98fec24..a6a7f38c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,6 +71,7 @@ remote = ["python-can-remote"] sontheim = ["python-can-sontheim>=0.1.2"] canine = ["python-can-canine>=0.2.2"] zlgcan = ["zlgcan"] +candle = ["python-can-candle>=1.2.2"] viewer = [ "windows-curses; platform_system == 'Windows' and platform_python_implementation=='CPython'" ] From c4808b744fac75b4d1a7a1658def9f7601413ada Mon Sep 17 00:00:00 2001 From: fl0gee <132301678+fl0gee@users.noreply.github.com> Date: Mon, 21 Jul 2025 11:10:56 +0200 Subject: [PATCH 089/127] Support "no init access" feature of Kvaser interfaces (#1955) * Add keyword argument and set flag accordingly * Add test case * Trim trailing whitespace * Run black formatting --- can/interfaces/kvaser/canlib.py | 6 +++++- test/test_kvaser.py | 13 +++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index 9731a4415..a1dd03e58 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -439,7 +439,8 @@ def __init__( :param int data_bitrate: Which bitrate to use for data phase in CAN FD. Defaults to arbitration bitrate. - + :param bool no_init_access: + Don't open the handle with init access. """ log.info(f"CAN Filters: {can_filters}") @@ -455,6 +456,7 @@ def __init__( exclusive = kwargs.get("exclusive", False) override_exclusive = kwargs.get("override_exclusive", False) accept_virtual = kwargs.get("accept_virtual", True) + no_init_access = kwargs.get("no_init_access", False) fd = isinstance(timing, BitTimingFd) if timing else kwargs.get("fd", False) data_bitrate = kwargs.get("data_bitrate", None) fd_non_iso = kwargs.get("fd_non_iso", False) @@ -491,6 +493,8 @@ def __init__( flags |= canstat.canOPEN_OVERRIDE_EXCLUSIVE if accept_virtual: flags |= canstat.canOPEN_ACCEPT_VIRTUAL + if no_init_access: + flags |= canstat.canOPEN_NO_INIT_ACCESS if fd: if fd_non_iso: flags |= canstat.canOPEN_CAN_FD_NONISO diff --git a/test/test_kvaser.py b/test/test_kvaser.py index c18b3bc15..1ad035dec 100644 --- a/test/test_kvaser.py +++ b/test/test_kvaser.py @@ -277,6 +277,19 @@ def test_bus_get_stats(self): self.assertTrue(canlib.canGetBusStatistics.called) self.assertIsInstance(stats, canlib.structures.BusStatistics) + def test_bus_no_init_access(self): + canlib.canOpenChannel.reset_mock() + bus = can.Bus(interface="kvaser", channel=0, no_init_access=True) + + self.assertGreater(canlib.canOpenChannel.call_count, 0) + for call in canlib.canOpenChannel.call_args_list: + self.assertEqual( + call[0][1] & constants.canOPEN_NO_INIT_ACCESS, + constants.canOPEN_NO_INIT_ACCESS, + ) + + bus.shutdown() + @staticmethod def canGetNumberOfChannels(count): count._obj.value = 2 From 85b1cb2dd19f781d8ff2aa224b814dddbda6e037 Mon Sep 17 00:00:00 2001 From: ssj71 Date: Tue, 22 Jul 2025 04:51:31 -0700 Subject: [PATCH 090/127] Add FD support to slcan according to CANable 2.0 impementation (#1920) * add FD support to slcan according to CANable 2.0 impementation * allow 0 data bitrate to allow non-FD settings * make interface more consistent with other HW * proper DLC handling for FD frames in slcan * adding tests for slcan FD support * black formatting * adding optional keyword to optional arg --------- Co-authored-by: spencer --- can/interfaces/slcan.py | 77 ++++++++++-- test/test_slcan.py | 258 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 328 insertions(+), 7 deletions(-) diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index c51b298cc..01ba9c995 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -16,7 +16,12 @@ CanOperationError, error_check, ) -from can.util import check_or_adjust_timing_clock, deprecated_args_alias +from can.util import ( + CAN_FD_DLC, + check_or_adjust_timing_clock, + deprecated_args_alias, + len2dlc, +) logger = logging.getLogger(__name__) @@ -48,6 +53,11 @@ class slcanBus(BusABC): 1000000: "S8", 83300: "S9", } + _DATA_BITRATES = { + 0: "", + 2000000: "Y2", + 5000000: "Y5", + } _SLEEP_AFTER_SERIAL_OPEN = 2 # in seconds @@ -86,7 +96,8 @@ def __init__( If this argument is set then it overrides the bitrate and btr arguments. The `f_clock` value of the timing instance must be set to 8_000_000 (8MHz) for standard CAN. - CAN FD and the :class:`~can.BitTimingFd` class are not supported. + CAN FD and the :class:`~can.BitTimingFd` class have partial support according to the non-standard + slcan protocol implementation in the CANABLE 2.0 firmware: currently only data rates of 2M and 5M. :param poll_interval: Poll interval in seconds when reading messages :param sleep_after_open: @@ -143,9 +154,7 @@ def __init__( timing = check_or_adjust_timing_clock(timing, valid_clocks=[8_000_000]) self.set_bitrate_reg(f"{timing.btr0:02X}{timing.btr1:02X}") elif isinstance(timing, BitTimingFd): - raise NotImplementedError( - f"CAN FD is not supported by {self.__class__.__name__}." - ) + self.set_bitrate(timing.nom_bitrate, timing.data_bitrate) else: if bitrate is not None and btr is not None: raise ValueError("Bitrate and btr mutually exclusive.") @@ -157,10 +166,12 @@ def __init__( super().__init__(channel, **kwargs) - def set_bitrate(self, bitrate: int) -> None: + def set_bitrate(self, bitrate: int, data_bitrate: Optional[int] = None) -> None: """ :param bitrate: Bitrate in bit/s + :param data_bitrate: + Data Bitrate in bit/s for FD frames :raise ValueError: if ``bitrate`` is not among the possible values """ @@ -169,9 +180,15 @@ def set_bitrate(self, bitrate: int) -> None: else: bitrates = ", ".join(str(k) for k in self._BITRATES.keys()) raise ValueError(f"Invalid bitrate, choose one of {bitrates}.") + if data_bitrate in self._DATA_BITRATES: + dbitrate_code = self._DATA_BITRATES[data_bitrate] + else: + dbitrates = ", ".join(str(k) for k in self._DATA_BITRATES.keys()) + raise ValueError(f"Invalid data bitrate, choose one of {dbitrates}.") self.close() self._write(bitrate_code) + self._write(dbitrate_code) self.open() def set_bitrate_reg(self, btr: str) -> None: @@ -235,6 +252,8 @@ def _recv_internal( remote = False extended = False data = None + isFd = False + fdBrs = False if self._queue.qsize(): string: Optional[str] = self._queue.get_nowait() @@ -268,6 +287,34 @@ def _recv_internal( dlc = int(string[9]) extended = True remote = True + elif string[0] == "d": + # FD standard frame + canId = int(string[1:4], 16) + dlc = int(string[4], 16) + isFd = True + data = bytearray.fromhex(string[5 : 5 + CAN_FD_DLC[dlc] * 2]) + elif string[0] == "D": + # FD extended frame + canId = int(string[1:9], 16) + dlc = int(string[9], 16) + extended = True + isFd = True + data = bytearray.fromhex(string[10 : 10 + CAN_FD_DLC[dlc] * 2]) + elif string[0] == "b": + # FD with bitrate switch + canId = int(string[1:4], 16) + dlc = int(string[4], 16) + isFd = True + fdBrs = True + data = bytearray.fromhex(string[5 : 5 + CAN_FD_DLC[dlc] * 2]) + elif string[0] == "B": + # FD extended with bitrate switch + canId = int(string[1:9], 16) + dlc = int(string[9], 16) + extended = True + isFd = True + fdBrs = True + data = bytearray.fromhex(string[10 : 10 + CAN_FD_DLC[dlc] * 2]) if canId is not None: msg = Message( @@ -275,7 +322,9 @@ def _recv_internal( is_extended_id=extended, timestamp=time.time(), # Better than nothing... is_remote_frame=remote, - dlc=dlc, + is_fd=isFd, + bitrate_switch=fdBrs, + dlc=CAN_FD_DLC[dlc], data=data, ) return msg, False @@ -289,6 +338,20 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: sendStr = f"R{msg.arbitration_id:08X}{msg.dlc:d}" else: sendStr = f"r{msg.arbitration_id:03X}{msg.dlc:d}" + elif msg.is_fd: + fd_dlc = len2dlc(msg.dlc) + if msg.bitrate_switch: + if msg.is_extended_id: + sendStr = f"B{msg.arbitration_id:08X}{fd_dlc:X}" + else: + sendStr = f"b{msg.arbitration_id:03X}{fd_dlc:X}" + sendStr += msg.data.hex().upper() + else: + if msg.is_extended_id: + sendStr = f"D{msg.arbitration_id:08X}{fd_dlc:X}" + else: + sendStr = f"d{msg.arbitration_id:03X}{fd_dlc:X}" + sendStr += msg.data.hex().upper() else: if msg.is_extended_id: sendStr = f"T{msg.arbitration_id:08X}{msg.dlc:d}" diff --git a/test/test_slcan.py b/test/test_slcan.py index 220a6d7e0..491800e24 100644 --- a/test/test_slcan.py +++ b/test/test_slcan.py @@ -178,6 +178,264 @@ def test_send_extended_remote(self): rx_msg = self.bus.recv(TIMEOUT) self.assertTrue(msg.equals(rx_msg, timestamp_delta=None)) + def test_recv_fd(self): + self.serial.set_input_buffer(b"d123A303132333435363738393a3b3c3d3e3f\r") + msg = self.bus.recv(TIMEOUT) + self.assertIsNotNone(msg) + self.assertEqual(msg.arbitration_id, 0x123) + self.assertEqual(msg.is_extended_id, False) + self.assertEqual(msg.is_remote_frame, False) + self.assertEqual(msg.is_fd, True) + self.assertEqual(msg.bitrate_switch, False) + self.assertEqual(msg.dlc, 16) + self.assertSequenceEqual( + msg.data, + [ + 0x30, + 0x31, + 0x32, + 0x33, + 0x34, + 0x35, + 0x36, + 0x37, + 0x38, + 0x39, + 0x3A, + 0x3B, + 0x3C, + 0x3D, + 0x3E, + 0x3F, + ], + ) + + def test_send_fd(self): + payload = b"d123A303132333435363738393A3B3C3D3E3F\r" + msg = can.Message( + arbitration_id=0x123, + is_extended_id=False, + is_fd=True, + data=[ + 0x30, + 0x31, + 0x32, + 0x33, + 0x34, + 0x35, + 0x36, + 0x37, + 0x38, + 0x39, + 0x3A, + 0x3B, + 0x3C, + 0x3D, + 0x3E, + 0x3F, + ], + ) + self.bus.send(msg) + self.assertEqual(payload, self.serial.get_output_buffer()) + + self.serial.set_input_buffer(payload) + rx_msg = self.bus.recv(TIMEOUT) + self.assertTrue(msg.equals(rx_msg, timestamp_delta=None)) + + def test_recv_fd_extended(self): + self.serial.set_input_buffer(b"D12ABCDEFA303132333435363738393A3B3C3D3E3F\r") + msg = self.bus.recv(TIMEOUT) + self.assertIsNotNone(msg) + self.assertEqual(msg.arbitration_id, 0x12ABCDEF) + self.assertEqual(msg.is_extended_id, True) + self.assertEqual(msg.is_remote_frame, False) + self.assertEqual(msg.dlc, 16) + self.assertEqual(msg.bitrate_switch, False) + self.assertTrue(msg.is_fd) + self.assertSequenceEqual( + msg.data, + [ + 0x30, + 0x31, + 0x32, + 0x33, + 0x34, + 0x35, + 0x36, + 0x37, + 0x38, + 0x39, + 0x3A, + 0x3B, + 0x3C, + 0x3D, + 0x3E, + 0x3F, + ], + ) + + def test_send_fd_extended(self): + payload = b"D12ABCDEFA303132333435363738393A3B3C3D3E3F\r" + msg = can.Message( + arbitration_id=0x12ABCDEF, + is_extended_id=True, + is_fd=True, + data=[ + 0x30, + 0x31, + 0x32, + 0x33, + 0x34, + 0x35, + 0x36, + 0x37, + 0x38, + 0x39, + 0x3A, + 0x3B, + 0x3C, + 0x3D, + 0x3E, + 0x3F, + ], + ) + self.bus.send(msg) + self.assertEqual(payload, self.serial.get_output_buffer()) + + self.serial.set_input_buffer(payload) + rx_msg = self.bus.recv(TIMEOUT) + self.assertTrue(msg.equals(rx_msg, timestamp_delta=None)) + + def test_recv_fd_brs(self): + self.serial.set_input_buffer(b"b123A303132333435363738393a3b3c3d3e3f\r") + msg = self.bus.recv(TIMEOUT) + self.assertIsNotNone(msg) + self.assertEqual(msg.arbitration_id, 0x123) + self.assertEqual(msg.is_extended_id, False) + self.assertEqual(msg.is_remote_frame, False) + self.assertEqual(msg.is_fd, True) + self.assertEqual(msg.bitrate_switch, True) + self.assertEqual(msg.dlc, 16) + self.assertSequenceEqual( + msg.data, + [ + 0x30, + 0x31, + 0x32, + 0x33, + 0x34, + 0x35, + 0x36, + 0x37, + 0x38, + 0x39, + 0x3A, + 0x3B, + 0x3C, + 0x3D, + 0x3E, + 0x3F, + ], + ) + + def test_send_fd_brs(self): + payload = b"b123A303132333435363738393A3B3C3D3E3F\r" + msg = can.Message( + arbitration_id=0x123, + is_extended_id=False, + is_fd=True, + bitrate_switch=True, + data=[ + 0x30, + 0x31, + 0x32, + 0x33, + 0x34, + 0x35, + 0x36, + 0x37, + 0x38, + 0x39, + 0x3A, + 0x3B, + 0x3C, + 0x3D, + 0x3E, + 0x3F, + ], + ) + self.bus.send(msg) + self.assertEqual(payload, self.serial.get_output_buffer()) + + self.serial.set_input_buffer(payload) + rx_msg = self.bus.recv(TIMEOUT) + self.assertTrue(msg.equals(rx_msg, timestamp_delta=None)) + + def test_recv_fd_brs_extended(self): + self.serial.set_input_buffer(b"B12ABCDEFA303132333435363738393A3B3C3D3E3F\r") + msg = self.bus.recv(TIMEOUT) + self.assertIsNotNone(msg) + self.assertEqual(msg.arbitration_id, 0x12ABCDEF) + self.assertEqual(msg.is_extended_id, True) + self.assertEqual(msg.is_remote_frame, False) + self.assertEqual(msg.dlc, 16) + self.assertEqual(msg.bitrate_switch, True) + self.assertTrue(msg.is_fd) + self.assertSequenceEqual( + msg.data, + [ + 0x30, + 0x31, + 0x32, + 0x33, + 0x34, + 0x35, + 0x36, + 0x37, + 0x38, + 0x39, + 0x3A, + 0x3B, + 0x3C, + 0x3D, + 0x3E, + 0x3F, + ], + ) + + def test_send_fd_brs_extended(self): + payload = b"B12ABCDEFA303132333435363738393A3B3C3D3E3F\r" + msg = can.Message( + arbitration_id=0x12ABCDEF, + is_extended_id=True, + is_fd=True, + bitrate_switch=True, + data=[ + 0x30, + 0x31, + 0x32, + 0x33, + 0x34, + 0x35, + 0x36, + 0x37, + 0x38, + 0x39, + 0x3A, + 0x3B, + 0x3C, + 0x3D, + 0x3E, + 0x3F, + ], + ) + self.bus.send(msg) + self.assertEqual(payload, self.serial.get_output_buffer()) + + self.serial.set_input_buffer(payload) + rx_msg = self.bus.recv(TIMEOUT) + self.assertTrue(msg.equals(rx_msg, timestamp_delta=None)) + def test_partial_recv(self): self.serial.set_input_buffer(b"T12ABCDEF") msg = self.bus.recv(TIMEOUT) From 32f07c3117cf9d3c3a20d7a91c271523144a0ad1 Mon Sep 17 00:00:00 2001 From: Greg Brooks Date: Tue, 22 Jul 2025 13:00:13 +0100 Subject: [PATCH 091/127] Convert BusState to enum when read with configparser (#1957) * Convert BusState to enum when read with configparser (#1956) * Move BusState conversion to util.py (#1956) * Check state argument type before attempting conversion (#1956) * Add tests (#1956) * Fix formatting (#1956) * Compare enums by identity (#1956) --------- Co-authored-by: Greg Brooks --- can/util.py | 6 ++++++ doc/interfaces/pcan.rst | 2 +- test/test_util.py | 23 +++++++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/can/util.py b/can/util.py index 2f32dda8e..895c721ae 100644 --- a/can/util.py +++ b/can/util.py @@ -249,6 +249,12 @@ def _create_bus_config(config: dict[str, Any]) -> typechecking.BusConfig: if "fd" in config: config["fd"] = config["fd"] not in (0, False) + if "state" in config and not isinstance(config["state"], can.BusState): + try: + config["state"] = can.BusState[config["state"]] + except KeyError as e: + raise ValueError("State config not valid!") from e + return cast("typechecking.BusConfig", config) diff --git a/doc/interfaces/pcan.rst b/doc/interfaces/pcan.rst index 2f73dd3a7..48e7dba05 100644 --- a/doc/interfaces/pcan.rst +++ b/doc/interfaces/pcan.rst @@ -15,7 +15,7 @@ Here is an example configuration file for using `PCAN-USB None: From 7bc904e66a84e30e8fd669731c452fd8174118e3 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Wed, 23 Jul 2025 10:42:28 +0200 Subject: [PATCH 092/127] Improve can.io type annotations (#1951) --- .github/workflows/ci.yml | 2 +- can/_entry_points.py | 2 +- can/io/asc.py | 4 - can/io/blf.py | 46 +++---- can/io/canutils.py | 11 +- can/io/csv.py | 7 +- can/io/generic.py | 273 +++++++++++++++++++++++++++++---------- can/io/logger.py | 59 +++++---- can/io/player.py | 29 ++--- can/io/printer.py | 27 ++-- can/io/sqlite.py | 49 +++---- can/io/trc.py | 22 ++-- can/listener.py | 4 +- can/typechecking.py | 30 ++--- can/util.py | 4 +- doc/conf.py | 6 + doc/file_io.rst | 2 +- doc/internal-api.rst | 10 +- doc/notifier.rst | 3 +- test/logformats_test.py | 21 +-- 20 files changed, 373 insertions(+), 238 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 588d9a96b..ec5c1bbac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,7 +76,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.13" - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/can/_entry_points.py b/can/_entry_points.py index e8ce92d7c..6320b797b 100644 --- a/can/_entry_points.py +++ b/can/_entry_points.py @@ -30,5 +30,5 @@ def read_entry_points(group: str) -> list[_EntryPoint]: def read_entry_points(group: str) -> list[_EntryPoint]: return [ _EntryPoint(ep.name, *ep.value.split(":", maxsplit=1)) - for ep in entry_points().get(group, []) + for ep in entry_points().get(group, []) # pylint: disable=no-member ] diff --git a/can/io/asc.py b/can/io/asc.py index 0bea823fd..e917953ff 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -39,8 +39,6 @@ class ASCReader(TextIOMessageReader): bus statistics, J1939 Transport Protocol messages) is ignored. """ - file: TextIO - def __init__( self, file: Union[StringPathLike, TextIO], @@ -322,8 +320,6 @@ class ASCWriter(TextIOMessageWriter): It the first message does not have a timestamp, it is set to zero. """ - file: TextIO - FORMAT_MESSAGE = "{channel} {id:<15} {dir:<4} {dtype} {data}" FORMAT_MESSAGE_FD = " ".join( [ diff --git a/can/io/blf.py b/can/io/blf.py index 6a1231fcc..2c9050d54 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -17,14 +17,14 @@ import struct import time import zlib -from collections.abc import Generator +from collections.abc import Generator, Iterator from decimal import Decimal from typing import Any, BinaryIO, Optional, Union, cast from ..message import Message from ..typechecking import StringPathLike from ..util import channel2int, dlc2len, len2dlc -from .generic import BinaryIOMessageReader, FileIOMessageWriter +from .generic import BinaryIOMessageReader, BinaryIOMessageWriter TSystemTime = tuple[int, int, int, int, int, int, int, int] @@ -104,7 +104,7 @@ class BLFParseError(Exception): TIME_ONE_NANS_FACTOR = Decimal("1e-9") -def timestamp_to_systemtime(timestamp: float) -> TSystemTime: +def timestamp_to_systemtime(timestamp: Optional[float]) -> TSystemTime: if timestamp is None or timestamp < 631152000: # Probably not a Unix timestamp return 0, 0, 0, 0, 0, 0, 0, 0 @@ -146,8 +146,6 @@ class BLFReader(BinaryIOMessageReader): silently ignored. """ - file: BinaryIO - def __init__( self, file: Union[StringPathLike, BinaryIO], @@ -206,7 +204,7 @@ def __iter__(self) -> Generator[Message, None, None]: yield from self._parse_container(data) self.stop() - def _parse_container(self, data): + def _parse_container(self, data: bytes) -> Iterator[Message]: if self._tail: data = b"".join((self._tail, data)) try: @@ -217,7 +215,7 @@ def _parse_container(self, data): # Save the remaining data that could not be processed self._tail = data[self._pos :] - def _parse_data(self, data): + def _parse_data(self, data: bytes) -> Iterator[Message]: """Optimized inner loop by making local copies of global variables and class members and hardcoding some values.""" unpack_obj_header_base = OBJ_HEADER_BASE_STRUCT.unpack_from @@ -375,13 +373,11 @@ def _parse_data(self, data): pos = next_pos -class BLFWriter(FileIOMessageWriter): +class BLFWriter(BinaryIOMessageWriter): """ Logs CAN data to a Binary Logging File compatible with Vector's tools. """ - file: BinaryIO - #: Max log container size of uncompressed data max_container_size = 128 * 1024 @@ -412,14 +408,12 @@ def __init__( Z_DEFAULT_COMPRESSION represents a default compromise between speed and compression (currently equivalent to level 6). """ - mode = "rb+" if append else "wb" try: - super().__init__(file, mode=mode) + super().__init__(file, mode="rb+" if append else "wb") except FileNotFoundError: # Trying to append to a non-existing file, create a new one append = False - mode = "wb" - super().__init__(file, mode=mode) + super().__init__(file, mode="wb") assert self.file is not None self.channel = channel self.compression_level = compression_level @@ -452,7 +446,7 @@ def __init__( # Write a default header which will be updated when stopped self._write_header(FILE_HEADER_SIZE) - def _write_header(self, filesize): + def _write_header(self, filesize: int) -> None: header = [b"LOGG", FILE_HEADER_SIZE, self.application_id, 0, 0, 0, 2, 6, 8, 1] # The meaning of "count of objects read" is unknown header.extend([filesize, self.uncompressed_size, self.object_count, 0]) @@ -462,7 +456,7 @@ def _write_header(self, filesize): # Pad to header size self.file.write(b"\x00" * (FILE_HEADER_SIZE - FILE_HEADER_STRUCT.size)) - def on_message_received(self, msg): + def on_message_received(self, msg: Message) -> None: channel = channel2int(msg.channel) if channel is None: channel = self.channel @@ -514,7 +508,7 @@ def on_message_received(self, msg): data = CAN_MSG_STRUCT.pack(channel, flags, msg.dlc, arb_id, can_data) self._add_object(CAN_MESSAGE, data, msg.timestamp) - def log_event(self, text, timestamp=None): + def log_event(self, text: str, timestamp: Optional[float] = None) -> None: """Add an arbitrary message to the log file as a global marker. :param str text: @@ -525,17 +519,19 @@ def log_event(self, text, timestamp=None): """ try: # Only works on Windows - text = text.encode("mbcs") + encoded = text.encode("mbcs") except LookupError: - text = text.encode("ascii") + encoded = text.encode("ascii") comment = b"Added by python-can" marker = b"python-can" data = GLOBAL_MARKER_STRUCT.pack( - 0, 0xFFFFFF, 0xFF3300, 0, len(text), len(marker), len(comment) + 0, 0xFFFFFF, 0xFF3300, 0, len(encoded), len(marker), len(comment) ) - self._add_object(GLOBAL_MARKER, data + text + marker + comment, timestamp) + self._add_object(GLOBAL_MARKER, data + encoded + marker + comment, timestamp) - def _add_object(self, obj_type, data, timestamp=None): + def _add_object( + self, obj_type: int, data: bytes, timestamp: Optional[float] = None + ) -> None: if timestamp is None: timestamp = self.stop_timestamp or time.time() if self.start_timestamp is None: @@ -564,7 +560,7 @@ def _add_object(self, obj_type, data, timestamp=None): if self._buffer_size >= self.max_container_size: self._flush() - def _flush(self): + def _flush(self) -> None: """Compresses and writes data in the buffer to file.""" if self.file.closed: return @@ -578,7 +574,7 @@ def _flush(self): self._buffer = [tail] self._buffer_size = len(tail) if not self.compression_level: - data = uncompressed_data + data: "Union[bytes, memoryview[int]]" = uncompressed_data # noqa: UP037 method = NO_COMPRESSION else: data = zlib.compress(uncompressed_data, self.compression_level) @@ -601,7 +597,7 @@ def file_size(self) -> int: """Return an estimate of the current file size in bytes.""" return self.file.tell() + self._buffer_size - def stop(self): + def stop(self) -> None: """Stops logging and closes the file.""" self._flush() if self.file.seekable(): diff --git a/can/io/canutils.py b/can/io/canutils.py index e83c21926..78d081637 100644 --- a/can/io/canutils.py +++ b/can/io/canutils.py @@ -6,7 +6,7 @@ import logging from collections.abc import Generator -from typing import Any, TextIO, Union +from typing import Any, Optional, TextIO, Union from can.message import Message @@ -34,8 +34,6 @@ class CanutilsLogReader(TextIOMessageReader): ``(0.0) vcan0 001#8d00100100820100`` """ - file: TextIO - def __init__( self, file: Union[StringPathLike, TextIO], @@ -148,13 +146,12 @@ def __init__( :param bool append: if set to `True` messages are appended to the file, else the file is truncated """ - mode = "a" if append else "w" - super().__init__(file, mode=mode) + super().__init__(file, mode="a" if append else "w") self.channel = channel - self.last_timestamp = None + self.last_timestamp: Optional[float] = None - def on_message_received(self, msg): + def on_message_received(self, msg: Message) -> None: # this is the case for the very first message: if self.last_timestamp is None: self.last_timestamp = msg.timestamp or 0.0 diff --git a/can/io/csv.py b/can/io/csv.py index dcc7996f7..865ef9af0 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -28,8 +28,6 @@ class CSVReader(TextIOMessageReader): Any line separator is accepted. """ - file: TextIO - def __init__( self, file: Union[StringPathLike, TextIO], @@ -89,8 +87,6 @@ class CSVWriter(TextIOMessageWriter): Each line is terminated with a platform specific line separator. """ - file: TextIO - def __init__( self, file: Union[StringPathLike, TextIO], @@ -106,8 +102,7 @@ def __init__( the file is truncated and starts with a newly written header line """ - mode = "a" if append else "w" - super().__init__(file, mode=mode) + super().__init__(file, mode="a" if append else "w") # Write a header row if not append: diff --git a/can/io/generic.py b/can/io/generic.py index 82523c3cd..21fc3e8e8 100644 --- a/can/io/generic.py +++ b/can/io/generic.py @@ -1,129 +1,260 @@ -"""Contains generic base classes for file IO.""" +"""This module provides abstract base classes for CAN message reading and writing operations +to various file formats. + +.. note:: + All classes in this module are abstract and should be subclassed to implement + specific file format handling. +""" -import gzip import locale -from abc import ABCMeta +import os +from abc import ABC, abstractmethod from collections.abc import Iterable from contextlib import AbstractContextManager +from io import BufferedIOBase, TextIOWrapper +from pathlib import Path from types import TracebackType from typing import ( + TYPE_CHECKING, Any, BinaryIO, + Generic, Literal, Optional, TextIO, + TypeVar, Union, - cast, ) from typing_extensions import Self -from .. import typechecking from ..listener import Listener from ..message import Message +from ..typechecking import FileLike, StringPathLike +if TYPE_CHECKING: + from _typeshed import ( + OpenBinaryModeReading, + OpenBinaryModeUpdating, + OpenBinaryModeWriting, + OpenTextModeReading, + OpenTextModeUpdating, + OpenTextModeWriting, + ) -class BaseIOHandler(AbstractContextManager): - """A generic file handler that can be used for reading and writing. - Can be used as a context manager. +#: type parameter used in generic classes :class:`MessageReader` and :class:`MessageWriter` +_IoTypeVar = TypeVar("_IoTypeVar", bound=FileLike) - :attr file: - the file-like object that is kept internally, or `None` if none - was opened - """ - file: Optional[typechecking.FileLike] +class MessageWriter(AbstractContextManager["MessageWriter"], Listener, ABC): + """Abstract base class for all CAN message writers. - def __init__( - self, - file: Optional[typechecking.AcceptedIOType], - mode: str = "rt", - **kwargs: Any, - ) -> None: - """ - :param file: a path-like object to open a file, a file-like object - to be used as a file or `None` to not use a file at all - :param mode: the mode that should be used to open the file, see - :func:`open`, ignored if *file* is `None` - """ - if file is None or (hasattr(file, "read") and hasattr(file, "write")): - # file is None or some file-like object - self.file = cast("Optional[typechecking.FileLike]", file) - else: - encoding: Optional[str] = ( - None - if "b" in mode - else kwargs.get("encoding", locale.getpreferredencoding(False)) - ) - # pylint: disable=consider-using-with - # file is some path-like object - self.file = cast( - "typechecking.FileLike", open(file, mode, encoding=encoding) - ) + This class serves as a foundation for implementing different message writer formats. + It combines context manager capabilities with the message listener interface. - # for multiple inheritance - super().__init__() + :param file: Path-like object or string representing the output file location + :param kwargs: Additional keyword arguments for specific writer implementations + """ + + @abstractmethod + def __init__(self, file: StringPathLike, **kwargs: Any) -> None: + pass + + @abstractmethod + def stop(self) -> None: + """Stop handling messages and cleanup any resources.""" def __enter__(self) -> Self: + """Enter the context manager.""" return self def __exit__( self, exc_type: Optional[type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], ) -> Literal[False]: + """Exit the context manager and ensure proper cleanup.""" self.stop() return False + +class SizedMessageWriter(MessageWriter, ABC): + """Abstract base class for message writers that can report their file size. + + This class extends :class:`MessageWriter` with the ability to determine the size + of the output file. + """ + + @abstractmethod + def file_size(self) -> int: + """Get the current size of the output file in bytes. + + :return: The size of the file in bytes + :rtype: int + """ + + +class FileIOMessageWriter(SizedMessageWriter, Generic[_IoTypeVar]): + """Base class for writers that operate on file descriptors. + + This class provides common functionality for writers that work with file objects. + + :param file: A path-like object or file object to write to + :param kwargs: Additional keyword arguments for specific writer implementations + + :ivar file: The file object being written to + """ + + file: _IoTypeVar + + @abstractmethod + def __init__(self, file: Union[StringPathLike, _IoTypeVar], **kwargs: Any) -> None: + pass + def stop(self) -> None: - """Closes the underlying file-like object and flushes it, if it was opened in write mode.""" - if self.file is not None: - # this also implies a flush() - self.file.close() + """Close the file and stop writing.""" + self.file.close() + def file_size(self) -> int: + """Get the current file size.""" + return self.file.tell() -class MessageWriter(BaseIOHandler, Listener, metaclass=ABCMeta): - """The base class for all writers.""" - file: Optional[typechecking.FileLike] +class TextIOMessageWriter(FileIOMessageWriter[Union[TextIO, TextIOWrapper]], ABC): + """Text-based message writer implementation. + :param file: Text file to write to + :param mode: File open mode for text operations + :param kwargs: Additional arguments like encoding + """ + + def __init__( + self, + file: Union[StringPathLike, TextIO, TextIOWrapper], + mode: "Union[OpenTextModeUpdating, OpenTextModeWriting]" = "w", + **kwargs: Any, + ) -> None: + if isinstance(file, (str, os.PathLike)): + encoding: str = kwargs.get("encoding", locale.getpreferredencoding(False)) + # pylint: disable=consider-using-with + self.file = Path(file).open(mode=mode, encoding=encoding) + else: + self.file = file -class FileIOMessageWriter(MessageWriter, metaclass=ABCMeta): - """A specialized base class for all writers with file descriptors.""" - file: typechecking.FileLike +class BinaryIOMessageWriter(FileIOMessageWriter[Union[BinaryIO, BufferedIOBase]], ABC): + """Binary file message writer implementation. + + :param file: Binary file to write to + :param mode: File open mode for binary operations + :param kwargs: Additional implementation specific arguments + """ def __init__( - self, file: typechecking.AcceptedIOType, mode: str = "wt", **kwargs: Any + self, + file: Union[StringPathLike, BinaryIO, BufferedIOBase], + mode: "Union[OpenBinaryModeUpdating, OpenBinaryModeWriting]" = "wb", + **kwargs: Any, ) -> None: - # Not possible with the type signature, but be verbose for user-friendliness - if file is None: - raise ValueError("The given file cannot be None") + if isinstance(file, (str, os.PathLike)): + # pylint: disable=consider-using-with,unspecified-encoding + self.file = Path(file).open(mode=mode) + else: + self.file = file - super().__init__(file, mode, **kwargs) - def file_size(self) -> int: - """Return an estimate of the current file size in bytes.""" - return self.file.tell() +class MessageReader(AbstractContextManager["MessageReader"], Iterable[Message], ABC): + """Abstract base class for all CAN message readers. + + This class serves as a foundation for implementing different message reader formats. + It combines context manager capabilities with iteration interface. + + :param file: Path-like object or string representing the input file location + :param kwargs: Additional keyword arguments for specific reader implementations + """ + + @abstractmethod + def __init__(self, file: StringPathLike, **kwargs: Any) -> None: + pass + + @abstractmethod + def stop(self) -> None: + """Stop reading messages and cleanup any resources.""" + + def __enter__(self) -> Self: + return self + + def __exit__( + self, + exc_type: Optional[type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> Literal[False]: + self.stop() + return False -class TextIOMessageWriter(FileIOMessageWriter, metaclass=ABCMeta): - file: TextIO +class FileIOMessageReader(MessageReader, Generic[_IoTypeVar]): + """Base class for readers that operate on file descriptors. + This class provides common functionality for readers that work with file objects. -class BinaryIOMessageWriter(FileIOMessageWriter, metaclass=ABCMeta): - file: Union[BinaryIO, gzip.GzipFile] + :param file: A path-like object or file object to read from + :param kwargs: Additional keyword arguments for specific reader implementations + :ivar file: The file object being read from + """ + + file: _IoTypeVar + + @abstractmethod + def __init__(self, file: Union[StringPathLike, _IoTypeVar], **kwargs: Any) -> None: + pass -class MessageReader(BaseIOHandler, Iterable[Message], metaclass=ABCMeta): - """The base class for all readers.""" + def stop(self) -> None: + self.file.close() -class TextIOMessageReader(MessageReader, metaclass=ABCMeta): - file: TextIO +class TextIOMessageReader(FileIOMessageReader[Union[TextIO, TextIOWrapper]], ABC): + """Text-based message reader implementation. + :param file: Text file to read from + :param mode: File open mode for text operations + :param kwargs: Additional arguments like encoding + """ -class BinaryIOMessageReader(MessageReader, metaclass=ABCMeta): - file: Union[BinaryIO, gzip.GzipFile] + def __init__( + self, + file: Union[StringPathLike, TextIO, TextIOWrapper], + mode: "OpenTextModeReading" = "r", + **kwargs: Any, + ) -> None: + if isinstance(file, (str, os.PathLike)): + encoding: str = kwargs.get("encoding", locale.getpreferredencoding(False)) + # pylint: disable=consider-using-with + self.file = Path(file).open(mode=mode, encoding=encoding) + else: + self.file = file + + +class BinaryIOMessageReader(FileIOMessageReader[Union[BinaryIO, BufferedIOBase]], ABC): + """Binary file message reader implementation. + + :param file: Binary file to read from + :param mode: File open mode for binary operations + :param kwargs: Additional implementation specific arguments + """ + + def __init__( + self, + file: Union[StringPathLike, BinaryIO, BufferedIOBase], + mode: "OpenBinaryModeReading" = "rb", + **kwargs: Any, + ) -> None: + if isinstance(file, (str, os.PathLike)): + # pylint: disable=consider-using-with,unspecified-encoding + self.file = Path(file).open(mode=mode) + else: + self.file = file diff --git a/can/io/logger.py b/can/io/logger.py index f9f029759..9febfe680 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -15,14 +15,13 @@ Final, Literal, Optional, - cast, ) from typing_extensions import Self from .._entry_points import read_entry_points from ..message import Message -from ..typechecking import AcceptedIOType, FileLike, StringPathLike +from ..typechecking import StringPathLike from .asc import ASCWriter from .blf import BLFWriter from .canutils import CanutilsLogWriter @@ -31,6 +30,8 @@ BinaryIOMessageWriter, FileIOMessageWriter, MessageWriter, + SizedMessageWriter, + TextIOMessageWriter, ) from .mf4 import MF4Writer from .printer import Printer @@ -71,9 +72,7 @@ def _get_logger_for_suffix(suffix: str) -> type[MessageWriter]: ) from None -def _compress( - filename: StringPathLike, **kwargs: Any -) -> tuple[type[MessageWriter], FileLike]: +def _compress(filename: StringPathLike, **kwargs: Any) -> FileIOMessageWriter[Any]: """ Return the suffix and io object of the decompressed file. File will automatically recompress upon close. @@ -93,11 +92,18 @@ def _compress( append = kwargs.get("append", False) if issubclass(logger_type, BinaryIOMessageWriter): - mode = "ab" if append else "wb" - else: - mode = "at" if append else "wt" + return logger_type( + file=gzip.open(filename=filename, mode="ab" if append else "wb"), **kwargs + ) + + elif issubclass(logger_type, TextIOMessageWriter): + return logger_type( + file=gzip.open(filename=filename, mode="at" if append else "wt"), **kwargs + ) - return logger_type, gzip.open(filename, mode) + raise ValueError( + f"The file type {real_suffix} is currently incompatible with gzip." + ) def Logger( # noqa: N802 @@ -143,12 +149,11 @@ def Logger( # noqa: N802 _update_writer_plugins() suffix = pathlib.PurePath(filename).suffix.lower() - file_or_filename: AcceptedIOType = filename if suffix == ".gz": - logger_type, file_or_filename = _compress(filename, **kwargs) - else: - logger_type = _get_logger_for_suffix(suffix) - return logger_type(file=file_or_filename, **kwargs) + return _compress(filename, **kwargs) + + logger_type = _get_logger_for_suffix(suffix) + return logger_type(file=filename, **kwargs) class BaseRotatingLogger(MessageWriter, ABC): @@ -183,13 +188,11 @@ class BaseRotatingLogger(MessageWriter, ABC): rollover_count: int = 0 def __init__(self, **kwargs: Any) -> None: - super().__init__(**{**kwargs, "file": None}) - self.writer_kwargs = kwargs @property @abstractmethod - def writer(self) -> FileIOMessageWriter: + def writer(self) -> MessageWriter: """This attribute holds an instance of a writer class which manages the actual file IO.""" raise NotImplementedError @@ -243,7 +246,7 @@ def on_message_received(self, msg: Message) -> None: self.writer.on_message_received(msg) - def _get_new_writer(self, filename: StringPathLike) -> FileIOMessageWriter: + def _get_new_writer(self, filename: StringPathLike) -> MessageWriter: """Instantiate a new writer. .. note:: @@ -261,10 +264,7 @@ def _get_new_writer(self, filename: StringPathLike) -> FileIOMessageWriter: if suffix not in self._supported_formats: continue logger = Logger(filename=filename, **self.writer_kwargs) - if isinstance(logger, FileIOMessageWriter): - return logger - elif isinstance(logger, Printer) and logger.file is not None: - return cast("FileIOMessageWriter", logger) + return logger raise ValueError( f'The log format of "{pathlib.Path(filename).name}" ' @@ -366,18 +366,25 @@ def __init__( self._writer = self._get_new_writer(self.base_filename) + def _get_new_writer(self, filename: StringPathLike) -> SizedMessageWriter: + writer = super()._get_new_writer(filename) + if isinstance(writer, SizedMessageWriter): + return writer + raise TypeError + @property - def writer(self) -> FileIOMessageWriter: + def writer(self) -> SizedMessageWriter: return self._writer def should_rollover(self, msg: Message) -> bool: if self.max_bytes <= 0: return False - if self.writer.file_size() >= self.max_bytes: - return True + file_size = self.writer.file_size() + if file_size is None: + return False - return False + return file_size >= self.max_bytes def do_rollover(self) -> None: if self.writer: diff --git a/can/io/player.py b/can/io/player.py index 2451eab41..c0015b185 100644 --- a/can/io/player.py +++ b/can/io/player.py @@ -11,17 +11,16 @@ from typing import ( Any, Final, - Union, ) from .._entry_points import read_entry_points from ..message import Message -from ..typechecking import AcceptedIOType, FileLike, StringPathLike +from ..typechecking import StringPathLike from .asc import ASCReader from .blf import BLFReader from .canutils import CanutilsLogReader from .csv import CSVReader -from .generic import BinaryIOMessageReader, MessageReader +from .generic import BinaryIOMessageReader, MessageReader, TextIOMessageReader from .mf4 import MF4Reader from .sqlite import SqliteReader from .trc import TRCReader @@ -58,24 +57,25 @@ def _get_logger_for_suffix(suffix: str) -> type[MessageReader]: raise ValueError(f'No read support for unknown log format "{suffix}"') from None -def _decompress( - filename: StringPathLike, -) -> tuple[type[MessageReader], Union[str, FileLike]]: +def _decompress(filename: StringPathLike, **kwargs: Any) -> MessageReader: """ Return the suffix and io object of the decompressed file. """ suffixes = pathlib.Path(filename).suffixes if len(suffixes) != 2: raise ValueError( - f"No write support for unknown log format \"{''.join(suffixes)}\"" - ) from None + f"No read support for unknown log format \"{''.join(suffixes)}\"" + ) real_suffix = suffixes[-2].lower() reader_type = _get_logger_for_suffix(real_suffix) - mode = "rb" if issubclass(reader_type, BinaryIOMessageReader) else "rt" + if issubclass(reader_type, TextIOMessageReader): + return reader_type(gzip.open(filename, mode="rt"), **kwargs) + elif issubclass(reader_type, BinaryIOMessageReader): + return reader_type(gzip.open(filename, mode="rb"), **kwargs) - return reader_type, gzip.open(filename, mode) + raise ValueError(f"No read support for unknown log format \"{''.join(suffixes)}\"") def LogReader(filename: StringPathLike, **kwargs: Any) -> MessageReader: # noqa: N802 @@ -118,12 +118,11 @@ def LogReader(filename: StringPathLike, **kwargs: Any) -> MessageReader: # noqa _update_reader_plugins() suffix = pathlib.PurePath(filename).suffix.lower() - file_or_filename: AcceptedIOType = filename if suffix == ".gz": - reader_type, file_or_filename = _decompress(filename) - else: - reader_type = _get_logger_for_suffix(suffix) - return reader_type(file=file_or_filename, **kwargs) + return _decompress(filename) + + reader_type = _get_logger_for_suffix(suffix) + return reader_type(file=filename, **kwargs) class MessageSync: diff --git a/can/io/printer.py b/can/io/printer.py index 30bc227ab..786cb7261 100644 --- a/can/io/printer.py +++ b/can/io/printer.py @@ -3,16 +3,18 @@ """ import logging -from typing import Any, Optional, TextIO, Union, cast +import sys +from io import TextIOWrapper +from typing import Any, TextIO, Union from ..message import Message from ..typechecking import StringPathLike -from .generic import MessageWriter +from .generic import TextIOMessageWriter log = logging.getLogger("can.io.printer") -class Printer(MessageWriter): +class Printer(TextIOMessageWriter): """ The Printer class is a subclass of :class:`~can.Listener` which simply prints any messages it receives to the terminal (stdout). A message is turned into a @@ -22,11 +24,9 @@ class Printer(MessageWriter): standard out """ - file: Optional[TextIO] - def __init__( self, - file: Optional[Union[StringPathLike, TextIO]] = None, + file: Union[StringPathLike, TextIO, TextIOWrapper] = sys.stdout, append: bool = False, **kwargs: Any, ) -> None: @@ -38,18 +38,17 @@ def __init__( :param append: If set to `True` messages, are appended to the file, else the file is truncated """ - self.write_to_file = file is not None - mode = "a" if append else "w" - super().__init__(file, mode=mode) + super().__init__(file, mode="a" if append else "w") def on_message_received(self, msg: Message) -> None: - if self.write_to_file: - cast("TextIO", self.file).write(str(msg) + "\n") - else: - print(msg) # noqa: T201 + self.file.write(str(msg) + "\n") def file_size(self) -> int: """Return an estimate of the current file size in bytes.""" - if self.file is not None: + if self.file is not sys.stdout: return self.file.tell() return 0 + + def stop(self) -> None: + if self.file is not sys.stdout: + super().stop() diff --git a/can/io/sqlite.py b/can/io/sqlite.py index 686e2d038..73aa2961c 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -8,9 +8,11 @@ import sqlite3 import threading import time -from collections.abc import Generator +from collections.abc import Generator, Iterator from typing import Any +from typing_extensions import TypeAlias + from can.listener import BufferedReader from can.message import Message @@ -19,6 +21,8 @@ log = logging.getLogger("can.io.sqlite") +_MessageTuple: TypeAlias = "tuple[float, int, bool, bool, bool, int, memoryview[int]]" + class SqliteReader(MessageReader): """ @@ -49,7 +53,6 @@ def __init__( do not accept file-like objects as the `file` parameter. It also runs in ``append=True`` mode all the time. """ - super().__init__(file=None) self._conn = sqlite3.connect(file) self._cursor = self._conn.cursor() self.table_name = table_name @@ -59,7 +62,7 @@ def __iter__(self) -> Generator[Message, None, None]: yield SqliteReader._assemble_message(frame_data) @staticmethod - def _assemble_message(frame_data): + def _assemble_message(frame_data: _MessageTuple) -> Message: timestamp, can_id, is_extended, is_remote, is_error, dlc, data = frame_data return Message( timestamp=timestamp, @@ -71,12 +74,12 @@ def _assemble_message(frame_data): data=data, ) - def __len__(self): + def __len__(self) -> int: # this might not run in constant time result = self._cursor.execute(f"SELECT COUNT(*) FROM {self.table_name}") return int(result.fetchone()[0]) - def read_all(self): + def read_all(self) -> Iterator[Message]: """Fetches all messages in the database. :rtype: Generator[can.Message] @@ -84,9 +87,8 @@ def read_all(self): result = self._cursor.execute(f"SELECT * FROM {self.table_name}").fetchall() return (SqliteReader._assemble_message(frame) for frame in result) - def stop(self): + def stop(self) -> None: """Closes the connection to the database.""" - super().stop() self._conn.close() @@ -154,11 +156,10 @@ def __init__( f"The append argument should not be used in " f"conjunction with the {self.__class__.__name__}." ) - super().__init__(file=None) + BufferedReader.__init__(self) self.table_name = table_name self._db_filename = file self._stop_running_event = threading.Event() - self._conn = None self._writer_thread = threading.Thread(target=self._db_writer_thread) self._writer_thread.start() self.num_frames = 0 @@ -167,7 +168,8 @@ def __init__( f"INSERT INTO {self.table_name} VALUES (?, ?, ?, ?, ?, ?, ?)" ) - def _create_db(self): + @staticmethod + def _create_db(file: StringPathLike, table_name: str) -> sqlite3.Connection: """Creates a new databae or opens a connection to an existing one. .. note:: @@ -175,11 +177,11 @@ def _create_db(self): hence we setup the db here. It has the upside of running async. """ log.debug("Creating sqlite database") - self._conn = sqlite3.connect(self._db_filename) + conn = sqlite3.connect(file) # create table structure - self._conn.cursor().execute( - f"""CREATE TABLE IF NOT EXISTS {self.table_name} + conn.cursor().execute( + f"""CREATE TABLE IF NOT EXISTS {table_name} ( ts REAL, arbitration_id INTEGER, @@ -190,14 +192,16 @@ def _create_db(self): data BLOB )""" ) - self._conn.commit() + conn.commit() + + return conn - def _db_writer_thread(self): - self._create_db() + def _db_writer_thread(self) -> None: + conn = SqliteWriter._create_db(self._db_filename, self.table_name) try: while True: - messages = [] # reset buffer + messages: list[_MessageTuple] = [] # reset buffer msg = self.get_message(self.GET_MESSAGE_TIMEOUT) while msg is not None: @@ -226,10 +230,10 @@ def _db_writer_thread(self): count = len(messages) if count > 0: - with self._conn: + with conn: # log.debug("Writing %d frames to db", count) - self._conn.executemany(self._insert_template, messages) - self._conn.commit() # make the changes visible to the entire database + conn.executemany(self._insert_template, messages) + conn.commit() # make the changes visible to the entire database self.num_frames += count self.last_write = time.time() @@ -238,14 +242,13 @@ def _db_writer_thread(self): break finally: - self._conn.close() + conn.close() log.info("Stopped sqlite writer after writing %d messages", self.num_frames) - def stop(self): + def stop(self) -> None: """Stops the reader an writes all remaining messages to the database. Thus, this might take a while and block. """ BufferedReader.stop(self) self._stop_running_event.set() self._writer_thread.join() - MessageReader.stop(self) diff --git a/can/io/trc.py b/can/io/trc.py index e1eaa077c..fa8ee88e7 100644 --- a/can/io/trc.py +++ b/can/io/trc.py @@ -12,6 +12,7 @@ from collections.abc import Generator from datetime import datetime, timedelta, timezone from enum import Enum +from io import TextIOWrapper from typing import Any, Callable, Optional, TextIO, Union from ..message import Message @@ -31,8 +32,8 @@ class TRCFileVersion(Enum): V2_0 = 200 V2_1 = 201 - def __ge__(self, other): - if self.__class__ is other.__class__: + def __ge__(self, other: Any) -> bool: + if isinstance(other, TRCFileVersion): return self.value >= other.value return NotImplemented @@ -42,8 +43,6 @@ class TRCReader(TextIOMessageReader): Iterator of CAN messages from a TRC logging file. """ - file: TextIO - def __init__( self, file: Union[StringPathLike, TextIO], @@ -73,7 +72,7 @@ def start_time(self) -> Optional[datetime]: return datetime.fromtimestamp(self._start_time, timezone.utc) return None - def _extract_header(self): + def _extract_header(self) -> str: line = "" for _line in self.file: line = _line.strip() @@ -286,9 +285,6 @@ class TRCWriter(TextIOMessageWriter): If the first message does not have a timestamp, it is set to zero. """ - file: TextIO - first_timestamp: Optional[float] - FORMAT_MESSAGE = ( "{msgnr:>7} {time:13.3f} DT {channel:>2} {id:>8} {dir:>2} - {dlc:<4} {data}" ) @@ -296,7 +292,7 @@ class TRCWriter(TextIOMessageWriter): def __init__( self, - file: Union[StringPathLike, TextIO], + file: Union[StringPathLike, TextIO, TextIOWrapper], channel: int = 1, **kwargs: Any, ) -> None: @@ -318,7 +314,7 @@ def __init__( self.filepath = os.path.abspath(self.file.name) self.header_written = False self.msgnr = 0 - self.first_timestamp = None + self.first_timestamp: Optional[float] = None self.file_version = TRCFileVersion.V2_1 self._msg_fmt_string = self.FORMAT_MESSAGE_V1_0 self._format_message = self._format_message_init @@ -370,7 +366,7 @@ def _write_header_v2_1(self, start_time: datetime) -> None: ] self.file.writelines(line + "\n" for line in lines) - def _format_message_by_format(self, msg, channel): + def _format_message_by_format(self, msg: Message, channel: int) -> str: if msg.is_extended_id: arb_id = f"{msg.arbitration_id:07X}" else: @@ -378,6 +374,8 @@ def _format_message_by_format(self, msg, channel): data = [f"{byte:02X}" for byte in msg.data] + if self.first_timestamp is None: + raise ValueError serialized = self._msg_fmt_string.format( msgnr=self.msgnr, time=(msg.timestamp - self.first_timestamp) * 1000, @@ -389,7 +387,7 @@ def _format_message_by_format(self, msg, channel): ) return serialized - def _format_message_init(self, msg, channel): + def _format_message_init(self, msg: Message, channel: int) -> str: if self.file_version == TRCFileVersion.V1_0: self._format_message = self._format_message_by_format self._msg_fmt_string = self.FORMAT_MESSAGE_V1_0 diff --git a/can/listener.py b/can/listener.py index b450cf36d..6256d33b6 100644 --- a/can/listener.py +++ b/can/listener.py @@ -147,7 +147,9 @@ def __init__(self, **kwargs: Any) -> None: stacklevel=2, ) if sys.version_info < (3, 10): - self.buffer = asyncio.Queue(loop=kwargs["loop"]) + self.buffer = asyncio.Queue( # pylint: disable=unexpected-keyword-arg + loop=kwargs["loop"] + ) return self.buffer = asyncio.Queue() diff --git a/can/typechecking.py b/can/typechecking.py index 36343ddaa..fc0c87c0d 100644 --- a/can/typechecking.py +++ b/can/typechecking.py @@ -1,9 +1,9 @@ """Types for mypy type-checking""" -import gzip -import struct +import io import sys -import typing +from collections.abc import Iterable, Sequence +from typing import IO, TYPE_CHECKING, Any, NewType, Union if sys.version_info >= (3, 10): from typing import TypeAlias @@ -16,8 +16,9 @@ from typing_extensions import TypedDict -if typing.TYPE_CHECKING: +if TYPE_CHECKING: import os + import struct class CanFilter(TypedDict): @@ -31,32 +32,31 @@ class CanFilterExtended(TypedDict): extended: bool -CanFilters = typing.Sequence[typing.Union[CanFilter, CanFilterExtended]] +CanFilters = Sequence[Union[CanFilter, CanFilterExtended]] # TODO: Once buffer protocol support lands in typing, we should switch to that, # since can.message.Message attempts to call bytearray() on the given data, so # this should have the same typing info. # # See: https://github.com/python/typing/issues/593 -CanData = typing.Union[bytes, bytearray, int, typing.Iterable[int]] +CanData = Union[bytes, bytearray, int, Iterable[int]] # Used for the Abstract Base Class ChannelStr = str ChannelInt = int -Channel = typing.Union[ChannelInt, ChannelStr] +Channel = Union[ChannelInt, ChannelStr, Sequence[ChannelInt]] # Used by the IO module -FileLike = typing.Union[typing.TextIO, typing.BinaryIO, gzip.GzipFile] -StringPathLike = typing.Union[str, "os.PathLike[str]"] -AcceptedIOType = typing.Union[FileLike, StringPathLike] +FileLike = Union[IO[Any], io.TextIOWrapper, io.BufferedIOBase] +StringPathLike = Union[str, "os.PathLike[str]"] -BusConfig = typing.NewType("BusConfig", dict[str, typing.Any]) +BusConfig = NewType("BusConfig", dict[str, Any]) # Used by CLI scripts -TAdditionalCliArgs: TypeAlias = dict[str, typing.Union[str, int, float, bool]] +TAdditionalCliArgs: TypeAlias = dict[str, Union[str, int, float, bool]] TDataStructs: TypeAlias = dict[ - typing.Union[int, tuple[int, ...]], - typing.Union[struct.Struct, tuple, None], + Union[int, tuple[int, ...]], + "Union[struct.Struct, tuple[struct.Struct, *tuple[float, ...]]]", ] @@ -65,7 +65,7 @@ class AutoDetectedConfig(TypedDict): channel: Channel -ReadableBytesLike = typing.Union[bytes, bytearray, memoryview] +ReadableBytesLike = Union[bytes, bytearray, memoryview] class BitTimingDict(TypedDict): diff --git a/can/util.py b/can/util.py index 895c721ae..584b7dfa9 100644 --- a/can/util.py +++ b/can/util.py @@ -50,7 +50,7 @@ def load_file_config( - path: Optional[typechecking.AcceptedIOType] = None, section: str = "default" + path: Optional[typechecking.StringPathLike] = None, section: str = "default" ) -> dict[str, str]: """ Loads configuration from file with following content:: @@ -120,7 +120,7 @@ def load_environment_config(context: Optional[str] = None) -> dict[str, str]: def load_config( - path: Optional[typechecking.AcceptedIOType] = None, + path: Optional[typechecking.StringPathLike] = None, config: Optional[dict[str, Any]] = None, context: Optional[str] = None, ) -> typechecking.BusConfig: diff --git a/doc/conf.py b/doc/conf.py index f4a9ab95f..7ae0480d5 100755 --- a/doc/conf.py +++ b/doc/conf.py @@ -122,6 +122,12 @@ # disable specific warnings nitpick_ignore = [ # Ignore warnings for type aliases. Remove once Sphinx supports PEP613 + ("py:class", "OpenTextModeUpdating"), + ("py:class", "OpenTextModeWriting"), + ("py:class", "OpenBinaryModeUpdating"), + ("py:class", "OpenBinaryModeWriting"), + ("py:class", "OpenTextModeReading"), + ("py:class", "OpenBinaryModeReading"), ("py:class", "BusConfig"), ("py:class", "can.typechecking.BusConfig"), ("py:class", "can.typechecking.CanFilter"), diff --git a/doc/file_io.rst b/doc/file_io.rst index ff9431695..329ccac53 100644 --- a/doc/file_io.rst +++ b/doc/file_io.rst @@ -94,7 +94,7 @@ as further references can-utils can be used: Log (.log can-utils Logging format) ----------------------------------- -CanutilsLogWriter logs CAN data to an ASCII log file compatible with `can-utils ` +CanutilsLogWriter logs CAN data to an ASCII log file compatible with `can-utils `_ As specification following references can-utils can be used: `asc2log `_, `log2asc `_. diff --git a/doc/internal-api.rst b/doc/internal-api.rst index 73984bf1a..9e544f052 100644 --- a/doc/internal-api.rst +++ b/doc/internal-api.rst @@ -90,8 +90,8 @@ About the IO module Handling of the different file formats is implemented in ``can.io``. Each file/IO type is within a separate module and ideally implements both a *Reader* and a *Writer*. -The reader usually extends :class:`can.io.generic.BaseIOHandler`, while -the writer often additionally extends :class:`can.Listener`, +The reader extends :class:`can.io.generic.MessageReader`, while the writer extends +:class:`can.io.generic.MessageWriter`, a subclass of the :class:`can.Listener`, to be able to be passed directly to a :class:`can.Notifier`. @@ -104,9 +104,9 @@ Ideally add both reading and writing support for the new file format, although t 1. Create a new module: *can/io/canstore.py* (*or* simply copy some existing one like *can/io/csv.py*) -2. Implement a reader ``CanstoreReader`` (which often extends :class:`can.io.generic.BaseIOHandler`, but does not have to). +2. Implement a reader ``CanstoreReader`` which extends :class:`can.io.generic.MessageReader`. Besides from a constructor, only ``__iter__(self)`` needs to be implemented. -3. Implement a writer ``CanstoreWriter`` (which often extends :class:`can.io.generic.BaseIOHandler` and :class:`can.Listener`, but does not have to). +3. Implement a writer ``CanstoreWriter`` which extends :class:`can.io.generic.MessageWriter`. Besides from a constructor, only ``on_message_received(self, msg)`` needs to be implemented. 4. Add a case to ``can.io.player.LogReader``'s ``__new__()``. 5. Document the two new classes (and possibly additional helpers) with docstrings and comments. @@ -126,7 +126,9 @@ IO Utilities .. automodule:: can.io.generic + :show-inheritance: :members: + :private-members: :member-order: bysource diff --git a/doc/notifier.rst b/doc/notifier.rst index 05edbd90d..e1b160a6e 100644 --- a/doc/notifier.rst +++ b/doc/notifier.rst @@ -58,7 +58,8 @@ readers are also documented here. be added using the ``can.io.message_writer`` entry point. The format of the entry point is ``reader_name=module:classname`` where ``classname`` - is a :class:`can.io.generic.BaseIOHandler` concrete implementation. + is a concrete implementation of :class:`~can.io.generic.MessageReader` or + :class:`~can.io.generic.MessageWriter`. :: diff --git a/test/logformats_test.py b/test/logformats_test.py index e9db47af3..f4bd1191f 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -137,6 +137,7 @@ def _setup_instance_helper( allowed_timestamp_delta=0.0, preserves_channel=True, adds_default_channel=None, + assert_file_closed=True, ): """ :param Callable writer_constructor: the constructor of the writer class @@ -187,6 +188,7 @@ def _setup_instance_helper( self.reader_constructor = reader_constructor self.binary_file = binary_file self.test_append_enabled = test_append + self.assert_file_closed = assert_file_closed ComparingMessagesTestCase.__init__( self, @@ -212,7 +214,7 @@ def test_path_like_explicit_stop(self): self._write_all(writer) self._ensure_fsync(writer) writer.stop() - if hasattr(writer.file, "closed"): + if self.assert_file_closed: self.assertTrue(writer.file.closed) print("reading all messages") @@ -220,7 +222,7 @@ def test_path_like_explicit_stop(self): read_messages = list(reader) # redundant, but this checks if stop() can be called multiple times reader.stop() - if hasattr(writer.file, "closed"): + if self.assert_file_closed: self.assertTrue(writer.file.closed) # check if at least the number of messages matches @@ -243,7 +245,7 @@ def test_path_like_context_manager(self): self._write_all(writer) self._ensure_fsync(writer) w = writer - if hasattr(w.file, "closed"): + if self.assert_file_closed: self.assertTrue(w.file.closed) # read all written messages @@ -251,7 +253,7 @@ def test_path_like_context_manager(self): with self.reader_constructor(self.test_file_name) as reader: read_messages = list(reader) r = reader - if hasattr(r.file, "closed"): + if self.assert_file_closed: self.assertTrue(r.file.closed) # check if at least the number of messages matches; @@ -274,7 +276,7 @@ def test_file_like_explicit_stop(self): self._write_all(writer) self._ensure_fsync(writer) writer.stop() - if hasattr(my_file, "closed"): + if self.assert_file_closed: self.assertTrue(my_file.closed) print("reading all messages") @@ -283,7 +285,7 @@ def test_file_like_explicit_stop(self): read_messages = list(reader) # redundant, but this checks if stop() can be called multiple times reader.stop() - if hasattr(my_file, "closed"): + if self.assert_file_closed: self.assertTrue(my_file.closed) # check if at least the number of messages matches @@ -307,7 +309,7 @@ def test_file_like_context_manager(self): self._write_all(writer) self._ensure_fsync(writer) w = writer - if hasattr(my_file, "closed"): + if self.assert_file_closed: self.assertTrue(my_file.closed) # read all written messages @@ -316,7 +318,7 @@ def test_file_like_context_manager(self): with self.reader_constructor(my_file) as reader: read_messages = list(reader) r = reader - if hasattr(my_file, "closed"): + if self.assert_file_closed: self.assertTrue(my_file.closed) # check if at least the number of messages matches; @@ -380,7 +382,7 @@ def _write_all(self, writer): writer(msg) def _ensure_fsync(self, io_handler): - if hasattr(io_handler.file, "fileno"): + if hasattr(io_handler, "file") and hasattr(io_handler.file, "fileno"): io_handler.file.flush() os.fsync(io_handler.file.fileno()) @@ -871,6 +873,7 @@ def _setup_instance(self): check_comments=False, preserves_channel=False, adds_default_channel=None, + assert_file_closed=False, ) @unittest.skip("not implemented") From 55474135591a1a1f27b6e42cc9c6fdc91908ea13 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 25 Jul 2025 19:15:59 +0200 Subject: [PATCH 093/127] Add CAN bridge script (#1961) * Add basic implementation for can bridge * Implement basic configuration parsing * Add implementation for bridge * Improve exit handling * Add debug logging * Add error handling for wrong arguments * Use stderr * Add custom usage * Add bus configuration info * Code format * Add exception for prints for can_bridge * Add from to exception in exception * Remove assignment to unused variable * Shorten line length * Organize imports * Remove unnecessary else * Add documentation for new script * Add handling for -h and help sub command Necessary for generation of documentation * Add from none to exception * Fix typo busses to bus * Add type annotations * Fix type annotations * Fix type annotations again * Add --help to get help * Add basic print help test * Add basic test file for bridge script * Add very basic test * Add different channels for virtual bus * Add assert for call to exit * Patch correct function * test * fjkdf * once again -.- * Try snakecase * use new api to create cli args for bus1 and bus2 --------- Co-authored-by: Peter Kessen --- can/bridge.py | 66 +++++++++++++++++++++++ doc/scripts.rst | 9 ++++ pyproject.toml | 2 + test/test_bridge.py | 126 +++++++++++++++++++++++++++++++++++++++++++ test/test_scripts.py | 14 +++++ 5 files changed, 217 insertions(+) create mode 100644 can/bridge.py create mode 100644 test/test_bridge.py diff --git a/can/bridge.py b/can/bridge.py new file mode 100644 index 000000000..57ebb368d --- /dev/null +++ b/can/bridge.py @@ -0,0 +1,66 @@ +""" +Creates a bridge between two CAN buses. + +This will connect to two CAN buses. Messages received on one +bus will be sent to the other bus and vice versa. +""" + +import argparse +import errno +import sys +import time +from datetime import datetime +from typing import Final + +from can.cli import add_bus_arguments, create_bus_from_namespace +from can.listener import RedirectReader +from can.notifier import Notifier + +BRIDGE_DESCRIPTION: Final = """\ +Bridge two CAN buses. + +Both can buses will be connected so that messages from bus1 will be sent on +bus2 and messages from bus2 will be sent to bus1. +""" +BUS_1_PREFIX: Final = "bus1" +BUS_2_PREFIX: Final = "bus2" + + +def _parse_bridge_args(args: list[str]) -> argparse.Namespace: + """Parse command line arguments for bridge script.""" + + parser = argparse.ArgumentParser(description=BRIDGE_DESCRIPTION) + add_bus_arguments(parser, prefix=BUS_1_PREFIX, group_title="Bus 1 arguments") + add_bus_arguments(parser, prefix=BUS_2_PREFIX, group_title="Bus 2 arguments") + + # print help message when no arguments were given + if not args: + parser.print_help(sys.stderr) + raise SystemExit(errno.EINVAL) + + results, _unknown_args = parser.parse_known_args(args) + return results + + +def main() -> None: + results = _parse_bridge_args(sys.argv[1:]) + + with ( + create_bus_from_namespace(results, prefix=BUS_1_PREFIX) as bus1, + create_bus_from_namespace(results, prefix=BUS_2_PREFIX) as bus2, + ): + reader1_to_2 = RedirectReader(bus2) + reader2_to_1 = RedirectReader(bus1) + with Notifier(bus1, [reader1_to_2]), Notifier(bus2, [reader2_to_1]): + print(f"CAN Bridge (Started on {datetime.now()})") + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + pass + + print(f"CAN Bridge (Stopped on {datetime.now()})") + + +if __name__ == "__main__": + main() diff --git a/doc/scripts.rst b/doc/scripts.rst index 2d59b7528..1d730a74b 100644 --- a/doc/scripts.rst +++ b/doc/scripts.rst @@ -57,6 +57,15 @@ The full usage page can be seen below: :shell: +can.bridge +---------- + +A small application that can be used to connect two can buses: + +.. command-output:: python -m can.bridge -h + :shell: + + can.logconvert -------------- diff --git a/pyproject.toml b/pyproject.toml index a6a7f38c4..8fd77de21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,7 @@ can_logconvert = "can.logconvert:main" can_logger = "can.logger:main" can_player = "can.player:main" can_viewer = "can.viewer:main" +can_bridge = "can.bridge:main" [project.urls] homepage = "https://github.com/hardbyte/python-can" @@ -186,6 +187,7 @@ ignore = [ "can/cli.py" = ["T20"] # flake8-print "can/logger.py" = ["T20"] # flake8-print "can/player.py" = ["T20"] # flake8-print +"can/bridge.py" = ["T20"] # flake8-print "can/viewer.py" = ["T20"] # flake8-print "examples/*" = ["T20"] # flake8-print diff --git a/test/test_bridge.py b/test/test_bridge.py new file mode 100644 index 000000000..ee41bd949 --- /dev/null +++ b/test/test_bridge.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python + +""" +This module tests the functions inside of bridge.py +""" + +import random +import string +import sys +import threading +import time +from time import sleep as real_sleep +import unittest.mock + +import can +import can.bridge +from can.interfaces import virtual + +from .message_helper import ComparingMessagesTestCase + + +class TestBridgeScriptModule(unittest.TestCase, ComparingMessagesTestCase): + + TIMEOUT = 3.0 + + def __init__(self, *args, **kwargs): + unittest.TestCase.__init__(self, *args, **kwargs) + ComparingMessagesTestCase.__init__( + self, + allowed_timestamp_delta=None, + preserves_channel=False, + ) + + def setUp(self) -> None: + self.stop_event = threading.Event() + + self.channel1 = "".join(random.choices(string.ascii_letters, k=8)) + self.channel2 = "".join(random.choices(string.ascii_letters, k=8)) + + self.cli_args = [ + "--bus1-interface", + "virtual", + "--bus1-channel", + self.channel1, + "--bus2-interface", + "virtual", + "--bus2-channel", + self.channel2, + ] + + self.testmsg = can.Message( + arbitration_id=0xC0FFEE, data=[0, 25, 0, 1, 3, 1, 4, 1], is_extended_id=True + ) + + def fake_sleep(self, duration): + """A fake replacement for time.sleep that checks periodically + whether self.stop_event is set, and raises KeyboardInterrupt + if so. + + This allows tests to simulate an interrupt (like Ctrl+C) + during long sleeps, in a controlled and responsive way. + """ + interval = 0.05 # Small interval for responsiveness + t_wakeup = time.perf_counter() + duration + while time.perf_counter() < t_wakeup: + if self.stop_event.is_set(): + raise KeyboardInterrupt("Simulated interrupt from fake_sleep") + real_sleep(interval) + + def test_bridge(self): + with ( + unittest.mock.patch("can.bridge.time.sleep", new=self.fake_sleep), + unittest.mock.patch("can.bridge.sys.argv", [sys.argv[0], *self.cli_args]), + ): + # start script + thread = threading.Thread(target=can.bridge.main) + thread.start() + + # wait until script instantiates virtual buses + t0 = time.perf_counter() + while True: + with virtual.channels_lock: + if ( + self.channel1 in virtual.channels + and self.channel2 in virtual.channels + ): + break + if time.perf_counter() > t0 + 2.0: + raise TimeoutError("Bridge script did not create virtual buses") + real_sleep(0.2) + + # create buses with the same channels as in scripts + with ( + can.interfaces.virtual.VirtualBus(self.channel1) as bus1, + can.interfaces.virtual.VirtualBus(self.channel2) as bus2, + ): + # send test message to bus1, it should be received on bus2 + bus1.send(self.testmsg) + recv_msg = bus2.recv(self.TIMEOUT) + self.assertMessageEqual(self.testmsg, recv_msg) + + # assert that both buses are empty + self.assertIsNone(bus1.recv(0)) + self.assertIsNone(bus2.recv(0)) + + # send test message to bus2, it should be received on bus1 + bus2.send(self.testmsg) + recv_msg = bus1.recv(self.TIMEOUT) + self.assertMessageEqual(self.testmsg, recv_msg) + + # assert that both buses are empty + self.assertIsNone(bus1.recv(0)) + self.assertIsNone(bus2.recv(0)) + + # stop the bridge script + self.stop_event.set() + thread.join() + + # assert that the virtual buses were closed + with virtual.channels_lock: + self.assertNotIn(self.channel1, virtual.channels) + self.assertNotIn(self.channel2, virtual.channels) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_scripts.py b/test/test_scripts.py index 9d8c059cf..c1a6c082d 100644 --- a/test/test_scripts.py +++ b/test/test_scripts.py @@ -98,6 +98,20 @@ def _import(self): return module +class TestBridgeScript(CanScriptTest): + def _commands(self): + commands = [ + "python -m can.bridge --help", + "can_bridge --help", + ] + return commands + + def _import(self): + import can.bridge as module + + return module + + class TestLogconvertScript(CanScriptTest): def _commands(self): commands = [ From afb1204c0e6fafc36ced42c8fc0be04a5c840454 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sun, 27 Jul 2025 13:21:26 +0200 Subject: [PATCH 094/127] Update contribution guide, move all checks to tox (#1960) * docs: remove unused extras, use py3.13 * move all checks to tox, use uv in CI * update contribution guidelines * minor type annotation fixes * add missing `install` cmd * disable free-threaded tests, TestThreadSafeBus::test_send_periodic_duration is flaky * update docs as requested by review --------- Co-authored-by: zariiii9003 --- .github/workflows/ci.yml | 116 +++++----------- .gitignore | 3 +- .readthedocs.yml | 5 +- CONTRIBUTING.md | 2 +- can/bus.py | 3 +- can/interfaces/virtual.py | 12 +- can/io/mf4.py | 3 +- doc/conf.py | 2 +- doc/development.rst | 273 ++++++++++++++++++++++++++------------ doc/installation.rst | 7 + pyproject.toml | 13 +- test/back2back_test.py | 10 +- test/test_socketcan.py | 4 + tox.ini | 73 +++++++--- 14 files changed, 309 insertions(+), 217 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec5c1bbac..8c3bb407f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,32 +12,28 @@ env: jobs: test: runs-on: ${{ matrix.os }} - continue-on-error: ${{ matrix.experimental }} # See: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idcontinue-on-error strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - experimental: [false] - python-version: [ - "3.9", - "3.10", - "3.11", - "3.12", - "3.13", - "pypy-3.9", - "pypy-3.10", + env: [ + "py39", + "py310", + "py311", + "py312", + "py313", + "py314", +# "py313t", +# "py314t", + "pypy310", + "pypy311", ] fail-fast: false steps: - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - allow-prereleases: true - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install tox + - name: Install uv + uses: astral-sh/setup-uv@v6 + - name: Install tox + run: uv tool install tox --with tox-uv - name: Setup SocketCAN if: ${{ matrix.os == 'ubuntu-latest' }} run: | @@ -46,11 +42,11 @@ jobs: sudo ./test/open_vcan.sh - name: Test with pytest via tox run: | - tox -e gh + tox -e ${{ matrix.env }} env: # SocketCAN tests currently fail with PyPy because it does not support raw CAN sockets # See: https://foss.heptapod.net/pypy/pypy/-/issues/3809 - TEST_SOCKETCAN: "${{ matrix.os == 'ubuntu-latest' && ! startsWith(matrix.python-version, 'pypy' ) }}" + TEST_SOCKETCAN: "${{ matrix.os == 'ubuntu-latest' && ! startsWith(matrix.env, 'pypy' ) }}" - name: Coveralls Parallel uses: coverallsapp/github-action@v2 with: @@ -73,69 +69,25 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install --group lint -e . - - name: mypy 3.9 - run: | - mypy --python-version 3.9 . - - name: mypy 3.10 + - name: Install uv + uses: astral-sh/setup-uv@v6 + - name: Install tox + run: uv tool install tox --with tox-uv + - name: Run linters run: | - mypy --python-version 3.10 . - - name: mypy 3.11 + tox -e lint + - name: Run type checker run: | - mypy --python-version 3.11 . - - name: mypy 3.12 - run: | - mypy --python-version 3.12 . - - name: mypy 3.13 - run: | - mypy --python-version 3.13 . - - name: ruff - run: | - ruff check can - - name: pylint - run: | - pylint \ - can/**.py \ - can/io \ - doc/conf.py \ - examples/**.py \ - can/interfaces/socketcan - - format: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.10" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install --group lint - - name: Code Format Check with Black - run: | - black --check --verbose . + tox -e type docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install tox + - name: Install uv + uses: astral-sh/setup-uv@v6 + - name: Install tox + run: uv tool install tox --with tox-uv - name: Build documentation run: | tox -e docs @@ -147,14 +99,12 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 # fetch tags for setuptools-scm - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.10" + - name: Install uv + uses: astral-sh/setup-uv@v6 - name: Build wheel and sdist - run: pipx run build + run: uvx --from build pyproject-build --installer uv - name: Check build artifacts - run: pipx run twine check --strict dist/* + run: uvx twine check --strict dist/* - name: Save artifacts uses: actions/upload-artifact@v4 with: diff --git a/.gitignore b/.gitignore index 03775bd7c..e4d402ff4 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,8 @@ __pycache__/ # Distribution / packaging .Python env/ -venv/ +.venv*/ +venv*/ build/ develop-eggs/ dist/ diff --git a/.readthedocs.yml b/.readthedocs.yml index 6fe4009e4..a8c61d2de 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -9,7 +9,7 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.12" + python: "3.13" jobs: post_install: - pip install --group docs @@ -31,6 +31,3 @@ python: extra_requirements: - canalystii - gs-usb - - mf4 - - remote - - serial diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c00e9bd32..2f4194b31 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1 +1 @@ -Please read the [Development - Contributing](https://python-can.readthedocs.io/en/stable/development.html#contributing) guidelines in the documentation site. +Please read the [Development - Contributing](https://python-can.readthedocs.io/en/main/development.html#contributing) guidelines in the documentation site. diff --git a/can/bus.py b/can/bus.py index 0d031a18b..f053c4aa7 100644 --- a/can/bus.py +++ b/can/bus.py @@ -11,7 +11,6 @@ from time import time from types import TracebackType from typing import ( - Any, Callable, Optional, Union, @@ -69,7 +68,7 @@ class BusABC(metaclass=ABCMeta): @abstractmethod def __init__( self, - channel: Any, + channel: Optional[can.typechecking.Channel], can_filters: Optional[can.typechecking.CanFilters] = None, **kwargs: object, ): diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index aa858913e..6b62e5ceb 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -12,22 +12,18 @@ from copy import deepcopy from random import randint from threading import RLock -from typing import TYPE_CHECKING, Any, Optional +from typing import Any, Optional from can import CanOperationError from can.bus import BusABC, CanProtocol from can.message import Message -from can.typechecking import AutoDetectedConfig +from can.typechecking import AutoDetectedConfig, Channel logger = logging.getLogger(__name__) # Channels are lists of queues, one for each connection -if TYPE_CHECKING: - # https://mypy.readthedocs.io/en/stable/runtime_troubles.html#using-classes-that-are-generic-in-stubs-but-not-at-runtime - channels: dict[Optional[Any], list[queue.Queue[Message]]] = {} -else: - channels = {} +channels: dict[Optional[Channel], list[queue.Queue[Message]]] = {} channels_lock = RLock() @@ -58,7 +54,7 @@ class VirtualBus(BusABC): def __init__( self, - channel: Any = None, + channel: Optional[Channel] = None, receive_own_messages: bool = False, rx_queue_size: int = 0, preserve_timestamps: bool = False, diff --git a/can/io/mf4.py b/can/io/mf4.py index 557d882e1..7007c3627 100644 --- a/can/io/mf4.py +++ b/can/io/mf4.py @@ -186,7 +186,8 @@ def file_size(self) -> int: """Return an estimate of the current file size in bytes.""" # TODO: find solution without accessing private attributes of asammdf return cast( - "int", self._mdf._tempfile.tell() # pylint: disable=protected-access + "int", + self._mdf._tempfile.tell(), # pylint: disable=protected-access,no-member ) def stop(self) -> None: diff --git a/doc/conf.py b/doc/conf.py index 7ae0480d5..5e413361c 100755 --- a/doc/conf.py +++ b/doc/conf.py @@ -138,7 +138,7 @@ ("py:class", "~P1"), # intersphinx fails to reference some builtins ("py:class", "asyncio.events.AbstractEventLoop"), - ("py:class", "_thread.allocate_lock"), + ("py:class", "_thread.lock"), ] # mock windows specific attributes diff --git a/doc/development.rst b/doc/development.rst index 074c1318d..97c175ada 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -1,133 +1,232 @@ Developer's Overview ==================== +Quick Start for Contributors +---------------------------- +* Fork the repository on GitHub and clone your fork. +* Create a new branch for your changes. +* Set up your development environment. +* Make your changes, add/update tests and documentation as needed. +* Run `tox` to check your changes. +* Push your branch and open a pull request. Contributing ------------ -Contribute to source code, documentation, examples and report issues: -https://github.com/hardbyte/python-can +Welcome! Thank you for your interest in python-can. Whether you want to fix a bug, +add a feature, improve documentation, write examples, help solve issues, +or simply report a problem, your contribution is valued. +Contributions are made via the `python-can GitHub repository `_. +If you have questions, feel free to open an issue or start a discussion on GitHub. -Note that the latest released version on PyPi may be significantly behind the -``main`` branch. Please open any feature requests against the ``main`` branch +If you're new to the codebase, see the next section for an overview of the code structure. +For more about the internals, see :ref:`internalapi` and information on extending the ``can.io`` module. -There is also a `python-can `__ -mailing list for development discussion. +Code Structure +^^^^^^^^^^^^^^ + +The modules in ``python-can`` are: + ++---------------------------------+------------------------------------------------------+ +|Module | Description | ++=================================+======================================================+ +|:doc:`interfaces ` | Contains interface dependent code. | ++---------------------------------+------------------------------------------------------+ +|:doc:`bus ` | Contains the interface independent Bus object. | ++---------------------------------+------------------------------------------------------+ +|:doc:`message ` | Contains the interface independent Message object. | ++---------------------------------+------------------------------------------------------+ +|:doc:`io ` | Contains a range of file readers and writers. | ++---------------------------------+------------------------------------------------------+ +|:doc:`broadcastmanager ` | Contains interface independent broadcast manager | +| | code. | ++---------------------------------+------------------------------------------------------+ -Some more information about the internals of this library can be found -in the chapter :ref:`internalapi`. -There is also additional information on extending the ``can.io`` module. +Step-by-Step Contribution Guide +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +1. **Fork and Clone the Repository** -Pre-releases ------------- + * Fork the python-can repository on GitHub to your own account. + * Clone your fork: + + .. code-block:: shell + + git clone https://github.com//python-can.git + cd python-can + + * Create a new branch for your work: + + .. code-block:: shell + + git checkout -b my-feature-branch + + * Ensure your branch is up to date with the latest changes from the main repository. + First, add the main repository as a remote (commonly named `upstream`) if you haven't already: + + .. code-block:: shell + + git remote add upstream https://github.com/hardbyte/python-can.git + + Then, regularly fetch and rebase from the main branch: + + .. code-block:: shell + + git fetch upstream + git rebase upstream/main + +2. **Set Up Your Development Environment** + + We recommend using `uv `__ to install development tools and run CLI utilities. + `uv` is a modern Python packaging tool that can quickly create virtual environments and manage dependencies, + including downloading required Python versions automatically. The `uvx` command allows you to run CLI tools + in isolated environments, separate from your global Python installation. This is useful for installing and + running Python applications (such as tox) without affecting your project's dependencies or environment. + + **Install tox (if not already available):** + + + .. code-block:: shell + + uv tool install tox --with tox-uv + + + **Quickly running your local python-can code** + + To run a local script (e.g., `snippet.py`) using your current python-can code, + you can use either the traditional `virtualenv` and `pip` workflow or the modern `uv` tool. + + **Traditional method (virtualenv + pip):** + + Create a virtual environment and install the package in editable mode. + This allows changes to your local code to be reflected immediately, without reinstalling. + + .. code-block:: shell + + # Create a new virtual environment + python -m venv .venv + + # Activate the environment + .venv\Scripts\activate # On Windows + source .venv/bin/activate # On Unix/macOS -The latest pre-release can be installed with:: + # Upgrade pip and install python-can in editable mode with development dependencies + python -m pip install --upgrade pip + pip install -e .[dev] - pip install --upgrade --pre python-can + # Run your script + python snippet.py + **Modern method (uv):** + With `uv`, you can run your script directly: -Building & Installing ---------------------- + .. code-block:: shell -The following assumes that the commands are executed from the root of the repository: + uv run snippet.py -The project can be built with:: + When ``uv run ...`` is called inside a project, + `uv` automatically sets up the environment and symlinks local packages. + No editable install is needed—changes to your code are reflected immediately. - pipx run build - pipx run twine check dist/* +3. **Make Your Changes** -The project can be installed in editable mode with:: + * Edit code, documentation, or tests as needed. + * If you fix a bug or add a feature, add or update tests in the ``test/`` directory. + * If your change affects users, update documentation in ``doc/`` and relevant docstrings. - pip install -e . +4. **Test Your Changes** -The unit tests can be run with:: + This project uses `tox `__ to automate all checks (tests, lint, type, docs). + Tox will set up isolated environments and run the right tools for you. - pipx run tox -e py + To run all checks: -The documentation can be built with:: + .. code-block:: shell - pipx run tox -e docs + tox -The linters can be run with:: + To run a specific check, use: - pip install --group lint -e . - black --check can - mypy can - ruff check can - pylint can/**.py can/io doc/conf.py examples/**.py can/interfaces/socketcan + .. code-block:: shell + tox -e lint # Run code style and lint checks (black, ruff, pylint) + tox -e type # Run type checks (mypy) + tox -e docs # Build and test documentation (sphinx) + tox -e py # Run tests (pytest) + + To run all checks in parallel (where supported), you can use: + + .. code-block:: shell + + tox p + + Some environments require specific Python versions. + If you use `uv`, it will automatically download and manage these for you. + +5. **(Optional) Build Source Distribution and Wheels** + + If you want to manually build the source distribution (sdist) and wheels for python-can, + you can use `uvx` to run the build and twine tools: + + .. code-block:: shell + + uv build + uvx twine check --strict dist/* + +6. **Push and Submit Your Contribution** + + * Push your branch: + + .. code-block:: shell + + git push origin my-feature-branch + + * Open a pull request from your branch to the ``main`` branch of the main python-can repository on GitHub. + + Please be patient — maintainers review contributions as time allows. Creating a new interface/backend -------------------------------- +.. attention:: + Please note: Pull requests that attempt to add new hardware interfaces directly to the + python-can codebase will not be accepted. Instead, we encourage contributors to create + plugins by publishing a Python package containing your :class:`can.BusABC` subclass and + using it within the python-can API. We will mention your package in this documentation + and add it as an optional dependency. For current best practices, please refer to + :ref:`plugin interface`. + + The following guideline is retained for informational purposes only and is not valid for new + contributions. + These steps are a guideline on how to add a new backend to python-can. -- Create a module (either a ``*.py`` or an entire subdirectory depending - on the complexity) inside ``can.interfaces`` -- Implement the central part of the backend: the bus class that extends +* Create a module (either a ``*.py`` or an entire subdirectory depending + on the complexity) inside ``can.interfaces``. See ``can/interfaces/socketcan`` for a reference implementation. +* Implement the central part of the backend: the bus class that extends :class:`can.BusABC`. See :ref:`businternals` for more info on this one! -- Register your backend bus class in ``BACKENDS`` in the file ``can.interfaces.__init__.py``. -- Add docs where appropriate. At a minimum add to ``doc/interfaces.rst`` and add +* Register your backend bus class in ``BACKENDS`` in the file ``can.interfaces.__init__.py``. +* Add docs where appropriate. At a minimum add to ``doc/interfaces.rst`` and add a new interface specific document in ``doc/interface/*``. It should document the supported platforms and also the hardware/software it requires. A small snippet of how to install the dependencies would also be useful to get people started without much friction. -- Also, don't forget to document your classes, methods and function with docstrings. -- Add tests in ``test/*`` where appropriate. - To get started, have a look at ``back2back_test.py``: - Simply add a test case like ``BasicTestSocketCan`` and some basic tests will be executed for the new interface. - -.. attention:: - We strongly recommend using the :ref:`plugin interface` to extend python-can. - Publish a python package that contains your :class:`can.BusABC` subclass and use - it within the python-can API. We will mention your package inside this documentation - and add it as an optional dependency. - -Code Structure --------------- - -The modules in ``python-can`` are: - -+---------------------------------+------------------------------------------------------+ -|Module | Description | -+=================================+======================================================+ -|:doc:`interfaces ` | Contains interface dependent code. | -+---------------------------------+------------------------------------------------------+ -|:doc:`bus ` | Contains the interface independent Bus object. | -+---------------------------------+------------------------------------------------------+ -|:doc:`message ` | Contains the interface independent Message object. | -+---------------------------------+------------------------------------------------------+ -|:doc:`io ` | Contains a range of file readers and writers. | -+---------------------------------+------------------------------------------------------+ -|:doc:`broadcastmanager ` | Contains interface independent broadcast manager | -| | code. | -+---------------------------------+------------------------------------------------------+ - +* Also, don't forget to document your classes, methods and function with docstrings. +* Add tests in ``test/*`` where appropriate. For example, see ``test/back2back_test.py`` and add a test case like ``BasicTestSocketCan`` for your new interface. Creating a new Release ---------------------- -- Release from the ``main`` branch (except for pre-releases). -- Check if any deprecations are pending. -- Run all tests and examples against available hardware. -- Update ``CONTRIBUTORS.txt`` with any new contributors. -- For larger changes update ``doc/history.rst``. -- Sanity check that documentation has stayed inline with code. -- In a new virtual env check that the package can be installed with pip: ``pip install python-can==X.Y.Z``. -- Create a new tag in the repository. -- Check the release on - `PyPi `__, - `Read the Docs `__ and - `GitHub `__. - +* Releases are automated via GitHub Actions. To create a new release: -Manual release steps (deprecated) ---------------------------------- + * Ensure all tests pass and documentation is up-to-date. + * Update ``CONTRIBUTORS.txt`` with any new contributors. + * For larger changes, update ``doc/history.rst``. + * Create a new tag and GitHub release (e.g., ``vX.Y.Z``) targeting the ``main`` branch. Add release notes and publish. + * The CI workflow will automatically build, check, and upload the release to PyPI and other platforms. -- Create a temporary virtual environment. -- Create a new tag in the repository. Use `semantic versioning `__. -- Build with ``pipx run build`` -- Sign the packages with gpg ``gpg --detach-sign -a dist/python_can-X.Y.Z-py3-none-any.whl``. -- Upload with twine ``twine upload dist/python-can-X.Y.Z*``. +* You can monitor the release status on: + `PyPi `__, + `Read the Docs `__ and + `GitHub Releases `__. diff --git a/doc/installation.rst b/doc/installation.rst index ff72ae21b..822de2ce0 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -21,6 +21,13 @@ Install the ``can`` package from PyPi with ``pip`` or similar:: $ pip install python-can[serial] +Pre-releases +------------ + +The latest pre-release can be installed with:: + + pip install --upgrade --pre python-can + GNU/Linux dependencies ---------------------- diff --git a/pyproject.toml b/pyproject.toml index 8fd77de21..9eb98e37b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools >= 67.7", "setuptools_scm>=8"] +requires = ["setuptools >= 77.0", "setuptools_scm>=8"] build-backend = "setuptools.build_meta" [project] @@ -13,7 +13,7 @@ dependencies = [ "typing_extensions>=3.10.0.0", ] requires-python = ">=3.9" -license = { text = "LGPL v3" } +license = "LGPL-3.0-only" classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", @@ -22,7 +22,6 @@ classifiers = [ "Intended Audience :: Information Technology", "Intended Audience :: Manufacturing", "Intended Audience :: Telecommunications Industry", - "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", "Natural Language :: English", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", @@ -100,9 +99,13 @@ test = [ "pytest-cov==4.0.0", "coverage==6.5.0", "hypothesis~=6.35.0", - "pyserial~=3.5", "parameterized~=0.8", ] +dev = [ + {include-group = "docs"}, + {include-group = "lint"}, + {include-group = "test"}, +] [tool.setuptools.dynamic] readme = { file = "README.rst" } @@ -195,8 +198,8 @@ ignore = [ known-first-party = ["can"] [tool.pylint] +extension-pkg-allow-list = ["curses"] disable = [ - "c-extension-no-member", "cyclic-import", "duplicate-code", "fixme", diff --git a/test/back2back_test.py b/test/back2back_test.py index a46597ef4..b52bae530 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -272,23 +272,21 @@ def test_sub_second_timestamp_resolution(self): self.bus2.recv(0) self.bus2.recv(0) - @unittest.skipIf(IS_CI, "fails randomly when run on CI server") def test_send_periodic_duration(self): """ Verify that send_periodic only transmits for the specified duration. Regression test for #1713. """ - for params in [(0.01, 0.003), (0.1, 0.011), (1, 0.4)]: - duration, period = params + for duration, period in [(0.01, 0.003), (0.1, 0.011), (1, 0.4)]: messages = [] self.bus2.send_periodic(can.Message(), period, duration) - while (msg := self.bus1.recv(period * 1.25)) is not None: + while (msg := self.bus1.recv(period + self.TIMEOUT)) is not None: messages.append(msg) - delta_t = round(messages[-1].timestamp - messages[0].timestamp, 2) - assert delta_t <= duration + delta_t = messages[-1].timestamp - messages[0].timestamp + assert delta_t < duration + 0.05 @unittest.skipUnless(TEST_INTERFACE_SOCKETCAN, "skip testing of socketcan") diff --git a/test/test_socketcan.py b/test/test_socketcan.py index af06b8169..3df233f96 100644 --- a/test/test_socketcan.py +++ b/test/test_socketcan.py @@ -5,6 +5,7 @@ """ import ctypes import struct +import sys import unittest import warnings from unittest.mock import patch @@ -34,6 +35,7 @@ def setUp(self): self._ctypes_sizeof = ctypes.sizeof self._ctypes_alignment = ctypes.alignment + @unittest.skipIf(sys.version_info >= (3, 14), "Fails on Python 3.14 or newer") @patch("ctypes.sizeof") @patch("ctypes.alignment") def test_bcm_header_factory_32_bit_sizeof_long_4_alignof_long_4( @@ -103,6 +105,7 @@ def side_effect_ctypes_alignment(value): ] self.assertEqual(expected_fields, BcmMsgHead._fields_) + @unittest.skipIf(sys.version_info >= (3, 14), "Fails on Python 3.14 or newer") @patch("ctypes.sizeof") @patch("ctypes.alignment") def test_bcm_header_factory_32_bit_sizeof_long_4_alignof_long_long_8( @@ -172,6 +175,7 @@ def side_effect_ctypes_alignment(value): ] self.assertEqual(expected_fields, BcmMsgHead._fields_) + @unittest.skipIf(sys.version_info >= (3, 14), "Fails on Python 3.14 or newer") @patch("ctypes.sizeof") @patch("ctypes.alignment") def test_bcm_header_factory_64_bit_sizeof_long_8_alignof_long_8( diff --git a/tox.ini b/tox.ini index c69f541f4..5f393cb93 100644 --- a/tox.ini +++ b/tox.ini @@ -1,46 +1,83 @@ +# https://tox.wiki/en/latest/config.html [tox] -min_version = 4.22 +min_version = 4.26 +env_list = py,lint,type,docs [testenv] +passenv = + CI + GITHUB_* + COVERALLS_* + PY_COLORS + TEST_SOCKETCAN dependency_groups = test -deps = - asammdf>=6.0; platform_python_implementation=="CPython" and python_version<"3.14" - msgpack~=1.1.0; python_version<"3.14" - pywin32>=305; platform_system=="Windows" and platform_python_implementation=="CPython" and python_version<"3.14" +extras = + canalystii + mf4 + multicast + pywin32 + serial + viewer commands = pytest {posargs} + +[testenv:py314] extras = canalystii + serial + pywin32 -[testenv:gh] -passenv = - CI - GITHUB_* - COVERALLS_* - PY_COLORS - TEST_SOCKETCAN +[testenv:{py313t,py314t,pypy310,pypy311}] +extras = + canalystii + serial [testenv:docs] description = Build and test the documentation -basepython = py312 +basepython = py313 dependency_groups = docs extras = canalystii gs-usb - mf4 - remote - serial commands = python -m sphinx -b html -Wan --keep-going doc build python -m sphinx -b doctest -W --keep-going doc build +[testenv:lint] +description = Run linters +basepython = py313 +dependency_groups = + lint +extras = + viewer +commands = + black --check . + ruff check can examples doc + pylint \ + can/**.py \ + can/io \ + doc/conf.py \ + examples/**.py \ + can/interfaces/socketcan + +[testenv:type] +description = Run type checker +basepython = py313 +dependency_groups = + lint +extras = +commands = + mypy --python-version 3.9 . + mypy --python-version 3.10 . + mypy --python-version 3.11 . + mypy --python-version 3.12 . + mypy --python-version 3.13 . [pytest] testpaths = test -addopts = -v --timeout=300 --cov=can --cov-config=tox.ini --cov-report=lcov --cov-report=term - +addopts = -v --timeout=300 --cov=can --cov-config=tox.ini --cov-report=lcov --cov-report=term --color=yes [coverage:run] # we could also use branch coverage From a4f4742df593b2de917ebbfb68a76f4210396a8d Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sun, 27 Jul 2025 13:52:23 +0200 Subject: [PATCH 095/127] Minor Typing Improvements (#1962) --- can/bit_timing.py | 4 +- can/bus.py | 10 ++-- can/interface.py | 6 +-- can/interfaces/cantact.py | 21 +++++--- can/interfaces/ixxat/canlib.py | 3 +- can/interfaces/ixxat/canlib_vcinpl.py | 8 ++- can/interfaces/serial/serial_can.py | 33 ++++++------ can/interfaces/socketcan/utils.py | 3 +- can/interfaces/udp_multicast/bus.py | 12 +++-- can/interfaces/udp_multicast/utils.py | 4 +- can/interfaces/vector/canlib.py | 22 +++++--- can/interfaces/vector/exceptions.py | 8 ++- can/interfaces/virtual.py | 17 +++--- can/io/mf4.py | 2 +- can/listener.py | 4 +- can/logconvert.py | 10 ++-- can/message.py | 4 +- can/notifier.py | 2 +- can/thread_safe_bus.py | 74 +++++++++++++++++---------- can/typechecking.py | 8 ++- can/viewer.py | 2 +- pyproject.toml | 6 --- 22 files changed, 150 insertions(+), 113 deletions(-) diff --git a/can/bit_timing.py b/can/bit_timing.py index 4b0074472..2bb04bfbe 100644 --- a/can/bit_timing.py +++ b/can/bit_timing.py @@ -7,7 +7,7 @@ from can.typechecking import BitTimingDict, BitTimingFdDict -class BitTiming(Mapping): +class BitTiming(Mapping[str, int]): """Representation of a bit timing configuration for a CAN 2.0 bus. The class can be constructed in multiple ways, depending on the information @@ -477,7 +477,7 @@ def __hash__(self) -> int: return tuple(self._data.values()).__hash__() -class BitTimingFd(Mapping): +class BitTimingFd(Mapping[str, int]): """Representation of a bit timing configuration for a CAN FD bus. The class can be constructed in multiple ways, depending on the information diff --git a/can/bus.py b/can/bus.py index f053c4aa7..ec9eb09b7 100644 --- a/can/bus.py +++ b/can/bus.py @@ -5,7 +5,7 @@ import contextlib import logging import threading -from abc import ABC, ABCMeta, abstractmethod +from abc import ABC, abstractmethod from collections.abc import Iterator, Sequence from enum import Enum, auto from time import time @@ -19,7 +19,6 @@ from typing_extensions import Self -import can import can.typechecking from can.broadcastmanager import CyclicSendTaskABC, ThreadBasedCyclicSendTask from can.message import Message @@ -44,7 +43,7 @@ class CanProtocol(Enum): CAN_XL = auto() -class BusABC(metaclass=ABCMeta): +class BusABC(ABC): """The CAN Bus Abstract Base Class that serves as the basis for all concrete interfaces. @@ -68,7 +67,7 @@ class BusABC(metaclass=ABCMeta): @abstractmethod def __init__( self, - channel: Optional[can.typechecking.Channel], + channel: can.typechecking.Channel, can_filters: Optional[can.typechecking.CanFilters] = None, **kwargs: object, ): @@ -447,7 +446,6 @@ def _matches_filters(self, msg: Message) -> bool: for _filter in self._filters: # check if this filter even applies to the message if "extended" in _filter: - _filter = cast("can.typechecking.CanFilterExtended", _filter) if _filter["extended"] != msg.is_extended_id: continue @@ -524,7 +522,7 @@ def protocol(self) -> CanProtocol: return self._can_protocol @staticmethod - def _detect_available_configs() -> list[can.typechecking.AutoDetectedConfig]: + def _detect_available_configs() -> Sequence[can.typechecking.AutoDetectedConfig]: """Detect all configurations/channels that this interface could currently connect with. diff --git a/can/interface.py b/can/interface.py index eee58ff41..4aa010a36 100644 --- a/can/interface.py +++ b/can/interface.py @@ -7,7 +7,7 @@ import concurrent.futures.thread import importlib import logging -from collections.abc import Callable, Iterable +from collections.abc import Callable, Iterable, Sequence from typing import Any, Optional, Union, cast from . import util @@ -142,7 +142,7 @@ def Bus( # noqa: N802 def detect_available_configs( interfaces: Union[None, str, Iterable[str]] = None, timeout: float = 5.0, -) -> list[AutoDetectedConfig]: +) -> Sequence[AutoDetectedConfig]: """Detect all configurations/channels that the interfaces could currently connect with. @@ -175,7 +175,7 @@ def detect_available_configs( # otherwise assume iterable of strings # Collect detection callbacks - callbacks: dict[str, Callable[[], list[AutoDetectedConfig]]] = {} + callbacks: dict[str, Callable[[], Sequence[AutoDetectedConfig]]] = {} for interface_keyword in interfaces: try: bus_class = _get_class_for_interface(interface_keyword) diff --git a/can/interfaces/cantact.py b/can/interfaces/cantact.py index 963a9ee3b..08fe15b72 100644 --- a/can/interfaces/cantact.py +++ b/can/interfaces/cantact.py @@ -4,6 +4,7 @@ import logging import time +from collections.abc import Sequence from typing import Any, Optional, Union from unittest.mock import Mock @@ -14,6 +15,7 @@ CanInterfaceNotImplementedError, error_check, ) +from ..typechecking import AutoDetectedConfig from ..util import check_or_adjust_timing_clock, deprecated_args_alias logger = logging.getLogger(__name__) @@ -31,7 +33,7 @@ class CantactBus(BusABC): """CANtact interface""" @staticmethod - def _detect_available_configs(): + def _detect_available_configs() -> Sequence[AutoDetectedConfig]: try: interface = cantact.Interface() except (NameError, SystemError, AttributeError): @@ -40,7 +42,7 @@ def _detect_available_configs(): ) return [] - channels = [] + channels: list[AutoDetectedConfig] = [] for i in range(0, interface.channel_count()): channels.append({"interface": "cantact", "channel": f"ch:{i}"}) return channels @@ -121,7 +123,14 @@ def __init__( **kwargs, ) - def _recv_internal(self, timeout): + def _recv_internal( + self, timeout: Optional[float] + ) -> tuple[Optional[Message], bool]: + if timeout is None: + raise TypeError( + f"{self.__class__.__name__} expects a numeric `timeout` value." + ) + with error_check("Cannot receive message"): frame = self.interface.recv(int(timeout * 1000)) if frame is None: @@ -140,7 +149,7 @@ def _recv_internal(self, timeout): ) return msg, False - def send(self, msg, timeout=None): + def send(self, msg: Message, timeout: Optional[float] = None) -> None: with error_check("Cannot send message"): self.interface.send( self.channel, @@ -151,13 +160,13 @@ def send(self, msg, timeout=None): msg.data, ) - def shutdown(self): + def shutdown(self) -> None: super().shutdown() with error_check("Cannot shutdown interface"): self.interface.stop() -def mock_recv(timeout): +def mock_recv(timeout: int) -> Optional[dict[str, Any]]: if timeout > 0: return { "id": 0x123, diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index e6ad25d57..6192367c4 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -9,7 +9,6 @@ CyclicSendTaskABC, Message, ) -from can.typechecking import AutoDetectedConfig class IXXATBus(BusABC): @@ -175,5 +174,5 @@ def state(self) -> BusState: return self.bus.state @staticmethod - def _detect_available_configs() -> list[AutoDetectedConfig]: + def _detect_available_configs() -> Sequence[vcinpl.AutoDetectedIxxatConfig]: return vcinpl._detect_available_configs() diff --git a/can/interfaces/ixxat/canlib_vcinpl.py b/can/interfaces/ixxat/canlib_vcinpl.py index 098b022bb..59f98417d 100644 --- a/can/interfaces/ixxat/canlib_vcinpl.py +++ b/can/interfaces/ixxat/canlib_vcinpl.py @@ -976,8 +976,8 @@ def get_ixxat_hwids(): return hwids -def _detect_available_configs() -> list[AutoDetectedConfig]: - config_list = [] # list in wich to store the resulting bus kwargs +def _detect_available_configs() -> Sequence["AutoDetectedIxxatConfig"]: + config_list = [] # list in which to store the resulting bus kwargs # used to detect HWID device_handle = HANDLE() @@ -1026,3 +1026,7 @@ def _detect_available_configs() -> list[AutoDetectedConfig]: pass # _canlib is None in the CI tests -> return a blank list return config_list + + +class AutoDetectedIxxatConfig(AutoDetectedConfig): + unique_hardware_id: int diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index 12ce5aff1..680cf10f6 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -10,7 +10,8 @@ import io import logging import struct -from typing import Any, Optional +from collections.abc import Sequence +from typing import Any, Optional, cast from can import ( BusABC, @@ -26,21 +27,14 @@ logger = logging.getLogger("can.serial") try: - import serial + import serial.tools.list_ports except ImportError: logger.warning( "You won't be able to use the serial can backend without " - "the serial module installed!" + "the `pyserial` package installed!" ) serial = None -try: - from serial.tools.list_ports import comports as list_comports -except ImportError: - # If unavailable on some platform, just return nothing - def list_comports() -> list[Any]: - return [] - CAN_ERR_FLAG = 0x20000000 CAN_RTR_FLAG = 0x40000000 @@ -63,8 +57,7 @@ def __init__( baudrate: int = 115200, timeout: float = 0.1, rtscts: bool = False, - *args, - **kwargs, + **kwargs: Any, ) -> None: """ :param channel: @@ -107,7 +100,7 @@ def __init__( "could not create the serial device" ) from error - super().__init__(channel, *args, **kwargs) + super().__init__(channel, **kwargs) def shutdown(self) -> None: """ @@ -232,7 +225,7 @@ def _recv_internal( def fileno(self) -> int: try: - return self._ser.fileno() + return cast("int", self._ser.fileno()) except io.UnsupportedOperation: raise NotImplementedError( "fileno is not implemented using current CAN bus on this platform" @@ -241,7 +234,11 @@ def fileno(self) -> int: raise CanOperationError("Cannot fetch fileno") from exception @staticmethod - def _detect_available_configs() -> list[AutoDetectedConfig]: - return [ - {"interface": "serial", "channel": port.device} for port in list_comports() - ] + def _detect_available_configs() -> Sequence[AutoDetectedConfig]: + configs: list[AutoDetectedConfig] = [] + if serial is None: + return configs + + for port in serial.tools.list_ports.comports(): + configs.append({"interface": "serial", "channel": port.device}) + return configs diff --git a/can/interfaces/socketcan/utils.py b/can/interfaces/socketcan/utils.py index 80dcb203f..1c096f66e 100644 --- a/can/interfaces/socketcan/utils.py +++ b/can/interfaces/socketcan/utils.py @@ -9,7 +9,7 @@ import struct import subprocess import sys -from typing import Optional, cast +from typing import Optional from can import typechecking from can.interfaces.socketcan.constants import CAN_EFF_FLAG @@ -28,7 +28,6 @@ def pack_filters(can_filters: Optional[typechecking.CanFilters] = None) -> bytes can_id = can_filter["can_id"] can_mask = can_filter["can_mask"] if "extended" in can_filter: - can_filter = cast("typechecking.CanFilterExtended", can_filter) # Match on either 11-bit OR 29-bit messages instead of both can_mask |= CAN_EFF_FLAG if can_filter["extended"]: diff --git a/can/interfaces/udp_multicast/bus.py b/can/interfaces/udp_multicast/bus.py index 45882ec07..9e0187ea2 100644 --- a/can/interfaces/udp_multicast/bus.py +++ b/can/interfaces/udp_multicast/bus.py @@ -6,10 +6,10 @@ import struct import time import warnings -from typing import Optional, Union +from typing import Any, Optional, Union import can -from can import BusABC, CanProtocol +from can import BusABC, CanProtocol, Message from can.typechecking import AutoDetectedConfig from .utils import is_msgpack_installed, pack_message, unpack_message @@ -98,7 +98,7 @@ def __init__( hop_limit: int = 1, receive_own_messages: bool = False, fd: bool = True, - **kwargs, + **kwargs: Any, ) -> None: is_msgpack_installed() @@ -126,7 +126,9 @@ def is_fd(self) -> bool: ) return self._can_protocol is CanProtocol.CAN_FD - def _recv_internal(self, timeout: Optional[float]): + def _recv_internal( + self, timeout: Optional[float] + ) -> tuple[Optional[Message], bool]: result = self._multicast.recv(timeout) if not result: return None, False @@ -204,7 +206,7 @@ def __init__( # Look up multicast group address in name server and find out IP version of the first suitable target # and then get the address family of it (socket.AF_INET or socket.AF_INET6) - connection_candidates = socket.getaddrinfo( # type: ignore + connection_candidates = socket.getaddrinfo( group, self.port, type=socket.SOCK_DGRAM ) sock = None diff --git a/can/interfaces/udp_multicast/utils.py b/can/interfaces/udp_multicast/utils.py index c6b2630a5..de39833a3 100644 --- a/can/interfaces/udp_multicast/utils.py +++ b/can/interfaces/udp_multicast/utils.py @@ -2,7 +2,7 @@ Defines common functions. """ -from typing import Any, Optional +from typing import Any, Optional, cast from can import CanInterfaceNotImplementedError, Message from can.typechecking import ReadableBytesLike @@ -51,7 +51,7 @@ def pack_message(message: Message) -> bytes: "bitrate_switch": message.bitrate_switch, "error_state_indicator": message.error_state_indicator, } - return msgpack.packb(as_dict, use_bin_type=True) + return cast("bytes", msgpack.packb(as_dict, use_bin_type=True)) def unpack_message( diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 986f52002..d15b89803 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -982,8 +982,8 @@ def reset(self) -> None: ) @staticmethod - def _detect_available_configs() -> list[AutoDetectedConfig]: - configs = [] + def _detect_available_configs() -> Sequence["AutoDetectedVectorConfig"]: + configs: list[AutoDetectedVectorConfig] = [] channel_configs = get_channel_configs() LOG.info("Found %d channels", len(channel_configs)) for channel_config in channel_configs: @@ -999,16 +999,13 @@ def _detect_available_configs() -> list[AutoDetectedConfig]: ) configs.append( { - # data for use in VectorBus.__init__(): "interface": "vector", "channel": channel_config.hw_channel, "serial": channel_config.serial_number, "channel_index": channel_config.channel_index, - # data for use in VectorBus.set_application_config(): "hw_type": channel_config.hw_type, "hw_index": channel_config.hw_index, "hw_channel": channel_config.hw_channel, - # additional information: "supports_fd": bool( channel_config.channel_capabilities & xldefine.XL_ChannelCapabilities.XL_CHANNEL_FLAG_CANFD_ISO_SUPPORT @@ -1016,7 +1013,7 @@ def _detect_available_configs() -> list[AutoDetectedConfig]: "vector_channel_config": channel_config, } ) - return configs # type: ignore + return configs @staticmethod def popup_vector_hw_configuration(wait_for_finish: int = 0) -> None: @@ -1188,6 +1185,19 @@ class VectorChannelConfig(NamedTuple): transceiver_name: str +class AutoDetectedVectorConfig(AutoDetectedConfig): + # data for use in VectorBus.__init__(): + serial: int + channel_index: int + # data for use in VectorBus.set_application_config(): + hw_type: int + hw_index: int + hw_channel: int + # additional information: + supports_fd: bool + vector_channel_config: VectorChannelConfig + + def _get_xl_driver_config() -> xlclass.XLdriverConfig: if xldriver is None: raise VectorError( diff --git a/can/interfaces/vector/exceptions.py b/can/interfaces/vector/exceptions.py index 53c774e6f..b43df5e6c 100644 --- a/can/interfaces/vector/exceptions.py +++ b/can/interfaces/vector/exceptions.py @@ -1,10 +1,14 @@ """Exception/error declarations for the vector interface.""" +from typing import Any, Optional, Union + from can import CanError, CanInitializationError, CanOperationError class VectorError(CanError): - def __init__(self, error_code, error_string, function): + def __init__( + self, error_code: Optional[int], error_string: str, function: str + ) -> None: super().__init__( message=f"{function} failed ({error_string})", error_code=error_code ) @@ -12,7 +16,7 @@ def __init__(self, error_code, error_string, function): # keep reference to args for pickling self._args = error_code, error_string, function - def __reduce__(self): + def __reduce__(self) -> Union[str, tuple[Any, ...]]: return type(self), self._args, {} diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index 6b62e5ceb..e4f68b0c4 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -12,7 +12,7 @@ from copy import deepcopy from random import randint from threading import RLock -from typing import Any, Optional +from typing import Any, Final, Optional from can import CanOperationError from can.bus import BusABC, CanProtocol @@ -21,10 +21,9 @@ logger = logging.getLogger(__name__) - # Channels are lists of queues, one for each connection -channels: dict[Optional[Channel], list[queue.Queue[Message]]] = {} -channels_lock = RLock() +channels: Final[dict[Channel, list[queue.Queue[Message]]]] = {} +channels_lock: Final = RLock() class VirtualBus(BusABC): @@ -54,7 +53,7 @@ class VirtualBus(BusABC): def __init__( self, - channel: Optional[Channel] = None, + channel: Channel = "channel-0", receive_own_messages: bool = False, rx_queue_size: int = 0, preserve_timestamps: bool = False, @@ -67,9 +66,9 @@ def __init__( bus by virtual instances constructed with the same channel identifier. :param channel: The channel identifier. This parameter can be an - arbitrary value. The bus instance will be able to see messages - from other virtual bus instances that were created with the same - value. + arbitrary hashable value. The bus instance will be able to see + messages from other virtual bus instances that were created with + the same value. :param receive_own_messages: If set to True, sent messages will be reflected back on the input queue. :param rx_queue_size: The size of the reception queue. The reception @@ -179,7 +178,7 @@ def _detect_available_configs() -> list[AutoDetectedConfig]: available_channels = list(channels.keys()) # find a currently unused channel - def get_extra(): + def get_extra() -> str: return f"channel-{randint(0, 9999)}" extra = get_extra() diff --git a/can/io/mf4.py b/can/io/mf4.py index 7007c3627..68aff87ae 100644 --- a/can/io/mf4.py +++ b/can/io/mf4.py @@ -274,7 +274,7 @@ def on_message_received(self, msg: Message) -> None: self._rtr_buffer = np.zeros(1, dtype=RTR_DTYPE) -class FrameIterator(metaclass=abc.ABCMeta): +class FrameIterator(abc.ABC): """ Iterator helper class for common handling among CAN DataFrames, ErrorFrames and RemoteFrames. """ diff --git a/can/listener.py b/can/listener.py index 6256d33b6..7f8f436a0 100644 --- a/can/listener.py +++ b/can/listener.py @@ -5,7 +5,7 @@ import asyncio import sys import warnings -from abc import ABCMeta, abstractmethod +from abc import ABC, abstractmethod from collections.abc import AsyncIterator from queue import Empty, SimpleQueue from typing import Any, Optional @@ -14,7 +14,7 @@ from can.message import Message -class Listener(metaclass=ABCMeta): +class Listener(ABC): """The basic listener that can be called directly to handle some CAN message:: diff --git a/can/logconvert.py b/can/logconvert.py index 49cdaf4bb..4527dc23e 100644 --- a/can/logconvert.py +++ b/can/logconvert.py @@ -5,17 +5,21 @@ import argparse import errno import sys +from typing import TYPE_CHECKING, NoReturn from can import Logger, LogReader, SizedRotatingLogger +if TYPE_CHECKING: + from can.io.generic import MessageWriter + class ArgumentParser(argparse.ArgumentParser): - def error(self, message): + def error(self, message: str) -> NoReturn: self.print_help(sys.stderr) self.exit(errno.EINVAL, f"{self.prog}: error: {message}\n") -def main(): +def main() -> None: parser = ArgumentParser( description="Convert a log file from one format to another.", ) @@ -47,7 +51,7 @@ def main(): with LogReader(args.input) as reader: if args.file_size: - logger = SizedRotatingLogger( + logger: MessageWriter = SizedRotatingLogger( base_filename=args.output, max_bytes=args.file_size ) else: diff --git a/can/message.py b/can/message.py index d8d94ea84..c1fbffd21 100644 --- a/can/message.py +++ b/can/message.py @@ -8,7 +8,7 @@ from copy import deepcopy from math import isinf, isnan -from typing import Optional +from typing import Any, Optional from . import typechecking @@ -210,7 +210,7 @@ def __copy__(self) -> "Message": error_state_indicator=self.error_state_indicator, ) - def __deepcopy__(self, memo: dict) -> "Message": + def __deepcopy__(self, memo: Optional[dict[int, Any]]) -> "Message": return Message( timestamp=self.timestamp, arbitration_id=self.arbitration_id, diff --git a/can/notifier.py b/can/notifier.py index 2b9944450..b2f550df7 100644 --- a/can/notifier.py +++ b/can/notifier.py @@ -103,7 +103,7 @@ def find_instances(self, bus: BusABC) -> tuple["Notifier", ...]: return tuple(instance_list) -class Notifier(AbstractContextManager): +class Notifier(AbstractContextManager["Notifier"]): _registry: Final = _NotifierRegistry() diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index 9b008667f..35a4f400c 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -1,4 +1,12 @@ +from contextlib import nullcontext from threading import RLock +from typing import Any, Optional + +from can import typechecking +from can.bus import BusABC, BusState, CanProtocol +from can.message import Message + +from .interface import Bus try: # Only raise an exception on instantiation but allow module @@ -10,10 +18,6 @@ ObjectProxy = object import_exc = exc -from contextlib import nullcontext - -from .interface import Bus - class ThreadSafeBus(ObjectProxy): # pylint: disable=abstract-method """ @@ -32,65 +36,81 @@ class ThreadSafeBus(ObjectProxy): # pylint: disable=abstract-method instead of :meth:`~can.BusABC.recv` directly. """ - def __init__(self, *args, **kwargs): + __wrapped__: BusABC + + def __init__( + self, + channel: Optional[typechecking.Channel] = None, + interface: Optional[str] = None, + config_context: Optional[str] = None, + ignore_config: bool = False, + **kwargs: Any, + ) -> None: if import_exc is not None: raise import_exc - super().__init__(Bus(*args, **kwargs)) + super().__init__( + Bus( + channel=channel, + interface=interface, + config_context=config_context, + ignore_config=ignore_config, + **kwargs, + ) + ) # now, BusABC.send_periodic() does not need a lock anymore, but the # implementation still requires a context manager - self.__wrapped__._lock_send_periodic = nullcontext() + self.__wrapped__._lock_send_periodic = nullcontext() # type: ignore[assignment] # init locks for sending and receiving separately self._lock_send = RLock() self._lock_recv = RLock() - def recv( - self, timeout=None, *args, **kwargs - ): # pylint: disable=keyword-arg-before-vararg + def recv(self, timeout: Optional[float] = None) -> Optional[Message]: with self._lock_recv: - return self.__wrapped__.recv(timeout=timeout, *args, **kwargs) + return self.__wrapped__.recv(timeout=timeout) - def send( - self, msg, timeout=None, *args, **kwargs - ): # pylint: disable=keyword-arg-before-vararg + def send(self, msg: Message, timeout: Optional[float] = None) -> None: with self._lock_send: - return self.__wrapped__.send(msg, timeout=timeout, *args, **kwargs) + return self.__wrapped__.send(msg=msg, timeout=timeout) # send_periodic does not need a lock, since the underlying # `send` method is already synchronized @property - def filters(self): + def filters(self) -> Optional[typechecking.CanFilters]: with self._lock_recv: return self.__wrapped__.filters @filters.setter - def filters(self, filters): + def filters(self, filters: Optional[typechecking.CanFilters]) -> None: with self._lock_recv: self.__wrapped__.filters = filters - def set_filters( - self, filters=None, *args, **kwargs - ): # pylint: disable=keyword-arg-before-vararg + def set_filters(self, filters: Optional[typechecking.CanFilters] = None) -> None: with self._lock_recv: - return self.__wrapped__.set_filters(filters=filters, *args, **kwargs) + return self.__wrapped__.set_filters(filters=filters) - def flush_tx_buffer(self, *args, **kwargs): + def flush_tx_buffer(self) -> None: with self._lock_send: - return self.__wrapped__.flush_tx_buffer(*args, **kwargs) + return self.__wrapped__.flush_tx_buffer() - def shutdown(self, *args, **kwargs): + def shutdown(self) -> None: with self._lock_send, self._lock_recv: - return self.__wrapped__.shutdown(*args, **kwargs) + return self.__wrapped__.shutdown() @property - def state(self): + def state(self) -> BusState: with self._lock_send, self._lock_recv: return self.__wrapped__.state @state.setter - def state(self, new_state): + def state(self, new_state: BusState) -> None: with self._lock_send, self._lock_recv: self.__wrapped__.state = new_state + + @property + def protocol(self) -> CanProtocol: + with self._lock_send, self._lock_recv: + return self.__wrapped__.protocol diff --git a/can/typechecking.py b/can/typechecking.py index fc0c87c0d..8c25e8b57 100644 --- a/can/typechecking.py +++ b/can/typechecking.py @@ -21,18 +21,16 @@ import struct -class CanFilter(TypedDict): +class _CanFilterBase(TypedDict): can_id: int can_mask: int -class CanFilterExtended(TypedDict): - can_id: int - can_mask: int +class CanFilter(_CanFilterBase, total=False): extended: bool -CanFilters = Sequence[Union[CanFilter, CanFilterExtended]] +CanFilters = Sequence[CanFilter] # TODO: Once buffer protocol support lands in typing, we should switch to that, # since can.message.Message attempts to call bytearray() on the given data, so diff --git a/can/viewer.py b/can/viewer.py index 81e8942a4..97bda1676 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -159,7 +159,7 @@ def run(self): # Unpack the data and then convert it into SI-units @staticmethod - def unpack_data(cmd: int, cmd_to_struct: dict, data: bytes) -> list[float]: + def unpack_data(cmd: int, cmd_to_struct: TDataStructs, data: bytes) -> list[float]: if not cmd_to_struct or not data: # These messages do not contain a data package return [] diff --git a/pyproject.toml b/pyproject.toml index 9eb98e37b..47d8e7a6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -130,11 +130,8 @@ disallow_incomplete_defs = true warn_redundant_casts = true warn_unused_ignores = true exclude = [ - "venv", "^doc/conf.py$", - "^build", "^test", - "^can/interfaces/__init__.py", "^can/interfaces/etas", "^can/interfaces/gs_usb", "^can/interfaces/ics_neovi", @@ -144,12 +141,9 @@ exclude = [ "^can/interfaces/nican", "^can/interfaces/neousys", "^can/interfaces/pcan", - "^can/interfaces/serial", "^can/interfaces/socketcan", "^can/interfaces/systec", - "^can/interfaces/udp_multicast", "^can/interfaces/usb2can", - "^can/interfaces/virtual", ] [tool.ruff] From 7c521af9dac83ee76c4ae9eeb092b51c5e21abc7 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sun, 27 Jul 2025 15:22:17 +0200 Subject: [PATCH 096/127] Update Test Dependencies (#1963) * update test dependencies * use pytest-modern for prettier pytest output * update ruff * fix coveralls flag-name * checkout to fix failing "git log" cmd --- .github/workflows/ci.yml | 3 ++- can/broadcastmanager.py | 4 ++-- can/interfaces/systec/structures.py | 5 +++++ pyproject.toml | 19 ++++++++++--------- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c3bb407f..b799b463e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,7 +51,7 @@ jobs: uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.github_token }} - flag-name: Unittests-${{ matrix.os }}-${{ matrix.python-version }} + flag-name: Unittests-${{ matrix.os }}-${{ matrix.env }} parallel: true path-to-lcov: ./coverage.lcov @@ -59,6 +59,7 @@ jobs: needs: test runs-on: ubuntu-latest steps: + - uses: actions/checkout@v4 - name: Coveralls Finished uses: coverallsapp/github-action@v2 with: diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index b2bc28e76..a71f6fd11 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -39,8 +39,8 @@ class _Pywin32Event: class _Pywin32: def __init__(self) -> None: - import pywintypes # pylint: disable=import-outside-toplevel,import-error - import win32event # pylint: disable=import-outside-toplevel,import-error + import pywintypes # noqa: PLC0415 # pylint: disable=import-outside-toplevel,import-error + import win32event # noqa: PLC0415 # pylint: disable=import-outside-toplevel,import-error self.pywintypes = pywintypes self.win32event = win32event diff --git a/can/interfaces/systec/structures.py b/can/interfaces/systec/structures.py index c80f21d44..a50ac4c26 100644 --- a/can/interfaces/systec/structures.py +++ b/can/interfaces/systec/structures.py @@ -51,6 +51,7 @@ class CanMsg(Structure): DWORD, ), # Receive time stamp in ms (for transmit messages no meaning) ] + __hash__ = Structure.__hash__ def __init__( self, id_=0, frame_format=MsgFrameFormat.MSG_FF_STD, data=None, dlc=None @@ -116,6 +117,7 @@ class Status(Structure): ("m_wCanStatus", WORD), # CAN error status (see enum :class:`CanStatus`) ("m_wUsbStatus", WORD), # USB error status (see enum :class:`UsbStatus`) ] + __hash__ = Structure.__hash__ def __eq__(self, other): if not isinstance(other, Status): @@ -171,6 +173,7 @@ class InitCanParam(Structure): WORD, ), # number of transmit buffer entries (default is 4096) ] + __hash__ = Structure.__hash__ def __init__( self, mode, BTR, OCR, AMR, ACR, baudrate, rx_buffer_entries, tx_buffer_entries @@ -277,6 +280,7 @@ class HardwareInfoEx(Structure): ("m_dwUniqueId3", DWORD), ("m_dwFlags", DWORD), # additional flags ] + __hash__ = Structure.__hash__ def __init__(self): super().__init__(sizeof(HardwareInfoEx)) @@ -389,6 +393,7 @@ class ChannelInfo(Structure): WORD, ), # CAN status (same as received by method :meth:`UcanServer.get_status`) ] + __hash__ = Structure.__hash__ def __init__(self): super().__init__(sizeof(ChannelInfo)) diff --git a/pyproject.toml b/pyproject.toml index 47d8e7a6d..51bcff693 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,18 +88,19 @@ docs = [ ] lint = [ "pylint==3.3.*", - "ruff==0.11.12", + "ruff==0.12.5", "black==25.1.*", - "mypy==1.16.*", + "mypy==1.17.*", ] test = [ - "pytest==8.3.*", - "pytest-timeout==2.1.*", - "coveralls==3.3.1", - "pytest-cov==4.0.0", - "coverage==6.5.0", - "hypothesis~=6.35.0", - "parameterized~=0.8", + "pytest==8.4.*", + "pytest-timeout==2.4.*", + "pytest-modern==0.7.*;platform_system!='Windows'", + "coveralls==4.0.*", + "pytest-cov==6.2.*", + "coverage==7.10.*", + "hypothesis==6.136.*", + "parameterized==0.9.*", ] dev = [ {include-group = "docs"}, From 51689bca00ce257a12d5b6b508f3da330f14a553 Mon Sep 17 00:00:00 2001 From: John Whittington Date: Tue, 5 Aug 2025 14:12:54 +0200 Subject: [PATCH 097/127] Mf4Reader: support files from ihedvall/mdflib (#1967) --- can/io/mf4.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/can/io/mf4.py b/can/io/mf4.py index 68aff87ae..bf594e3a5 100644 --- a/can/io/mf4.py +++ b/can/io/mf4.py @@ -352,7 +352,10 @@ def __iter__(self) -> Generator[Message, None, None]: if "CAN_DataFrame.BusChannel" in names: kv["channel"] = int(data["CAN_DataFrame.BusChannel"][i]) if "CAN_DataFrame.Dir" in names: - kv["is_rx"] = int(data["CAN_DataFrame.Dir"][i]) == 0 + if data["CAN_DataFrame.Dir"][i].dtype.kind == "S": + kv["is_rx"] = data["CAN_DataFrame.Dir"][i] == b"Rx" + else: + kv["is_rx"] = int(data["CAN_DataFrame.Dir"][i]) == 0 if "CAN_DataFrame.IDE" in names: kv["is_extended_id"] = bool(data["CAN_DataFrame.IDE"][i]) if "CAN_DataFrame.EDL" in names: @@ -387,7 +390,10 @@ def __iter__(self) -> Generator[Message, None, None]: if "CAN_ErrorFrame.BusChannel" in names: kv["channel"] = int(data["CAN_ErrorFrame.BusChannel"][i]) if "CAN_ErrorFrame.Dir" in names: - kv["is_rx"] = int(data["CAN_ErrorFrame.Dir"][i]) == 0 + if data["CAN_ErrorFrame.Dir"][i].dtype.kind == "S": + kv["is_rx"] = data["CAN_ErrorFrame.Dir"][i] == b"Rx" + else: + kv["is_rx"] = int(data["CAN_ErrorFrame.Dir"][i]) == 0 if "CAN_ErrorFrame.ID" in names: kv["arbitration_id"] = ( int(data["CAN_ErrorFrame.ID"][i]) & 0x1FFFFFFF @@ -441,7 +447,10 @@ def __iter__(self) -> Generator[Message, None, None]: if "CAN_RemoteFrame.BusChannel" in names: kv["channel"] = int(data["CAN_RemoteFrame.BusChannel"][i]) if "CAN_RemoteFrame.Dir" in names: - kv["is_rx"] = int(data["CAN_RemoteFrame.Dir"][i]) == 0 + if data["CAN_RemoteFrame.Dir"][i].dtype.kind == "S": + kv["is_rx"] = data["CAN_RemoteFrame.Dir"][i] == b"Rx" + else: + kv["is_rx"] = int(data["CAN_RemoteFrame.Dir"][i]) == 0 if "CAN_RemoteFrame.IDE" in names: kv["is_extended_id"] = bool(data["CAN_RemoteFrame.IDE"][i]) From 6160f76a214d1813cb1314371730586cb11416bb Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Tue, 5 Aug 2025 14:36:42 +0200 Subject: [PATCH 098/127] Use `towncrier` for Automated Changelog Generation, Add PR Template (#1965) * add towncrier configuration * add pull request template * add news fragments * add fragment for 1967 --- .github/pull_request_template.md | 37 ++++ CHANGELOG.md | 281 +++++++++++++------------------ doc/changelog.d/.gitignore | 11 ++ doc/changelog.d/1758.added.md | 1 + doc/changelog.d/1851.changed.md | 1 + doc/changelog.d/1890.added.md | 1 + doc/changelog.d/1904.fixed.md | 1 + doc/changelog.d/1906.fixed.md | 1 + doc/changelog.d/1908.fixed.md | 1 + doc/changelog.d/1914.added.md | 1 + doc/changelog.d/1920.added.md | 1 + doc/changelog.d/1921.fixed.md | 1 + doc/changelog.d/1927.fixed.md | 1 + doc/changelog.d/1931.removed.md | 1 + doc/changelog.d/1934.fixed.md | 1 + doc/changelog.d/1940.fixed.md | 1 + doc/changelog.d/1941.added.md | 1 + doc/changelog.d/1945.changed.md | 2 + doc/changelog.d/1946.changed.md | 1 + doc/changelog.d/1947.changed.md | 1 + doc/changelog.d/1948.added.md | 1 + doc/changelog.d/1949.added.md | 1 + doc/changelog.d/1951.removed.md | 1 + doc/changelog.d/1953.added.md | 1 + doc/changelog.d/1954.added.md | 1 + doc/changelog.d/1957.fixed.md | 1 + doc/changelog.d/1960.changed.md | 1 + doc/changelog.d/1961.added.md | 1 + doc/changelog.d/1967.fixed.md | 1 + doc/development.rst | 65 ++++++- pyproject.toml | 40 ++++- 31 files changed, 284 insertions(+), 177 deletions(-) create mode 100644 .github/pull_request_template.md create mode 100644 doc/changelog.d/.gitignore create mode 100644 doc/changelog.d/1758.added.md create mode 100644 doc/changelog.d/1851.changed.md create mode 100644 doc/changelog.d/1890.added.md create mode 100644 doc/changelog.d/1904.fixed.md create mode 100644 doc/changelog.d/1906.fixed.md create mode 100644 doc/changelog.d/1908.fixed.md create mode 100644 doc/changelog.d/1914.added.md create mode 100644 doc/changelog.d/1920.added.md create mode 100644 doc/changelog.d/1921.fixed.md create mode 100644 doc/changelog.d/1927.fixed.md create mode 100644 doc/changelog.d/1931.removed.md create mode 100644 doc/changelog.d/1934.fixed.md create mode 100644 doc/changelog.d/1940.fixed.md create mode 100644 doc/changelog.d/1941.added.md create mode 100644 doc/changelog.d/1945.changed.md create mode 100644 doc/changelog.d/1946.changed.md create mode 100644 doc/changelog.d/1947.changed.md create mode 100644 doc/changelog.d/1948.added.md create mode 100644 doc/changelog.d/1949.added.md create mode 100644 doc/changelog.d/1951.removed.md create mode 100644 doc/changelog.d/1953.added.md create mode 100644 doc/changelog.d/1954.added.md create mode 100644 doc/changelog.d/1957.fixed.md create mode 100644 doc/changelog.d/1960.changed.md create mode 100644 doc/changelog.d/1961.added.md create mode 100644 doc/changelog.d/1967.fixed.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..e6ce365d1 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,37 @@ + + +## Summary of Changes + + + +- + +## Related Issues / Pull Requests + + + +- Closes # +- Related to # + +## Type of Change + +- [ ] Bug fix +- [ ] New feature +- [ ] Documentation update +- [ ] Refactoring +- [ ] Other (please describe): + +## Checklist + +- [ ] I have followed the [contribution guide](https://python-can.readthedocs.io/en/main/development.html). +- [ ] I have added or updated tests as appropriate. +- [ ] I have added or updated documentation as appropriate. +- [ ] I have added a [news fragment](doc/changelog.d/) for towncrier. +- [ ] All checks and tests pass (`tox`). + +## Additional Notes + + diff --git a/CHANGELOG.md b/CHANGELOG.md index 39cbaa716..a8ec28f12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,17 @@ -Version 4.5.0 -============= +# Changelog -Features --------- + + + + + +## Version 4.5.0 + +### Features * gs_usb command-line support (and documentation updates and stability fixes) by @BenGardiner in https://github.com/hardbyte/python-can/pull/1790 * Faster and more general MF4 support by @cssedev in https://github.com/hardbyte/python-can/pull/1892 @@ -13,8 +22,7 @@ Features * Improve TestBusConfig by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1804 * Improve speed of TRCReader by @lebuni in https://github.com/hardbyte/python-can/pull/1893 -Bug Fixes ---------- +### Bug Fixes * Fix Kvaser timestamp by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1878 * Set end_time in ThreadBasedCyclicSendTask.start() by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1871 @@ -25,8 +33,7 @@ Bug Fixes * Resolve AttributeError within NicanError by @vijaysubbiah20 in https://github.com/hardbyte/python-can/pull/1806 -Miscellaneous -------------- +### Miscellaneous * Fix CI by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1889 * Update msgpack dependency by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1875 @@ -43,11 +50,10 @@ Miscellaneous * Add zlgcan to docs by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1839 -Version 4.4.2 -============= +## Version 4.4.2 + +### Bug Fixes -Bug Fixes ---------- * Remove `abstractmethod` decorator from `Listener.stop()` (#1770, #1795) * Fix `SizedRotatingLogger` file suffix bug (#1792, #1793) * gs_usb: Use `BitTiming` class internally to configure bitrate (#1747, #1748) @@ -56,8 +62,7 @@ Bug Fixes * socketcan: Do not log exception on non-linux platforms (#1800) * vector, kvaser: Activate channels after CAN filters were applied (#1413, #1708, #1796) -Features --------- +### Features * kvaser: Add support for non-ISO CAN FD (#1752) * neovi: Return timestamps relative to epoch (#1789) @@ -66,11 +71,9 @@ Features * vector: Add support for `listen_only` mode (#1764) -Version 4.4.0 -============= +## Version 4.4.0 -Features --------- +### Features * TRC 1.3 Support: Added support for .trc log files as generated by PCAN Explorer v5 and other tools, expanding compatibility with common log file formats (#1753). * ASCReader refactor: improved the ASCReader code (#1717). @@ -80,17 +83,14 @@ Features * CAN FD Bus Connection for VectorBus: Enabled connecting to CAN FD buses without specifying bus timings, simplifying the connection process for users (#1716). * Neousys Configs Detection: Updated the detection mechanism for available Neousys configurations, ensuring more accurate and comprehensive configuration discovery (#1744). - -Bug Fixes ---------- +### Bug Fixes * Send Periodic Messages: Fixed an issue where fixed-duration periodic messages were sent one extra time beyond their intended count (#1713). * Vector Interface on Windows 11: Addressed compatibility issues with the Vector interface on Windows 11, ensuring stable operation across the latest OS version (#1731). * ASCWriter Millisecond Handling: Corrected the handling of milliseconds in ASCWriter, ensuring accurate time representation in log files (#1734). * Various minor bug fixes: Addressed several minor bugs to improve overall stability and performance. -Miscellaneous -------------- +### Miscellaneous * Invert default value logic for BusABC._is_shutdown. (#1774) * Implemented various logging enhancements to provide more detailed and useful operational insights (#1703). @@ -100,29 +100,27 @@ Miscellaneous The release also includes various other minor enhancements and bug fixes aimed at improving the reliability and performance of the software. -Version 4.3.1 -============= +## Version 4.3.1 + +### Bug Fixes -Bug Fixes ---------- * Fix socketcand erroneously discarding frames (#1700) * Fix initialization order in EtasBus (#1693, #1704) -Documentation -------------- +### Documentation + * Fix install instructions for neovi (#1694, #1697) -Version 4.3.0 -============= +## Version 4.3.0 + +### Breaking Changes -Breaking Changes ----------------- * Raise Minimum Python Version to 3.8 (#1597) * Do not stop notifier if exception was handled (#1645) -Bug Fixes ---------- +### Bug Fixes + * Vector: channel detection fails, if there is an active flexray channel (#1634) * ixxat: Fix exception in 'state' property on bus coupling errors (#1647) * NeoVi: Fixed serial number range (#1650) @@ -133,20 +131,22 @@ Bug Fixes * Vector: Skip the `can_op_mode check` if the device reports `can_op_mode=0` (#1678) * Vector: using the config from `detect_available_configs` might raise XL_ERR_INVALID_CHANNEL_MASK error (#1681) -Features --------- +### Features + +#### API -### API * Add `modifier_callback` parameter to `BusABC.send_periodic` for auto-modifying cyclic tasks (#703) * Add `protocol` property to BusABC to determine active CAN Protocol (#1532) * Change Bus constructor implementation and typing (#1557) * Add optional `strict` parameter to relax BitTiming & BitTimingFd Validation (#1618) * Add `BitTiming.iterate_from_sample_point` static methods (#1671) -### IO +#### IO + * Can Player compatibility with interfaces that use additional configuration (#1610) -### Interface Improvements +#### Interface Improvements + * Kvaser: Add BitTiming/BitTimingFd support to KvaserBus (#1510) * Ixxat: Implement `detect_available_configs` for the Ixxat bus. (#1607) * NeoVi: Enable send and receive on network ID above 255 (#1627) @@ -156,7 +156,8 @@ Features * Kvaser: add parameter exclusive and `override_exclusive` (#1660) * socketcand: Add parameter `tcp_tune` to reduce latency (#1683) -### Miscellaneous +#### Miscellaneous + * Distinguish Text/Binary-IO for Reader/Writer classes. (#1585) * Convert setup.py to pyproject.toml (#1592) * activate ruff pycodestyle checks (#1602) @@ -170,32 +171,29 @@ Features * Add Python 3.12 Support / Test Python 3.12 (#1673) -Version 4.2.2 -============= +## Version 4.2.2 + +### Bug Fixes -Bug Fixes ---------- * Fix socketcan KeyError (#1598, #1599). * Fix IXXAT not properly shutdown message (#1606). * Fix Mf4Reader and TRCReader incompatibility with extra CLI args (#1610). * Fix decoding error in Kvaser constructor for non-ASCII product name (#1613). -Version 4.2.1 -============= +## Version 4.2.1 + +### Bug Fixes -Bug Fixes ---------- * The ASCWriter now logs the correct channel for error frames (#1578, #1583). * Fix PCAN library detection (#1579, #1580). * On Windows, the first two periodic frames were sent without delay (#1590). -Version 4.2.0 -============= +## Version 4.2.0 + +### Breaking Changes -Breaking Changes ----------------- * The ``can.BitTiming`` class was replaced with the new ``can.BitTiming`` and `can.BitTimingFd` classes (#1468, #1515). Early adopters of ``can.BitTiming`` will need to update their code. Check the @@ -210,17 +208,16 @@ Breaking Changes There are open pull requests for kvaser (#1510), slcan (#1512) and usb2can (#1511). Testing and reviewing of these open PRs would be most appreciated. -Features --------- +### Features -### IO +#### IO * Add support for MF4 files (#1289). * Add support for version 2 TRC files and other TRC file enhancements (#1530). -### Type Annotations +#### Type Annotations * Export symbols to satisfy type checkers (#1547, #1551, #1558, #1568). -### Interface Improvements +#### Interface Improvements * Add ``__del__`` method to ``can.BusABC`` to automatically release resources (#1489, #1564). * pcan: Update PCAN Basic to 4.6.2.753 (#1481). * pcan: Use select instead of polling on Linux (#1410). @@ -232,21 +229,21 @@ Features * vector: Only check sample point instead of tseg & sjw (#1486). * vector: add VN5611 hwtype (#1501). -Documentation -------------- +### Documentation + * Add new section about related tools to documentation. Add a list of plugin interface packages (#1457). -Bug Fixes ---------- +### Bug Fixes + * Automatic type conversion for config values (#1498, #1499). * pcan: Fix ``Bus.__new__`` for CAN-FD interfaces (#1458, #1460). * pcan: Fix Detection of Library on Windows on ARM (#1463). * socketcand: extended ID bug fixes (#1504, #1508). * vector: improve robustness against unknown HardwareType values (#1500, #1502). -Deprecations ------------- +### Deprecations + * The ``bustype`` parameter of ``can.Bus`` is deprecated and will be removed in version 5.0, use ``interface`` instead. (#1462). * The ``context`` parameter of ``can.Bus`` is deprecated and will be @@ -258,8 +255,8 @@ Deprecations * The ``brs`` and ``log_errors`` parameters of `` NiXNETcanBus`` are deprecated and will be removed in version 5.0. (#1520). -Miscellaneous -------------- +### Miscellaneous + * Use high resolution timer on Windows to improve timing precision for BroadcastManager (#1449). * Improve ThreadBasedCyclicSendTask timing (#1539). @@ -271,11 +268,9 @@ Miscellaneous * Add deprecation period to utility function ``deprecated_args_alias`` (#1477). * Add `ruff` to the CI system (#1551) -Version 4.1.0 -============= +## Version 4.1.0 -Breaking Changes ----------------- +### Breaking Changes * ``windows-curses`` was moved to optional dependencies (#1395). Use ``pip install python-can[viewer]`` if you are using the ``can.viewer`` @@ -284,11 +279,9 @@ Breaking Changes from camelCase to snake_case (#1422). -Features --------- - -### IO +### Features +#### IO * The canutils logger preserves message direction (#1244) and uses common interface names (e.g. can0) instead of just channel numbers (#1271). @@ -299,11 +292,11 @@ Features and player initialisation (#1366). * Initial support for TRC files (#1217) -### Type Annotations +#### Type Annotations * python-can now includes the ``py.typed`` marker to support type checking according to PEP 561 (#1344). -### Interface Improvements +#### Interface Improvements * The gs_usb interface can be selected by device index instead of USB bus/address. Loopback frames are now correctly marked with the ``is_rx`` flag (#1270). @@ -317,8 +310,7 @@ Features be applied according to the arguments of ``VectorBus.__init__`` (#1426). * Ixxat bus now implements BusState api and detects errors (#1141) -Bug Fixes ---------- +### Bug Fixes * Improve robustness of USB2CAN serial number detection (#1129). * Fix channel2int conversion (#1268, #1269). @@ -340,8 +332,7 @@ Bug Fixes * Raise ValueError if gzip is used with incompatible log formats (#1429). * Allow restarting of transmission tasks for socketcan (#1440) -Miscellaneous -------------- +### Miscellaneous * Allow ICSApiError to be pickled and un-pickled (#1341) * Sort interface names in CLI API to make documentation reproducible (#1342) @@ -351,8 +342,7 @@ Miscellaneous * Migrate code coverage reporting from Codecov to Coveralls (#1430) * Migrate building docs and publishing releases to PyPi from Travis-CI to GitHub Actions (#1433) -Version 4.0.0 -==== +## Version 4.0.0 TL;DR: This release includes a ton of improvements from 2.5 years of development! 🎉 Test thoroughly after switching. @@ -366,8 +356,7 @@ Therefore, users are strongly advised to thoroughly test their programs against Re-reading the documentation for your interfaces might be helpful too as limitations and capabilities might have changed or are more explicit. While we did try to avoid breaking changes, in some cases it was not feasible and in particular, many implementation details have changed. -Major features --------------- +### Major features * Type hints for the core library and some interfaces (#652 and many others) * Support for Python 3.7-3.10+ only (dropped support for Python 2.* and 3.5-3.6) (#528 and many others) @@ -375,8 +364,7 @@ Major features * [Support for automatic configuration detection](https://python-can.readthedocs.io/en/develop/api.html#can.detect_available_configs) in most interfaces (#303, #640, #641, #811, #1077, #1085) * Better alignment of interfaces and IO to common conventions and semantics -New interfaces --------------- +### New interfaces * udp_multicast (#644) * robotell (#731) @@ -387,8 +375,7 @@ New interfaces * socketcand (#1140) * etas (#1144) -Improved interfaces -------------------- +### Improved interfaces * socketcan * Support for multiple Cyclic Messages in Tasks (#610) @@ -465,8 +452,7 @@ Improved interfaces * Fix transmitting onto a busy bus (#1114) * Replace binary library with python driver (#726, #1127) -Other API changes and improvements ----------------------------------- +### Other API changes and improvements * CAN FD frame support is pretty complete (#963) * ASCWriter (#604) and ASCReader (#741) @@ -497,8 +483,7 @@ Other API changes and improvements * Add changed byte highlighting to viewer.py (#1159) * Change DLC to DL in Message.\_\_str\_\_() (#1212) -Other Bugfixes --------------- +### Other Bugfixes * BLF PDU padding (#459) * stop_all_periodic_tasks skipping every other task (#634, #637, #645) @@ -524,8 +509,7 @@ Other Bugfixes * Some smaller bugfixes are not listed here since the problems were never part of a proper release * ASCReader & ASCWriter using DLC as data length (#1245, #1246) -Behind the scenes & Quality assurance -------------------------------------- +### Behind the scenes & Quality assurance * We publish both source distributions (`sdist`) and binary wheels (`bdist_wheel`) (#1059, #1071) * Many interfaces were partly rewritten to modernize the code or to better handle errors @@ -543,16 +527,13 @@ Behind the scenes & Quality assurance * [Good test coverage](https://app.codecov.io/gh/hardbyte/python-can/branch/develop) for all but the interfaces * Testing: Many of the new features directly added tests, and coverage of existing code was improved too (for example: #1031, #581, #585, #586, #942, #1196, #1198) -Version 3.3.4 -==== +## Version 3.3.4 Last call for Python2 support. * #850 Fix socket.error is a deprecated alias of OSError used on Python versions lower than 3.3. -Version 3.3.3 -==== - +## Version 3.3.3 Backported fixes from 4.x development branch which targets Python 3. * #798 Backport caching msg.data value in neovi interface. @@ -570,37 +551,30 @@ Backported fixes from 4.x development branch which targets Python 3. * #605 Socketcan BCM status fix. -Version 3.3.2 -==== +## Version 3.3.2 Minor bug fix release addressing issue in PCAN RTR. -Version 3.3.1 -==== +## Version 3.3.1 Minor fix to setup.py to only require pytest-runner when necessary. -Version 3.3.0 -==== +## Version 3.3.0 * Adding CAN FD 64 frame support to blf reader * Updates to installation instructions * Clean up bits generator in PCAN interface #588 * Minor fix to use latest tools when building wheels on travis. -Version 3.2.1 -==== +## Version 3.2.1 * CAN FD 64 frame support to blf reader * Minor fix to use latest tools when building wheels on travis. * Updates links in documentation. -Version 3.2.0 -==== +## Version 3.2.0 - -Major features --------------- +### Major features * FD support added for Pcan by @bmeisels with input from @markuspi, @christiansandberg & @felixdivo in PR #537 @@ -608,8 +582,7 @@ Major features and Python 3.5. Support has been removed for Python 3.4 in this release in PR #532 -Other notable changes ---------------------- +### Other notable changes * #533 BusState is now an enum. * #535 This release should automatically be published to PyPi by travis. @@ -624,26 +597,21 @@ https://github.com/hardbyte/python-can/milestone/7?closed=1 Pulls: #522, #526, #527, #536, #540, #546, #547, #548, #533, #559, #569, #571, #572, #575 -Backend Specific Changes ------------------------- +### Backend Specific Changes -pcan -~~~~ +#### pcan * FD -slcan -~~~~ +#### slcan * ability to set custom can speed instead of using predefined speed values. #553 -socketcan -~~~~ +#### socketcan * Bug fix to properly support 32bit systems. #573 -usb2can -~~~~ +#### usb2can * slightly better error handling * multiple serial devices can be found @@ -651,25 +619,20 @@ usb2can Pulls #511, #535 -vector -~~~~ +#### vector * handle `app_name`. #525 -Version 3.1.1 -==== +## Version 3.1.1 -Major features --------------- +### Major features Two new interfaces this release: - SYSTEC contributed by @idaniel86 in PR #466 - CANalyst-II contributed by @smeng9 in PR #476 - -Other notable changes ---------------------- +### Other notable changes * #477 The kvaser interface now supports bus statistics via a custom bus method. * #434 neovi now supports receiving own messages @@ -686,18 +649,15 @@ Other notable changes * #455 Fix to `Message` initializer * Small bugfixes and improvements -Version 3.1.0 -==== +## Version 3.1.0 Version 3.1.0 was built with old wheel and/or setuptools packages and was replaced with v3.1.1 after an installation but was discovered. -Version 3.0.0 -==== +## Version 3.0.0 -Major features --------------- +### Major features * Adds support for developing `asyncio` applications with `python-can` more easily. This can be useful when implementing protocols that handles simultaneous connections to many nodes since you can write @@ -710,8 +670,7 @@ Major features by calling the bus's new `stop_all_periodic_tasks` method. #412 -Breaking changes ----------------- +### Breaking changes * Interfaces should no longer override `send_periodic` and instead implement `_send_periodic_internal` to allow the Bus base class to manage tasks. #426 @@ -721,8 +680,7 @@ Breaking changes read/writer constructors from `filename` to `file`. -Other notable changes ---------------------- +### Other notable changes * can.Message class updated #413 - Addition of a `Message.equals` method. @@ -754,59 +712,48 @@ Other notable changes General fixes, cleanup and docs changes: (#347, #348, #367, #368, #370, #371, #373, #420, #417, #419, #432) -Backend Specific Changes ------------------------- +### Backend Specific Changes -3rd party interfaces -~~~~~~~~~~~~~~~~~~~~ +#### 3rd party interfaces * Deprecated `python_can.interface` entry point instead use `can.interface`. #389 -neovi -~~~~~ +#### neovi * Added support for CAN-FD #408 * Fix issues checking if bus is open. #381 * Adding multiple channels support. #415 -nican -~~~~~ +#### nican * implements reset instead of custom `flush_tx_buffer`. #364 -pcan -~~~~ +#### pcan * now supported on OSX. #365 - -serial -~~~~~~ +#### serial * Removed TextIOWrapper from serial. #383 * switch to `serial_for_url` enabling using remote ports via `loop://`, ``socket://` and `rfc2217://` URLs. #393 * hardware handshake using `rtscts` kwarg #402 -socketcan -~~~~~~~~~ +#### socketcan * socketcan tasks now reuse a bcm socket #404, #425, #426, * socketcan bugfix to receive error frames #384 -vector -~~~~~~ +#### vector * Vector interface now implements `_detect_available_configs`. #362 * Added support to select device by serial number. #387 -Version 2.2.1 (2018-07-12) -===== +## Version 2.2.1 (2018-07-12) * Fix errors and warnings when importing library on Windows * Fix Vector backend raising ValueError when hardware is not connected -Version 2.2.0 (2018-06-30) -===== +## Version 2.2.0 (2018-06-30) * Fallback message filtering implemented in Python for interfaces that don't offer better accelerated mechanism. * SocketCAN interfaces have been merged (Now use `socketcan` instead of either `socketcan_native` and `socketcan_ctypes`), @@ -817,8 +764,7 @@ Version 2.2.0 (2018-06-30) * Dropped support for Python 3.3 (officially reached end-of-life in Sept. 2017) * Deprecated the old `CAN` module, please use the newer `can` entry point (will be removed in an upcoming major version) -Version 2.1.0 (2018-02-17) -===== +## Version 2.1.0 (2018-02-17) * Support for out of tree can interfaces with pluggy. * Initial support for CAN-FD for socketcan_native and kvaser interfaces. @@ -830,8 +776,7 @@ Version 2.1.0 (2018-02-17) * Other misc improvements and bug fixes -Version 2.0.0 (2018-01-05 -===== +## Version 2.0.0 (2018-01-05) After an extended baking period we have finally tagged version 2.0.0! diff --git a/doc/changelog.d/.gitignore b/doc/changelog.d/.gitignore new file mode 100644 index 000000000..b56b00acb --- /dev/null +++ b/doc/changelog.d/.gitignore @@ -0,0 +1,11 @@ +# Ignore everything... +* +!.gitignore + +# ...except markdown news fragments +!*.security.md +!*.removed.md +!*.deprecated.md +!*.added.md +!*.changed.md +!*.fixed.md diff --git a/doc/changelog.d/1758.added.md b/doc/changelog.d/1758.added.md new file mode 100644 index 000000000..0b95b14e2 --- /dev/null +++ b/doc/changelog.d/1758.added.md @@ -0,0 +1 @@ +Support 11-bit identifiers in the `serial` interface. diff --git a/doc/changelog.d/1851.changed.md b/doc/changelog.d/1851.changed.md new file mode 100644 index 000000000..672f7bd7d --- /dev/null +++ b/doc/changelog.d/1851.changed.md @@ -0,0 +1 @@ +Allow sending Classic CAN frames with a DLC value larger than 8 using the `socketcan` interface. \ No newline at end of file diff --git a/doc/changelog.d/1890.added.md b/doc/changelog.d/1890.added.md new file mode 100644 index 000000000..802629ed3 --- /dev/null +++ b/doc/changelog.d/1890.added.md @@ -0,0 +1 @@ +Keep track of active Notifiers and make Notifier usable as a context manager. Add function `Notifier.find_instances(bus)` to find the active Notifier for a given bus instance. diff --git a/doc/changelog.d/1904.fixed.md b/doc/changelog.d/1904.fixed.md new file mode 100644 index 000000000..80b665a6b --- /dev/null +++ b/doc/changelog.d/1904.fixed.md @@ -0,0 +1 @@ +Fix a bug in `slcanBus.get_version()` and `slcanBus.get_serial_number()`: If any other data was received during the function call, then `None` was returned. \ No newline at end of file diff --git a/doc/changelog.d/1906.fixed.md b/doc/changelog.d/1906.fixed.md new file mode 100644 index 000000000..f8988ff48 --- /dev/null +++ b/doc/changelog.d/1906.fixed.md @@ -0,0 +1 @@ +Fix incorrect padding of CAN FD payload in `BlfReader`. \ No newline at end of file diff --git a/doc/changelog.d/1908.fixed.md b/doc/changelog.d/1908.fixed.md new file mode 100644 index 000000000..ce8947029 --- /dev/null +++ b/doc/changelog.d/1908.fixed.md @@ -0,0 +1 @@ +Set correct message direction for messages received with `kvaser` interface and `receive_own_messages=True`. \ No newline at end of file diff --git a/doc/changelog.d/1914.added.md b/doc/changelog.d/1914.added.md new file mode 100644 index 000000000..a1f838001 --- /dev/null +++ b/doc/changelog.d/1914.added.md @@ -0,0 +1 @@ +Add Windows support to `udp_multicast` interface. \ No newline at end of file diff --git a/doc/changelog.d/1920.added.md b/doc/changelog.d/1920.added.md new file mode 100644 index 000000000..c4f0e532e --- /dev/null +++ b/doc/changelog.d/1920.added.md @@ -0,0 +1 @@ +Add FD support to `slcan` according to CANable 2.0 implementation. diff --git a/doc/changelog.d/1921.fixed.md b/doc/changelog.d/1921.fixed.md new file mode 100644 index 000000000..139d2979f --- /dev/null +++ b/doc/changelog.d/1921.fixed.md @@ -0,0 +1 @@ +Fix timestamp rounding error in `BlfWriter`. diff --git a/doc/changelog.d/1927.fixed.md b/doc/changelog.d/1927.fixed.md new file mode 100644 index 000000000..5fb005d05 --- /dev/null +++ b/doc/changelog.d/1927.fixed.md @@ -0,0 +1 @@ +Fix timestamp rounding error in `BlfReader`. \ No newline at end of file diff --git a/doc/changelog.d/1931.removed.md b/doc/changelog.d/1931.removed.md new file mode 100644 index 000000000..416329a83 --- /dev/null +++ b/doc/changelog.d/1931.removed.md @@ -0,0 +1 @@ +Remove support for Python 3.8. diff --git a/doc/changelog.d/1934.fixed.md b/doc/changelog.d/1934.fixed.md new file mode 100644 index 000000000..a12e4ffb2 --- /dev/null +++ b/doc/changelog.d/1934.fixed.md @@ -0,0 +1 @@ +Handle timer overflow message and build timestamp according to the epoch in the `ixxat` interface. \ No newline at end of file diff --git a/doc/changelog.d/1940.fixed.md b/doc/changelog.d/1940.fixed.md new file mode 100644 index 000000000..9f4fc09ba --- /dev/null +++ b/doc/changelog.d/1940.fixed.md @@ -0,0 +1 @@ +Avoid unsupported `ioctl` function call to allow usage of the `udp_multicast` interface on MacOS. \ No newline at end of file diff --git a/doc/changelog.d/1941.added.md b/doc/changelog.d/1941.added.md new file mode 100644 index 000000000..a3d87cb6b --- /dev/null +++ b/doc/changelog.d/1941.added.md @@ -0,0 +1 @@ +Add support for error messages to the `socketcand` interface. \ No newline at end of file diff --git a/doc/changelog.d/1945.changed.md b/doc/changelog.d/1945.changed.md new file mode 100644 index 000000000..59a48774f --- /dev/null +++ b/doc/changelog.d/1945.changed.md @@ -0,0 +1,2 @@ +The `gs_usb` extra dependency was renamed to `gs-usb`. +The `lint` extra dependency was removed and replaced with new PEP 735 dependency groups `lint`, `docs` and `test`. \ No newline at end of file diff --git a/doc/changelog.d/1946.changed.md b/doc/changelog.d/1946.changed.md new file mode 100644 index 000000000..d5dad4225 --- /dev/null +++ b/doc/changelog.d/1946.changed.md @@ -0,0 +1 @@ +Update dependency name from `zlgcan-driver-py` to `zlgcan`. \ No newline at end of file diff --git a/doc/changelog.d/1947.changed.md b/doc/changelog.d/1947.changed.md new file mode 100644 index 000000000..db12a0318 --- /dev/null +++ b/doc/changelog.d/1947.changed.md @@ -0,0 +1 @@ +Use ThreadPoolExecutor in `detect_available_configs()` to reduce runtime and add `timeout` parameter. \ No newline at end of file diff --git a/doc/changelog.d/1948.added.md b/doc/changelog.d/1948.added.md new file mode 100644 index 000000000..132d49d98 --- /dev/null +++ b/doc/changelog.d/1948.added.md @@ -0,0 +1 @@ +Add support for remote and error frames in the `serial` interface. \ No newline at end of file diff --git a/doc/changelog.d/1949.added.md b/doc/changelog.d/1949.added.md new file mode 100644 index 000000000..6e8ac79b5 --- /dev/null +++ b/doc/changelog.d/1949.added.md @@ -0,0 +1 @@ +Add public functions `can.cli.add_bus_arguments` and `can.cli.create_bus_from_namespace` for creating bus command line options. Currently downstream packages need to implement their own logic to configure *python-can* buses. Now *python-can* can create and parse bus options for third party packages. \ No newline at end of file diff --git a/doc/changelog.d/1951.removed.md b/doc/changelog.d/1951.removed.md new file mode 100644 index 000000000..6d56c5d50 --- /dev/null +++ b/doc/changelog.d/1951.removed.md @@ -0,0 +1 @@ +Remove `can.io.generic.BaseIOHandler` class. Improve `can.io.*` type annotations by using `typing.Generic`. diff --git a/doc/changelog.d/1953.added.md b/doc/changelog.d/1953.added.md new file mode 100644 index 000000000..76ef5137a --- /dev/null +++ b/doc/changelog.d/1953.added.md @@ -0,0 +1 @@ +Add support for remote frames to `TRCReader`. diff --git a/doc/changelog.d/1954.added.md b/doc/changelog.d/1954.added.md new file mode 100644 index 000000000..d2d50669b --- /dev/null +++ b/doc/changelog.d/1954.added.md @@ -0,0 +1 @@ +Mention the `python-can-candle` package in the plugin interface section of the documentation. diff --git a/doc/changelog.d/1957.fixed.md b/doc/changelog.d/1957.fixed.md new file mode 100644 index 000000000..9d5dd6071 --- /dev/null +++ b/doc/changelog.d/1957.fixed.md @@ -0,0 +1 @@ +Fix configuration file parsing for the `state` bus parameter. diff --git a/doc/changelog.d/1960.changed.md b/doc/changelog.d/1960.changed.md new file mode 100644 index 000000000..f3977aedb --- /dev/null +++ b/doc/changelog.d/1960.changed.md @@ -0,0 +1 @@ +Update contribution guide. diff --git a/doc/changelog.d/1961.added.md b/doc/changelog.d/1961.added.md new file mode 100644 index 000000000..483427ec0 --- /dev/null +++ b/doc/changelog.d/1961.added.md @@ -0,0 +1 @@ +Add new CLI tool `python -m can.bridge` (or just `can_bridge`) to create a software bridge between two physical buses. diff --git a/doc/changelog.d/1967.fixed.md b/doc/changelog.d/1967.fixed.md new file mode 100644 index 000000000..fdd72b363 --- /dev/null +++ b/doc/changelog.d/1967.fixed.md @@ -0,0 +1 @@ +Mf4Reader: support non-standard `CAN_DataFrame.Dir` values in mf4 files created by [ihedvall/mdflib](https://github.com/ihedvall/mdflib). diff --git a/doc/development.rst b/doc/development.rst index 97c175ada..40604c346 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -164,7 +164,36 @@ Step-by-Step Contribution Guide Some environments require specific Python versions. If you use `uv`, it will automatically download and manage these for you. -5. **(Optional) Build Source Distribution and Wheels** + + +5. **Add a News Fragment for the Changelog** + + This project uses `towncrier `__ to manage the changelog in + ``CHANGELOG.md``. For every user-facing change (new feature, bugfix, deprecation, etc.), you + must add a news fragment: + + * News fragments are short files describing your change, stored in ``doc/changelog.d``. + * Name each fragment ``..md``, where ```` is one of: + ``added``, ``changed``, ``deprecated``, ``removed``, ``fixed``, or ``security``. + * Example (for a feature added in PR #1234): + + .. code-block:: shell + + echo "Added support for CAN FD." > doc/changelog.d/1234.added.md + + * Or use the towncrier CLI: + + .. code-block:: shell + + uvx towncrier create --dir doc/changelog.d -c "Added support for CAN FD." 1234.added.md + + * For changes not tied to an issue/PR, the fragment name must start with a plus symbol + (e.g., ``+mychange.added.md``). Towncrier calls these "orphan fragments". + + .. note:: You do not need to manually update ``CHANGELOG.md``—maintainers will build the + changelog at release time. + +6. **(Optional) Build Source Distribution and Wheels** If you want to manually build the source distribution (sdist) and wheels for python-can, you can use `uvx` to run the build and twine tools: @@ -174,7 +203,7 @@ Step-by-Step Contribution Guide uv build uvx twine check --strict dist/* -6. **Push and Submit Your Contribution** +7. **Push and Submit Your Contribution** * Push your branch: @@ -218,13 +247,33 @@ These steps are a guideline on how to add a new backend to python-can. Creating a new Release ---------------------- -* Releases are automated via GitHub Actions. To create a new release: +Releases are automated via GitHub Actions. To create a new release: + +* Build the changelog with towncrier: + + + * Collect all news fragments and update ``CHANGELOG.md`` by running: + + .. code-block:: shell + + uvx towncrier build --yes --version vX.Y.Z + + (Replace ``vX.Y.Z`` with the new version number. **The version must exactly match the tag you will create for the release.**) + This will add all news fragments to the changelog and remove the fragments by default. + + .. note:: You can generate the changelog for prereleases, but keep the news + fragments so they are included in the final release. To do this, replace ``--yes`` with ``--keep``. + This will update ``CHANGELOG.md`` but leave the fragments in place for future builds. + + * Review ``CHANGELOG.md`` for accuracy and completeness. - * Ensure all tests pass and documentation is up-to-date. - * Update ``CONTRIBUTORS.txt`` with any new contributors. - * For larger changes, update ``doc/history.rst``. - * Create a new tag and GitHub release (e.g., ``vX.Y.Z``) targeting the ``main`` branch. Add release notes and publish. - * The CI workflow will automatically build, check, and upload the release to PyPI and other platforms. +* Ensure all tests pass and documentation is up-to-date. +* Update ``CONTRIBUTORS.txt`` with any new contributors. +* For larger changes, update ``doc/history.rst``. +* Create a new tag and GitHub release (e.g., ``vX.Y.Z``) targeting the ``main`` + branch. Add release notes and publish. +* The CI workflow will automatically build, check, and upload the release to PyPI + and other platforms. * You can monitor the release status on: `PyPi `__, diff --git a/pyproject.toml b/pyproject.toml index 51bcff693..72e706740 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,7 +88,7 @@ docs = [ ] lint = [ "pylint==3.3.*", - "ruff==0.12.5", + "ruff==0.12.7", "black==25.1.*", "mypy==1.17.*", ] @@ -213,3 +213,41 @@ disable = [ "too-many-public-methods", "too-many-statements", ] + +[tool.towncrier] +directory = "doc/changelog.d" +filename = "CHANGELOG.md" +start_string = "\n" +underlines = ["", "", ""] +title_format = "## Version [{version}](https://github.com/hardbyte/python-can/tree/{version}) - {project_date}" +issue_format = "[#{issue}](https://github.com/hardbyte/python-can/issues/{issue})" + +[[tool.towncrier.type]] +directory = "security" +name = "Security" +showcontent = true + +[[tool.towncrier.type]] +directory = "removed" +name = "Removed" +showcontent = true + +[[tool.towncrier.type]] +directory = "deprecated" +name = "Deprecated" +showcontent = true + +[[tool.towncrier.type]] +directory = "added" +name = "Added" +showcontent = true + +[[tool.towncrier.type]] +directory = "changed" +name = "Changed" +showcontent = true + +[[tool.towncrier.type]] +directory = "fixed" +name = "Fixed" +showcontent = true From 963bbee9a5911f9286a4f6eeace86cd4b56b88ed Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Tue, 5 Aug 2025 16:01:45 +0200 Subject: [PATCH 099/127] fix tests to avoid unclosed files and failed PyPy tests (#1968) --- can/io/logger.py | 3 +- test/test_rotating_loggers.py | 103 +++++++++++++++------------------- 2 files changed, 48 insertions(+), 58 deletions(-) diff --git a/can/io/logger.py b/can/io/logger.py index 9febfe680..4d8ddc070 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -290,7 +290,8 @@ def __exit__( exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> Literal[False]: - return self.writer.__exit__(exc_type, exc_val, exc_tb) + self.stop() + return False @abstractmethod def should_rollover(self, msg: Message) -> bool: diff --git a/test/test_rotating_loggers.py b/test/test_rotating_loggers.py index ab977e3ce..77a2c7f5d 100644 --- a/test/test_rotating_loggers.py +++ b/test/test_rotating_loggers.py @@ -29,7 +29,7 @@ def __init__(self, file: StringPathLike, **kwargs) -> None: suffix = Path(file).suffix.lower() if suffix not in self._supported_formats: raise ValueError(f"Unsupported file format: {suffix}") - self._writer = can.Printer(file=file) + self._writer = can.Logger(filename=file) @property def writer(self) -> FileIOMessageWriter: @@ -59,26 +59,20 @@ def test_attributes(self): assert hasattr(can.io.BaseRotatingLogger, "do_rollover") def test_get_new_writer(self, tmp_path): - with self._get_instance(tmp_path / "__unused.txt") as logger_instance: - writer = logger_instance._get_new_writer(tmp_path / "file.ASC") - assert isinstance(writer, can.ASCWriter) - writer.stop() + with self._get_instance(tmp_path / "file.ASC") as logger_instance: + assert isinstance(logger_instance.writer, can.ASCWriter) - writer = logger_instance._get_new_writer(tmp_path / "file.BLF") - assert isinstance(writer, can.BLFWriter) - writer.stop() + with self._get_instance(tmp_path / "file.BLF") as logger_instance: + assert isinstance(logger_instance.writer, can.BLFWriter) - writer = logger_instance._get_new_writer(tmp_path / "file.CSV") - assert isinstance(writer, can.CSVWriter) - writer.stop() + with self._get_instance(tmp_path / "file.CSV") as logger_instance: + assert isinstance(logger_instance.writer, can.CSVWriter) - writer = logger_instance._get_new_writer(tmp_path / "file.LOG") - assert isinstance(writer, can.CanutilsLogWriter) - writer.stop() + with self._get_instance(tmp_path / "file.LOG") as logger_instance: + assert isinstance(logger_instance.writer, can.CanutilsLogWriter) - writer = logger_instance._get_new_writer(tmp_path / "file.TXT") - assert isinstance(writer, can.Printer) - writer.stop() + with self._get_instance(tmp_path / "file.TXT") as logger_instance: + assert isinstance(logger_instance.writer, can.Printer) def test_rotation_filename(self, tmp_path): with self._get_instance(tmp_path / "__unused.txt") as logger_instance: @@ -89,63 +83,61 @@ def test_rotation_filename(self, tmp_path): assert logger_instance.rotation_filename(default_name) == "default_by_namer" def test_rotate_without_rotator(self, tmp_path): - with self._get_instance(tmp_path / "__unused.txt") as logger_instance: - source = str(tmp_path / "source.txt") - dest = str(tmp_path / "dest.txt") + source = str(tmp_path / "source.txt") + dest = str(tmp_path / "dest.txt") - assert os.path.exists(source) is False - assert os.path.exists(dest) is False + assert os.path.exists(source) is False + assert os.path.exists(dest) is False - logger_instance._writer = logger_instance._get_new_writer(source) - logger_instance.stop() + with self._get_instance(source) as logger_instance: + # use context manager to create `source` file and close it + pass - assert os.path.exists(source) is True - assert os.path.exists(dest) is False + assert os.path.exists(source) is True + assert os.path.exists(dest) is False - logger_instance.rotate(source, dest) + logger_instance.rotate(source, dest) - assert os.path.exists(source) is False - assert os.path.exists(dest) is True + assert os.path.exists(source) is False + assert os.path.exists(dest) is True def test_rotate_with_rotator(self, tmp_path): - with self._get_instance(tmp_path / "__unused.txt") as logger_instance: - rotator_func = Mock() - logger_instance.rotator = rotator_func + source = str(tmp_path / "source.txt") + dest = str(tmp_path / "dest.txt") - source = str(tmp_path / "source.txt") - dest = str(tmp_path / "dest.txt") + assert os.path.exists(source) is False + assert os.path.exists(dest) is False - assert os.path.exists(source) is False - assert os.path.exists(dest) is False + with self._get_instance(source) as logger_instance: + # use context manager to create `source` file and close it + pass - logger_instance._writer = logger_instance._get_new_writer(source) - logger_instance.stop() + rotator_func = Mock() + logger_instance.rotator = rotator_func + logger_instance._writer = logger_instance._get_new_writer(source) + logger_instance.stop() - assert os.path.exists(source) is True - assert os.path.exists(dest) is False + assert os.path.exists(source) is True + assert os.path.exists(dest) is False - logger_instance.rotate(source, dest) - rotator_func.assert_called_with(source, dest) + logger_instance.rotate(source, dest) + rotator_func.assert_called_with(source, dest) - # assert that no rotation was performed since rotator_func - # does not do anything - assert os.path.exists(source) is True - assert os.path.exists(dest) is False + # assert that no rotation was performed since rotator_func + # does not do anything + assert os.path.exists(source) is True + assert os.path.exists(dest) is False def test_stop(self, tmp_path): """Test if stop() method of writer is called.""" with self._get_instance(tmp_path / "file.ASC") as logger_instance: # replace stop method of writer with Mock - original_stop = logger_instance.writer.stop - mock_stop = Mock() + mock_stop = Mock(side_effect=logger_instance.writer.stop) logger_instance.writer.stop = mock_stop logger_instance.stop() mock_stop.assert_called() - # close file.ASC to enable cleanup of temp_dir - original_stop() - def test_on_message_received(self, tmp_path): with self._get_instance(tmp_path / "file.ASC") as logger_instance: # Test without rollover @@ -181,12 +173,9 @@ def test_on_message_received(self, tmp_path): writers_on_message_received.assert_called_with(msg) def test_issue_1792(self, tmp_path): - with self._get_instance(tmp_path / "__unused.log") as logger_instance: - writer = logger_instance._get_new_writer( - tmp_path / "2017_Jeep_Grand_Cherokee_3.6L_V6.log" - ) - assert isinstance(writer, can.CanutilsLogWriter) - writer.stop() + filepath = tmp_path / "2017_Jeep_Grand_Cherokee_3.6L_V6.log" + with self._get_instance(filepath) as logger_instance: + assert isinstance(logger_instance.writer, can.CanutilsLogWriter) class TestSizedRotatingLogger: From 54d271844e8841d7e9ef36c91e41cfd3065f21a8 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Tue, 5 Aug 2025 19:55:10 +0200 Subject: [PATCH 100/127] set Message.channel in PcanBus.recv (#1969) --- can/interfaces/pcan/pcan.py | 1 + doc/changelog.d/1969.fixed.md | 1 + test/test_pcan.py | 2 ++ 3 files changed, 4 insertions(+) create mode 100644 doc/changelog.d/1969.fixed.md diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index ef3b23e3b..d63981580 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -590,6 +590,7 @@ def _recv_internal( ) rx_msg = Message( + channel=self.channel_info, timestamp=timestamp, arbitration_id=pcan_msg.ID, is_extended_id=is_extended_id, diff --git a/doc/changelog.d/1969.fixed.md b/doc/changelog.d/1969.fixed.md new file mode 100644 index 000000000..3ee6f6c50 --- /dev/null +++ b/doc/changelog.d/1969.fixed.md @@ -0,0 +1 @@ +PcanBus: Set `Message.channel` attribute in `PcanBus.recv()`. diff --git a/test/test_pcan.py b/test/test_pcan.py index 31c541f0a..a9c6ea922 100644 --- a/test/test_pcan.py +++ b/test/test_pcan.py @@ -232,6 +232,7 @@ def test_recv(self): self.assertEqual(recv_msg.is_fd, False) self.assertSequenceEqual(recv_msg.data, msg.DATA) self.assertEqual(recv_msg.timestamp, 0) + self.assertEqual(recv_msg.channel, "PCAN_USBBUS1") def test_recv_fd(self): data = (ctypes.c_ubyte * 64)(*[x for x in range(64)]) @@ -255,6 +256,7 @@ def test_recv_fd(self): self.assertEqual(recv_msg.is_fd, True) self.assertSequenceEqual(recv_msg.data, msg.DATA) self.assertEqual(recv_msg.timestamp, 0) + self.assertEqual(recv_msg.channel, "PCAN_USBBUS1") @pytest.mark.timeout(3.0) @patch("select.select", return_value=([], [], [])) From 380a4239a03649df9b179b2c7cac0238bd0578db Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sat, 9 Aug 2025 01:40:59 +1200 Subject: [PATCH 101/127] Create dependabot.yml (#1972) --- .github/dependabot.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..34c1a3a8c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "uv" + directory: "/" + schedule: + interval: "weekly" From df8819f40c20210af6145f51dbae6719fb40640b Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:55:16 +0200 Subject: [PATCH 102/127] Fix zizmor warnings and update dependabot.yml (#1971) * fix broken url * fix zizmor warnings * update dependabot.yml --- .github/dependabot.yml | 17 ++++++++++++- .github/workflows/ci.yml | 55 +++++++++++++++++++++++++++------------- test/test_socketcan.py | 4 +-- 3 files changed, 56 insertions(+), 20 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 34c1a3a8c..e2781e2ef 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,6 +6,21 @@ version: 2 updates: - package-ecosystem: "uv" + # Enable version updates for development dependencies directory: "/" schedule: - interval: "weekly" + interval: "monthly" + groups: + dev-deps: + patterns: + - "*" + + - package-ecosystem: "github-actions" + # Enable version updates for GitHub Actions + directory: "/" + schedule: + interval: "monthly" + groups: + github-actions: + patterns: + - "*" \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b799b463e..f85b08d20 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,9 @@ on: env: PY_COLORS: "1" +permissions: + contents: read + jobs: test: runs-on: ${{ matrix.os }} @@ -29,9 +32,12 @@ jobs: ] fail-fast: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2 + with: + fetch-depth: 0 + persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # 6.4.3 - name: Install tox run: uv tool install tox --with tox-uv - name: Setup SocketCAN @@ -45,10 +51,10 @@ jobs: tox -e ${{ matrix.env }} env: # SocketCAN tests currently fail with PyPy because it does not support raw CAN sockets - # See: https://foss.heptapod.net/pypy/pypy/-/issues/3809 + # See: https://github.com/pypy/pypy/issues/3808 TEST_SOCKETCAN: "${{ matrix.os == 'ubuntu-latest' && ! startsWith(matrix.env, 'pypy' ) }}" - name: Coveralls Parallel - uses: coverallsapp/github-action@v2 + uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b # 2.3.6 with: github-token: ${{ secrets.github_token }} flag-name: Unittests-${{ matrix.os }}-${{ matrix.env }} @@ -59,9 +65,12 @@ jobs: needs: test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2 + with: + fetch-depth: 0 + persist-credentials: false - name: Coveralls Finished - uses: coverallsapp/github-action@v2 + uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b # 2.3.6 with: github-token: ${{ secrets.github_token }} parallel-finished: true @@ -69,9 +78,12 @@ jobs: static-code-analysis: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2 + with: + fetch-depth: 0 + persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # 6.4.3 - name: Install tox run: uv tool install tox --with tox-uv - name: Run linters @@ -84,9 +96,12 @@ jobs: docs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2 + with: + fetch-depth: 0 + persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # 6.4.3 - name: Install tox run: uv tool install tox --with tox-uv - name: Build documentation @@ -97,17 +112,18 @@ jobs: name: Packaging runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2 with: - fetch-depth: 0 # fetch tags for setuptools-scm + fetch-depth: 0 + persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # 6.4.3 - name: Build wheel and sdist - run: uvx --from build pyproject-build --installer uv + run: uv build - name: Check build artifacts run: uvx twine check --strict dist/* - name: Save artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # 4.6.2 with: name: release path: ./dist @@ -123,10 +139,15 @@ jobs: # upload to PyPI only on release if: github.event.release && github.event.action == 'published' steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # 4.3.0 with: path: dist merge-multiple: true + - name: Generate artifact attestation + uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # 2.4.0 + with: + subject-path: 'dist/*' + - name: Publish release distributions to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 + uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # 1.12.4 diff --git a/test/test_socketcan.py b/test/test_socketcan.py index 3df233f96..534ee2a61 100644 --- a/test/test_socketcan.py +++ b/test/test_socketcan.py @@ -377,7 +377,7 @@ def test_pypy_socketcan_support(self): This test shall document raw CAN socket support under PyPy. Once this test fails, it is likely that PyPy either implemented raw CAN socket support or at least changed the error that is thrown. - https://foss.heptapod.net/pypy/pypy/-/issues/3809 + https://github.com/pypy/pypy/issues/3808 https://github.com/hardbyte/python-can/issues/1479 """ try: @@ -386,7 +386,7 @@ def test_pypy_socketcan_support(self): if "unknown address family" not in str(e): warnings.warn( "Please check if PyPy has implemented raw CAN socket support! " - "See: https://foss.heptapod.net/pypy/pypy/-/issues/3809" + "See: https://github.com/pypy/pypy/issues/3808" ) From 8ebb9e2d920706fbfe6b52187b6892c7b1533b0e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 16:22:15 +0200 Subject: [PATCH 103/127] Bump the dev-deps group with 2 updates (#1976) Updates the requirements on [ruff](https://github.com/astral-sh/ruff) and [hypothesis](https://github.com/HypothesisWorks/hypothesis) to permit the latest version. Updates `ruff` from 0.12.7 to 0.12.8 - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.12.7...0.12.8) Updates `hypothesis` to 6.137.1 - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.136.0...hypothesis-python-6.137.1) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.12.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dev-deps - dependency-name: hypothesis dependency-version: 6.137.1 dependency-type: direct:production dependency-group: dev-deps ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 72e706740..52a910067 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,7 +88,7 @@ docs = [ ] lint = [ "pylint==3.3.*", - "ruff==0.12.7", + "ruff==0.12.8", "black==25.1.*", "mypy==1.17.*", ] @@ -99,7 +99,7 @@ test = [ "coveralls==4.0.*", "pytest-cov==6.2.*", "coverage==7.10.*", - "hypothesis==6.136.*", + "hypothesis>=6.136,<6.138", "parameterized==0.9.*", ] dev = [ From 9dc14896783d8ee30e49c29785927fb3e8693e2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 16:22:31 +0200 Subject: [PATCH 104/127] Bump actions/download-artifact in the github-actions group (#1975) Bumps the github-actions group with 1 update: [actions/download-artifact](https://github.com/actions/download-artifact). Updates `actions/download-artifact` from 4.3.0 to 5.0.0 - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/d3f86a106a0bac45b974a628896c90dbdf5c8093...634f93cb2916e3fdff6788551b99b062d0335ce0) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: 5.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f85b08d20..77f1cc1f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -139,7 +139,7 @@ jobs: # upload to PyPI only on release if: github.event.release && github.event.action == 'published' steps: - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # 4.3.0 + - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # 5.0.0 with: path: dist merge-multiple: true From 9c7115146c60b2ae3dc9d23674f61c0802e18f7c Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 8 Aug 2025 21:17:31 +0200 Subject: [PATCH 105/127] Generate Changelog for v4.6.0 (#1970) * add python 3.14 classifier * add another fragmet for 1949 * generate changelog for v4.6.0 * dependabot PRs should not trigger workflow twice --- .github/workflows/ci.yml | 2 ++ CHANGELOG.md | 43 +++++++++++++++++++++++++++++++++ doc/changelog.d/1758.added.md | 1 - doc/changelog.d/1851.changed.md | 1 - doc/changelog.d/1890.added.md | 1 - doc/changelog.d/1904.fixed.md | 1 - doc/changelog.d/1906.fixed.md | 1 - doc/changelog.d/1908.fixed.md | 1 - doc/changelog.d/1914.added.md | 1 - doc/changelog.d/1920.added.md | 1 - doc/changelog.d/1921.fixed.md | 1 - doc/changelog.d/1927.fixed.md | 1 - doc/changelog.d/1931.removed.md | 1 - doc/changelog.d/1934.fixed.md | 1 - doc/changelog.d/1940.fixed.md | 1 - doc/changelog.d/1941.added.md | 1 - doc/changelog.d/1945.changed.md | 2 -- doc/changelog.d/1946.changed.md | 1 - doc/changelog.d/1947.changed.md | 1 - doc/changelog.d/1948.added.md | 1 - doc/changelog.d/1949.added.md | 1 - doc/changelog.d/1951.removed.md | 1 - doc/changelog.d/1953.added.md | 1 - doc/changelog.d/1954.added.md | 1 - doc/changelog.d/1957.fixed.md | 1 - doc/changelog.d/1960.changed.md | 1 - doc/changelog.d/1961.added.md | 1 - doc/changelog.d/1967.fixed.md | 1 - doc/changelog.d/1969.fixed.md | 1 - pyproject.toml | 1 + 30 files changed, 46 insertions(+), 28 deletions(-) delete mode 100644 doc/changelog.d/1758.added.md delete mode 100644 doc/changelog.d/1851.changed.md delete mode 100644 doc/changelog.d/1890.added.md delete mode 100644 doc/changelog.d/1904.fixed.md delete mode 100644 doc/changelog.d/1906.fixed.md delete mode 100644 doc/changelog.d/1908.fixed.md delete mode 100644 doc/changelog.d/1914.added.md delete mode 100644 doc/changelog.d/1920.added.md delete mode 100644 doc/changelog.d/1921.fixed.md delete mode 100644 doc/changelog.d/1927.fixed.md delete mode 100644 doc/changelog.d/1931.removed.md delete mode 100644 doc/changelog.d/1934.fixed.md delete mode 100644 doc/changelog.d/1940.fixed.md delete mode 100644 doc/changelog.d/1941.added.md delete mode 100644 doc/changelog.d/1945.changed.md delete mode 100644 doc/changelog.d/1946.changed.md delete mode 100644 doc/changelog.d/1947.changed.md delete mode 100644 doc/changelog.d/1948.added.md delete mode 100644 doc/changelog.d/1949.added.md delete mode 100644 doc/changelog.d/1951.removed.md delete mode 100644 doc/changelog.d/1953.added.md delete mode 100644 doc/changelog.d/1954.added.md delete mode 100644 doc/changelog.d/1957.fixed.md delete mode 100644 doc/changelog.d/1960.changed.md delete mode 100644 doc/changelog.d/1961.added.md delete mode 100644 doc/changelog.d/1967.fixed.md delete mode 100644 doc/changelog.d/1969.fixed.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77f1cc1f9..a80f1e247 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,8 @@ on: types: [ published ] pull_request: push: + branches-ignore: + - 'dependabot/**' env: PY_COLORS: "1" diff --git a/CHANGELOG.md b/CHANGELOG.md index a8ec28f12..8fdbcecf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,49 @@ This project uses [*towncrier*](https://towncrier.readthedocs.io/) and the chang +## Version [v4.6.0](https://github.com/hardbyte/python-can/tree/v4.6.0) - 2025-08-05 + +### Removed + +- Remove support for Python 3.8. ([#1931](https://github.com/hardbyte/python-can/issues/1931)) +- Unknown command line arguments ("extra args") are no longer passed down to `can.Bus()` instantiation. Use the `--bus-kwargs` argument instead. ([#1949](https://github.com/hardbyte/python-can/issues/1949)) +- Remove `can.io.generic.BaseIOHandler` class. Improve `can.io.*` type annotations by using `typing.Generic`. ([#1951](https://github.com/hardbyte/python-can/issues/1951)) + +### Added + +- Support 11-bit identifiers in the `serial` interface. ([#1758](https://github.com/hardbyte/python-can/issues/1758)) +- Keep track of active Notifiers and make Notifier usable as a context manager. Add function `Notifier.find_instances(bus)` to find the active Notifier for a given bus instance. ([#1890](https://github.com/hardbyte/python-can/issues/1890)) +- Add Windows support to `udp_multicast` interface. ([#1914](https://github.com/hardbyte/python-can/issues/1914)) +- Add FD support to `slcan` according to CANable 2.0 implementation. ([#1920](https://github.com/hardbyte/python-can/issues/1920)) +- Add support for error messages to the `socketcand` interface. ([#1941](https://github.com/hardbyte/python-can/issues/1941)) +- Add support for remote and error frames in the `serial` interface. ([#1948](https://github.com/hardbyte/python-can/issues/1948)) +- Add public functions `can.cli.add_bus_arguments` and `can.cli.create_bus_from_namespace` for creating bus command line options. Currently downstream packages need to implement their own logic to configure *python-can* buses. Now *python-can* can create and parse bus options for third party packages. ([#1949](https://github.com/hardbyte/python-can/issues/1949)) +- Add support for remote frames to `TRCReader`. ([#1953](https://github.com/hardbyte/python-can/issues/1953)) +- Mention the `python-can-candle` package in the plugin interface section of the documentation. ([#1954](https://github.com/hardbyte/python-can/issues/1954)) +- Add new CLI tool `python -m can.bridge` (or just `can_bridge`) to create a software bridge between two physical buses. ([#1961](https://github.com/hardbyte/python-can/issues/1961)) + +### Changed + +- Allow sending Classic CAN frames with a DLC value larger than 8 using the `socketcan` interface. ([#1851](https://github.com/hardbyte/python-can/issues/1851)) +- The `gs_usb` extra dependency was renamed to `gs-usb`. + The `lint` extra dependency was removed and replaced with new PEP 735 dependency groups `lint`, `docs` and `test`. ([#1945](https://github.com/hardbyte/python-can/issues/1945)) +- Update dependency name from `zlgcan-driver-py` to `zlgcan`. ([#1946](https://github.com/hardbyte/python-can/issues/1946)) +- Use ThreadPoolExecutor in `detect_available_configs()` to reduce runtime and add `timeout` parameter. ([#1947](https://github.com/hardbyte/python-can/issues/1947)) +- Update contribution guide. ([#1960](https://github.com/hardbyte/python-can/issues/1960)) + +### Fixed + +- Fix a bug in `slcanBus.get_version()` and `slcanBus.get_serial_number()`: If any other data was received during the function call, then `None` was returned. ([#1904](https://github.com/hardbyte/python-can/issues/1904)) +- Fix incorrect padding of CAN FD payload in `BlfReader`. ([#1906](https://github.com/hardbyte/python-can/issues/1906)) +- Set correct message direction for messages received with `kvaser` interface and `receive_own_messages=True`. ([#1908](https://github.com/hardbyte/python-can/issues/1908)) +- Fix timestamp rounding error in `BlfWriter`. ([#1921](https://github.com/hardbyte/python-can/issues/1921)) +- Fix timestamp rounding error in `BlfReader`. ([#1927](https://github.com/hardbyte/python-can/issues/1927)) +- Handle timer overflow message and build timestamp according to the epoch in the `ixxat` interface. ([#1934](https://github.com/hardbyte/python-can/issues/1934)) +- Avoid unsupported `ioctl` function call to allow usage of the `udp_multicast` interface on MacOS. ([#1940](https://github.com/hardbyte/python-can/issues/1940)) +- Fix configuration file parsing for the `state` bus parameter. ([#1957](https://github.com/hardbyte/python-can/issues/1957)) +- Mf4Reader: support non-standard `CAN_DataFrame.Dir` values in mf4 files created by [ihedvall/mdflib](https://github.com/ihedvall/mdflib). ([#1967](https://github.com/hardbyte/python-can/issues/1967)) +- PcanBus: Set `Message.channel` attribute in `PcanBus.recv()`. ([#1969](https://github.com/hardbyte/python-can/issues/1969)) + ## Version 4.5.0 diff --git a/doc/changelog.d/1758.added.md b/doc/changelog.d/1758.added.md deleted file mode 100644 index 0b95b14e2..000000000 --- a/doc/changelog.d/1758.added.md +++ /dev/null @@ -1 +0,0 @@ -Support 11-bit identifiers in the `serial` interface. diff --git a/doc/changelog.d/1851.changed.md b/doc/changelog.d/1851.changed.md deleted file mode 100644 index 672f7bd7d..000000000 --- a/doc/changelog.d/1851.changed.md +++ /dev/null @@ -1 +0,0 @@ -Allow sending Classic CAN frames with a DLC value larger than 8 using the `socketcan` interface. \ No newline at end of file diff --git a/doc/changelog.d/1890.added.md b/doc/changelog.d/1890.added.md deleted file mode 100644 index 802629ed3..000000000 --- a/doc/changelog.d/1890.added.md +++ /dev/null @@ -1 +0,0 @@ -Keep track of active Notifiers and make Notifier usable as a context manager. Add function `Notifier.find_instances(bus)` to find the active Notifier for a given bus instance. diff --git a/doc/changelog.d/1904.fixed.md b/doc/changelog.d/1904.fixed.md deleted file mode 100644 index 80b665a6b..000000000 --- a/doc/changelog.d/1904.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix a bug in `slcanBus.get_version()` and `slcanBus.get_serial_number()`: If any other data was received during the function call, then `None` was returned. \ No newline at end of file diff --git a/doc/changelog.d/1906.fixed.md b/doc/changelog.d/1906.fixed.md deleted file mode 100644 index f8988ff48..000000000 --- a/doc/changelog.d/1906.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix incorrect padding of CAN FD payload in `BlfReader`. \ No newline at end of file diff --git a/doc/changelog.d/1908.fixed.md b/doc/changelog.d/1908.fixed.md deleted file mode 100644 index ce8947029..000000000 --- a/doc/changelog.d/1908.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Set correct message direction for messages received with `kvaser` interface and `receive_own_messages=True`. \ No newline at end of file diff --git a/doc/changelog.d/1914.added.md b/doc/changelog.d/1914.added.md deleted file mode 100644 index a1f838001..000000000 --- a/doc/changelog.d/1914.added.md +++ /dev/null @@ -1 +0,0 @@ -Add Windows support to `udp_multicast` interface. \ No newline at end of file diff --git a/doc/changelog.d/1920.added.md b/doc/changelog.d/1920.added.md deleted file mode 100644 index c4f0e532e..000000000 --- a/doc/changelog.d/1920.added.md +++ /dev/null @@ -1 +0,0 @@ -Add FD support to `slcan` according to CANable 2.0 implementation. diff --git a/doc/changelog.d/1921.fixed.md b/doc/changelog.d/1921.fixed.md deleted file mode 100644 index 139d2979f..000000000 --- a/doc/changelog.d/1921.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix timestamp rounding error in `BlfWriter`. diff --git a/doc/changelog.d/1927.fixed.md b/doc/changelog.d/1927.fixed.md deleted file mode 100644 index 5fb005d05..000000000 --- a/doc/changelog.d/1927.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix timestamp rounding error in `BlfReader`. \ No newline at end of file diff --git a/doc/changelog.d/1931.removed.md b/doc/changelog.d/1931.removed.md deleted file mode 100644 index 416329a83..000000000 --- a/doc/changelog.d/1931.removed.md +++ /dev/null @@ -1 +0,0 @@ -Remove support for Python 3.8. diff --git a/doc/changelog.d/1934.fixed.md b/doc/changelog.d/1934.fixed.md deleted file mode 100644 index a12e4ffb2..000000000 --- a/doc/changelog.d/1934.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Handle timer overflow message and build timestamp according to the epoch in the `ixxat` interface. \ No newline at end of file diff --git a/doc/changelog.d/1940.fixed.md b/doc/changelog.d/1940.fixed.md deleted file mode 100644 index 9f4fc09ba..000000000 --- a/doc/changelog.d/1940.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Avoid unsupported `ioctl` function call to allow usage of the `udp_multicast` interface on MacOS. \ No newline at end of file diff --git a/doc/changelog.d/1941.added.md b/doc/changelog.d/1941.added.md deleted file mode 100644 index a3d87cb6b..000000000 --- a/doc/changelog.d/1941.added.md +++ /dev/null @@ -1 +0,0 @@ -Add support for error messages to the `socketcand` interface. \ No newline at end of file diff --git a/doc/changelog.d/1945.changed.md b/doc/changelog.d/1945.changed.md deleted file mode 100644 index 59a48774f..000000000 --- a/doc/changelog.d/1945.changed.md +++ /dev/null @@ -1,2 +0,0 @@ -The `gs_usb` extra dependency was renamed to `gs-usb`. -The `lint` extra dependency was removed and replaced with new PEP 735 dependency groups `lint`, `docs` and `test`. \ No newline at end of file diff --git a/doc/changelog.d/1946.changed.md b/doc/changelog.d/1946.changed.md deleted file mode 100644 index d5dad4225..000000000 --- a/doc/changelog.d/1946.changed.md +++ /dev/null @@ -1 +0,0 @@ -Update dependency name from `zlgcan-driver-py` to `zlgcan`. \ No newline at end of file diff --git a/doc/changelog.d/1947.changed.md b/doc/changelog.d/1947.changed.md deleted file mode 100644 index db12a0318..000000000 --- a/doc/changelog.d/1947.changed.md +++ /dev/null @@ -1 +0,0 @@ -Use ThreadPoolExecutor in `detect_available_configs()` to reduce runtime and add `timeout` parameter. \ No newline at end of file diff --git a/doc/changelog.d/1948.added.md b/doc/changelog.d/1948.added.md deleted file mode 100644 index 132d49d98..000000000 --- a/doc/changelog.d/1948.added.md +++ /dev/null @@ -1 +0,0 @@ -Add support for remote and error frames in the `serial` interface. \ No newline at end of file diff --git a/doc/changelog.d/1949.added.md b/doc/changelog.d/1949.added.md deleted file mode 100644 index 6e8ac79b5..000000000 --- a/doc/changelog.d/1949.added.md +++ /dev/null @@ -1 +0,0 @@ -Add public functions `can.cli.add_bus_arguments` and `can.cli.create_bus_from_namespace` for creating bus command line options. Currently downstream packages need to implement their own logic to configure *python-can* buses. Now *python-can* can create and parse bus options for third party packages. \ No newline at end of file diff --git a/doc/changelog.d/1951.removed.md b/doc/changelog.d/1951.removed.md deleted file mode 100644 index 6d56c5d50..000000000 --- a/doc/changelog.d/1951.removed.md +++ /dev/null @@ -1 +0,0 @@ -Remove `can.io.generic.BaseIOHandler` class. Improve `can.io.*` type annotations by using `typing.Generic`. diff --git a/doc/changelog.d/1953.added.md b/doc/changelog.d/1953.added.md deleted file mode 100644 index 76ef5137a..000000000 --- a/doc/changelog.d/1953.added.md +++ /dev/null @@ -1 +0,0 @@ -Add support for remote frames to `TRCReader`. diff --git a/doc/changelog.d/1954.added.md b/doc/changelog.d/1954.added.md deleted file mode 100644 index d2d50669b..000000000 --- a/doc/changelog.d/1954.added.md +++ /dev/null @@ -1 +0,0 @@ -Mention the `python-can-candle` package in the plugin interface section of the documentation. diff --git a/doc/changelog.d/1957.fixed.md b/doc/changelog.d/1957.fixed.md deleted file mode 100644 index 9d5dd6071..000000000 --- a/doc/changelog.d/1957.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix configuration file parsing for the `state` bus parameter. diff --git a/doc/changelog.d/1960.changed.md b/doc/changelog.d/1960.changed.md deleted file mode 100644 index f3977aedb..000000000 --- a/doc/changelog.d/1960.changed.md +++ /dev/null @@ -1 +0,0 @@ -Update contribution guide. diff --git a/doc/changelog.d/1961.added.md b/doc/changelog.d/1961.added.md deleted file mode 100644 index 483427ec0..000000000 --- a/doc/changelog.d/1961.added.md +++ /dev/null @@ -1 +0,0 @@ -Add new CLI tool `python -m can.bridge` (or just `can_bridge`) to create a software bridge between two physical buses. diff --git a/doc/changelog.d/1967.fixed.md b/doc/changelog.d/1967.fixed.md deleted file mode 100644 index fdd72b363..000000000 --- a/doc/changelog.d/1967.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Mf4Reader: support non-standard `CAN_DataFrame.Dir` values in mf4 files created by [ihedvall/mdflib](https://github.com/ihedvall/mdflib). diff --git a/doc/changelog.d/1969.fixed.md b/doc/changelog.d/1969.fixed.md deleted file mode 100644 index 3ee6f6c50..000000000 --- a/doc/changelog.d/1969.fixed.md +++ /dev/null @@ -1 +0,0 @@ -PcanBus: Set `Message.channel` attribute in `PcanBus.recv()`. diff --git a/pyproject.toml b/pyproject.toml index 52a910067..e36b50ad0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Embedded Systems", From c0e5a8ed9f92eff10415d8c4d59d8d38fb2a0b99 Mon Sep 17 00:00:00 2001 From: David Charles Ambler Snowdon <3860429+dcasnowdon@users.noreply.github.com> Date: Tue, 12 Aug 2025 16:52:28 +1000 Subject: [PATCH 106/127] Resolve an unset data_bitrate to no operation in slcan. (#1978) * Change the default to 0, which is a no-op for data_bitrate. * Added a changelog fragment. * Fix black formatting for test_slcan.py * Revert the function signature, and explicitly check for `None` in data_bitrate. * Update the town crier news fragment. --- can/interfaces/slcan.py | 5 +++++ doc/changelog.d/1978.fixed.md | 1 + test/test_slcan.py | 8 +++++++- 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 doc/changelog.d/1978.fixed.md diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index 01ba9c995..d9eab5edf 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -180,6 +180,11 @@ def set_bitrate(self, bitrate: int, data_bitrate: Optional[int] = None) -> None: else: bitrates = ", ".join(str(k) for k in self._BITRATES.keys()) raise ValueError(f"Invalid bitrate, choose one of {bitrates}.") + + # If data_bitrate is None, we set it to 0 which means no data bitrate + if data_bitrate is None: + data_bitrate = 0 + if data_bitrate in self._DATA_BITRATES: dbitrate_code = self._DATA_BITRATES[data_bitrate] else: diff --git a/doc/changelog.d/1978.fixed.md b/doc/changelog.d/1978.fixed.md new file mode 100644 index 000000000..1de50bb60 --- /dev/null +++ b/doc/changelog.d/1978.fixed.md @@ -0,0 +1 @@ +Fix initialisation of an slcan bus, when setting a bitrate. When using CAN 2.0 (not FD), the default setting for `data_bitrate` was invalid, causing an exception. \ No newline at end of file diff --git a/test/test_slcan.py b/test/test_slcan.py index 491800e24..b757ad04d 100644 --- a/test/test_slcan.py +++ b/test/test_slcan.py @@ -74,7 +74,13 @@ class slcanTestCase(unittest.TestCase): def setUp(self): self.bus = cast( can.interfaces.slcan.slcanBus, - can.Bus("loop://", interface="slcan", sleep_after_open=0, timeout=TIMEOUT), + can.Bus( + "loop://", + interface="slcan", + sleep_after_open=0, + timeout=TIMEOUT, + bitrate=500000, + ), ) self.serial = cast(SerialMock, self.bus.serialPortOrig) self.serial.reset_input_buffer() From 702ec3eb79530b9242a1167c049cfe2cfec235e5 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Tue, 12 Aug 2025 09:32:01 +0200 Subject: [PATCH 107/127] prepare CHANGELOG.md for v4.6.1 (#1979) --- CHANGELOG.md | 7 +++++++ README.rst | 4 ++-- doc/changelog.d/1978.fixed.md | 1 - 3 files changed, 9 insertions(+), 3 deletions(-) delete mode 100644 doc/changelog.d/1978.fixed.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fdbcecf1..75ecab49e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ This project uses [*towncrier*](https://towncrier.readthedocs.io/) and the chang +## Version [v4.6.1](https://github.com/hardbyte/python-can/tree/v4.6.1) - 2025-08-12 + +### Fixed + +- Fix initialisation of an slcan bus, when setting a bitrate. When using CAN 2.0 (not FD), the default setting for `data_bitrate` was invalid, causing an exception. ([#1978](https://github.com/hardbyte/python-can/issues/1978)) + + ## Version [v4.6.0](https://github.com/hardbyte/python-can/tree/v4.6.0) - 2025-08-05 ### Removed diff --git a/README.rst b/README.rst index 3c185f6cb..2579871b9 100644 --- a/README.rst +++ b/README.rst @@ -37,8 +37,8 @@ python-can :target: https://github.com/hardbyte/python-can/actions/workflows/ci.yml :alt: Github Actions workflow status -.. |coverage| image:: https://coveralls.io/repos/github/hardbyte/python-can/badge.svg?branch=develop - :target: https://coveralls.io/github/hardbyte/python-can?branch=develop +.. |coverage| image:: https://coveralls.io/repos/github/hardbyte/python-can/badge.svg?branch=main + :target: https://coveralls.io/github/hardbyte/python-can?branch=main :alt: Test coverage reports on Coveralls.io The **C**\ ontroller **A**\ rea **N**\ etwork is a bus standard designed diff --git a/doc/changelog.d/1978.fixed.md b/doc/changelog.d/1978.fixed.md deleted file mode 100644 index 1de50bb60..000000000 --- a/doc/changelog.d/1978.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix initialisation of an slcan bus, when setting a bitrate. When using CAN 2.0 (not FD), the default setting for `data_bitrate` was invalid, causing an exception. \ No newline at end of file From bc248e8aaf96280a574c06e8e7d2778a67f091e3 Mon Sep 17 00:00:00 2001 From: Ben Beasley Date: Tue, 12 Aug 2025 19:10:35 -0400 Subject: [PATCH 108/127] Allow wrapt 2.x (#1980) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e36b50ad0..e125ea84f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ dynamic = ["readme", "version"] description = "Controller Area Network interface module for Python" authors = [{ name = "python-can contributors" }] dependencies = [ - "wrapt~=1.10", + "wrapt >= 1.10, < 3", "packaging >= 23.1", "typing_extensions>=3.10.0.0", ] From 921b47ecb97f07f1012032fe6740941a68116a70 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Tue, 19 Aug 2025 17:57:40 +0200 Subject: [PATCH 109/127] keep a reference to asyncio tasks (#1982) --- can/notifier.py | 7 +++++-- doc/changelog.d/1938.fixed.md | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 doc/changelog.d/1938.fixed.md diff --git a/can/notifier.py b/can/notifier.py index b2f550df7..a2ee512fc 100644 --- a/can/notifier.py +++ b/can/notifier.py @@ -148,6 +148,7 @@ def __init__( self._lock = threading.Lock() self._readers: list[Union[int, threading.Thread]] = [] + self._tasks: set[asyncio.Task] = set() _bus_list: list[BusABC] = bus if isinstance(bus, list) else [bus] for each_bus in _bus_list: self.add_bus(each_bus) @@ -256,8 +257,10 @@ def _on_message_received(self, msg: Message) -> None: for callback in self.listeners: res = callback(msg) if res and self._loop and asyncio.iscoroutine(res): - # Schedule coroutine - self._loop.create_task(res) + # Schedule coroutine and keep a reference to the task + task = self._loop.create_task(res) + self._tasks.add(task) + task.add_done_callback(self._tasks.discard) def _on_error(self, exc: Exception) -> bool: """Calls ``on_error()`` for all listeners if they implement it. diff --git a/doc/changelog.d/1938.fixed.md b/doc/changelog.d/1938.fixed.md new file mode 100644 index 000000000..f9aad1089 --- /dev/null +++ b/doc/changelog.d/1938.fixed.md @@ -0,0 +1 @@ +Keep a reference to asyncio tasks in `can.Notifier` as recommended by [python documentation](https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task). From efa9f0d401ae0f800429dd58385431deda604609 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 23 Aug 2025 14:27:47 +0200 Subject: [PATCH 110/127] Ensure that all virtual channels are closed after tests (#1984) --- test/back2back_test.py | 14 +-- test/conftest.py | 24 ++++ test/notifier_test.py | 36 +++--- test/simplecyclic_test.py | 224 +++++++++++++++++++------------------- test/test_bus.py | 8 +- test/zero_dlc_test.py | 45 ++++---- 6 files changed, 187 insertions(+), 164 deletions(-) create mode 100644 test/conftest.py diff --git a/test/back2back_test.py b/test/back2back_test.py index b52bae530..738c9d16b 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -164,37 +164,33 @@ def test_message_is_rx(self): ) def test_message_is_rx_receive_own_messages(self): """The same as `test_message_direction` but testing with `receive_own_messages=True`.""" - bus3 = can.Bus( + with can.Bus( channel=self.CHANNEL_2, interface=self.INTERFACE_2, bitrate=self.BITRATE, fd=TEST_CAN_FD, single_handle=True, receive_own_messages=True, - ) - try: + ) as bus3: msg = can.Message( is_extended_id=False, arbitration_id=0x300, data=[2, 1, 3], is_rx=False ) bus3.send(msg) self_recv_msg_bus3 = bus3.recv(self.TIMEOUT) self.assertTrue(self_recv_msg_bus3.is_rx) - finally: - bus3.shutdown() def test_unique_message_instances(self): """Verify that we have a different instances of message for each bus even with `receive_own_messages=True`. """ - bus3 = can.Bus( + with can.Bus( channel=self.CHANNEL_2, interface=self.INTERFACE_2, bitrate=self.BITRATE, fd=TEST_CAN_FD, single_handle=True, receive_own_messages=True, - ) - try: + ) as bus3: msg = can.Message( is_extended_id=False, arbitration_id=0x300, data=[2, 1, 3] ) @@ -209,8 +205,6 @@ def test_unique_message_instances(self): recv_msg_bus1.data[0] = 4 self.assertNotEqual(recv_msg_bus1.data, recv_msg_bus2.data) self.assertEqual(recv_msg_bus2.data, self_recv_msg_bus3.data) - finally: - bus3.shutdown() def test_fd_message(self): msg = can.Message( diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 000000000..c54238be1 --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,24 @@ +import pytest + +from can.interfaces import virtual + + +@pytest.fixture(autouse=True) +def check_unclosed_virtual_channel(): + """ + Pytest fixture for detecting leaked virtual CAN channels. + + - The fixture yields control to the test. + - After the test completes, it acquires `virtual.channels_lock` and asserts + that `virtual.channels` is empty. + - If a test leaves behind any unclosed virtual CAN channels, the assertion + will fail, surfacing resource leaks early. + + This helps maintain test isolation and prevents subtle bugs caused by + leftover state between tests. + """ + + yield + + with virtual.channels_lock: + assert len(virtual.channels) == 0 diff --git a/test/notifier_test.py b/test/notifier_test.py index c21d51f04..d8512a00b 100644 --- a/test/notifier_test.py +++ b/test/notifier_test.py @@ -20,23 +20,25 @@ def test_single_bus(self): self.assertTrue(notifier.stopped) def test_multiple_bus(self): - with can.Bus(0, interface="virtual", receive_own_messages=True) as bus1: - with can.Bus(1, interface="virtual", receive_own_messages=True) as bus2: - reader = can.BufferedReader() - notifier = can.Notifier([bus1, bus2], [reader], 0.1) - self.assertFalse(notifier.stopped) - msg = can.Message() - bus1.send(msg) - time.sleep(0.1) - bus2.send(msg) - recv_msg = reader.get_message(1) - self.assertIsNotNone(recv_msg) - self.assertEqual(recv_msg.channel, 0) - recv_msg = reader.get_message(1) - self.assertIsNotNone(recv_msg) - self.assertEqual(recv_msg.channel, 1) - notifier.stop() - self.assertTrue(notifier.stopped) + with ( + can.Bus(0, interface="virtual", receive_own_messages=True) as bus1, + can.Bus(1, interface="virtual", receive_own_messages=True) as bus2, + ): + reader = can.BufferedReader() + notifier = can.Notifier([bus1, bus2], [reader], 0.1) + self.assertFalse(notifier.stopped) + msg = can.Message() + bus1.send(msg) + time.sleep(0.1) + bus2.send(msg) + recv_msg = reader.get_message(1) + self.assertIsNotNone(recv_msg) + self.assertEqual(recv_msg.channel, 0) + recv_msg = reader.get_message(1) + self.assertIsNotNone(recv_msg) + self.assertEqual(recv_msg.channel, 1) + notifier.stop() + self.assertTrue(notifier.stopped) def test_context_manager(self): with can.Bus("test", interface="virtual", receive_own_messages=True) as bus: diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py index c4c1a2340..91bbf3bbe 100644 --- a/test/simplecyclic_test.py +++ b/test/simplecyclic_test.py @@ -36,123 +36,120 @@ def test_cycle_time(self): is_extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7] ) - with can.interface.Bus(interface="virtual") as bus1: - with can.interface.Bus(interface="virtual") as bus2: - # disabling the garbage collector makes the time readings more reliable - gc.disable() - - task = bus1.send_periodic(msg, 0.01, 1) - self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC) + with ( + can.interface.Bus(interface="virtual") as bus1, + can.interface.Bus(interface="virtual") as bus2, + ): + # disabling the garbage collector makes the time readings more reliable + gc.disable() + + task = bus1.send_periodic(msg, 0.01, 1) + self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC) - sleep(2) - size = bus2.queue.qsize() - # About 100 messages should have been transmitted - self.assertTrue( - 80 <= size <= 120, - "100 +/- 20 messages should have been transmitted. But queue contained {}".format( - size - ), - ) - last_msg = bus2.recv() - next_last_msg = bus2.recv() + sleep(2) + size = bus2.queue.qsize() + # About 100 messages should have been transmitted + self.assertTrue( + 80 <= size <= 120, + "100 +/- 20 messages should have been transmitted. But queue contained {}".format( + size + ), + ) + last_msg = bus2.recv() + next_last_msg = bus2.recv() - # we need to reenable the garbage collector again - gc.enable() + # we need to reenable the garbage collector again + gc.enable() - # Check consecutive messages are spaced properly in time and have - # the same id/data - self.assertMessageEqual(last_msg, next_last_msg) + # Check consecutive messages are spaced properly in time and have + # the same id/data + self.assertMessageEqual(last_msg, next_last_msg) - # Check the message id/data sent is the same as message received - # Set timestamp and channel to match recv'd because we don't care - # and they are not initialized by the can.Message constructor. - msg.timestamp = last_msg.timestamp - msg.channel = last_msg.channel - self.assertMessageEqual(msg, last_msg) + # Check the message id/data sent is the same as message received + # Set timestamp and channel to match recv'd because we don't care + # and they are not initialized by the can.Message constructor. + msg.timestamp = last_msg.timestamp + msg.channel = last_msg.channel + self.assertMessageEqual(msg, last_msg) def test_removing_bus_tasks(self): - bus = can.interface.Bus(interface="virtual") - tasks = [] - for task_i in range(10): - msg = can.Message( - is_extended_id=False, - arbitration_id=0x123, - data=[0, 1, 2, 3, 4, 5, 6, 7], - ) - msg.arbitration_id = task_i - task = bus.send_periodic(msg, 0.1, 1) - tasks.append(task) - self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC) + with can.interface.Bus(interface="virtual") as bus: + tasks = [] + for task_i in range(10): + msg = can.Message( + is_extended_id=False, + arbitration_id=0x123, + data=[0, 1, 2, 3, 4, 5, 6, 7], + ) + msg.arbitration_id = task_i + task = bus.send_periodic(msg, 0.1, 1) + tasks.append(task) + self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC) - assert len(bus._periodic_tasks) == 10 + assert len(bus._periodic_tasks) == 10 - for task in tasks: - # Note calling task.stop will remove the task from the Bus's internal task management list - task.stop() + for task in tasks: + # Note calling task.stop will remove the task from the Bus's internal task management list + task.stop() - self.join_threads([task.thread for task in tasks], 5.0) + self.join_threads([task.thread for task in tasks], 5.0) - assert len(bus._periodic_tasks) == 0 - bus.shutdown() + assert len(bus._periodic_tasks) == 0 def test_managed_tasks(self): - bus = can.interface.Bus(interface="virtual", receive_own_messages=True) - tasks = [] - for task_i in range(3): - msg = can.Message( - is_extended_id=False, - arbitration_id=0x123, - data=[0, 1, 2, 3, 4, 5, 6, 7], - ) - msg.arbitration_id = task_i - task = bus.send_periodic(msg, 0.1, 10, store_task=False) - tasks.append(task) - self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC) - - assert len(bus._periodic_tasks) == 0 + with can.interface.Bus(interface="virtual", receive_own_messages=True) as bus: + tasks = [] + for task_i in range(3): + msg = can.Message( + is_extended_id=False, + arbitration_id=0x123, + data=[0, 1, 2, 3, 4, 5, 6, 7], + ) + msg.arbitration_id = task_i + task = bus.send_periodic(msg, 0.1, 10, store_task=False) + tasks.append(task) + self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC) - # Self managed tasks should still be sending messages - for _ in range(50): - received_msg = bus.recv(timeout=5.0) - assert received_msg is not None - assert received_msg.arbitration_id in {0, 1, 2} + assert len(bus._periodic_tasks) == 0 - for task in tasks: - task.stop() + # Self managed tasks should still be sending messages + for _ in range(50): + received_msg = bus.recv(timeout=5.0) + assert received_msg is not None + assert received_msg.arbitration_id in {0, 1, 2} - self.join_threads([task.thread for task in tasks], 5.0) + for task in tasks: + task.stop() - bus.shutdown() + self.join_threads([task.thread for task in tasks], 5.0) def test_stopping_perodic_tasks(self): - bus = can.interface.Bus(interface="virtual") - tasks = [] - for task_i in range(10): - msg = can.Message( - is_extended_id=False, - arbitration_id=0x123, - data=[0, 1, 2, 3, 4, 5, 6, 7], - ) - msg.arbitration_id = task_i - task = bus.send_periodic(msg, 0.1, 1) - tasks.append(task) - - assert len(bus._periodic_tasks) == 10 - # stop half the tasks using the task object - for task in tasks[::2]: - task.stop() + with can.interface.Bus(interface="virtual") as bus: + tasks = [] + for task_i in range(10): + msg = can.Message( + is_extended_id=False, + arbitration_id=0x123, + data=[0, 1, 2, 3, 4, 5, 6, 7], + ) + msg.arbitration_id = task_i + task = bus.send_periodic(msg, 0.1, 1) + tasks.append(task) - assert len(bus._periodic_tasks) == 5 + assert len(bus._periodic_tasks) == 10 + # stop half the tasks using the task object + for task in tasks[::2]: + task.stop() - # stop the other half using the bus api - bus.stop_all_periodic_tasks(remove_tasks=False) - self.join_threads([task.thread for task in tasks], 5.0) + assert len(bus._periodic_tasks) == 5 - # Tasks stopped via `stop_all_periodic_tasks` with remove_tasks=False should - # still be associated with the bus (e.g. for restarting) - assert len(bus._periodic_tasks) == 5 + # stop the other half using the bus api + bus.stop_all_periodic_tasks(remove_tasks=False) + self.join_threads([task.thread for task in tasks], 5.0) - bus.shutdown() + # Tasks stopped via `stop_all_periodic_tasks` with remove_tasks=False should + # still be associated with the bus (e.g. for restarting) + assert len(bus._periodic_tasks) == 5 def test_restart_perodic_tasks(self): period = 0.01 @@ -214,25 +211,26 @@ def _read_all_messages(_bus: "can.interfaces.virtual.VirtualBus") -> None: @unittest.skipIf(IS_CI, "fails randomly when run on CI server") def test_thread_based_cyclic_send_task(self): - bus = can.ThreadSafeBus(interface="virtual") - msg = can.Message( - is_extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7] - ) + with can.ThreadSafeBus(interface="virtual") as bus: + msg = can.Message( + is_extended_id=False, + arbitration_id=0x123, + data=[0, 1, 2, 3, 4, 5, 6, 7], + ) - # good case, bus is up - on_error_mock = MagicMock(return_value=False) - task = can.broadcastmanager.ThreadBasedCyclicSendTask( - bus=bus, - lock=bus._lock_send_periodic, - messages=msg, - period=0.1, - duration=3, - on_error=on_error_mock, - ) - sleep(1) - on_error_mock.assert_not_called() - task.stop() - bus.shutdown() + # good case, bus is up + on_error_mock = MagicMock(return_value=False) + task = can.broadcastmanager.ThreadBasedCyclicSendTask( + bus=bus, + lock=bus._lock_send_periodic, + messages=msg, + period=0.1, + duration=3, + on_error=on_error_mock, + ) + sleep(1) + on_error_mock.assert_not_called() + task.stop() # bus has been shut down on_error_mock = MagicMock(return_value=False) diff --git a/test/test_bus.py b/test/test_bus.py index 24421b2fd..6a09a6deb 100644 --- a/test/test_bus.py +++ b/test/test_bus.py @@ -8,11 +8,11 @@ def test_bus_ignore_config(): with patch.object( target=can.util, attribute="load_config", side_effect=can.util.load_config ): - _ = can.Bus(interface="virtual", ignore_config=True) - assert not can.util.load_config.called + with can.Bus(interface="virtual", ignore_config=True): + assert not can.util.load_config.called - _ = can.Bus(interface="virtual") - assert can.util.load_config.called + with can.Bus(interface="virtual"): + assert can.util.load_config.called @patch.object(can.bus.BusABC, "shutdown") diff --git a/test/zero_dlc_test.py b/test/zero_dlc_test.py index d6693e294..e8ae8c293 100644 --- a/test/zero_dlc_test.py +++ b/test/zero_dlc_test.py @@ -12,35 +12,40 @@ class ZeroDLCTest(unittest.TestCase): def test_recv_non_zero_dlc(self): - bus_send = can.interface.Bus(interface="virtual") - bus_recv = can.interface.Bus(interface="virtual") - data = [0, 1, 2, 3, 4, 5, 6, 7] - msg_send = can.Message(is_extended_id=False, arbitration_id=0x100, data=data) + with ( + can.interface.Bus(interface="virtual") as bus_send, + can.interface.Bus(interface="virtual") as bus_recv, + ): + data = [0, 1, 2, 3, 4, 5, 6, 7] + msg_send = can.Message( + is_extended_id=False, arbitration_id=0x100, data=data + ) - bus_send.send(msg_send) - msg_recv = bus_recv.recv() + bus_send.send(msg_send) + msg_recv = bus_recv.recv() - # Receiving a frame with data should evaluate msg_recv to True - self.assertTrue(msg_recv) + # Receiving a frame with data should evaluate msg_recv to True + self.assertTrue(msg_recv) def test_recv_none(self): - bus_recv = can.interface.Bus(interface="virtual") + with can.interface.Bus(interface="virtual") as bus_recv: + msg_recv = bus_recv.recv(timeout=0) - msg_recv = bus_recv.recv(timeout=0) - - # Receiving nothing should evaluate msg_recv to False - self.assertFalse(msg_recv) + # Receiving nothing should evaluate msg_recv to False + self.assertFalse(msg_recv) def test_recv_zero_dlc(self): - bus_send = can.interface.Bus(interface="virtual") - bus_recv = can.interface.Bus(interface="virtual") - msg_send = can.Message(is_extended_id=False, arbitration_id=0x100, data=[]) + with ( + can.interface.Bus(interface="virtual") as bus_send, + can.interface.Bus(interface="virtual") as bus_recv, + ): + msg_send = can.Message(is_extended_id=False, arbitration_id=0x100, data=[]) - bus_send.send(msg_send) - msg_recv = bus_recv.recv() + bus_send.send(msg_send) + msg_recv = bus_recv.recv() - # Receiving a frame without data (dlc == 0) should evaluate msg_recv to True - self.assertTrue(msg_recv) + # Receiving a frame without data (dlc == 0) should evaluate msg_recv to True + self.assertTrue(msg_recv) if __name__ == "__main__": From b6280b1d8a5fc3c17e3a6f888b41092733d563d7 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 23 Aug 2025 16:09:10 +0200 Subject: [PATCH 111/127] Improve PyPy test robustness (#1985) * increase timeout for PyPy * remove duration arg --- test/back2back_test.py | 2 +- test/simplecyclic_test.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/back2back_test.py b/test/back2back_test.py index 738c9d16b..ce7c39e2a 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -33,7 +33,7 @@ class Back2BackTestCase(unittest.TestCase): """ BITRATE = 500000 - TIMEOUT = 0.1 + TIMEOUT = 1.0 if IS_PYPY else 0.1 INTERFACE_1 = "virtual" CHANNEL_1 = "virtual_channel_0" diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py index 91bbf3bbe..22a11e643 100644 --- a/test/simplecyclic_test.py +++ b/test/simplecyclic_test.py @@ -5,18 +5,18 @@ """ import gc +import platform import sys import time import traceback import unittest from threading import Thread from time import sleep -from typing import List from unittest.mock import MagicMock import can -from .config import * +from .config import IS_CI, IS_PYPY from .message_helper import ComparingMessagesTestCase @@ -133,7 +133,7 @@ def test_stopping_perodic_tasks(self): data=[0, 1, 2, 3, 4, 5, 6, 7], ) msg.arbitration_id = task_i - task = bus.send_periodic(msg, 0.1, 1) + task = bus.send_periodic(msg, period=0.1) tasks.append(task) assert len(bus._periodic_tasks) == 10 @@ -261,7 +261,7 @@ def test_thread_based_cyclic_send_task(self): task.stop() def test_modifier_callback(self) -> None: - msg_list: List[can.Message] = [] + msg_list: list[can.Message] = [] def increment_first_byte(msg: can.Message) -> None: msg.data[0] = (msg.data[0] + 1) % 256 @@ -288,8 +288,8 @@ def increment_first_byte(msg: can.Message) -> None: self.assertEqual(b"\x07\x00\x00\x00\x00\x00\x00\x00", bytes(msg_list[6].data)) @staticmethod - def join_threads(threads: List[Thread], timeout: float) -> None: - stuck_threads: List[Thread] = [] + def join_threads(threads: list[Thread], timeout: float) -> None: + stuck_threads: list[Thread] = [] t0 = time.perf_counter() for thread in threads: time_left = timeout - (time.perf_counter() - t0) From e142868ee494414e6079b1a5e4fec56196aacc94 Mon Sep 17 00:00:00 2001 From: Gerrit Beine Date: Sun, 31 Aug 2025 15:32:41 +0200 Subject: [PATCH 112/127] Add CAN-over-Ethernet interface plugin to documentation (#1987) * Add COE interface plugin to the list. * Add news fragment for documentation change --------- Co-authored-by: Gerrit Beine --- doc/changelog.d/1987.added.md | 1 + doc/plugin-interface.rst | 37 +++++++++++++++++++---------------- 2 files changed, 21 insertions(+), 17 deletions(-) create mode 100644 doc/changelog.d/1987.added.md diff --git a/doc/changelog.d/1987.added.md b/doc/changelog.d/1987.added.md new file mode 100644 index 000000000..398add3e3 --- /dev/null +++ b/doc/changelog.d/1987.added.md @@ -0,0 +1 @@ +Add [python-can-coe](https://c0d3.sh/smarthome/python-can-coe) interface plugin to the documentation. diff --git a/doc/plugin-interface.rst b/doc/plugin-interface.rst index d841281e8..2f295b678 100644 --- a/doc/plugin-interface.rst +++ b/doc/plugin-interface.rst @@ -62,23 +62,25 @@ The table below lists interface drivers that can be added by installing addition .. note:: The packages listed below are maintained by other authors. Any issues should be reported in their corresponding repository and **not** in the python-can repository. -+----------------------------+-------------------------------------------------------+ -| Name | Description | -+============================+=======================================================+ -| `python-can-canine`_ | CAN Driver for the CANine CAN interface | -+----------------------------+-------------------------------------------------------+ -| `python-can-cvector`_ | Cython based version of the 'VectorBus' | -+----------------------------+-------------------------------------------------------+ -| `python-can-remote`_ | CAN over network bridge | -+----------------------------+-------------------------------------------------------+ -| `python-can-sontheim`_ | CAN Driver for Sontheim CAN interfaces (e.g. CANfox) | -+----------------------------+-------------------------------------------------------+ -| `zlgcan`_ | Python wrapper for zlgcan-driver-rs | -+----------------------------+-------------------------------------------------------+ -| `python-can-cando`_ | Python wrapper for Netronics' CANdo and CANdoISO | -+----------------------------+-------------------------------------------------------+ -| `python-can-candle`_ | A full-featured driver for candleLight | -+----------------------------+-------------------------------------------------------+ ++----------------------------+----------------------------------------------------------+ +| Name | Description | ++============================+==========================================================+ +| `python-can-canine`_ | CAN Driver for the CANine CAN interface | ++----------------------------+----------------------------------------------------------+ +| `python-can-cvector`_ | Cython based version of the 'VectorBus' | ++----------------------------+----------------------------------------------------------+ +| `python-can-remote`_ | CAN over network bridge | ++----------------------------+----------------------------------------------------------+ +| `python-can-sontheim`_ | CAN Driver for Sontheim CAN interfaces (e.g. CANfox) | ++----------------------------+----------------------------------------------------------+ +| `zlgcan`_ | Python wrapper for zlgcan-driver-rs | ++----------------------------+----------------------------------------------------------+ +| `python-can-cando`_ | Python wrapper for Netronics' CANdo and CANdoISO | ++----------------------------+----------------------------------------------------------+ +| `python-can-candle`_ | A full-featured driver for candleLight | ++----------------------------+----------------------------------------------------------+ +| `python-can-coe`_ | A CAN-over-Ethernet interface for Technische Alternative | ++----------------------------+----------------------------------------------------------+ .. _python-can-canine: https://github.com/tinymovr/python-can-canine .. _python-can-cvector: https://github.com/zariiii9003/python-can-cvector @@ -87,4 +89,5 @@ The table below lists interface drivers that can be added by installing addition .. _zlgcan: https://github.com/jesses2025smith/zlgcan-driver .. _python-can-cando: https://github.com/belliriccardo/python-can-cando .. _python-can-candle: https://github.com/BIRLab/python-can-candle +.. _python-can-coe: https://c0d3.sh/smarthome/python-can-coe From ef30d088740b2c448cf3db29de66413e926f0589 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 06:12:38 +0000 Subject: [PATCH 113/127] Bump the github-actions group with 3 updates Bumps the github-actions group with 3 updates: [actions/checkout](https://github.com/actions/checkout), [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) and [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance). Updates `actions/checkout` from 4.2.2 to 5.0.0 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/11bd71901bbe5b1630ceea73d27597364c9af683...08c6903cd8c0fde910a37f88322edcfb5dd907a8) Updates `astral-sh/setup-uv` from 6.4.3 to 6.6.1 - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](https://github.com/astral-sh/setup-uv/compare/e92bafb6253dcd438e0484186d7669ea7a8ca1cc...557e51de59eb14aaaba2ed9621916900a91d50c6) Updates `actions/attest-build-provenance` from 2.4.0 to 3.0.0 - [Release notes](https://github.com/actions/attest-build-provenance/releases) - [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md) - [Commits](https://github.com/actions/attest-build-provenance/compare/e8998f949152b193b063cb0ec769d69d929409be...977bb373ede98d70efdf65b84cb5f73e068dcc2a) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 5.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: astral-sh/setup-uv dependency-version: 6.6.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: actions/attest-build-provenance dependency-version: 3.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a80f1e247..5078a33ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,12 +34,12 @@ jobs: ] fail-fast: false steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 with: fetch-depth: 0 persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # 6.4.3 + uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # 6.6.1 - name: Install tox run: uv tool install tox --with tox-uv - name: Setup SocketCAN @@ -67,7 +67,7 @@ jobs: needs: test runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 with: fetch-depth: 0 persist-credentials: false @@ -80,12 +80,12 @@ jobs: static-code-analysis: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 with: fetch-depth: 0 persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # 6.4.3 + uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # 6.6.1 - name: Install tox run: uv tool install tox --with tox-uv - name: Run linters @@ -98,12 +98,12 @@ jobs: docs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 with: fetch-depth: 0 persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # 6.4.3 + uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # 6.6.1 - name: Install tox run: uv tool install tox --with tox-uv - name: Build documentation @@ -114,12 +114,12 @@ jobs: name: Packaging runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 with: fetch-depth: 0 persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # 6.4.3 + uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # 6.6.1 - name: Build wheel and sdist run: uv build - name: Check build artifacts @@ -147,7 +147,7 @@ jobs: merge-multiple: true - name: Generate artifact attestation - uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # 2.4.0 + uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # 3.0.0 with: subject-path: 'dist/*' From 4ac05e56e85536b120af42f28af6611979b7e404 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 04:26:23 +0000 Subject: [PATCH 114/127] Bump ruff from 0.12.8 to 0.12.11 in the dev-deps group Bumps the dev-deps group with 1 update: [ruff](https://github.com/astral-sh/ruff). Updates `ruff` from 0.12.8 to 0.12.11 - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.12.8...0.12.11) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.12.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dev-deps ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e125ea84f..bc526ce73 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,7 @@ docs = [ ] lint = [ "pylint==3.3.*", - "ruff==0.12.8", + "ruff==0.12.11", "black==25.1.*", "mypy==1.17.*", ] From f17c37622c143ec4990fb57d86654e9c25a62e91 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 10 Oct 2025 16:00:41 +0200 Subject: [PATCH 115/127] remove support for Python 3.9 (#1996) Co-authored-by: zariiii9003 --- .github/dependabot.yml | 5 +- .github/workflows/ci.yml | 1 - README.rst | 1 + can/_entry_points.py | 21 ++------ can/broadcastmanager.py | 37 ++++++------- can/bus.py | 43 +++++++-------- can/cli.py | 26 ++++----- can/ctypesutil.py | 7 +-- can/exceptions.py | 5 +- can/interface.py | 10 ++-- can/interfaces/canalystii.py | 20 +++---- can/interfaces/cantact.py | 12 ++--- can/interfaces/etas/__init__.py | 14 +++-- can/interfaces/gs_usb.py | 7 +-- can/interfaces/iscan.py | 9 ++-- can/interfaces/ixxat/canlib.py | 31 ++++++----- can/interfaces/ixxat/canlib_vcinpl.py | 13 +++-- can/interfaces/ixxat/canlib_vcinpl2.py | 27 +++++----- can/interfaces/kvaser/canlib.py | 5 +- can/interfaces/nican.py | 11 ++-- can/interfaces/nixnet.py | 16 +++--- can/interfaces/pcan/pcan.py | 14 +++-- can/interfaces/robotell.py | 3 +- can/interfaces/serial/serial_can.py | 8 ++- can/interfaces/slcan.py | 26 ++++----- can/interfaces/socketcan/socketcan.py | 37 ++++++------- can/interfaces/socketcan/utils.py | 5 +- can/interfaces/udp_multicast/bus.py | 18 +++---- can/interfaces/udp_multicast/utils.py | 4 +- can/interfaces/usb2can/usb2canInterface.py | 9 ++-- can/interfaces/vector/canlib.py | 61 ++++++++++------------ can/interfaces/vector/exceptions.py | 6 +-- can/interfaces/virtual.py | 8 ++- can/io/asc.py | 14 ++--- can/io/blf.py | 18 +++---- can/io/canutils.py | 10 ++-- can/io/csv.py | 6 +-- can/io/generic.py | 42 +++++++-------- can/io/logger.py | 15 +++--- can/io/mf4.py | 10 ++-- can/io/printer.py | 4 +- can/io/sqlite.py | 4 +- can/io/trc.py | 32 ++++++------ can/listener.py | 11 +--- can/logger.py | 3 +- can/message.py | 14 ++--- can/notifier.py | 23 ++++---- can/thread_safe_bus.py | 18 +++---- can/typechecking.py | 25 ++++----- can/util.py | 31 +++++------ can/viewer.py | 4 +- doc/changelog.d/1996.removed.md | 1 + pyproject.toml | 12 ++--- tox.ini | 2 +- 54 files changed, 363 insertions(+), 456 deletions(-) create mode 100644 doc/changelog.d/1996.removed.md diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e2781e2ef..dbe907783 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,11 +5,12 @@ version: 2 updates: - - package-ecosystem: "uv" + - package-ecosystem: "pip" # Enable version updates for development dependencies directory: "/" schedule: interval: "monthly" + versioning-strategy: "increase-if-necessary" groups: dev-deps: patterns: @@ -23,4 +24,4 @@ updates: groups: github-actions: patterns: - - "*" \ No newline at end of file + - "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5078a33ee..12022ba35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,6 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] env: [ - "py39", "py310", "py311", "py312", diff --git a/README.rst b/README.rst index 2579871b9..6e75d8d7d 100644 --- a/README.rst +++ b/README.rst @@ -61,6 +61,7 @@ Library Version Python 4.0+ 3.7+ 4.3+ 3.8+ 4.6+ 3.9+ + main branch 3.10+ ============================== =========== diff --git a/can/_entry_points.py b/can/_entry_points.py index 6320b797b..fd1a62d24 100644 --- a/can/_entry_points.py +++ b/can/_entry_points.py @@ -1,5 +1,4 @@ import importlib -import sys from dataclasses import dataclass from importlib.metadata import entry_points from typing import Any @@ -16,19 +15,7 @@ def load(self) -> Any: return getattr(module, self.class_name) -# See https://docs.python.org/3/library/importlib.metadata.html#entry-points, -# "Compatibility Note". -if sys.version_info >= (3, 10): - - def read_entry_points(group: str) -> list[_EntryPoint]: - return [ - _EntryPoint(ep.name, ep.module, ep.attr) for ep in entry_points(group=group) - ] - -else: - - def read_entry_points(group: str) -> list[_EntryPoint]: - return [ - _EntryPoint(ep.name, *ep.value.split(":", maxsplit=1)) - for ep in entry_points().get(group, []) # pylint: disable=no-member - ] +def read_entry_points(group: str) -> list[_EntryPoint]: + return [ + _EntryPoint(ep.name, ep.module, ep.attr) for ep in entry_points(group=group) + ] diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index a71f6fd11..1fea9ac50 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -12,13 +12,10 @@ import threading import time import warnings -from collections.abc import Sequence +from collections.abc import Callable, Sequence from typing import ( TYPE_CHECKING, - Callable, Final, - Optional, - Union, cast, ) @@ -78,7 +75,7 @@ def wait_inf(self, event: _Pywin32Event) -> None: ) -PYWIN32: Optional[_Pywin32] = None +PYWIN32: _Pywin32 | None = None if sys.platform == "win32" and sys.version_info < (3, 11): try: PYWIN32 = _Pywin32() @@ -105,9 +102,7 @@ class CyclicSendTaskABC(CyclicTask, abc.ABC): Message send task with defined period """ - def __init__( - self, messages: Union[Sequence[Message], Message], period: float - ) -> None: + def __init__(self, messages: Sequence[Message] | Message, period: float) -> None: """ :param messages: The messages to be sent periodically. @@ -125,7 +120,7 @@ def __init__( @staticmethod def _check_and_convert_messages( - messages: Union[Sequence[Message], Message], + messages: Sequence[Message] | Message, ) -> tuple[Message, ...]: """Helper function to convert a Message or Sequence of messages into a tuple, and raises an error when the given value is invalid. @@ -164,9 +159,9 @@ def _check_and_convert_messages( class LimitedDurationCyclicSendTaskABC(CyclicSendTaskABC, abc.ABC): def __init__( self, - messages: Union[Sequence[Message], Message], + messages: Sequence[Message] | Message, period: float, - duration: Optional[float], + duration: float | None, ) -> None: """Message send task with a defined duration and period. @@ -181,7 +176,7 @@ def __init__( """ super().__init__(messages, period) self.duration = duration - self.end_time: Optional[float] = None + self.end_time: float | None = None class RestartableCyclicTaskABC(CyclicSendTaskABC, abc.ABC): @@ -215,7 +210,7 @@ def _check_modified_messages(self, messages: tuple[Message, ...]) -> None: "from when the task was created" ) - def modify_data(self, messages: Union[Sequence[Message], Message]) -> None: + def modify_data(self, messages: Sequence[Message] | Message) -> None: """Update the contents of the periodically sent messages, without altering the timing. @@ -242,7 +237,7 @@ class MultiRateCyclicSendTaskABC(CyclicSendTaskABC, abc.ABC): def __init__( self, channel: typechecking.Channel, - messages: Union[Sequence[Message], Message], + messages: Sequence[Message] | Message, count: int, # pylint: disable=unused-argument initial_period: float, # pylint: disable=unused-argument subsequent_period: float, @@ -272,12 +267,12 @@ def __init__( self, bus: "BusABC", lock: threading.Lock, - messages: Union[Sequence[Message], Message], + messages: Sequence[Message] | Message, period: float, - duration: Optional[float] = None, - on_error: Optional[Callable[[Exception], bool]] = None, + duration: float | None = None, + on_error: Callable[[Exception], bool] | None = None, autostart: bool = True, - modifier_callback: Optional[Callable[[Message], None]] = None, + modifier_callback: Callable[[Message], None] | None = None, ) -> None: """Transmits `messages` with a `period` seconds for `duration` seconds on a `bus`. @@ -298,13 +293,13 @@ def __init__( self.bus = bus self.send_lock = lock self.stopped = True - self.thread: Optional[threading.Thread] = None + self.thread: threading.Thread | None = None self.on_error = on_error self.modifier_callback = modifier_callback self.period_ms = int(round(period * 1000, 0)) - self.event: Optional[_Pywin32Event] = None + self.event: _Pywin32Event | None = None if PYWIN32: if self.period_ms == 0: # A period of 0 would mean that the timer is signaled only once @@ -338,7 +333,7 @@ def start(self) -> None: self.thread = threading.Thread(target=self._run, name=name) self.thread.daemon = True - self.end_time: Optional[float] = ( + self.end_time: float | None = ( time.perf_counter() + self.duration if self.duration else None ) diff --git a/can/bus.py b/can/bus.py index ec9eb09b7..03425caaa 100644 --- a/can/bus.py +++ b/can/bus.py @@ -6,14 +6,11 @@ import logging import threading from abc import ABC, abstractmethod -from collections.abc import Iterator, Sequence +from collections.abc import Callable, Iterator, Sequence from enum import Enum, auto from time import time from types import TracebackType from typing import ( - Callable, - Optional, - Union, cast, ) @@ -68,7 +65,7 @@ class BusABC(ABC): def __init__( self, channel: can.typechecking.Channel, - can_filters: Optional[can.typechecking.CanFilters] = None, + can_filters: can.typechecking.CanFilters | None = None, **kwargs: object, ): """Construct and open a CAN bus instance of the specified type. @@ -101,7 +98,7 @@ def __init__( def __str__(self) -> str: return self.channel_info - def recv(self, timeout: Optional[float] = None) -> Optional[Message]: + def recv(self, timeout: float | None = None) -> Message | None: """Block waiting for a message from the Bus. :param timeout: @@ -139,9 +136,7 @@ def recv(self, timeout: Optional[float] = None) -> Optional[Message]: return None - def _recv_internal( - self, timeout: Optional[float] - ) -> tuple[Optional[Message], bool]: + def _recv_internal(self, timeout: float | None) -> tuple[Message | None, bool]: """ Read a message from the bus and tell whether it was filtered. This methods may be called by :meth:`~can.BusABC.recv` @@ -184,7 +179,7 @@ def _recv_internal( raise NotImplementedError("Trying to read from a write only bus?") @abstractmethod - def send(self, msg: Message, timeout: Optional[float] = None) -> None: + def send(self, msg: Message, timeout: float | None = None) -> None: """Transmit a message to the CAN bus. Override this method to enable the transmit path. @@ -205,12 +200,12 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: def send_periodic( self, - msgs: Union[Message, Sequence[Message]], + msgs: Message | Sequence[Message], period: float, - duration: Optional[float] = None, + duration: float | None = None, store_task: bool = True, autostart: bool = True, - modifier_callback: Optional[Callable[[Message], None]] = None, + modifier_callback: Callable[[Message], None] | None = None, ) -> can.broadcastmanager.CyclicSendTaskABC: """Start sending messages at a given period on this bus. @@ -297,11 +292,11 @@ def wrapped_stop_method(remove_task: bool = True) -> None: def _send_periodic_internal( self, - msgs: Union[Sequence[Message], Message], + msgs: Sequence[Message] | Message, period: float, - duration: Optional[float] = None, + duration: float | None = None, autostart: bool = True, - modifier_callback: Optional[Callable[[Message], None]] = None, + modifier_callback: Callable[[Message], None] | None = None, ) -> can.broadcastmanager.CyclicSendTaskABC: """Default implementation of periodic message sending using threading. @@ -378,7 +373,7 @@ def __iter__(self) -> Iterator[Message]: yield msg @property - def filters(self) -> Optional[can.typechecking.CanFilters]: + def filters(self) -> can.typechecking.CanFilters | None: """ Modify the filters of this bus. See :meth:`~can.BusABC.set_filters` for details. @@ -386,12 +381,10 @@ def filters(self) -> Optional[can.typechecking.CanFilters]: return self._filters @filters.setter - def filters(self, filters: Optional[can.typechecking.CanFilters]) -> None: + def filters(self, filters: can.typechecking.CanFilters | None) -> None: self.set_filters(filters) - def set_filters( - self, filters: Optional[can.typechecking.CanFilters] = None - ) -> None: + def set_filters(self, filters: can.typechecking.CanFilters | None = None) -> None: """Apply filtering to all messages received by this Bus. All messages that match at least one filter are returned. @@ -417,7 +410,7 @@ def set_filters( with contextlib.suppress(NotImplementedError): self._apply_filters(self._filters) - def _apply_filters(self, filters: Optional[can.typechecking.CanFilters]) -> None: + def _apply_filters(self, filters: can.typechecking.CanFilters | None) -> None: """ Hook for applying the filters to the underlying kernel or hardware if supported/implemented by the interface. @@ -484,9 +477,9 @@ def __enter__(self) -> Self: def __exit__( self, - exc_type: Optional[type[BaseException]], - exc_value: Optional[BaseException], - traceback: Optional[TracebackType], + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, ) -> None: self.shutdown() diff --git a/can/cli.py b/can/cli.py index 6e3850354..d0ff70126 100644 --- a/can/cli.py +++ b/can/cli.py @@ -1,7 +1,7 @@ import argparse import re from collections.abc import Sequence -from typing import Any, Optional, Union +from typing import Any import can from can.typechecking import CanFilter, TAdditionalCliArgs @@ -12,8 +12,8 @@ def add_bus_arguments( parser: argparse.ArgumentParser, *, filter_arg: bool = False, - prefix: Optional[str] = None, - group_title: Optional[str] = None, + prefix: str | None = None, + group_title: str | None = None, ) -> None: """Adds CAN bus configuration options to an argument parser. @@ -144,7 +144,7 @@ def add_bus_arguments( def create_bus_from_namespace( namespace: argparse.Namespace, *, - prefix: Optional[str] = None, + prefix: str | None = None, **kwargs: Any, ) -> can.BusABC: """Creates and returns a CAN bus instance based on the provided namespace and arguments. @@ -192,8 +192,8 @@ def __call__( self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, - values: Union[str, Sequence[Any], None], - option_string: Optional[str] = None, + values: str | Sequence[Any] | None, + option_string: str | None = None, ) -> None: if not isinstance(values, list): raise argparse.ArgumentError(self, "Invalid filter argument") @@ -222,8 +222,8 @@ def __call__( self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, - values: Union[str, Sequence[Any], None], - option_string: Optional[str] = None, + values: str | Sequence[Any] | None, + option_string: str | None = None, ) -> None: if not isinstance(values, list): raise argparse.ArgumentError(self, "Invalid --timing argument") @@ -252,13 +252,13 @@ def __call__( self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, - values: Union[str, Sequence[Any], None], - option_string: Optional[str] = None, + values: str | Sequence[Any] | None, + option_string: str | None = None, ) -> None: if not isinstance(values, list): raise argparse.ArgumentError(self, "Invalid --bus-kwargs argument") - bus_kwargs: dict[str, Union[str, int, float, bool]] = {} + bus_kwargs: dict[str, str | int | float | bool] = {} for arg in values: try: @@ -281,7 +281,7 @@ def __call__( def _add_extra_args( - parser: Union[argparse.ArgumentParser, argparse._ArgumentGroup], + parser: argparse.ArgumentParser | argparse._ArgumentGroup, ) -> None: parser.add_argument( "extra_args", @@ -301,7 +301,7 @@ def _split_arg(_arg: str) -> tuple[str, str]: left, right = _arg.split("=", 1) return left.lstrip("-").replace("-", "_"), right - args: dict[str, Union[str, int, float, bool]] = {} + args: dict[str, str | int | float | bool] = {} for key, string_val in map(_split_arg, unknown_args): args[key] = cast_from_string(string_val) return args diff --git a/can/ctypesutil.py b/can/ctypesutil.py index 8336941be..0798de910 100644 --- a/can/ctypesutil.py +++ b/can/ctypesutil.py @@ -5,7 +5,8 @@ import ctypes import logging import sys -from typing import Any, Callable, Optional, Union +from collections.abc import Callable +from typing import Any log = logging.getLogger("can.ctypesutil") @@ -20,7 +21,7 @@ class CLibrary(_LibBase): - def __init__(self, library_or_path: Union[str, ctypes.CDLL]) -> None: + def __init__(self, library_or_path: str | ctypes.CDLL) -> None: self.func_name: Any if isinstance(library_or_path, str): @@ -33,7 +34,7 @@ def map_symbol( func_name: str, restype: Any = None, argtypes: tuple[Any, ...] = (), - errcheck: Optional[Callable[..., Any]] = None, + errcheck: Callable[..., Any] | None = None, ) -> Any: """ Map and return a symbol (function) from a C library. A reference to the diff --git a/can/exceptions.py b/can/exceptions.py index 8abc75147..696701399 100644 --- a/can/exceptions.py +++ b/can/exceptions.py @@ -17,7 +17,6 @@ from collections.abc import Generator from contextlib import contextmanager -from typing import Optional class CanError(Exception): @@ -51,7 +50,7 @@ class CanError(Exception): def __init__( self, message: str = "", - error_code: Optional[int] = None, + error_code: int | None = None, ) -> None: self.error_code = error_code super().__init__( @@ -108,7 +107,7 @@ class CanTimeoutError(CanError, TimeoutError): @contextmanager def error_check( - error_message: Optional[str] = None, + error_message: str | None = None, exception_type: type[CanError] = CanOperationError, ) -> Generator[None, None, None]: """Catches any exceptions and turns them into the new type while preserving the stack trace.""" diff --git a/can/interface.py b/can/interface.py index 4aa010a36..efde5b214 100644 --- a/can/interface.py +++ b/can/interface.py @@ -8,7 +8,7 @@ import importlib import logging from collections.abc import Callable, Iterable, Sequence -from typing import Any, Optional, Union, cast +from typing import Any, cast from . import util from .bus import BusABC @@ -64,9 +64,9 @@ def _get_class_for_interface(interface: str) -> type[BusABC]: context="config_context", ) def Bus( # noqa: N802 - channel: Optional[Channel] = None, - interface: Optional[str] = None, - config_context: Optional[str] = None, + channel: Channel | None = None, + interface: str | None = None, + config_context: str | None = None, ignore_config: bool = False, **kwargs: Any, ) -> BusABC: @@ -140,7 +140,7 @@ def Bus( # noqa: N802 def detect_available_configs( - interfaces: Union[None, str, Iterable[str]] = None, + interfaces: None | str | Iterable[str] = None, timeout: float = 5.0, ) -> Sequence[AutoDetectedConfig]: """Detect all configurations/channels that the interfaces could diff --git a/can/interfaces/canalystii.py b/can/interfaces/canalystii.py index d85211130..e2bf7555e 100644 --- a/can/interfaces/canalystii.py +++ b/can/interfaces/canalystii.py @@ -3,7 +3,7 @@ from collections import deque from collections.abc import Sequence from ctypes import c_ubyte -from typing import Any, Optional, Union +from typing import Any import canalystii as driver @@ -21,12 +21,12 @@ class CANalystIIBus(BusABC): ) def __init__( self, - channel: Union[int, Sequence[int], str] = (0, 1), + channel: int | Sequence[int] | str = (0, 1), device: int = 0, - bitrate: Optional[int] = None, - timing: Optional[Union[BitTiming, BitTimingFd]] = None, - can_filters: Optional[CanFilters] = None, - rx_queue_size: Optional[int] = None, + bitrate: int | None = None, + timing: BitTiming | BitTimingFd | None = None, + can_filters: CanFilters | None = None, + rx_queue_size: int | None = None, **kwargs: dict[str, Any], ): """ @@ -94,7 +94,7 @@ def __init__( # system. RX_POLL_DELAY = 0.020 - def send(self, msg: Message, timeout: Optional[float] = None) -> None: + def send(self, msg: Message, timeout: float | None = None) -> None: """Send a CAN message to the bus :param msg: message to send @@ -166,8 +166,8 @@ def poll_received_messages(self) -> None: ) def _recv_internal( - self, timeout: Optional[float] = None - ) -> tuple[Optional[Message], bool]: + self, timeout: float | None = None + ) -> tuple[Message | None, bool]: """ :param timeout: float in seconds @@ -194,7 +194,7 @@ def _recv_internal( return (None, False) - def flush_tx_buffer(self, channel: Optional[int] = None) -> None: + def flush_tx_buffer(self, channel: int | None = None) -> None: """Flush the TX buffer of the device. :param channel: diff --git a/can/interfaces/cantact.py b/can/interfaces/cantact.py index 08fe15b72..ee01fbf94 100644 --- a/can/interfaces/cantact.py +++ b/can/interfaces/cantact.py @@ -5,7 +5,7 @@ import logging import time from collections.abc import Sequence -from typing import Any, Optional, Union +from typing import Any from unittest.mock import Mock from can import BitTiming, BitTimingFd, BusABC, CanProtocol, Message @@ -56,7 +56,7 @@ def __init__( bitrate: int = 500_000, poll_interval: float = 0.01, monitor: bool = False, - timing: Optional[Union[BitTiming, BitTimingFd]] = None, + timing: BitTiming | BitTimingFd | None = None, **kwargs: Any, ) -> None: """ @@ -123,9 +123,7 @@ def __init__( **kwargs, ) - def _recv_internal( - self, timeout: Optional[float] - ) -> tuple[Optional[Message], bool]: + def _recv_internal(self, timeout: float | None) -> tuple[Message | None, bool]: if timeout is None: raise TypeError( f"{self.__class__.__name__} expects a numeric `timeout` value." @@ -149,7 +147,7 @@ def _recv_internal( ) return msg, False - def send(self, msg: Message, timeout: Optional[float] = None) -> None: + def send(self, msg: Message, timeout: float | None = None) -> None: with error_check("Cannot send message"): self.interface.send( self.channel, @@ -166,7 +164,7 @@ def shutdown(self) -> None: self.interface.stop() -def mock_recv(timeout: int) -> Optional[dict[str, Any]]: +def mock_recv(timeout: int) -> dict[str, Any] | None: if timeout > 0: return { "id": 0x123, diff --git a/can/interfaces/etas/__init__.py b/can/interfaces/etas/__init__.py index 9d4d0bd2a..f8364a3fd 100644 --- a/can/interfaces/etas/__init__.py +++ b/can/interfaces/etas/__init__.py @@ -1,5 +1,5 @@ import time -from typing import Optional +from typing import Any import can from can.exceptions import CanInitializationError @@ -11,12 +11,12 @@ class EtasBus(can.BusABC): def __init__( self, channel: str, - can_filters: Optional[can.typechecking.CanFilters] = None, + can_filters: can.typechecking.CanFilters | None = None, receive_own_messages: bool = False, bitrate: int = 1000000, fd: bool = True, data_bitrate: int = 2000000, - **kwargs: dict[str, any], + **kwargs: dict[str, Any], ): self.receive_own_messages = receive_own_messages self._can_protocol = can.CanProtocol.CAN_FD if fd else can.CanProtocol.CAN_20 @@ -120,9 +120,7 @@ def __init__( # Super call must be after child init since super calls set_filters super().__init__(channel=channel, **kwargs) - def _recv_internal( - self, timeout: Optional[float] - ) -> tuple[Optional[can.Message], bool]: + def _recv_internal(self, timeout: float | None) -> tuple[can.Message | None, bool]: ociMsgs = (ctypes.POINTER(OCI_CANMessageEx) * 1)() ociMsg = OCI_CANMessageEx() ociMsgs[0] = ctypes.pointer(ociMsg) @@ -189,7 +187,7 @@ def _recv_internal( return (msg, True) - def send(self, msg: can.Message, timeout: Optional[float] = None) -> None: + def send(self, msg: can.Message, timeout: float | None = None) -> None: ociMsgs = (ctypes.POINTER(OCI_CANMessageEx) * 1)() ociMsg = OCI_CANMessageEx() ociMsgs[0] = ctypes.pointer(ociMsg) @@ -219,7 +217,7 @@ def send(self, msg: can.Message, timeout: Optional[float] = None) -> None: OCI_WriteCANDataEx(self.txQueue, OCI_NO_TIME, ociMsgs, 1, None) - def _apply_filters(self, filters: Optional[can.typechecking.CanFilters]) -> None: + def _apply_filters(self, filters: can.typechecking.CanFilters | None) -> None: if self._oci_filters: OCI_RemoveCANFrameFilterEx(self.rxQueue, self._oci_filters, 1) diff --git a/can/interfaces/gs_usb.py b/can/interfaces/gs_usb.py index 4ab541f43..6297fc1f5 100644 --- a/can/interfaces/gs_usb.py +++ b/can/interfaces/gs_usb.py @@ -1,5 +1,4 @@ import logging -from typing import Optional import usb from gs_usb.constants import CAN_EFF_FLAG, CAN_ERR_FLAG, CAN_MAX_DLC, CAN_RTR_FLAG @@ -82,7 +81,7 @@ def __init__( **kwargs, ) - def send(self, msg: can.Message, timeout: Optional[float] = None): + def send(self, msg: can.Message, timeout: float | None = None): """Transmit a message to the CAN bus. :param Message msg: A message object. @@ -117,9 +116,7 @@ def send(self, msg: can.Message, timeout: Optional[float] = None): except usb.core.USBError as exc: raise CanOperationError("The message could not be sent") from exc - def _recv_internal( - self, timeout: Optional[float] - ) -> tuple[Optional[can.Message], bool]: + def _recv_internal(self, timeout: float | None) -> tuple[can.Message | None, bool]: """ Read a message from the bus and tell whether it was filtered. This methods may be called by :meth:`~can.BusABC.recv` diff --git a/can/interfaces/iscan.py b/can/interfaces/iscan.py index 79b4f754d..2fa19942a 100644 --- a/can/interfaces/iscan.py +++ b/can/interfaces/iscan.py @@ -5,7 +5,6 @@ import ctypes import logging import time -from typing import Optional, Union from can import ( BusABC, @@ -82,7 +81,7 @@ class IscanBus(BusABC): def __init__( self, - channel: Union[str, int], + channel: str | int, bitrate: int = 500000, poll_interval: float = 0.01, **kwargs, @@ -115,9 +114,7 @@ def __init__( **kwargs, ) - def _recv_internal( - self, timeout: Optional[float] - ) -> tuple[Optional[Message], bool]: + def _recv_internal(self, timeout: float | None) -> tuple[Message | None, bool]: raw_msg = MessageExStruct() end_time = time.time() + timeout if timeout is not None else None while True: @@ -147,7 +144,7 @@ def _recv_internal( ) return msg, False - def send(self, msg: Message, timeout: Optional[float] = None) -> None: + def send(self, msg: Message, timeout: float | None = None) -> None: raw_msg = MessageExStruct( msg.arbitration_id, bool(msg.is_extended_id), diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 6192367c4..528e86d5e 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -1,5 +1,4 @@ -from collections.abc import Sequence -from typing import Callable, Optional, Union +from collections.abc import Callable, Sequence import can.interfaces.ixxat.canlib_vcinpl as vcinpl import can.interfaces.ixxat.canlib_vcinpl2 as vcinpl2 @@ -26,20 +25,20 @@ def __init__( channel: int, can_filters=None, receive_own_messages: bool = False, - unique_hardware_id: Optional[int] = None, + unique_hardware_id: int | None = None, extended: bool = True, fd: bool = False, - rx_fifo_size: Optional[int] = None, - tx_fifo_size: Optional[int] = None, + rx_fifo_size: int | None = None, + tx_fifo_size: int | None = None, bitrate: int = 500000, data_bitrate: int = 2000000, - sjw_abr: Optional[int] = None, - tseg1_abr: Optional[int] = None, - tseg2_abr: Optional[int] = None, - sjw_dbr: Optional[int] = None, - tseg1_dbr: Optional[int] = None, - tseg2_dbr: Optional[int] = None, - ssp_dbr: Optional[int] = None, + sjw_abr: int | None = None, + tseg1_abr: int | None = None, + tseg2_abr: int | None = None, + sjw_dbr: int | None = None, + tseg1_dbr: int | None = None, + tseg2_dbr: int | None = None, + ssp_dbr: int | None = None, **kwargs, ): """ @@ -147,16 +146,16 @@ def _recv_internal(self, timeout): """Read a message from IXXAT device.""" return self.bus._recv_internal(timeout) - def send(self, msg: Message, timeout: Optional[float] = None) -> None: + def send(self, msg: Message, timeout: float | None = None) -> None: return self.bus.send(msg, timeout) def _send_periodic_internal( self, - msgs: Union[Sequence[Message], Message], + msgs: Sequence[Message] | Message, period: float, - duration: Optional[float] = None, + duration: float | None = None, autostart: bool = True, - modifier_callback: Optional[Callable[[Message], None]] = None, + modifier_callback: Callable[[Message], None] | None = None, ) -> CyclicSendTaskABC: return self.bus._send_periodic_internal( msgs, period, duration, autostart, modifier_callback diff --git a/can/interfaces/ixxat/canlib_vcinpl.py b/can/interfaces/ixxat/canlib_vcinpl.py index 59f98417d..7c4becafd 100644 --- a/can/interfaces/ixxat/canlib_vcinpl.py +++ b/can/interfaces/ixxat/canlib_vcinpl.py @@ -15,8 +15,7 @@ import sys import time import warnings -from collections.abc import Sequence -from typing import Callable, Optional, Union +from collections.abc import Callable, Sequence from can import ( BusABC, @@ -436,7 +435,7 @@ def __init__( channel: int, can_filters=None, receive_own_messages: bool = False, - unique_hardware_id: Optional[int] = None, + unique_hardware_id: int | None = None, extended: bool = True, rx_fifo_size: int = 16, tx_fifo_size: int = 16, @@ -770,7 +769,7 @@ def _recv_internal(self, timeout): return rx_msg, True - def send(self, msg: Message, timeout: Optional[float] = None) -> None: + def send(self, msg: Message, timeout: float | None = None) -> None: """ Sends a message on the bus. The interface may buffer the message. @@ -805,11 +804,11 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: def _send_periodic_internal( self, - msgs: Union[Sequence[Message], Message], + msgs: Sequence[Message] | Message, period: float, - duration: Optional[float] = None, + duration: float | None = None, autostart: bool = True, - modifier_callback: Optional[Callable[[Message], None]] = None, + modifier_callback: Callable[[Message], None] | None = None, ) -> CyclicSendTaskABC: """Send a message using built-in cyclic transmit list functionality.""" if modifier_callback is None: diff --git a/can/interfaces/ixxat/canlib_vcinpl2.py b/can/interfaces/ixxat/canlib_vcinpl2.py index b7698277f..b6789885a 100644 --- a/can/interfaces/ixxat/canlib_vcinpl2.py +++ b/can/interfaces/ixxat/canlib_vcinpl2.py @@ -15,8 +15,7 @@ import sys import time import warnings -from collections.abc import Sequence -from typing import Callable, Optional, Union +from collections.abc import Callable, Sequence from can import ( BusABC, @@ -431,19 +430,19 @@ def __init__( channel: int, can_filters=None, receive_own_messages: int = False, - unique_hardware_id: Optional[int] = None, + unique_hardware_id: int | None = None, extended: bool = True, rx_fifo_size: int = 1024, tx_fifo_size: int = 128, bitrate: int = 500000, data_bitrate: int = 2000000, - sjw_abr: Optional[int] = None, - tseg1_abr: Optional[int] = None, - tseg2_abr: Optional[int] = None, - sjw_dbr: Optional[int] = None, - tseg1_dbr: Optional[int] = None, - tseg2_dbr: Optional[int] = None, - ssp_dbr: Optional[int] = None, + sjw_abr: int | None = None, + tseg1_abr: int | None = None, + tseg2_abr: int | None = None, + sjw_dbr: int | None = None, + tseg1_dbr: int | None = None, + tseg2_dbr: int | None = None, + ssp_dbr: int | None = None, **kwargs, ): """ @@ -902,7 +901,7 @@ def _recv_internal(self, timeout): return rx_msg, True - def send(self, msg: Message, timeout: Optional[float] = None) -> None: + def send(self, msg: Message, timeout: float | None = None) -> None: """ Sends a message on the bus. The interface may buffer the message. @@ -947,11 +946,11 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: def _send_periodic_internal( self, - msgs: Union[Sequence[Message], Message], + msgs: Sequence[Message] | Message, period: float, - duration: Optional[float] = None, + duration: float | None = None, autostart: bool = True, - modifier_callback: Optional[Callable[[Message], None]] = None, + modifier_callback: Callable[[Message], None] | None = None, ) -> CyclicSendTaskABC: """Send a message using built-in cyclic transmit list functionality.""" if modifier_callback is None: diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index a1dd03e58..4403b60ca 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -10,7 +10,6 @@ import logging import sys import time -from typing import Optional, Union from can import BitTiming, BitTimingFd, BusABC, CanProtocol, Message from can.exceptions import CanError, CanInitializationError, CanOperationError @@ -375,8 +374,8 @@ class KvaserBus(BusABC): def __init__( self, channel: int, - can_filters: Optional[CanFilters] = None, - timing: Optional[Union[BitTiming, BitTimingFd]] = None, + can_filters: CanFilters | None = None, + timing: BitTiming | BitTimingFd | None = None, **kwargs, ): """ diff --git a/can/interfaces/nican.py b/can/interfaces/nican.py index 1abf0b35f..ba5b991c9 100644 --- a/can/interfaces/nican.py +++ b/can/interfaces/nican.py @@ -16,7 +16,6 @@ import ctypes import logging import sys -from typing import Optional import can.typechecking from can import ( @@ -187,8 +186,8 @@ class NicanBus(BusABC): def __init__( self, channel: str, - can_filters: Optional[can.typechecking.CanFilters] = None, - bitrate: Optional[int] = None, + can_filters: can.typechecking.CanFilters | None = None, + bitrate: int | None = None, log_errors: bool = True, **kwargs, ) -> None: @@ -279,9 +278,7 @@ def __init__( **kwargs, ) - def _recv_internal( - self, timeout: Optional[float] - ) -> tuple[Optional[Message], bool]: + def _recv_internal(self, timeout: float | None) -> tuple[Message | None, bool]: """ Read a message from a NI-CAN bus. @@ -330,7 +327,7 @@ def _recv_internal( ) return msg, True - def send(self, msg: Message, timeout: Optional[float] = None) -> None: + def send(self, msg: Message, timeout: float | None = None) -> None: """ Send a message to NI-CAN. diff --git a/can/interfaces/nixnet.py b/can/interfaces/nixnet.py index c723d1f52..ec303a364 100644 --- a/can/interfaces/nixnet.py +++ b/can/interfaces/nixnet.py @@ -14,7 +14,7 @@ import warnings from queue import SimpleQueue from types import ModuleType -from typing import Any, Optional, Union +from typing import Any import can.typechecking from can import BitTiming, BitTimingFd, BusABC, CanProtocol, Message @@ -27,7 +27,7 @@ logger = logging.getLogger(__name__) -nixnet: Optional[ModuleType] = None +nixnet: ModuleType | None = None try: import nixnet # type: ignore import nixnet.constants # type: ignore @@ -52,12 +52,12 @@ def __init__( self, channel: str = "CAN1", bitrate: int = 500_000, - timing: Optional[Union[BitTiming, BitTimingFd]] = None, - can_filters: Optional[can.typechecking.CanFilters] = None, + timing: BitTiming | BitTimingFd | None = None, + can_filters: can.typechecking.CanFilters | None = None, receive_own_messages: bool = False, can_termination: bool = False, fd: bool = False, - fd_bitrate: Optional[int] = None, + fd_bitrate: int | None = None, poll_interval: float = 0.001, **kwargs: Any, ) -> None: @@ -201,9 +201,7 @@ def fd(self) -> bool: ) return self._can_protocol is CanProtocol.CAN_FD - def _recv_internal( - self, timeout: Optional[float] - ) -> tuple[Optional[Message], bool]: + def _recv_internal(self, timeout: float | None) -> tuple[Message | None, bool]: end_time = time.perf_counter() + timeout if timeout is not None else None while True: @@ -256,7 +254,7 @@ def _recv_internal( ) return msg, False - def send(self, msg: Message, timeout: Optional[float] = None) -> None: + def send(self, msg: Message, timeout: float | None = None) -> None: """ Send a message using NI-XNET. diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index d63981580..a2f5f361f 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -6,7 +6,7 @@ import platform import time import warnings -from typing import Any, Optional, Union +from typing import Any from packaging import version @@ -120,9 +120,9 @@ class PcanBus(BusABC): def __init__( self, channel: str = "PCAN_USBBUS1", - device_id: Optional[int] = None, + device_id: int | None = None, state: BusState = BusState.ACTIVE, - timing: Optional[Union[BitTiming, BitTimingFd]] = None, + timing: BitTiming | BitTimingFd | None = None, bitrate: int = 500000, receive_own_messages: bool = False, **kwargs: Any, @@ -500,9 +500,7 @@ def set_device_number(self, device_number): return False return True - def _recv_internal( - self, timeout: Optional[float] - ) -> tuple[Optional[Message], bool]: + def _recv_internal(self, timeout: float | None) -> tuple[Message | None, bool]: end_time = time.time() + timeout if timeout is not None else None while True: @@ -523,7 +521,7 @@ def _recv_internal( # receive queue is empty, wait or return on timeout if end_time is None: - time_left: Optional[float] = None + time_left: float | None = None timed_out = False else: time_left = max(0.0, end_time - time.time()) @@ -793,7 +791,7 @@ def _detect_available_configs(): pass return channels - def status_string(self) -> Optional[str]: + def status_string(self) -> str | None: """ Query the PCAN bus status. diff --git a/can/interfaces/robotell.py b/can/interfaces/robotell.py index 16668bdda..b24543856 100644 --- a/can/interfaces/robotell.py +++ b/can/interfaces/robotell.py @@ -5,7 +5,6 @@ import io import logging import time -from typing import Optional from can import BusABC, CanProtocol, Message @@ -380,7 +379,7 @@ def fileno(self): except Exception as exception: raise CanOperationError("Cannot fetch fileno") from exception - def get_serial_number(self, timeout: Optional[int]) -> Optional[str]: + def get_serial_number(self, timeout: int | None) -> str | None: """Get serial number of the slcan interface. :param timeout: diff --git a/can/interfaces/serial/serial_can.py b/can/interfaces/serial/serial_can.py index 680cf10f6..efef6cf59 100644 --- a/can/interfaces/serial/serial_can.py +++ b/can/interfaces/serial/serial_can.py @@ -11,7 +11,7 @@ import logging import struct from collections.abc import Sequence -from typing import Any, Optional, cast +from typing import Any, cast from can import ( BusABC, @@ -109,7 +109,7 @@ def shutdown(self) -> None: super().shutdown() self._ser.close() - def send(self, msg: Message, timeout: Optional[float] = None) -> None: + def send(self, msg: Message, timeout: float | None = None) -> None: """ Send a message over the serial device. @@ -161,9 +161,7 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: except serial.SerialTimeoutException as error: raise CanTimeoutError() from error - def _recv_internal( - self, timeout: Optional[float] - ) -> tuple[Optional[Message], bool]: + def _recv_internal(self, timeout: float | None) -> tuple[Message | None, bool]: """ Read a message from the serial device. diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py index d9eab5edf..086d9ed32 100644 --- a/can/interfaces/slcan.py +++ b/can/interfaces/slcan.py @@ -7,7 +7,7 @@ import time import warnings from queue import SimpleQueue -from typing import Any, Optional, Union, cast +from typing import Any, cast from can import BitTiming, BitTimingFd, BusABC, CanProtocol, Message, typechecking from can.exceptions import ( @@ -75,8 +75,8 @@ def __init__( self, channel: typechecking.ChannelStr, tty_baudrate: int = 115200, - bitrate: Optional[int] = None, - timing: Optional[Union[BitTiming, BitTimingFd]] = None, + bitrate: int | None = None, + timing: BitTiming | BitTimingFd | None = None, sleep_after_open: float = _SLEEP_AFTER_SERIAL_OPEN, rtscts: bool = False, listen_only: bool = False, @@ -119,7 +119,7 @@ def __init__( if serial is None: raise CanInterfaceNotImplementedError("The serial module is not installed") - btr: Optional[str] = kwargs.get("btr", None) + btr: str | None = kwargs.get("btr", None) if btr is not None: warnings.warn( "The 'btr' argument is deprecated since python-can v4.5.0 " @@ -166,7 +166,7 @@ def __init__( super().__init__(channel, **kwargs) - def set_bitrate(self, bitrate: int, data_bitrate: Optional[int] = None) -> None: + def set_bitrate(self, bitrate: int, data_bitrate: int | None = None) -> None: """ :param bitrate: Bitrate in bit/s @@ -211,7 +211,7 @@ def _write(self, string: str) -> None: self.serialPortOrig.write(string.encode() + self.LINE_TERMINATOR) self.serialPortOrig.flush() - def _read(self, timeout: Optional[float]) -> Optional[str]: + def _read(self, timeout: float | None) -> str | None: _timeout = serial.Timeout(timeout) with error_check("Could not read from serial device"): @@ -250,9 +250,7 @@ def open(self) -> None: def close(self) -> None: self._write("C") - def _recv_internal( - self, timeout: Optional[float] - ) -> tuple[Optional[Message], bool]: + def _recv_internal(self, timeout: float | None) -> tuple[Message | None, bool]: canId = None remote = False extended = False @@ -261,7 +259,7 @@ def _recv_internal( fdBrs = False if self._queue.qsize(): - string: Optional[str] = self._queue.get_nowait() + string: str | None = self._queue.get_nowait() else: string = self._read(timeout) @@ -335,7 +333,7 @@ def _recv_internal( return msg, False return None, False - def send(self, msg: Message, timeout: Optional[float] = None) -> None: + def send(self, msg: Message, timeout: float | None = None) -> None: if timeout != self.serialPortOrig.write_timeout: self.serialPortOrig.write_timeout = timeout if msg.is_remote_frame: @@ -381,9 +379,7 @@ def fileno(self) -> int: except Exception as exception: raise CanOperationError("Cannot fetch fileno") from exception - def get_version( - self, timeout: Optional[float] - ) -> tuple[Optional[int], Optional[int]]: + def get_version(self, timeout: float | None) -> tuple[int | None, int | None]: """Get HW and SW version of the slcan interface. :param timeout: @@ -411,7 +407,7 @@ def get_version( break return None, None - def get_serial_number(self, timeout: Optional[float]) -> Optional[str]: + def get_serial_number(self, timeout: float | None) -> str | None: """Get serial number of the slcan interface. :param timeout: diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py index 30b75108a..6dc856cbf 100644 --- a/can/interfaces/socketcan/socketcan.py +++ b/can/interfaces/socketcan/socketcan.py @@ -15,8 +15,7 @@ import threading import time import warnings -from collections.abc import Sequence -from typing import Callable, Optional, Union +from collections.abc import Callable, Sequence import can from can import BusABC, CanProtocol, Message @@ -51,14 +50,12 @@ # Setup BCM struct def bcm_header_factory( - fields: list[tuple[str, Union[type[ctypes.c_uint32], type[ctypes.c_long]]]], + fields: list[tuple[str, type[ctypes.c_uint32] | type[ctypes.c_long]]], alignment: int = 8, ): curr_stride = 0 results: list[ - tuple[ - str, Union[type[ctypes.c_uint8], type[ctypes.c_uint32], type[ctypes.c_long]] - ] + tuple[str, type[ctypes.c_uint8] | type[ctypes.c_uint32] | type[ctypes.c_long]] ] = [] pad_index = 0 for field in fields: @@ -405,9 +402,9 @@ def __init__( self, bcm_socket: socket.socket, task_id: int, - messages: Union[Sequence[Message], Message], + messages: Sequence[Message] | Message, period: float, - duration: Optional[float] = None, + duration: float | None = None, autostart: bool = True, ) -> None: """Construct and :meth:`~start` a task. @@ -507,7 +504,7 @@ def stop(self) -> None: stopframe = build_bcm_tx_delete_header(self.task_id, self.flags) send_bcm(self.bcm_socket, stopframe) - def modify_data(self, messages: Union[Sequence[Message], Message]) -> None: + def modify_data(self, messages: Sequence[Message] | Message) -> None: """Update the contents of the periodically sent CAN messages by sending TX_SETUP message to Linux kernel. @@ -605,9 +602,7 @@ def bind_socket(sock: socket.socket, channel: str = "can0") -> None: log.debug("Bound socket.") -def capture_message( - sock: socket.socket, get_channel: bool = False -) -> Optional[Message]: +def capture_message(sock: socket.socket, get_channel: bool = False) -> Message | None: """ Captures a message from given socket. @@ -702,7 +697,7 @@ def __init__( receive_own_messages: bool = False, local_loopback: bool = True, fd: bool = False, - can_filters: Optional[CanFilters] = None, + can_filters: CanFilters | None = None, ignore_rx_error_frames=False, **kwargs, ) -> None: @@ -818,9 +813,7 @@ def shutdown(self) -> None: log.debug("Closing raw can socket") self.socket.close() - def _recv_internal( - self, timeout: Optional[float] - ) -> tuple[Optional[Message], bool]: + def _recv_internal(self, timeout: float | None) -> tuple[Message | None, bool]: try: # get all sockets that are ready (can be a list with a single value # being self.socket or an empty list if self.socket is not ready) @@ -842,7 +835,7 @@ def _recv_internal( # socket wasn't readable or timeout occurred return None, self._is_filtered - def send(self, msg: Message, timeout: Optional[float] = None) -> None: + def send(self, msg: Message, timeout: float | None = None) -> None: """Transmit a message to the CAN bus. :param msg: A message object. @@ -880,7 +873,7 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: raise can.CanOperationError("Transmit buffer full") - def _send_once(self, data: bytes, channel: Optional[str] = None) -> int: + def _send_once(self, data: bytes, channel: str | None = None) -> int: try: if self.channel == "" and channel: # Message must be addressed to a specific channel @@ -895,11 +888,11 @@ def _send_once(self, data: bytes, channel: Optional[str] = None) -> int: def _send_periodic_internal( self, - msgs: Union[Sequence[Message], Message], + msgs: Sequence[Message] | Message, period: float, - duration: Optional[float] = None, + duration: float | None = None, autostart: bool = True, - modifier_callback: Optional[Callable[[Message], None]] = None, + modifier_callback: Callable[[Message], None] | None = None, ) -> can.broadcastmanager.CyclicSendTaskABC: """Start sending messages at a given period on this bus. @@ -974,7 +967,7 @@ def _get_bcm_socket(self, channel: str) -> socket.socket: self._bcm_sockets[channel] = create_bcm_socket(self.channel) return self._bcm_sockets[channel] - def _apply_filters(self, filters: Optional[can.typechecking.CanFilters]) -> None: + def _apply_filters(self, filters: can.typechecking.CanFilters | None) -> None: try: self.socket.setsockopt( constants.SOL_CAN_RAW, constants.CAN_RAW_FILTER, pack_filters(filters) diff --git a/can/interfaces/socketcan/utils.py b/can/interfaces/socketcan/utils.py index 1c096f66e..0740f769d 100644 --- a/can/interfaces/socketcan/utils.py +++ b/can/interfaces/socketcan/utils.py @@ -9,7 +9,6 @@ import struct import subprocess import sys -from typing import Optional from can import typechecking from can.interfaces.socketcan.constants import CAN_EFF_FLAG @@ -17,7 +16,7 @@ log = logging.getLogger(__name__) -def pack_filters(can_filters: Optional[typechecking.CanFilters] = None) -> bytes: +def pack_filters(can_filters: typechecking.CanFilters | None = None) -> bytes: if can_filters is None: # Pass all messages can_filters = [{"can_id": 0, "can_mask": 0}] @@ -72,7 +71,7 @@ def find_available_interfaces() -> list[str]: return interfaces -def error_code_to_str(code: Optional[int]) -> str: +def error_code_to_str(code: int | None) -> str: """ Converts a given error code (errno) to a useful and human readable string. diff --git a/can/interfaces/udp_multicast/bus.py b/can/interfaces/udp_multicast/bus.py index 9e0187ea2..87a0800fa 100644 --- a/can/interfaces/udp_multicast/bus.py +++ b/can/interfaces/udp_multicast/bus.py @@ -6,7 +6,7 @@ import struct import time import warnings -from typing import Any, Optional, Union +from typing import Any import can from can import BusABC, CanProtocol, Message @@ -24,7 +24,7 @@ # see socket.getaddrinfo() IPv4_ADDRESS_INFO = tuple[str, int] # address, port IPv6_ADDRESS_INFO = tuple[str, int, int, int] # address, port, flowinfo, scope_id -IP_ADDRESS_INFO = Union[IPv4_ADDRESS_INFO, IPv6_ADDRESS_INFO] +IP_ADDRESS_INFO = IPv4_ADDRESS_INFO | IPv6_ADDRESS_INFO # Additional constants for the interaction with Unix kernels SO_TIMESTAMPNS = 35 @@ -126,9 +126,7 @@ def is_fd(self) -> bool: ) return self._can_protocol is CanProtocol.CAN_FD - def _recv_internal( - self, timeout: Optional[float] - ) -> tuple[Optional[Message], bool]: + def _recv_internal(self, timeout: float | None) -> tuple[Message | None, bool]: result = self._multicast.recv(timeout) if not result: return None, False @@ -148,7 +146,7 @@ def _recv_internal( return can_message, False - def send(self, msg: can.Message, timeout: Optional[float] = None) -> None: + def send(self, msg: can.Message, timeout: float | None = None) -> None: if self._can_protocol is not CanProtocol.CAN_FD and msg.is_fd: raise can.CanOperationError( "cannot send FD message over bus with CAN FD disabled" @@ -242,7 +240,7 @@ def __init__( # used by send() self._send_destination = (self.group, self.port) - self._last_send_timeout: Optional[float] = None + self._last_send_timeout: float | None = None def _create_socket(self, address_family: socket.AddressFamily) -> socket.socket: """Creates a new socket. This might fail and raise an exception! @@ -319,7 +317,7 @@ def _create_socket(self, address_family: socket.AddressFamily) -> socket.socket: "could not create or configure socket" ) from error - def send(self, data: bytes, timeout: Optional[float] = None) -> None: + def send(self, data: bytes, timeout: float | None = None) -> None: """Send data to all group members. This call blocks. :param timeout: the timeout in seconds after which an Exception is raised is sending has failed @@ -342,8 +340,8 @@ def send(self, data: bytes, timeout: Optional[float] = None) -> None: raise can.CanOperationError("failed to send via socket") from error def recv( - self, timeout: Optional[float] = None - ) -> Optional[tuple[bytes, IP_ADDRESS_INFO, float]]: + self, timeout: float | None = None + ) -> tuple[bytes, IP_ADDRESS_INFO, float] | None: """ Receive up to **max_buffer** bytes. diff --git a/can/interfaces/udp_multicast/utils.py b/can/interfaces/udp_multicast/utils.py index de39833a3..1e1d62c23 100644 --- a/can/interfaces/udp_multicast/utils.py +++ b/can/interfaces/udp_multicast/utils.py @@ -2,7 +2,7 @@ Defines common functions. """ -from typing import Any, Optional, cast +from typing import Any, cast from can import CanInterfaceNotImplementedError, Message from can.typechecking import ReadableBytesLike @@ -56,7 +56,7 @@ def pack_message(message: Message) -> bytes: def unpack_message( data: ReadableBytesLike, - replace: Optional[dict[str, Any]] = None, + replace: dict[str, Any] | None = None, check: bool = False, ) -> Message: """Unpack a can.Message from a msgpack byte blob. diff --git a/can/interfaces/usb2can/usb2canInterface.py b/can/interfaces/usb2can/usb2canInterface.py index adc16e8b3..66c171f4d 100644 --- a/can/interfaces/usb2can/usb2canInterface.py +++ b/can/interfaces/usb2can/usb2canInterface.py @@ -4,7 +4,6 @@ import logging from ctypes import byref -from typing import Optional, Union from can import ( BitTiming, @@ -110,12 +109,12 @@ class Usb2canBus(BusABC): def __init__( self, - channel: Optional[str] = None, + channel: str | None = None, dll: str = "usb2can.dll", flags: int = 0x00000008, bitrate: int = 500000, - timing: Optional[Union[BitTiming, BitTimingFd]] = None, - serial: Optional[str] = None, + timing: BitTiming | BitTimingFd | None = None, + serial: str | None = None, **kwargs, ): self.can = Usb2CanAbstractionLayer(dll) @@ -207,7 +206,7 @@ def _detect_available_configs(): return Usb2canBus.detect_available_configs() @staticmethod - def detect_available_configs(serial_matcher: Optional[str] = None): + def detect_available_configs(serial_matcher: str | None = None): """ Uses the *Windows Management Instrumentation* to identify serial devices. diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index d15b89803..8bdd77b83 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -10,14 +10,11 @@ import os import time import warnings -from collections.abc import Iterator, Sequence +from collections.abc import Callable, Iterator, Sequence from types import ModuleType from typing import ( Any, - Callable, NamedTuple, - Optional, - Union, cast, ) @@ -45,14 +42,14 @@ LOG = logging.getLogger(__name__) # Import safely Vector API module for Travis tests -xldriver: Optional[ModuleType] = None +xldriver: ModuleType | None = None try: from . import xldriver except FileNotFoundError as exc: LOG.warning("Could not import vxlapi: %s", exc) -WaitForSingleObject: Optional[Callable[[int, int], int]] -INFINITE: Optional[int] +WaitForSingleObject: Callable[[int, int], int] | None +INFINITE: int | None try: # Try builtin Python 3 Windows API from _winapi import ( # type: ignore[attr-defined,no-redef,unused-ignore] @@ -83,24 +80,24 @@ class VectorBus(BusABC): ) def __init__( self, - channel: Union[int, Sequence[int], str], - can_filters: Optional[CanFilters] = None, + channel: int | Sequence[int] | str, + can_filters: CanFilters | None = None, poll_interval: float = 0.01, receive_own_messages: bool = False, - timing: Optional[Union[BitTiming, BitTimingFd]] = None, - bitrate: Optional[int] = None, + timing: BitTiming | BitTimingFd | None = None, + bitrate: int | None = None, rx_queue_size: int = 2**14, - app_name: Optional[str] = "CANalyzer", - serial: Optional[int] = None, + app_name: str | None = "CANalyzer", + serial: int | None = None, fd: bool = False, - data_bitrate: Optional[int] = None, + data_bitrate: int | None = None, sjw_abr: int = 2, tseg1_abr: int = 6, tseg2_abr: int = 3, sjw_dbr: int = 2, tseg1_dbr: int = 6, tseg2_dbr: int = 3, - listen_only: Optional[bool] = False, + listen_only: bool | None = False, **kwargs: Any, ) -> None: """ @@ -377,8 +374,8 @@ def fd(self) -> bool: def _find_global_channel_idx( self, channel: int, - serial: Optional[int], - app_name: Optional[str], + serial: int | None, + app_name: str | None, channel_configs: list["VectorChannelConfig"], ) -> int: if serial is not None: @@ -565,10 +562,10 @@ def _check_can_settings( self, channel_mask: int, bitrate: int, - sample_point: Optional[float] = None, + sample_point: float | None = None, fd: bool = False, - data_bitrate: Optional[int] = None, - data_sample_point: Optional[float] = None, + data_bitrate: int | None = None, + data_sample_point: float | None = None, ) -> None: """Compare requested CAN settings to active settings in driver.""" vcc_list = get_channel_configs() @@ -656,7 +653,7 @@ def _check_can_settings( f"These are the currently active settings: {settings_string}." ) - def _apply_filters(self, filters: Optional[CanFilters]) -> None: + def _apply_filters(self, filters: CanFilters | None) -> None: if filters: # Only up to one filter per ID type allowed if len(filters) == 1 or ( @@ -706,9 +703,7 @@ def _apply_filters(self, filters: Optional[CanFilters]) -> None: except VectorOperationError as exc: LOG.warning("Could not reset filters: %s", exc) - def _recv_internal( - self, timeout: Optional[float] - ) -> tuple[Optional[Message], bool]: + def _recv_internal(self, timeout: float | None) -> tuple[Message | None, bool]: end_time = time.time() + timeout if timeout is not None else None while True: @@ -741,7 +736,7 @@ def _recv_internal( # Wait a short time until we try again time.sleep(self.poll_interval) - def _recv_canfd(self) -> Optional[Message]: + def _recv_canfd(self) -> Message | None: xl_can_rx_event = xlclass.XLcanRxEvent() self.xldriver.xlCanReceive(self.port_handle, xl_can_rx_event) @@ -786,7 +781,7 @@ def _recv_canfd(self) -> Optional[Message]: data=data_struct.data[:dlc], ) - def _recv_can(self) -> Optional[Message]: + def _recv_can(self) -> Message | None: xl_event = xlclass.XLevent() event_count = ctypes.c_uint(1) self.xldriver.xlReceive(self.port_handle, event_count, xl_event) @@ -842,7 +837,7 @@ def handle_canfd_event(self, event: xlclass.XLcanRxEvent) -> None: `XL_CAN_EV_TAG_TX_ERROR`, `XL_TIMER` or `XL_CAN_EV_TAG_CHIP_STATE` tag. """ - def send(self, msg: Message, timeout: Optional[float] = None) -> None: + def send(self, msg: Message, timeout: float | None = None) -> None: self._send_sequence([msg]) def _send_sequence(self, msgs: Sequence[Message]) -> int: @@ -1030,7 +1025,7 @@ def popup_vector_hw_configuration(wait_for_finish: int = 0) -> None: @staticmethod def get_application_config( app_name: str, app_channel: int - ) -> tuple[Union[int, xldefine.XL_HardwareType], int, int]: + ) -> tuple[int | xldefine.XL_HardwareType, int, int]: """Retrieve information for an application in Vector Hardware Configuration. :param app_name: @@ -1076,7 +1071,7 @@ def get_application_config( def set_application_config( app_name: str, app_channel: int, - hw_type: Union[int, xldefine.XL_HardwareType], + hw_type: int | xldefine.XL_HardwareType, hw_index: int, hw_channel: int, **kwargs: Any, @@ -1170,7 +1165,7 @@ class VectorChannelConfig(NamedTuple): """NamedTuple which contains the channel properties from Vector XL API.""" name: str - hw_type: Union[int, xldefine.XL_HardwareType] + hw_type: int | xldefine.XL_HardwareType hw_index: int hw_channel: int channel_index: int @@ -1179,7 +1174,7 @@ class VectorChannelConfig(NamedTuple): channel_bus_capabilities: xldefine.XL_BusCapabilities is_on_bus: bool connected_bus_type: xldefine.XL_BusTypes - bus_params: Optional[VectorBusParams] + bus_params: VectorBusParams | None serial_number: int article_number: int transceiver_name: str @@ -1214,7 +1209,7 @@ def _get_xl_driver_config() -> xlclass.XLdriverConfig: def _read_bus_params_from_c_struct( bus_params: xlclass.XLbusParams, -) -> Optional[VectorBusParams]: +) -> VectorBusParams | None: bus_type = xldefine.XL_BusTypes(bus_params.busType) if bus_type is not xldefine.XL_BusTypes.XL_BUS_TYPE_CAN: return None @@ -1283,7 +1278,7 @@ def get_channel_configs() -> list[VectorChannelConfig]: return channel_list -def _hw_type(hw_type: int) -> Union[int, xldefine.XL_HardwareType]: +def _hw_type(hw_type: int) -> int | xldefine.XL_HardwareType: try: return xldefine.XL_HardwareType(hw_type) except ValueError: diff --git a/can/interfaces/vector/exceptions.py b/can/interfaces/vector/exceptions.py index b43df5e6c..779365893 100644 --- a/can/interfaces/vector/exceptions.py +++ b/can/interfaces/vector/exceptions.py @@ -1,13 +1,13 @@ """Exception/error declarations for the vector interface.""" -from typing import Any, Optional, Union +from typing import Any from can import CanError, CanInitializationError, CanOperationError class VectorError(CanError): def __init__( - self, error_code: Optional[int], error_string: str, function: str + self, error_code: int | None, error_string: str, function: str ) -> None: super().__init__( message=f"{function} failed ({error_string})", error_code=error_code @@ -16,7 +16,7 @@ def __init__( # keep reference to args for pickling self._args = error_code, error_string, function - def __reduce__(self) -> Union[str, tuple[Any, ...]]: + def __reduce__(self) -> str | tuple[Any, ...]: return type(self), self._args, {} diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index e4f68b0c4..ba33a6ea8 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -12,7 +12,7 @@ from copy import deepcopy from random import randint from threading import RLock -from typing import Any, Final, Optional +from typing import Any, Final from can import CanOperationError from can.bus import BusABC, CanProtocol @@ -118,9 +118,7 @@ def _check_if_open(self) -> None: if not self._open: raise CanOperationError("Cannot operate on a closed bus") - def _recv_internal( - self, timeout: Optional[float] - ) -> tuple[Optional[Message], bool]: + def _recv_internal(self, timeout: float | None) -> tuple[Message | None, bool]: self._check_if_open() try: msg = self.queue.get(block=True, timeout=timeout) @@ -129,7 +127,7 @@ def _recv_internal( else: return msg, False - def send(self, msg: Message, timeout: Optional[float] = None) -> None: + def send(self, msg: Message, timeout: float | None = None) -> None: self._check_if_open() timestamp = msg.timestamp if self.preserve_timestamps else time.time() diff --git a/can/io/asc.py b/can/io/asc.py index e917953ff..2c80458c4 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -10,7 +10,7 @@ import re from collections.abc import Generator from datetime import datetime -from typing import Any, Final, Optional, TextIO, Union +from typing import Any, Final, TextIO from ..message import Message from ..typechecking import StringPathLike @@ -41,7 +41,7 @@ class ASCReader(TextIOMessageReader): def __init__( self, - file: Union[StringPathLike, TextIO], + file: StringPathLike | TextIO, base: str = "hex", relative_timestamp: bool = True, **kwargs: Any, @@ -64,10 +64,10 @@ def __init__( self.base = base self._converted_base = self._check_base(base) self.relative_timestamp = relative_timestamp - self.date: Optional[str] = None + self.date: str | None = None self.start_time = 0.0 # TODO - what is this used for? The ASC Writer only prints `absolute` - self.timestamps_format: Optional[str] = None + self.timestamps_format: str | None = None self.internal_events_logged = False def _extract_header(self) -> None: @@ -284,7 +284,7 @@ def __iter__(self) -> Generator[Message, None, None]: # J1939 message or some other unsupported event continue - msg_kwargs: dict[str, Union[float, bool, int]] = {} + msg_kwargs: dict[str, float | bool | int] = {} try: _timestamp, channel, rest_of_message = line.split(None, 2) timestamp = float(_timestamp) + self.start_time @@ -347,7 +347,7 @@ class ASCWriter(TextIOMessageWriter): def __init__( self, - file: Union[StringPathLike, TextIO], + file: StringPathLike | TextIO, channel: int = 1, **kwargs: Any, ) -> None: @@ -393,7 +393,7 @@ def stop(self) -> None: self.file.write("End TriggerBlock\n") super().stop() - def log_event(self, message: str, timestamp: Optional[float] = None) -> None: + def log_event(self, message: str, timestamp: float | None = None) -> None: """Add a message to the log file. :param message: an arbitrary message diff --git a/can/io/blf.py b/can/io/blf.py index 2c9050d54..77bd02fae 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -19,7 +19,7 @@ import zlib from collections.abc import Generator, Iterator from decimal import Decimal -from typing import Any, BinaryIO, Optional, Union, cast +from typing import Any, BinaryIO, cast from ..message import Message from ..typechecking import StringPathLike @@ -104,7 +104,7 @@ class BLFParseError(Exception): TIME_ONE_NANS_FACTOR = Decimal("1e-9") -def timestamp_to_systemtime(timestamp: Optional[float]) -> TSystemTime: +def timestamp_to_systemtime(timestamp: float | None) -> TSystemTime: if timestamp is None or timestamp < 631152000: # Probably not a Unix timestamp return 0, 0, 0, 0, 0, 0, 0, 0 @@ -148,7 +148,7 @@ class BLFReader(BinaryIOMessageReader): def __init__( self, - file: Union[StringPathLike, BinaryIO], + file: StringPathLike | BinaryIO, **kwargs: Any, ) -> None: """ @@ -386,7 +386,7 @@ class BLFWriter(BinaryIOMessageWriter): def __init__( self, - file: Union[StringPathLike, BinaryIO], + file: StringPathLike | BinaryIO, append: bool = False, channel: int = 1, compression_level: int = -1, @@ -430,10 +430,10 @@ def __init__( raise BLFParseError("Unexpected file format") self.uncompressed_size = header[11] self.object_count = header[12] - self.start_timestamp: Optional[float] = systemtime_to_timestamp( + self.start_timestamp: float | None = systemtime_to_timestamp( cast("TSystemTime", header[14:22]) ) - self.stop_timestamp: Optional[float] = systemtime_to_timestamp( + self.stop_timestamp: float | None = systemtime_to_timestamp( cast("TSystemTime", header[22:30]) ) # Jump to the end of the file @@ -508,7 +508,7 @@ def on_message_received(self, msg: Message) -> None: data = CAN_MSG_STRUCT.pack(channel, flags, msg.dlc, arb_id, can_data) self._add_object(CAN_MESSAGE, data, msg.timestamp) - def log_event(self, text: str, timestamp: Optional[float] = None) -> None: + def log_event(self, text: str, timestamp: float | None = None) -> None: """Add an arbitrary message to the log file as a global marker. :param str text: @@ -530,7 +530,7 @@ def log_event(self, text: str, timestamp: Optional[float] = None) -> None: self._add_object(GLOBAL_MARKER, data + encoded + marker + comment, timestamp) def _add_object( - self, obj_type: int, data: bytes, timestamp: Optional[float] = None + self, obj_type: int, data: bytes, timestamp: float | None = None ) -> None: if timestamp is None: timestamp = self.stop_timestamp or time.time() @@ -574,7 +574,7 @@ def _flush(self) -> None: self._buffer = [tail] self._buffer_size = len(tail) if not self.compression_level: - data: "Union[bytes, memoryview[int]]" = uncompressed_data # noqa: UP037 + data: "bytes | memoryview[int]" = uncompressed_data # noqa: UP037 method = NO_COMPRESSION else: data = zlib.compress(uncompressed_data, self.compression_level) diff --git a/can/io/canutils.py b/can/io/canutils.py index 78d081637..800125b73 100644 --- a/can/io/canutils.py +++ b/can/io/canutils.py @@ -6,7 +6,7 @@ import logging from collections.abc import Generator -from typing import Any, Optional, TextIO, Union +from typing import Any, TextIO from can.message import Message @@ -36,7 +36,7 @@ class CanutilsLogReader(TextIOMessageReader): def __init__( self, - file: Union[StringPathLike, TextIO], + file: StringPathLike | TextIO, **kwargs: Any, ) -> None: """ @@ -63,7 +63,7 @@ def __iter__(self) -> Generator[Message, None, None]: timestamp = float(timestamp_string[1:-1]) can_id_string, data = frame.split("#", maxsplit=1) - channel: Union[int, str] + channel: int | str if channel_string.isdigit(): channel = int(channel_string) else: @@ -132,7 +132,7 @@ class CanutilsLogWriter(TextIOMessageWriter): def __init__( self, - file: Union[StringPathLike, TextIO], + file: StringPathLike | TextIO, channel: str = "vcan0", append: bool = False, **kwargs: Any, @@ -149,7 +149,7 @@ def __init__( super().__init__(file, mode="a" if append else "w") self.channel = channel - self.last_timestamp: Optional[float] = None + self.last_timestamp: float | None = None def on_message_received(self, msg: Message) -> None: # this is the case for the very first message: diff --git a/can/io/csv.py b/can/io/csv.py index 865ef9af0..0c8ba02a4 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -11,7 +11,7 @@ from base64 import b64decode, b64encode from collections.abc import Generator -from typing import Any, TextIO, Union +from typing import Any, TextIO from can.message import Message @@ -30,7 +30,7 @@ class CSVReader(TextIOMessageReader): def __init__( self, - file: Union[StringPathLike, TextIO], + file: StringPathLike | TextIO, **kwargs: Any, ) -> None: """ @@ -89,7 +89,7 @@ class CSVWriter(TextIOMessageWriter): def __init__( self, - file: Union[StringPathLike, TextIO], + file: StringPathLike | TextIO, append: bool = False, **kwargs: Any, ) -> None: diff --git a/can/io/generic.py b/can/io/generic.py index 21fc3e8e8..bda4e1cce 100644 --- a/can/io/generic.py +++ b/can/io/generic.py @@ -20,10 +20,8 @@ BinaryIO, Generic, Literal, - Optional, TextIO, TypeVar, - Union, ) from typing_extensions import Self @@ -71,9 +69,9 @@ def __enter__(self) -> Self: def __exit__( self, - exc_type: Optional[type[BaseException]], - exc_value: Optional[BaseException], - traceback: Optional[TracebackType], + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, ) -> Literal[False]: """Exit the context manager and ensure proper cleanup.""" self.stop() @@ -110,7 +108,7 @@ class FileIOMessageWriter(SizedMessageWriter, Generic[_IoTypeVar]): file: _IoTypeVar @abstractmethod - def __init__(self, file: Union[StringPathLike, _IoTypeVar], **kwargs: Any) -> None: + def __init__(self, file: StringPathLike | _IoTypeVar, **kwargs: Any) -> None: pass def stop(self) -> None: @@ -122,7 +120,7 @@ def file_size(self) -> int: return self.file.tell() -class TextIOMessageWriter(FileIOMessageWriter[Union[TextIO, TextIOWrapper]], ABC): +class TextIOMessageWriter(FileIOMessageWriter[TextIO | TextIOWrapper], ABC): """Text-based message writer implementation. :param file: Text file to write to @@ -132,8 +130,8 @@ class TextIOMessageWriter(FileIOMessageWriter[Union[TextIO, TextIOWrapper]], ABC def __init__( self, - file: Union[StringPathLike, TextIO, TextIOWrapper], - mode: "Union[OpenTextModeUpdating, OpenTextModeWriting]" = "w", + file: StringPathLike | TextIO | TextIOWrapper, + mode: "OpenTextModeUpdating | OpenTextModeWriting" = "w", **kwargs: Any, ) -> None: if isinstance(file, (str, os.PathLike)): @@ -144,7 +142,7 @@ def __init__( self.file = file -class BinaryIOMessageWriter(FileIOMessageWriter[Union[BinaryIO, BufferedIOBase]], ABC): +class BinaryIOMessageWriter(FileIOMessageWriter[BinaryIO | BufferedIOBase], ABC): """Binary file message writer implementation. :param file: Binary file to write to @@ -152,10 +150,10 @@ class BinaryIOMessageWriter(FileIOMessageWriter[Union[BinaryIO, BufferedIOBase]] :param kwargs: Additional implementation specific arguments """ - def __init__( + def __init__( # pylint: disable=unused-argument self, - file: Union[StringPathLike, BinaryIO, BufferedIOBase], - mode: "Union[OpenBinaryModeUpdating, OpenBinaryModeWriting]" = "wb", + file: StringPathLike | BinaryIO | BufferedIOBase, + mode: "OpenBinaryModeUpdating | OpenBinaryModeWriting" = "wb", **kwargs: Any, ) -> None: if isinstance(file, (str, os.PathLike)): @@ -188,9 +186,9 @@ def __enter__(self) -> Self: def __exit__( self, - exc_type: Optional[type[BaseException]], - exc_value: Optional[BaseException], - traceback: Optional[TracebackType], + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, ) -> Literal[False]: self.stop() return False @@ -210,14 +208,14 @@ class FileIOMessageReader(MessageReader, Generic[_IoTypeVar]): file: _IoTypeVar @abstractmethod - def __init__(self, file: Union[StringPathLike, _IoTypeVar], **kwargs: Any) -> None: + def __init__(self, file: StringPathLike | _IoTypeVar, **kwargs: Any) -> None: pass def stop(self) -> None: self.file.close() -class TextIOMessageReader(FileIOMessageReader[Union[TextIO, TextIOWrapper]], ABC): +class TextIOMessageReader(FileIOMessageReader[TextIO | TextIOWrapper], ABC): """Text-based message reader implementation. :param file: Text file to read from @@ -227,7 +225,7 @@ class TextIOMessageReader(FileIOMessageReader[Union[TextIO, TextIOWrapper]], ABC def __init__( self, - file: Union[StringPathLike, TextIO, TextIOWrapper], + file: StringPathLike | TextIO | TextIOWrapper, mode: "OpenTextModeReading" = "r", **kwargs: Any, ) -> None: @@ -239,7 +237,7 @@ def __init__( self.file = file -class BinaryIOMessageReader(FileIOMessageReader[Union[BinaryIO, BufferedIOBase]], ABC): +class BinaryIOMessageReader(FileIOMessageReader[BinaryIO | BufferedIOBase], ABC): """Binary file message reader implementation. :param file: Binary file to read from @@ -247,9 +245,9 @@ class BinaryIOMessageReader(FileIOMessageReader[Union[BinaryIO, BufferedIOBase]] :param kwargs: Additional implementation specific arguments """ - def __init__( + def __init__( # pylint: disable=unused-argument self, - file: Union[StringPathLike, BinaryIO, BufferedIOBase], + file: StringPathLike | BinaryIO | BufferedIOBase, mode: "OpenBinaryModeReading" = "rb", **kwargs: Any, ) -> None: diff --git a/can/io/logger.py b/can/io/logger.py index 4d8ddc070..5009c9756 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -6,15 +6,14 @@ import os import pathlib from abc import ABC, abstractmethod +from collections.abc import Callable from datetime import datetime from types import TracebackType from typing import ( Any, - Callable, ClassVar, Final, Literal, - Optional, ) from typing_extensions import Self @@ -107,7 +106,7 @@ def _compress(filename: StringPathLike, **kwargs: Any) -> FileIOMessageWriter[An def Logger( # noqa: N802 - filename: Optional[StringPathLike], **kwargs: Any + filename: StringPathLike | None, **kwargs: Any ) -> MessageWriter: """Find and return the appropriate :class:`~can.io.generic.MessageWriter` instance for a given file suffix. @@ -177,12 +176,12 @@ class BaseRotatingLogger(MessageWriter, ABC): #: If this attribute is set to a callable, the :meth:`~BaseRotatingLogger.rotation_filename` #: method delegates to this callable. The parameters passed to the callable are #: those passed to :meth:`~BaseRotatingLogger.rotation_filename`. - namer: Optional[Callable[[StringPathLike], StringPathLike]] = None + namer: Callable[[StringPathLike], StringPathLike] | None = None #: If this attribute is set to a callable, the :meth:`~BaseRotatingLogger.rotate` method #: delegates to this callable. The parameters passed to the callable are those #: passed to :meth:`~BaseRotatingLogger.rotate`. - rotator: Optional[Callable[[StringPathLike, StringPathLike], None]] = None + rotator: Callable[[StringPathLike, StringPathLike], None] | None = None #: An integer counter to track the number of rollovers. rollover_count: int = 0 @@ -286,9 +285,9 @@ def __enter__(self) -> Self: def __exit__( self, - exc_type: Optional[type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, ) -> Literal[False]: self.stop() return False diff --git a/can/io/mf4.py b/can/io/mf4.py index bf594e3a5..fcde2e193 100644 --- a/can/io/mf4.py +++ b/can/io/mf4.py @@ -13,7 +13,7 @@ from hashlib import md5 from io import BufferedIOBase, BytesIO from pathlib import Path -from typing import Any, BinaryIO, Optional, Union, cast +from typing import Any, BinaryIO, cast from ..message import Message from ..typechecking import StringPathLike @@ -93,8 +93,8 @@ class MF4Writer(BinaryIOMessageWriter): def __init__( self, - file: Union[StringPathLike, BinaryIO], - database: Optional[StringPathLike] = None, + file: StringPathLike | BinaryIO, + database: StringPathLike | None = None, compression_level: int = 2, **kwargs: Any, ) -> None: @@ -458,7 +458,7 @@ def __iter__(self) -> Generator[Message, None, None]: def __init__( self, - file: Union[StringPathLike, BinaryIO], + file: StringPathLike | BinaryIO, **kwargs: Any, ) -> None: """ @@ -497,7 +497,7 @@ def __iter__(self) -> Iterator[Message]: # No data, skip continue - acquisition_source: Optional[Source] = channel_group.acq_source + acquisition_source: Source | None = channel_group.acq_source if acquisition_source is None: # No source information, skip diff --git a/can/io/printer.py b/can/io/printer.py index 786cb7261..c41a83691 100644 --- a/can/io/printer.py +++ b/can/io/printer.py @@ -5,7 +5,7 @@ import logging import sys from io import TextIOWrapper -from typing import Any, TextIO, Union +from typing import Any, TextIO from ..message import Message from ..typechecking import StringPathLike @@ -26,7 +26,7 @@ class Printer(TextIOMessageWriter): def __init__( self, - file: Union[StringPathLike, TextIO, TextIOWrapper] = sys.stdout, + file: StringPathLike | TextIO | TextIOWrapper = sys.stdout, append: bool = False, **kwargs: Any, ) -> None: diff --git a/can/io/sqlite.py b/can/io/sqlite.py index 73aa2961c..5f4885adb 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -9,9 +9,7 @@ import threading import time from collections.abc import Generator, Iterator -from typing import Any - -from typing_extensions import TypeAlias +from typing import Any, TypeAlias from can.listener import BufferedReader from can.message import Message diff --git a/can/io/trc.py b/can/io/trc.py index fa8ee88e7..c02bdcfe9 100644 --- a/can/io/trc.py +++ b/can/io/trc.py @@ -9,11 +9,11 @@ import logging import os -from collections.abc import Generator +from collections.abc import Callable, Generator from datetime import datetime, timedelta, timezone from enum import Enum from io import TextIOWrapper -from typing import Any, Callable, Optional, TextIO, Union +from typing import Any, TextIO from ..message import Message from ..typechecking import StringPathLike @@ -45,7 +45,7 @@ class TRCReader(TextIOMessageReader): def __init__( self, - file: Union[StringPathLike, TextIO], + file: StringPathLike | TextIO, **kwargs: Any, ) -> None: """ @@ -62,12 +62,10 @@ def __init__( if not self.file: raise ValueError("The given file cannot be None") - self._parse_cols: Callable[[tuple[str, ...]], Optional[Message]] = ( - lambda x: None - ) + self._parse_cols: Callable[[tuple[str, ...]], Message | None] = lambda x: None @property - def start_time(self) -> Optional[datetime]: + def start_time(self) -> datetime | None: if self._start_time: return datetime.fromtimestamp(self._start_time, timezone.utc) return None @@ -140,7 +138,7 @@ def _extract_header(self) -> str: return line - def _parse_msg_v1_0(self, cols: tuple[str, ...]) -> Optional[Message]: + def _parse_msg_v1_0(self, cols: tuple[str, ...]) -> Message | None: arbit_id = cols[2] if arbit_id == "FFFFFFFF": logger.info("TRCReader: Dropping bus info line") @@ -158,7 +156,7 @@ def _parse_msg_v1_0(self, cols: tuple[str, ...]) -> Optional[Message]: msg.data = bytearray([int(cols[i + 4], 16) for i in range(msg.dlc)]) return msg - def _parse_msg_v1_1(self, cols: tuple[str, ...]) -> Optional[Message]: + def _parse_msg_v1_1(self, cols: tuple[str, ...]) -> Message | None: arbit_id = cols[3] msg = Message() @@ -174,7 +172,7 @@ def _parse_msg_v1_1(self, cols: tuple[str, ...]) -> Optional[Message]: msg.is_rx = cols[2] == "Rx" return msg - def _parse_msg_v1_3(self, cols: tuple[str, ...]) -> Optional[Message]: + def _parse_msg_v1_3(self, cols: tuple[str, ...]) -> Message | None: arbit_id = cols[4] msg = Message() @@ -190,7 +188,7 @@ def _parse_msg_v1_3(self, cols: tuple[str, ...]) -> Optional[Message]: msg.is_rx = cols[3] == "Rx" return msg - def _parse_msg_v2_x(self, cols: tuple[str, ...]) -> Optional[Message]: + def _parse_msg_v2_x(self, cols: tuple[str, ...]) -> Message | None: type_ = cols[self.columns["T"]] bus = self.columns.get("B", None) @@ -218,7 +216,7 @@ def _parse_msg_v2_x(self, cols: tuple[str, ...]) -> Optional[Message]: return msg - def _parse_cols_v1_1(self, cols: tuple[str, ...]) -> Optional[Message]: + def _parse_cols_v1_1(self, cols: tuple[str, ...]) -> Message | None: dtype = cols[2] if dtype in ("Tx", "Rx"): return self._parse_msg_v1_1(cols) @@ -226,7 +224,7 @@ def _parse_cols_v1_1(self, cols: tuple[str, ...]) -> Optional[Message]: logger.info("TRCReader: Unsupported type '%s'", dtype) return None - def _parse_cols_v1_3(self, cols: tuple[str, ...]) -> Optional[Message]: + def _parse_cols_v1_3(self, cols: tuple[str, ...]) -> Message | None: dtype = cols[3] if dtype in ("Tx", "Rx"): return self._parse_msg_v1_3(cols) @@ -234,7 +232,7 @@ def _parse_cols_v1_3(self, cols: tuple[str, ...]) -> Optional[Message]: logger.info("TRCReader: Unsupported type '%s'", dtype) return None - def _parse_cols_v2_x(self, cols: tuple[str, ...]) -> Optional[Message]: + def _parse_cols_v2_x(self, cols: tuple[str, ...]) -> Message | None: dtype = cols[self.columns["T"]] if dtype in {"DT", "FD", "FB", "FE", "BI", "RR"}: return self._parse_msg_v2_x(cols) @@ -242,7 +240,7 @@ def _parse_cols_v2_x(self, cols: tuple[str, ...]) -> Optional[Message]: logger.info("TRCReader: Unsupported type '%s'", dtype) return None - def _parse_line(self, line: str) -> Optional[Message]: + def _parse_line(self, line: str) -> Message | None: logger.debug("TRCReader: Parse '%s'", line) try: cols = tuple(line.split(maxsplit=self._num_columns)) @@ -292,7 +290,7 @@ class TRCWriter(TextIOMessageWriter): def __init__( self, - file: Union[StringPathLike, TextIO, TextIOWrapper], + file: StringPathLike | TextIO | TextIOWrapper, channel: int = 1, **kwargs: Any, ) -> None: @@ -314,7 +312,7 @@ def __init__( self.filepath = os.path.abspath(self.file.name) self.header_written = False self.msgnr = 0 - self.first_timestamp: Optional[float] = None + self.first_timestamp: float | None = None self.file_version = TRCFileVersion.V2_1 self._msg_fmt_string = self.FORMAT_MESSAGE_V1_0 self._format_message = self._format_message_init diff --git a/can/listener.py b/can/listener.py index 7f8f436a0..1e289bea6 100644 --- a/can/listener.py +++ b/can/listener.py @@ -3,12 +3,11 @@ """ import asyncio -import sys import warnings from abc import ABC, abstractmethod from collections.abc import AsyncIterator from queue import Empty, SimpleQueue -from typing import Any, Optional +from typing import Any from can.bus import BusABC from can.message import Message @@ -99,7 +98,7 @@ def on_message_received(self, msg: Message) -> None: else: self.buffer.put(msg) - def get_message(self, timeout: float = 0.5) -> Optional[Message]: + def get_message(self, timeout: float = 0.5) -> Message | None: """ Attempts to retrieve the message that has been in the queue for the longest amount of time (FIFO). If no message is available, it blocks for given timeout or until a @@ -146,12 +145,6 @@ def __init__(self, **kwargs: Any) -> None: DeprecationWarning, stacklevel=2, ) - if sys.version_info < (3, 10): - self.buffer = asyncio.Queue( # pylint: disable=unexpected-keyword-arg - loop=kwargs["loop"] - ) - return - self.buffer = asyncio.Queue() def on_message_received(self, msg: Message) -> None: diff --git a/can/logger.py b/can/logger.py index 8274d6668..537356643 100644 --- a/can/logger.py +++ b/can/logger.py @@ -4,7 +4,6 @@ from datetime import datetime from typing import ( TYPE_CHECKING, - Union, ) from can import BusState, Logger, SizedRotatingLogger @@ -110,7 +109,7 @@ def main() -> None: print(f"Connected to {bus.__class__.__name__}: {bus.channel_info}") print(f"Can Logger (Started on {datetime.now()})") - logger: Union[MessageWriter, BaseRotatingLogger] + logger: MessageWriter | BaseRotatingLogger if results.file_size: logger = SizedRotatingLogger( base_filename=results.log_file, diff --git a/can/message.py b/can/message.py index c1fbffd21..3e60ca641 100644 --- a/can/message.py +++ b/can/message.py @@ -8,7 +8,7 @@ from copy import deepcopy from math import isinf, isnan -from typing import Any, Optional +from typing import Any from . import typechecking @@ -54,9 +54,9 @@ def __init__( # pylint: disable=too-many-locals, too-many-arguments is_extended_id: bool = True, is_remote_frame: bool = False, is_error_frame: bool = False, - channel: Optional[typechecking.Channel] = None, - dlc: Optional[int] = None, - data: Optional[typechecking.CanData] = None, + channel: typechecking.Channel | None = None, + dlc: int | None = None, + data: typechecking.CanData | None = None, is_fd: bool = False, is_rx: bool = True, bitrate_switch: bool = False, @@ -185,7 +185,7 @@ def __repr__(self) -> str: return f"can.Message({', '.join(args)})" - def __format__(self, format_spec: Optional[str]) -> str: + def __format__(self, format_spec: str | None) -> str: if not format_spec: return self.__str__() else: @@ -210,7 +210,7 @@ def __copy__(self) -> "Message": error_state_indicator=self.error_state_indicator, ) - def __deepcopy__(self, memo: Optional[dict[int, Any]]) -> "Message": + def __deepcopy__(self, memo: dict[int, Any] | None) -> "Message": return Message( timestamp=self.timestamp, arbitration_id=self.arbitration_id, @@ -289,7 +289,7 @@ def _check(self) -> None: def equals( self, other: "Message", - timestamp_delta: Optional[float] = 1.0e-6, + timestamp_delta: float | None = 1.0e-6, check_channel: bool = True, check_direction: bool = True, ) -> bool: diff --git a/can/notifier.py b/can/notifier.py index a2ee512fc..cb91cf7b4 100644 --- a/can/notifier.py +++ b/can/notifier.py @@ -7,16 +7,13 @@ import logging import threading import time -from collections.abc import Awaitable, Iterable +from collections.abc import Awaitable, Callable, Iterable from contextlib import AbstractContextManager from types import TracebackType from typing import ( Any, - Callable, Final, NamedTuple, - Optional, - Union, ) from can.bus import BusABC @@ -25,7 +22,7 @@ logger = logging.getLogger("can.Notifier") -MessageRecipient = Union[Listener, Callable[[Message], Union[Awaitable[None], None]]] +MessageRecipient = Listener | Callable[[Message], Awaitable[None] | None] class _BusNotifierPair(NamedTuple): @@ -109,10 +106,10 @@ class Notifier(AbstractContextManager["Notifier"]): def __init__( self, - bus: Union[BusABC, list[BusABC]], + bus: BusABC | list[BusABC], listeners: Iterable[MessageRecipient], timeout: float = 1.0, - loop: Optional[asyncio.AbstractEventLoop] = None, + loop: asyncio.AbstractEventLoop | None = None, ) -> None: """Manages the distribution of :class:`~can.Message` instances to listeners. @@ -142,19 +139,19 @@ def __init__( self._loop = loop #: Exception raised in thread - self.exception: Optional[Exception] = None + self.exception: Exception | None = None self._stopped = False self._lock = threading.Lock() - self._readers: list[Union[int, threading.Thread]] = [] + self._readers: list[int | threading.Thread] = [] self._tasks: set[asyncio.Task] = set() _bus_list: list[BusABC] = bus if isinstance(bus, list) else [bus] for each_bus in _bus_list: self.add_bus(each_bus) @property - def bus(self) -> Union[BusABC, tuple["BusABC", ...]]: + def bus(self) -> BusABC | tuple["BusABC", ...]: """Return the associated bus or a tuple of buses.""" if len(self._bus_list) == 1: return self._bus_list[0] @@ -322,9 +319,9 @@ def find_instances(bus: BusABC) -> tuple["Notifier", ...]: def __exit__( self, - exc_type: Optional[type[BaseException]], - exc_value: Optional[BaseException], - traceback: Optional[TracebackType], + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, ) -> None: if not self._stopped: self.stop() diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index 35a4f400c..71d6a5536 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -1,6 +1,6 @@ from contextlib import nullcontext from threading import RLock -from typing import Any, Optional +from typing import Any from can import typechecking from can.bus import BusABC, BusState, CanProtocol @@ -40,9 +40,9 @@ class ThreadSafeBus(ObjectProxy): # pylint: disable=abstract-method def __init__( self, - channel: Optional[typechecking.Channel] = None, - interface: Optional[str] = None, - config_context: Optional[str] = None, + channel: typechecking.Channel | None = None, + interface: str | None = None, + config_context: str | None = None, ignore_config: bool = False, **kwargs: Any, ) -> None: @@ -67,11 +67,11 @@ def __init__( self._lock_send = RLock() self._lock_recv = RLock() - def recv(self, timeout: Optional[float] = None) -> Optional[Message]: + def recv(self, timeout: float | None = None) -> Message | None: with self._lock_recv: return self.__wrapped__.recv(timeout=timeout) - def send(self, msg: Message, timeout: Optional[float] = None) -> None: + def send(self, msg: Message, timeout: float | None = None) -> None: with self._lock_send: return self.__wrapped__.send(msg=msg, timeout=timeout) @@ -79,16 +79,16 @@ def send(self, msg: Message, timeout: Optional[float] = None) -> None: # `send` method is already synchronized @property - def filters(self) -> Optional[typechecking.CanFilters]: + def filters(self) -> typechecking.CanFilters | None: with self._lock_recv: return self.__wrapped__.filters @filters.setter - def filters(self, filters: Optional[typechecking.CanFilters]) -> None: + def filters(self, filters: typechecking.CanFilters | None) -> None: with self._lock_recv: self.__wrapped__.filters = filters - def set_filters(self, filters: Optional[typechecking.CanFilters] = None) -> None: + def set_filters(self, filters: typechecking.CanFilters | None = None) -> None: with self._lock_recv: return self.__wrapped__.set_filters(filters=filters) diff --git a/can/typechecking.py b/can/typechecking.py index 8c25e8b57..56ac5927f 100644 --- a/can/typechecking.py +++ b/can/typechecking.py @@ -1,14 +1,10 @@ """Types for mypy type-checking""" import io +import os import sys from collections.abc import Iterable, Sequence -from typing import IO, TYPE_CHECKING, Any, NewType, Union - -if sys.version_info >= (3, 10): - from typing import TypeAlias -else: - from typing_extensions import TypeAlias +from typing import IO, TYPE_CHECKING, Any, NewType, TypeAlias if sys.version_info >= (3, 12): from typing import TypedDict @@ -17,7 +13,6 @@ if TYPE_CHECKING: - import os import struct @@ -37,24 +32,24 @@ class CanFilter(_CanFilterBase, total=False): # this should have the same typing info. # # See: https://github.com/python/typing/issues/593 -CanData = Union[bytes, bytearray, int, Iterable[int]] +CanData = bytes | bytearray | int | Iterable[int] # Used for the Abstract Base Class ChannelStr = str ChannelInt = int -Channel = Union[ChannelInt, ChannelStr, Sequence[ChannelInt]] +Channel = ChannelInt | ChannelStr | Sequence[ChannelInt] # Used by the IO module -FileLike = Union[IO[Any], io.TextIOWrapper, io.BufferedIOBase] -StringPathLike = Union[str, "os.PathLike[str]"] +FileLike = IO[Any] | io.TextIOWrapper | io.BufferedIOBase +StringPathLike = str | os.PathLike[str] BusConfig = NewType("BusConfig", dict[str, Any]) # Used by CLI scripts -TAdditionalCliArgs: TypeAlias = dict[str, Union[str, int, float, bool]] +TAdditionalCliArgs: TypeAlias = dict[str, str | int | float | bool] TDataStructs: TypeAlias = dict[ - Union[int, tuple[int, ...]], - "Union[struct.Struct, tuple[struct.Struct, *tuple[float, ...]]]", + int | tuple[int, ...], + "struct.Struct | tuple[struct.Struct, *tuple[float, ...]]", ] @@ -63,7 +58,7 @@ class AutoDetectedConfig(TypedDict): channel: Channel -ReadableBytesLike = Union[bytes, bytearray, memoryview] +ReadableBytesLike = bytes | bytearray | memoryview class BitTimingDict(TypedDict): diff --git a/can/util.py b/can/util.py index 584b7dfa9..4cbeec60e 100644 --- a/can/util.py +++ b/can/util.py @@ -12,15 +12,12 @@ import platform import re import warnings -from collections.abc import Iterable +from collections.abc import Callable, Iterable from configparser import ConfigParser from time import get_clock_info, perf_counter, time from typing import ( Any, - Callable, - Optional, TypeVar, - Union, cast, ) @@ -50,7 +47,7 @@ def load_file_config( - path: Optional[typechecking.StringPathLike] = None, section: str = "default" + path: typechecking.StringPathLike | None = None, section: str = "default" ) -> dict[str, str]: """ Loads configuration from file with following content:: @@ -83,7 +80,7 @@ def load_file_config( return _config -def load_environment_config(context: Optional[str] = None) -> dict[str, str]: +def load_environment_config(context: str | None = None) -> dict[str, str]: """ Loads config dict from environmental variables (if set): @@ -120,9 +117,9 @@ def load_environment_config(context: Optional[str] = None) -> dict[str, str]: def load_config( - path: Optional[typechecking.StringPathLike] = None, - config: Optional[dict[str, Any]] = None, - context: Optional[str] = None, + path: typechecking.StringPathLike | None = None, + config: dict[str, Any] | None = None, + context: str | None = None, ) -> typechecking.BusConfig: """ Returns a dict with configuration details which is loaded from (in this order): @@ -176,7 +173,7 @@ def load_config( # Use the given dict for default values config_sources = cast( - "Iterable[Union[dict[str, Any], Callable[[Any], dict[str, Any]]]]", + "Iterable[dict[str, Any] | Callable[[Any], dict[str, Any]]]", [ given_config, can.rc, @@ -258,7 +255,7 @@ def _create_bus_config(config: dict[str, Any]) -> typechecking.BusConfig: return cast("typechecking.BusConfig", config) -def _dict2timing(data: dict[str, Any]) -> Union[BitTiming, BitTimingFd, None]: +def _dict2timing(data: dict[str, Any]) -> BitTiming | BitTimingFd | None: """Try to instantiate a :class:`~can.BitTiming` or :class:`~can.BitTimingFd` from a dictionary. Return `None` if not possible.""" @@ -325,7 +322,7 @@ def dlc2len(dlc: int) -> int: return CAN_FD_DLC[dlc] if dlc <= 15 else 64 -def channel2int(channel: Optional[typechecking.Channel]) -> Optional[int]: +def channel2int(channel: typechecking.Channel | None) -> int | None: """Try to convert the channel to an integer. :param channel: @@ -348,8 +345,8 @@ def channel2int(channel: Optional[typechecking.Channel]) -> Optional[int]: def deprecated_args_alias( deprecation_start: str, - deprecation_end: Optional[str] = None, - **aliases: Optional[str], + deprecation_end: str | None = None, + **aliases: str | None, ) -> Callable[[Callable[P1, T1]], Callable[P1, T1]]: """Allows to rename/deprecate a function kwarg(s) and optionally have the deprecated kwarg(s) set as alias(es) @@ -399,9 +396,9 @@ def wrapper(*args: P1.args, **kwargs: P1.kwargs) -> T1: def _rename_kwargs( func_name: str, start: str, - end: Optional[str], + end: str | None, kwargs: dict[str, Any], - aliases: dict[str, Optional[str]], + aliases: dict[str, str | None], ) -> None: """Helper function for `deprecated_args_alias`""" for alias, new in aliases.items(): @@ -501,7 +498,7 @@ def time_perfcounter_correlation() -> tuple[float, float]: return t1, performance_counter -def cast_from_string(string_val: str) -> Union[str, int, float, bool]: +def cast_from_string(string_val: str) -> str | int | float | bool: """Perform trivial type conversion from :class:`str` values. :param string_val: diff --git a/can/viewer.py b/can/viewer.py index 97bda1676..8d9d228bb 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -173,7 +173,9 @@ def unpack_data(cmd: int, cmd_to_struct: TDataStructs, data: bytes) -> list[floa # The conversion from raw values to SI-units are given in the rest of the tuple values = [ d // val if isinstance(val, int) else float(d) / val - for d, val in zip(struct_t.unpack(data), value[1:]) + for d, val in zip( + struct_t.unpack(data), value[1:], strict=False + ) ] else: # No conversion from SI-units is needed diff --git a/doc/changelog.d/1996.removed.md b/doc/changelog.d/1996.removed.md new file mode 100644 index 000000000..77458ad75 --- /dev/null +++ b/doc/changelog.d/1996.removed.md @@ -0,0 +1 @@ +Remove support for end-of-life Python 3.9. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index bc526ce73..551a7f3bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ "packaging >= 23.1", "typing_extensions>=3.10.0.0", ] -requires-python = ">=3.9" +requires-python = ">=3.10" license = "LGPL-3.0-only" classifiers = [ "Development Status :: 5 - Production/Stable", @@ -27,7 +27,6 @@ classifiers = [ "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", "Programming Language :: Python", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -89,9 +88,9 @@ docs = [ ] lint = [ "pylint==3.3.*", - "ruff==0.12.11", - "black==25.1.*", - "mypy==1.17.*", + "ruff==0.14.*", + "black==25.9.*", + "mypy==1.18.*", ] test = [ "pytest==8.4.*", @@ -100,7 +99,7 @@ test = [ "coveralls==4.0.*", "pytest-cov==6.2.*", "coverage==7.10.*", - "hypothesis>=6.136,<6.138", + "hypothesis==6.*", "parameterized==0.9.*", ] dev = [ @@ -132,6 +131,7 @@ disallow_incomplete_defs = true warn_redundant_casts = true warn_unused_ignores = true exclude = [ + "^build", "^doc/conf.py$", "^test", "^can/interfaces/etas", diff --git a/tox.ini b/tox.ini index 5f393cb93..4e96291ed 100644 --- a/tox.ini +++ b/tox.ini @@ -69,11 +69,11 @@ dependency_groups = lint extras = commands = - mypy --python-version 3.9 . mypy --python-version 3.10 . mypy --python-version 3.11 . mypy --python-version 3.12 . mypy --python-version 3.13 . + mypy --python-version 3.14 . [pytest] testpaths = test From 39a2c7fedcde0564957e10c78ebd49e8f03463c2 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 10 Oct 2025 17:03:48 +0200 Subject: [PATCH 116/127] add loop arg to player script (#1986) --- can/player.py | 95 +++++++++++++++++++++++---------- doc/changelog.d/1815.added.md | 1 + doc/changelog.d/1815.removed.md | 1 + test/test_player.py | 55 ++++++++++++++++--- 4 files changed, 117 insertions(+), 35 deletions(-) create mode 100644 doc/changelog.d/1815.added.md create mode 100644 doc/changelog.d/1815.removed.md diff --git a/can/player.py b/can/player.py index a92cccc3d..6190a58d8 100644 --- a/can/player.py +++ b/can/player.py @@ -7,6 +7,7 @@ import argparse import errno +import math import sys from datetime import datetime from typing import TYPE_CHECKING, cast @@ -26,19 +27,41 @@ from can import Message +def _parse_loop(value: str) -> int | float: + """Parse the loop argument, allowing integer or 'i' for infinite.""" + if value == "i": + return float("inf") + try: + return int(value) + except ValueError as exc: + err_msg = "Loop count must be an integer or 'i' for infinite." + raise argparse.ArgumentTypeError(err_msg) from exc + + +def _format_player_start_message(iteration: int, loop_count: int | float) -> str: + """ + Generate a status message indicating the start of a CAN log replay iteration. + + :param iteration: + The current loop iteration (zero-based). + :param loop_count: + Total number of replay loops, or infinity for endless replay. + :return: + A formatted string describing the replay start and loop information. + """ + if loop_count < 2: + loop_info = "" + else: + loop_val = "∞" if math.isinf(loop_count) else str(loop_count) + loop_info = f" [loop {iteration + 1}/{loop_val}]" + return f"Can LogReader (Started on {datetime.now()}){loop_info}" + + def main() -> None: parser = argparse.ArgumentParser(description="Replay CAN traffic.") player_group = parser.add_argument_group("Player arguments") - player_group.add_argument( - "-f", - "--file_name", - dest="log_file", - help="Path and base log filename, for supported types see can.LogReader.", - default=None, - ) - player_group.add_argument( "-v", action="count", @@ -73,9 +96,20 @@ def main() -> None: "--skip", type=float, default=60 * 60 * 24, - help=" skip gaps greater than 's' seconds", + help="Skip gaps greater than 's' seconds between messages. " + "Default is 86400 (24 hours), meaning only very large gaps are skipped. " + "Set to 0 to never skip any gaps (all delays are preserved). " + "Set to a very small value (e.g., 1e-4) " + "to skip all gaps and send messages as fast as possible.", + ) + player_group.add_argument( + "-l", + "--loop", + type=_parse_loop, + metavar="NUM", + default=1, + help="Replay file NUM times. Use 'i' for infinite loop (default: 1)", ) - player_group.add_argument( "infile", metavar="input-file", @@ -103,25 +137,28 @@ def main() -> None: error_frames = results.error_frames with create_bus_from_namespace(results) as bus: - with LogReader(results.infile, **additional_config) as reader: - in_sync = MessageSync( - cast("Iterable[Message]", reader), - timestamps=results.timestamps, - gap=results.gap, - skip=results.skip, - ) - - print(f"Can LogReader (Started on {datetime.now()})") - - try: - for message in in_sync: - if message.is_error_frame and not error_frames: - continue - if verbosity >= 3: - print(message) - bus.send(message) - except KeyboardInterrupt: - pass + loop_count: int | float = results.loop + iteration = 0 + try: + while iteration < loop_count: + with LogReader(results.infile, **additional_config) as reader: + in_sync = MessageSync( + cast("Iterable[Message]", reader), + timestamps=results.timestamps, + gap=results.gap, + skip=results.skip, + ) + print(_format_player_start_message(iteration, loop_count)) + + for message in in_sync: + if message.is_error_frame and not error_frames: + continue + if verbosity >= 3: + print(message) + bus.send(message) + iteration += 1 + except KeyboardInterrupt: + pass if __name__ == "__main__": diff --git a/doc/changelog.d/1815.added.md b/doc/changelog.d/1815.added.md new file mode 100644 index 000000000..65756fb41 --- /dev/null +++ b/doc/changelog.d/1815.added.md @@ -0,0 +1 @@ +Added support for replaying CAN log files multiple times or infinitely in the player script via the new --loop/-l argument. diff --git a/doc/changelog.d/1815.removed.md b/doc/changelog.d/1815.removed.md new file mode 100644 index 000000000..61b4e9b1d --- /dev/null +++ b/doc/changelog.d/1815.removed.md @@ -0,0 +1 @@ +Removed the unused --file_name/-f argument from the player CLI. diff --git a/test/test_player.py b/test/test_player.py index e5e77fe8a..c4c3c90ef 100755 --- a/test/test_player.py +++ b/test/test_player.py @@ -11,6 +11,8 @@ from unittest import mock from unittest.mock import Mock +from parameterized import parameterized + import can import can.player @@ -38,7 +40,7 @@ def assertSuccessfulCleanup(self): self.mock_virtual_bus.__exit__.assert_called_once() def test_play_virtual(self): - sys.argv = self.baseargs + [self.logfile] + sys.argv = [*self.baseargs, self.logfile] can.player.main() msg1 = can.Message( timestamp=2.501, @@ -65,8 +67,8 @@ def test_play_virtual(self): self.assertSuccessfulCleanup() def test_play_virtual_verbose(self): - sys.argv = self.baseargs + ["-v", self.logfile] - with unittest.mock.patch("sys.stdout", new_callable=io.StringIO) as mock_stdout: + sys.argv = [*self.baseargs, "-v", self.logfile] + with mock.patch("sys.stdout", new_callable=io.StringIO) as mock_stdout: can.player.main() self.assertIn("09 08 07 06 05 04 03 02", mock_stdout.getvalue()) self.assertIn("05 0c 00 00 00 00 00 00", mock_stdout.getvalue()) @@ -76,7 +78,7 @@ def test_play_virtual_verbose(self): def test_play_virtual_exit(self): self.MockSleep.side_effect = [None, KeyboardInterrupt] - sys.argv = self.baseargs + [self.logfile] + sys.argv = [*self.baseargs, self.logfile] can.player.main() assert self.mock_virtual_bus.send.call_count <= 2 self.assertSuccessfulCleanup() @@ -85,7 +87,7 @@ def test_play_skip_error_frame(self): logfile = os.path.join( os.path.dirname(__file__), "data", "logfile_errorframes.asc" ) - sys.argv = self.baseargs + ["-v", logfile] + sys.argv = [*self.baseargs, "-v", logfile] can.player.main() self.assertEqual(self.mock_virtual_bus.send.call_count, 9) self.assertSuccessfulCleanup() @@ -94,11 +96,52 @@ def test_play_error_frame(self): logfile = os.path.join( os.path.dirname(__file__), "data", "logfile_errorframes.asc" ) - sys.argv = self.baseargs + ["-v", "--error-frames", logfile] + sys.argv = [*self.baseargs, "-v", "--error-frames", logfile] can.player.main() self.assertEqual(self.mock_virtual_bus.send.call_count, 12) self.assertSuccessfulCleanup() + @parameterized.expand([0, 1, 2, 3]) + def test_play_loop(self, loop_val): + sys.argv = [*self.baseargs, "--loop", str(loop_val), self.logfile] + can.player.main() + msg1 = can.Message( + timestamp=2.501, + arbitration_id=0xC8, + is_extended_id=False, + is_fd=False, + is_rx=False, + channel=1, + dlc=8, + data=[0x9, 0x8, 0x7, 0x6, 0x5, 0x4, 0x3, 0x2], + ) + msg2 = can.Message( + timestamp=17.876708, + arbitration_id=0x6F9, + is_extended_id=False, + is_fd=False, + is_rx=True, + channel=0, + dlc=8, + data=[0x5, 0xC, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0], + ) + for i in range(loop_val): + self.assertTrue( + msg1.equals(self.mock_virtual_bus.send.mock_calls[2 * i + 0].args[0]) + ) + self.assertTrue( + msg2.equals(self.mock_virtual_bus.send.mock_calls[2 * i + 1].args[0]) + ) + self.assertEqual(self.mock_virtual_bus.send.call_count, 2 * loop_val) + self.assertSuccessfulCleanup() + + def test_play_loop_infinite(self): + self.mock_virtual_bus.send.side_effect = [None] * 99 + [KeyboardInterrupt] + sys.argv = [*self.baseargs, "-l", "i", self.logfile] + can.player.main() + self.assertEqual(self.mock_virtual_bus.send.call_count, 100) + self.assertSuccessfulCleanup() + class TestPlayerCompressedFile(TestPlayerScriptModule): """ From ec78efc79c6c456822f3eefaab650ac35286c564 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 14:06:05 +0000 Subject: [PATCH 117/127] Bump the github-actions group with 2 updates Bumps the github-actions group with 2 updates: [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) and [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish). Updates `astral-sh/setup-uv` from 6.6.1 to 6.8.0 - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](https://github.com/astral-sh/setup-uv/compare/557e51de59eb14aaaba2ed9621916900a91d50c6...d0cc045d04ccac9d8b7881df0226f9e82c39688e) Updates `pypa/gh-action-pypi-publish` from 1.12.4 to 1.13.0 - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/76f52bc884231f62b9a034ebfe128415bbaabdfc...ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e) --- updated-dependencies: - dependency-name: astral-sh/setup-uv dependency-version: 6.8.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: pypa/gh-action-pypi-publish dependency-version: 1.13.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12022ba35..e85906a23 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,7 @@ jobs: fetch-depth: 0 persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # 6.6.1 + uses: astral-sh/setup-uv@eb1897b8dc4b5d5bfe39a428a8f2304605e0983c # 7.0.0 - name: Install tox run: uv tool install tox --with tox-uv - name: Setup SocketCAN @@ -84,7 +84,7 @@ jobs: fetch-depth: 0 persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # 6.6.1 + uses: astral-sh/setup-uv@eb1897b8dc4b5d5bfe39a428a8f2304605e0983c # 7.0.0 - name: Install tox run: uv tool install tox --with tox-uv - name: Run linters @@ -102,7 +102,7 @@ jobs: fetch-depth: 0 persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # 6.6.1 + uses: astral-sh/setup-uv@eb1897b8dc4b5d5bfe39a428a8f2304605e0983c # 7.0.0 - name: Install tox run: uv tool install tox --with tox-uv - name: Build documentation @@ -118,7 +118,7 @@ jobs: fetch-depth: 0 persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # 6.6.1 + uses: astral-sh/setup-uv@eb1897b8dc4b5d5bfe39a428a8f2304605e0983c # 7.0.0 - name: Build wheel and sdist run: uv build - name: Check build artifacts @@ -151,4 +151,4 @@ jobs: subject-path: 'dist/*' - name: Publish release distributions to PyPI - uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # 1.12.4 + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # 1.13.0 From efd4de5619331d3ae8da10688979624cbb6d0342 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 14:03:50 +0000 Subject: [PATCH 118/127] Update pytest-cov requirement in the dev-deps group Updates the requirements on [pytest-cov](https://github.com/pytest-dev/pytest-cov) to permit the latest version. Updates `pytest-cov` to 7.0.0 - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v6.2.0...v7.0.0) --- updated-dependencies: - dependency-name: pytest-cov dependency-version: 7.0.0 dependency-type: direct:production dependency-group: dev-deps ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 551a7f3bd..3c263579f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,7 +97,7 @@ test = [ "pytest-timeout==2.4.*", "pytest-modern==0.7.*;platform_system!='Windows'", "coveralls==4.0.*", - "pytest-cov==6.2.*", + "pytest-cov==7.0.*", "coverage==7.10.*", "hypothesis==6.*", "parameterized==0.9.*", From b4d5094fa70277228fa2dc9fa1f30228fbdd79b7 Mon Sep 17 00:00:00 2001 From: Varun Penumudi <73636316+varunpenumudi@users.noreply.github.com> Date: Tue, 21 Oct 2025 19:00:26 +0530 Subject: [PATCH 119/127] SeedBus: Added can_filters parameter in SeedBus.__init__ method (#1999) * Added hardware filter support for SeeedBus during initialization * Implement software fallback for can_filters in SeedBus - Updated the __init__ function in SeedBus - Implemented software fallback, if the user passes multiple filters in can_filters dict. * updated changelog for PR #1999 * Updated _recv_internal method of SeedBus - updated _recv_internal method of SeedBus to return also the Boolean value based on whether hw filter is enabled or not --- can/interfaces/seeedstudio/seeedstudio.py | 36 +++++++++++++++++------ doc/changelog.d/1995.added.md | 1 + doc/interfaces/seeedstudio.rst | 17 ++++++++++- 3 files changed, 44 insertions(+), 10 deletions(-) create mode 100644 doc/changelog.d/1995.added.md diff --git a/can/interfaces/seeedstudio/seeedstudio.py b/can/interfaces/seeedstudio/seeedstudio.py index a0817e932..26339616c 100644 --- a/can/interfaces/seeedstudio/seeedstudio.py +++ b/can/interfaces/seeedstudio/seeedstudio.py @@ -63,6 +63,7 @@ def __init__( frame_type="STD", operation_mode="normal", bitrate=500000, + can_filters=None, **kwargs, ): """ @@ -85,6 +86,12 @@ def __init__( :param bitrate CAN bus bit rate, selected from available list. + :param can_filters: + A list of CAN filter dictionaries. If one filter is provided, + it will be used by the high-performance hardware filter. If + zero or more than one filter is provided, software-based + filtering will be used. Defaults to None (no filtering). + :raises can.CanInitializationError: If the given parameters are invalid. :raises can.CanInterfaceNotImplementedError: If the serial module is not installed. """ @@ -94,11 +101,21 @@ def __init__( "the serial module is not installed" ) + can_id = 0x00 + can_mask = 0x00 + self._is_filtered = False + + if can_filters and len(can_filters) == 1: + self._is_filtered = True + hw_filter = can_filters[0] + can_id = hw_filter["can_id"] + can_mask = hw_filter["can_mask"] + self.bit_rate = bitrate self.frame_type = frame_type self.op_mode = operation_mode - self.filter_id = bytearray([0x00, 0x00, 0x00, 0x00]) - self.mask_id = bytearray([0x00, 0x00, 0x00, 0x00]) + self.filter_id = struct.pack(" Date: Sun, 16 Nov 2025 12:27:37 +0100 Subject: [PATCH 120/127] fix pylint complaints (#2006) --- can/thread_safe_bus.py | 75 ++++++++++++++++++++++-------------------- pyproject.toml | 2 +- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py index 71d6a5536..518604364 100644 --- a/can/thread_safe_bus.py +++ b/can/thread_safe_bus.py @@ -1,12 +1,14 @@ from contextlib import nullcontext from threading import RLock -from typing import Any - -from can import typechecking -from can.bus import BusABC, BusState, CanProtocol -from can.message import Message +from typing import TYPE_CHECKING, Any, cast +from . import typechecking +from .bus import BusState, CanProtocol from .interface import Bus +from .message import Message + +if TYPE_CHECKING: + from .bus import BusABC try: # Only raise an exception on instantiation but allow module @@ -15,11 +17,13 @@ import_exc = None except ImportError as exc: - ObjectProxy = object + ObjectProxy = None # type: ignore[misc,assignment] import_exc = exc -class ThreadSafeBus(ObjectProxy): # pylint: disable=abstract-method +class ThreadSafeBus( + ObjectProxy +): # pylint: disable=abstract-method # type: ignore[assignment] """ Contains a thread safe :class:`can.BusABC` implementation that wraps around an existing interface instance. All public methods @@ -36,8 +40,6 @@ class ThreadSafeBus(ObjectProxy): # pylint: disable=abstract-method instead of :meth:`~can.BusABC.recv` directly. """ - __wrapped__: BusABC - def __init__( self, channel: typechecking.Channel | None = None, @@ -59,58 +61,61 @@ def __init__( ) ) + # store wrapped bus as a proxy-local attribute. Name it with the + # `_self_` prefix so wrapt won't forward it onto the wrapped object. + self._self_wrapped = cast( + "BusABC", object.__getattribute__(self, "__wrapped__") + ) + # now, BusABC.send_periodic() does not need a lock anymore, but the # implementation still requires a context manager - self.__wrapped__._lock_send_periodic = nullcontext() # type: ignore[assignment] + self._self_wrapped._lock_send_periodic = nullcontext() # type: ignore[assignment] # init locks for sending and receiving separately - self._lock_send = RLock() - self._lock_recv = RLock() + self._self_lock_send = RLock() + self._self_lock_recv = RLock() def recv(self, timeout: float | None = None) -> Message | None: - with self._lock_recv: - return self.__wrapped__.recv(timeout=timeout) + with self._self_lock_recv: + return self._self_wrapped.recv(timeout=timeout) def send(self, msg: Message, timeout: float | None = None) -> None: - with self._lock_send: - return self.__wrapped__.send(msg=msg, timeout=timeout) - - # send_periodic does not need a lock, since the underlying - # `send` method is already synchronized + with self._self_lock_send: + return self._self_wrapped.send(msg=msg, timeout=timeout) @property def filters(self) -> typechecking.CanFilters | None: - with self._lock_recv: - return self.__wrapped__.filters + with self._self_lock_recv: + return self._self_wrapped.filters @filters.setter def filters(self, filters: typechecking.CanFilters | None) -> None: - with self._lock_recv: - self.__wrapped__.filters = filters + with self._self_lock_recv: + self._self_wrapped.filters = filters def set_filters(self, filters: typechecking.CanFilters | None = None) -> None: - with self._lock_recv: - return self.__wrapped__.set_filters(filters=filters) + with self._self_lock_recv: + return self._self_wrapped.set_filters(filters=filters) def flush_tx_buffer(self) -> None: - with self._lock_send: - return self.__wrapped__.flush_tx_buffer() + with self._self_lock_send: + return self._self_wrapped.flush_tx_buffer() def shutdown(self) -> None: - with self._lock_send, self._lock_recv: - return self.__wrapped__.shutdown() + with self._self_lock_send, self._self_lock_recv: + return self._self_wrapped.shutdown() @property def state(self) -> BusState: - with self._lock_send, self._lock_recv: - return self.__wrapped__.state + with self._self_lock_send, self._self_lock_recv: + return self._self_wrapped.state @state.setter def state(self, new_state: BusState) -> None: - with self._lock_send, self._lock_recv: - self.__wrapped__.state = new_state + with self._self_lock_send, self._self_lock_recv: + self._self_wrapped.state = new_state @property def protocol(self) -> CanProtocol: - with self._lock_send, self._lock_recv: - return self.__wrapped__.protocol + with self._self_lock_send, self._self_lock_recv: + return self._self_wrapped.protocol diff --git a/pyproject.toml b/pyproject.toml index 3c263579f..c59970544 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,7 +87,7 @@ docs = [ "furo", ] lint = [ - "pylint==3.3.*", + "pylint==4.0.*", "ruff==0.14.*", "black==25.9.*", "mypy==1.18.*", From 61a1e7610b3ef14b3c5fa150e9df09251fef571f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 16 Nov 2025 11:30:07 +0000 Subject: [PATCH 121/127] Bump the dev-deps group across 1 directory with 3 updates Updates the requirements on [black](https://github.com/psf/black), [pytest](https://github.com/pytest-dev/pytest) and [coverage](https://github.com/coveragepy/coveragepy) to permit the latest version. Updates `black` to 25.11.0 - [Release notes](https://github.com/psf/black/releases) - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) - [Commits](https://github.com/psf/black/compare/25.9.0...25.11.0) Updates `pytest` to 9.0.1 - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.4.0.dev0...9.0.1) Updates `coverage` to 7.11.3 - [Release notes](https://github.com/coveragepy/coveragepy/releases) - [Changelog](https://github.com/coveragepy/coveragepy/blob/main/CHANGES.rst) - [Commits](https://github.com/coveragepy/coveragepy/compare/7.10.0...7.11.3) --- updated-dependencies: - dependency-name: black dependency-version: 25.11.0 dependency-type: direct:development dependency-group: dev-deps - dependency-name: pytest dependency-version: 9.0.1 dependency-type: direct:development dependency-group: dev-deps - dependency-name: coverage dependency-version: 7.11.3 dependency-type: direct:development dependency-group: dev-deps ... Signed-off-by: dependabot[bot] --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c59970544..eb823bd64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,16 +89,16 @@ docs = [ lint = [ "pylint==4.0.*", "ruff==0.14.*", - "black==25.9.*", + "black==25.11.*", "mypy==1.18.*", ] test = [ - "pytest==8.4.*", + "pytest==9.0.*", "pytest-timeout==2.4.*", "pytest-modern==0.7.*;platform_system!='Windows'", "coveralls==4.0.*", "pytest-cov==7.0.*", - "coverage==7.10.*", + "coverage==7.11.*", "hypothesis==6.*", "parameterized==0.9.*", ] From 3f54ccaf0cb900af9bcf616069e4ce8d8a5eb4d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Nov 2025 16:19:36 +0000 Subject: [PATCH 122/127] Bump the github-actions group with 3 updates Bumps the github-actions group with 3 updates: [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv), [actions/upload-artifact](https://github.com/actions/upload-artifact) and [actions/download-artifact](https://github.com/actions/download-artifact). Updates `astral-sh/setup-uv` from 7.0.0 to 7.1.2 - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](https://github.com/astral-sh/setup-uv/compare/eb1897b8dc4b5d5bfe39a428a8f2304605e0983c...85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41) Updates `actions/upload-artifact` from 4.6.2 to 5.0.0 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/ea165f8d65b6e75b540449e92b4886f43607fa02...330a01c490aca151604b8cf639adc76d48f6c5d4) Updates `actions/download-artifact` from 5.0.0 to 6.0.0 - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/634f93cb2916e3fdff6788551b99b062d0335ce0...018cc2cf5baa6db3ef3c5f8a56943fffe632ef53) --- updated-dependencies: - dependency-name: astral-sh/setup-uv dependency-version: 7.1.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: actions/upload-artifact dependency-version: 5.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/download-artifact dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e85906a23..ffa24aca7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,7 @@ jobs: fetch-depth: 0 persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@eb1897b8dc4b5d5bfe39a428a8f2304605e0983c # 7.0.0 + uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # 7.1.2 - name: Install tox run: uv tool install tox --with tox-uv - name: Setup SocketCAN @@ -84,7 +84,7 @@ jobs: fetch-depth: 0 persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@eb1897b8dc4b5d5bfe39a428a8f2304605e0983c # 7.0.0 + uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # 7.1.2 - name: Install tox run: uv tool install tox --with tox-uv - name: Run linters @@ -102,7 +102,7 @@ jobs: fetch-depth: 0 persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@eb1897b8dc4b5d5bfe39a428a8f2304605e0983c # 7.0.0 + uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # 7.1.2 - name: Install tox run: uv tool install tox --with tox-uv - name: Build documentation @@ -118,13 +118,13 @@ jobs: fetch-depth: 0 persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@eb1897b8dc4b5d5bfe39a428a8f2304605e0983c # 7.0.0 + uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # 7.1.2 - name: Build wheel and sdist run: uv build - name: Check build artifacts run: uvx twine check --strict dist/* - name: Save artifacts - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # 4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # 5.0.0 with: name: release path: ./dist @@ -140,7 +140,7 @@ jobs: # upload to PyPI only on release if: github.event.release && github.event.action == 'published' steps: - - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # 5.0.0 + - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # 6.0.0 with: path: dist merge-multiple: true From 58860affe829f4b9979b33f9ff5bd1c702ba69f4 Mon Sep 17 00:00:00 2001 From: Ben Gardiner Date: Sat, 29 Nov 2025 23:21:41 -0500 Subject: [PATCH 123/127] add RP1210 python-can driver --- doc/plugin-interface.rst | 3 +++ pyproject.toml | 1 + 2 files changed, 4 insertions(+) diff --git a/doc/plugin-interface.rst b/doc/plugin-interface.rst index 2f295b678..a18e27daf 100644 --- a/doc/plugin-interface.rst +++ b/doc/plugin-interface.rst @@ -81,6 +81,8 @@ The table below lists interface drivers that can be added by installing addition +----------------------------+----------------------------------------------------------+ | `python-can-coe`_ | A CAN-over-Ethernet interface for Technische Alternative | +----------------------------+----------------------------------------------------------+ +| `RP1210`_ | CAN channels in RP1210 Vehicle Diagnostic Adapters | ++----------------------------+----------------------------------------------------------+ .. _python-can-canine: https://github.com/tinymovr/python-can-canine .. _python-can-cvector: https://github.com/zariiii9003/python-can-cvector @@ -90,4 +92,5 @@ The table below lists interface drivers that can be added by installing addition .. _python-can-cando: https://github.com/belliriccardo/python-can-cando .. _python-can-candle: https://github.com/BIRLab/python-can-candle .. _python-can-coe: https://c0d3.sh/smarthome/python-can-coe +.. _RP1210: https://github.com/dfieschko/RP1210 diff --git a/pyproject.toml b/pyproject.toml index eb823bd64..90412b95f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,6 +72,7 @@ sontheim = ["python-can-sontheim>=0.1.2"] canine = ["python-can-canine>=0.2.2"] zlgcan = ["zlgcan"] candle = ["python-can-candle>=1.2.2"] +rp1210 = ["rp1210>=1.0.1"] viewer = [ "windows-curses; platform_system == 'Windows' and platform_python_implementation=='CPython'" ] From 2630fb3eba9e756351406acfa81e1e7a4687d3d3 Mon Sep 17 00:00:00 2001 From: Ged Lex <79421501+Gedlex@users.noreply.github.com> Date: Tue, 2 Dec 2025 20:30:20 +0100 Subject: [PATCH 124/127] ASCReader: Improve datetime parsing and support double-defined AM/PM cases (#2009) * ASCReader: Improve datetime parsing and support double-defined AM/PM cases * Fixed code formatting and add a news fragment for PR 2009 * Updated formatting of logformats_test.py * Updated test for cross-platform compatibility and (hopefully) fixed formatting for good now * Corrected micro- to milliseconds --- can/io/asc.py | 49 +++++++++++++++++++-------------- doc/changelog.d/2009.changed.md | 1 + test/logformats_test.py | 34 +++++++++++++++++++++++ 3 files changed, 64 insertions(+), 20 deletions(-) create mode 100644 doc/changelog.d/2009.changed.md diff --git a/can/io/asc.py b/can/io/asc.py index 2c80458c4..fcf8fc5e4 100644 --- a/can/io/asc.py +++ b/can/io/asc.py @@ -116,41 +116,50 @@ def _extract_header(self) -> None: @staticmethod def _datetime_to_timestamp(datetime_string: str) -> float: - # ugly locale independent solution month_map = { - "Jan": 1, - "Feb": 2, - "Mar": 3, - "Apr": 4, - "May": 5, - "Jun": 6, - "Jul": 7, - "Aug": 8, - "Sep": 9, - "Oct": 10, - "Nov": 11, - "Dec": 12, - "Mär": 3, - "Mai": 5, - "Okt": 10, - "Dez": 12, + "jan": 1, + "feb": 2, + "mar": 3, + "apr": 4, + "may": 5, + "jun": 6, + "jul": 7, + "aug": 8, + "sep": 9, + "oct": 10, + "nov": 11, + "dec": 12, + "mär": 3, + "mai": 5, + "okt": 10, + "dez": 12, } - for name, number in month_map.items(): - datetime_string = datetime_string.replace(name, str(number).zfill(2)) datetime_formats = ( "%m %d %I:%M:%S.%f %p %Y", "%m %d %I:%M:%S %p %Y", "%m %d %H:%M:%S.%f %Y", "%m %d %H:%M:%S %Y", + "%m %d %H:%M:%S.%f %p %Y", + "%m %d %H:%M:%S %p %Y", ) + + datetime_string_parts = datetime_string.split(" ", 1) + month = datetime_string_parts[0].strip().lower() + + try: + datetime_string_parts[0] = f"{month_map[month]:02d}" + except KeyError: + raise ValueError(f"Unsupported month abbreviation: {month}") from None + datetime_string = " ".join(datetime_string_parts) + for format_str in datetime_formats: try: return datetime.strptime(datetime_string, format_str).timestamp() except ValueError: continue - raise ValueError(f"Incompatible datetime string {datetime_string}") + raise ValueError(f"Unsupported datetime format: '{datetime_string}'") def _extract_can_id(self, str_can_id: str, msg_kwargs: dict[str, Any]) -> None: if str_can_id[-1:].lower() == "x": diff --git a/doc/changelog.d/2009.changed.md b/doc/changelog.d/2009.changed.md new file mode 100644 index 000000000..6e68198a1 --- /dev/null +++ b/doc/changelog.d/2009.changed.md @@ -0,0 +1 @@ +Improved datetime parsing and added support for “double-defined” datetime strings (such as, e.g., `"30 15:06:13.191 pm 2017"`) for ASCReader class. \ No newline at end of file diff --git a/test/logformats_test.py b/test/logformats_test.py index f4bd1191f..f8a8de91d 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -680,6 +680,40 @@ def test_write(self): self.assertEqual(expected_file.read_text(), actual_file.read_text()) + @parameterized.expand( + [ + ( + "May 27 04:09:35.000 pm 2014", + datetime(2014, 5, 27, 16, 9, 35, 0).timestamp(), + ), + ( + "Mai 27 04:09:35.000 pm 2014", + datetime(2014, 5, 27, 16, 9, 35, 0).timestamp(), + ), + ( + "Apr 28 10:44:52.480 2022", + datetime(2022, 4, 28, 10, 44, 52, 480000).timestamp(), + ), + ( + "Sep 30 15:06:13.191 2017", + datetime(2017, 9, 30, 15, 6, 13, 191000).timestamp(), + ), + ( + "Sep 30 15:06:13.191 pm 2017", + datetime(2017, 9, 30, 15, 6, 13, 191000).timestamp(), + ), + ( + "Sep 30 15:06:13.191 am 2017", + datetime(2017, 9, 30, 15, 6, 13, 191000).timestamp(), + ), + ] + ) + def test_datetime_to_timestamp( + self, datetime_string: str, expected_timestamp: float + ): + timestamp = can.ASCReader._datetime_to_timestamp(datetime_string) + self.assertAlmostEqual(timestamp, expected_timestamp) + class TestBlfFileFormat(ReaderWriterTest): """Tests can.BLFWriter and can.BLFReader. From d48dcb8c7ee80843b074341af8f3f17f341e0d9a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 18:57:16 +0000 Subject: [PATCH 125/127] Bump the dev-deps group with 2 updates Updates the requirements on [mypy](https://github.com/python/mypy) and [coverage](https://github.com/coveragepy/coveragepy) to permit the latest version. Updates `mypy` to 1.19.0 - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.18.1...v1.19.0) Updates `coverage` to 7.12.0 - [Release notes](https://github.com/coveragepy/coveragepy/releases) - [Changelog](https://github.com/coveragepy/coveragepy/blob/main/CHANGES.rst) - [Commits](https://github.com/coveragepy/coveragepy/compare/7.11.0...7.12.0) --- updated-dependencies: - dependency-name: mypy dependency-version: 1.19.0 dependency-type: direct:development dependency-group: dev-deps - dependency-name: coverage dependency-version: 7.12.0 dependency-type: direct:development dependency-group: dev-deps ... Signed-off-by: dependabot[bot] --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 90412b95f..740d02a96 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,7 +91,7 @@ lint = [ "pylint==4.0.*", "ruff==0.14.*", "black==25.11.*", - "mypy==1.18.*", + "mypy==1.19.*", ] test = [ "pytest==9.0.*", @@ -99,7 +99,7 @@ test = [ "pytest-modern==0.7.*;platform_system!='Windows'", "coveralls==4.0.*", "pytest-cov==7.0.*", - "coverage==7.11.*", + "coverage==7.12.*", "hypothesis==6.*", "parameterized==0.9.*", ] From c8bb0f8f7cbf65317fcb4b4623264d6bba1e2a5a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 18:08:00 +0000 Subject: [PATCH 126/127] Bump the github-actions group with 3 updates Bumps the github-actions group with 3 updates: [actions/checkout](https://github.com/actions/checkout), [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) and [coverallsapp/github-action](https://github.com/coverallsapp/github-action). Updates `actions/checkout` from 5.0.0 to 6.0.0 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/08c6903cd8c0fde910a37f88322edcfb5dd907a8...1af3b93b6815bc44a9784bd300feb67ff0d1eeb3) Updates `astral-sh/setup-uv` from 7.1.2 to 7.1.4 - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](https://github.com/astral-sh/setup-uv/compare/85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41...1e862dfacbd1d6d858c55d9b792c756523627244) Updates `coverallsapp/github-action` from 2.3.6 to 2.3.7 - [Release notes](https://github.com/coverallsapp/github-action/releases) - [Commits](https://github.com/coverallsapp/github-action/compare/648a8eb78e6d50909eff900e4ec85cab4524a45b...5cbfd81b66ca5d10c19b062c04de0199c215fb6e) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: astral-sh/setup-uv dependency-version: 7.1.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: coverallsapp/github-action dependency-version: 2.3.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ffa24aca7..e340b2508 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,12 +33,12 @@ jobs: ] fail-fast: false steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0 with: fetch-depth: 0 persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # 7.1.2 + uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # 7.1.4 - name: Install tox run: uv tool install tox --with tox-uv - name: Setup SocketCAN @@ -55,7 +55,7 @@ jobs: # See: https://github.com/pypy/pypy/issues/3808 TEST_SOCKETCAN: "${{ matrix.os == 'ubuntu-latest' && ! startsWith(matrix.env, 'pypy' ) }}" - name: Coveralls Parallel - uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b # 2.3.6 + uses: coverallsapp/github-action@5cbfd81b66ca5d10c19b062c04de0199c215fb6e # 2.3.7 with: github-token: ${{ secrets.github_token }} flag-name: Unittests-${{ matrix.os }}-${{ matrix.env }} @@ -66,12 +66,12 @@ jobs: needs: test runs-on: ubuntu-latest steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0 with: fetch-depth: 0 persist-credentials: false - name: Coveralls Finished - uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b # 2.3.6 + uses: coverallsapp/github-action@5cbfd81b66ca5d10c19b062c04de0199c215fb6e # 2.3.7 with: github-token: ${{ secrets.github_token }} parallel-finished: true @@ -79,12 +79,12 @@ jobs: static-code-analysis: runs-on: ubuntu-latest steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0 with: fetch-depth: 0 persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # 7.1.2 + uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # 7.1.4 - name: Install tox run: uv tool install tox --with tox-uv - name: Run linters @@ -97,12 +97,12 @@ jobs: docs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0 with: fetch-depth: 0 persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # 7.1.2 + uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # 7.1.4 - name: Install tox run: uv tool install tox --with tox-uv - name: Build documentation @@ -113,12 +113,12 @@ jobs: name: Packaging runs-on: ubuntu-latest steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0 with: fetch-depth: 0 persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # 7.1.2 + uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # 7.1.4 - name: Build wheel and sdist run: uv build - name: Check build artifacts From b57bc514a64e61ff1579a7d456310ef2bc30c80d Mon Sep 17 00:00:00 2001 From: Gao Yichuan Date: Sat, 6 Dec 2025 19:22:06 +0800 Subject: [PATCH 127/127] Add python-can-damiao plugin support (#2014) Add python-can-damiao as an optional dependency plugin, providing support for Damiao USB-CAN adapters. The plugin enables CAN communication through Damiao USB-CAN adapters with a simple interface following the python-can plugin API. Tested with: - Damiao USB-CAN adapter - DM4310 motor Repository: https://github.com/gaoyichuan/python-can-damiao --- doc/plugin-interface.rst | 3 +++ pyproject.toml | 1 + 2 files changed, 4 insertions(+) diff --git a/doc/plugin-interface.rst b/doc/plugin-interface.rst index a18e27daf..612148033 100644 --- a/doc/plugin-interface.rst +++ b/doc/plugin-interface.rst @@ -83,6 +83,8 @@ The table below lists interface drivers that can be added by installing addition +----------------------------+----------------------------------------------------------+ | `RP1210`_ | CAN channels in RP1210 Vehicle Diagnostic Adapters | +----------------------------+----------------------------------------------------------+ +| `python-can-damiao`_ | Interface for Damiao USB-CAN adapters | ++----------------------------+----------------------------------------------------------+ .. _python-can-canine: https://github.com/tinymovr/python-can-canine .. _python-can-cvector: https://github.com/zariiii9003/python-can-cvector @@ -93,4 +95,5 @@ The table below lists interface drivers that can be added by installing addition .. _python-can-candle: https://github.com/BIRLab/python-can-candle .. _python-can-coe: https://c0d3.sh/smarthome/python-can-coe .. _RP1210: https://github.com/dfieschko/RP1210 +.. _python-can-damiao: https://github.com/gaoyichuan/python-can-damiao diff --git a/pyproject.toml b/pyproject.toml index 740d02a96..94bf60823 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,6 +73,7 @@ canine = ["python-can-canine>=0.2.2"] zlgcan = ["zlgcan"] candle = ["python-can-candle>=1.2.2"] rp1210 = ["rp1210>=1.0.1"] +damiao = ["python-can-damiao"] viewer = [ "windows-curses; platform_system == 'Windows' and platform_python_implementation=='CPython'" ]