Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test on Python 3.13 #1833

Merged
merged 11 commits into from
Aug 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ jobs:
"3.10",
"3.11",
"3.12",
"3.13",
"pypy-3.8",
"pypy-3.9",
]
Expand All @@ -34,19 +35,14 @@ 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
- 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
Expand Down
158 changes: 104 additions & 54 deletions can/broadcastmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -281,54 +336,49 @@ 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()

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()
Expand Down
4 changes: 3 additions & 1 deletion can/interfaces/usb2can/serial_selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
6 changes: 4 additions & 2 deletions can/io/trc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)}",
Expand Down Expand Up @@ -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)
Expand Down
19 changes: 8 additions & 11 deletions can/notifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
10 changes: 6 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand All @@ -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",
Expand All @@ -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"]
Expand Down Expand Up @@ -171,6 +172,7 @@ known-first-party = ["can"]

[tool.pylint]
disable = [
"c-extension-no-member",
"cyclic-import",
"duplicate-code",
"fixme",
Expand Down
2 changes: 1 addition & 1 deletion test/network_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions test/simplecyclic_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ isolated_build = true

[testenv]
deps =
pytest==7.3.*
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.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}
Expand Down
Loading