From b40349f07e6640432a567386b40ebb2cd21b29f7 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 10 Aug 2024 12:10:58 +0200 Subject: [PATCH 01/11] update linters --- can/notifier.py | 19 ++++++++----------- pyproject.toml | 6 +++--- 2 files changed, 11 insertions(+), 14 deletions(-) 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..446b071ee 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.4.8", - "black==24.4.*", - "mypy==1.10.*", + "ruff==0.5.7", + "black==24.8.*", + "mypy==1.11.*", ] seeedstudio = ["pyserial>=3.0"] serial = ["pyserial~=3.0"] From 558ffc7209a8fa324d3b583c6ff20d294a117996 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 10 Aug 2024 12:16:16 +0200 Subject: [PATCH 02/11] test python 3.13 --- .github/workflows/ci.yml | 8 ++------ tox.ini | 2 +- 2 files changed, 3 insertions(+), 7 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/tox.ini b/tox.ini index 477b1d4fc..703af5f75 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,7 @@ 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" commands = pytest {posargs} From 123207ef8173ab8379f562186c21af34de1643f3 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 10 Aug 2024 14:10:23 +0200 Subject: [PATCH 03/11] make pywin32 optional, refactor broadcastmanager.py --- can/broadcastmanager.py | 123 +++++++++++++++------- can/interfaces/usb2can/serial_selector.py | 4 +- pyproject.toml | 3 +- tox.ini | 3 +- 4 files changed, 91 insertions(+), 42 deletions(-) diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index a610b7a8a..00420b9e1 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -10,7 +10,17 @@ 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 +29,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 win32event # pylint: disable=import-outside-toplevel + + 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 +303,26 @@ 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): + 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 +331,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,9 +340,9 @@ 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: @@ -319,16 +367,13 @@ def _run(self) -> None: 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/pyproject.toml b/pyproject.toml index 446b071ee..e9fbbbaa2 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" } @@ -65,6 +64,7 @@ lint = [ "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 +171,7 @@ known-first-party = ["can"] [tool.pylint] disable = [ + "c-extension-no-member", "cyclic-import", "duplicate-code", "fixme", diff --git a/tox.ini b/tox.ini index 703af5f75..339bcee14 100644 --- a/tox.ini +++ b/tox.ini @@ -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.13" + 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 709c2f39a610efd350c887d127a40e9ab1874331 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 10 Aug 2024 14:14:47 +0200 Subject: [PATCH 04/11] fix pylint --- can/broadcastmanager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 00420b9e1..8e3991a1c 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 win32event # pylint: disable=import-outside-toplevel + import pywintypes # pylint: disable=import-outside-toplevel,import-error + import win32event # pylint: disable=import-outside-toplevel,import-error self.pywintypes = pywintypes self.win32event = win32event From bbde36af83d67c429ca2e500cc2a9a7c39e1daa6 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 10 Aug 2024 14:18:36 +0200 Subject: [PATCH 05/11] update pytest to fix python3.12 CI --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 339bcee14..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 From 456cb5640472ab696d82d5ec8167bd8fa43a728a Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 10 Aug 2024 14:28:06 +0200 Subject: [PATCH 06/11] fix test --- test/simplecyclic_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py index 21e88e9f0..03534d616 100644 --- a/test/simplecyclic_test.py +++ b/test/simplecyclic_test.py @@ -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 From 9f22e92fbd0102ea1bef3452f2377a9c9f2c38e8 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 10 Aug 2024 14:38:06 +0200 Subject: [PATCH 07/11] fix deprecation warning --- can/io/trc.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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) From 566fa3252cebc577f6ad19a193cb8113d7f15113 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 10 Aug 2024 14:51:42 +0200 Subject: [PATCH 08/11] make _Pywin32Event private --- can/broadcastmanager.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 8e3991a1c..8fd03b0e9 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -33,7 +33,7 @@ NANOSECONDS_IN_SECOND: Final[int] = 1_000_000_000 -class Pywin32Event: +class _Pywin32Event: handle: int @@ -45,7 +45,7 @@ def __init__(self) -> None: self.pywintypes = pywintypes self.win32event = win32event - def create_timer(self) -> Pywin32Event: + def create_timer(self) -> _Pywin32Event: try: event = self.win32event.CreateWaitableTimerEx( None, @@ -60,18 +60,18 @@ 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: + 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: + def stop_timer(self, event: _Pywin32Event) -> None: self.win32event.SetWaitableTimer(event.handle, 0, 0, None, None, False) - def wait_0(self, event: Pywin32Event) -> None: + def wait_0(self, event: _Pywin32Event) -> None: self.win32event.WaitForSingleObject(event.handle, 0) - def wait_inf(self, event: Pywin32Event) -> None: + def wait_inf(self, event: _Pywin32Event) -> None: self.win32event.WaitForSingleObject( event.handle, self.win32event.INFINITE, @@ -305,7 +305,7 @@ def __init__( self.period_ms = int(round(period * 1000, 0)) - self.event: Optional[Pywin32Event] = None + self.event: Optional[_Pywin32Event] = None if PYWIN32: self.event = PYWIN32.create_timer() elif sys.platform == "win32" and sys.version_info < (3, 11): From 736d30c0fa889af11a7cd316dcead77e7a8d024e Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 10 Aug 2024 15:36:13 +0200 Subject: [PATCH 09/11] try to fix PyPy --- can/broadcastmanager.py | 7 ++++++- test/network_test.py | 2 +- test/simplecyclic_test.py | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 8fd03b0e9..e41bdb785 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -7,6 +7,7 @@ import abc import logging +import platform import sys import threading import time @@ -308,7 +309,11 @@ def __init__( self.event: Optional[_Pywin32Event] = None if PYWIN32: self.event = PYWIN32.create_timer() - elif sys.platform == "win32" and sys.version_info < (3, 11): + 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.", 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 03534d616..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] From 4bd088dffe981f746db83f46ad476028481d5937 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 10 Aug 2024 15:52:46 +0200 Subject: [PATCH 10/11] add classifier for 3.13 --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index e9fbbbaa2..8b76bc5e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,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", From bb5b8921771c0298830c15c3a47426ef403b8040 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sat, 10 Aug 2024 16:09:47 +0200 Subject: [PATCH 11/11] reduce scope of send_lock context manager --- can/broadcastmanager.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index e41bdb785..6ca9c61b3 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -353,24 +353,24 @@ def _run(self) -> None: 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) - - # 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 + 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` returns False + if not self.on_error(exc): + self.stop() + break if not self.event: msg_due_time_ns += self.period_ns