diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed757acd..abf9001e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: with: CODE_FOLDER: bellows CACHE_VERSION: 2 - PYTHON_VERSION_DEFAULT: 3.9.15 + PYTHON_VERSION_DEFAULT: 3.11.0 PRE_COMMIT_CACHE_PATH: ~/.cache/pre-commit MINIMUM_COVERAGE_PERCENTAGE: 99 secrets: diff --git a/bellows/ash.py b/bellows/ash.py index 0f3e0133..7ba8cd14 100644 --- a/bellows/ash.py +++ b/bellows/ash.py @@ -2,6 +2,7 @@ import abc import asyncio +from asyncio import timeout as asyncio_timeout import binascii from collections.abc import Coroutine import contextlib @@ -12,11 +13,6 @@ import time import typing -if sys.version_info[:2] < (3, 11): - from async_timeout import timeout as asyncio_timeout # pragma: no cover -else: - from asyncio import timeout as asyncio_timeout # pragma: no cover - from zigpy.types import BaseDataclassMixin import bellows.types as t @@ -675,7 +671,7 @@ async def _send_data_frame(self, frame: AshFrame) -> None: "NCP has entered into a failed state, not retrying" ) raise - except asyncio.TimeoutError: + except TimeoutError: _LOGGER.debug( "No ACK received in %0.2fs (attempt %d) for %r", self._t_rx_ack, diff --git a/bellows/ezsp/__init__.py b/bellows/ezsp/__init__.py index 2b7c7657..70a1f220 100644 --- a/bellows/ezsp/__init__.py +++ b/bellows/ezsp/__init__.py @@ -3,24 +3,19 @@ from __future__ import annotations import asyncio +from asyncio import timeout as asyncio_timeout import collections +from collections.abc import Callable, Generator import contextlib import dataclasses import functools import logging -import sys -from typing import Any, Callable, Generator +from typing import Any import urllib.parse -from bellows.ash import NcpFailure - -if sys.version_info[:2] < (3, 11): - from async_timeout import timeout as asyncio_timeout # pragma: no cover -else: - from asyncio import timeout as asyncio_timeout # pragma: no cover - import zigpy.config +from bellows.ash import NcpFailure import bellows.config as conf from bellows.exception import EzspError, InvalidCommandError, InvalidCommandPayload from bellows.ezsp import xncp @@ -119,7 +114,7 @@ async def startup_reset(self) -> None: try: async with asyncio_timeout(NETWORK_COORDINATOR_STARTUP_RESET_WAIT): await self._gw.wait_for_startup_reset() - except asyncio.TimeoutError: + except TimeoutError: pass else: LOGGER.debug("Received a reset on startup, not resetting again") diff --git a/bellows/ezsp/protocol.py b/bellows/ezsp/protocol.py index 886af86f..30006dc4 100644 --- a/bellows/ezsp/protocol.py +++ b/bellows/ezsp/protocol.py @@ -2,21 +2,16 @@ import abc import asyncio +from asyncio import timeout as asyncio_timeout import binascii +from collections.abc import AsyncGenerator, Callable, Iterable import functools import logging -import sys import time -from typing import TYPE_CHECKING, Any, AsyncGenerator, Callable, Iterable - -import zigpy.state - -if sys.version_info[:2] < (3, 11): - from async_timeout import timeout as asyncio_timeout # pragma: no cover -else: - from asyncio import timeout as asyncio_timeout # pragma: no cover +from typing import TYPE_CHECKING, Any from zigpy.datastructures import PriorityDynamicBoundedSemaphore +import zigpy.state from bellows.config import CONF_EZSP_POLICIES from bellows.exception import InvalidCommandError diff --git a/bellows/ezsp/v13/__init__.py b/bellows/ezsp/v13/__init__.py index 95f1d796..96137133 100644 --- a/bellows/ezsp/v13/__init__.py +++ b/bellows/ezsp/v13/__init__.py @@ -1,8 +1,8 @@ """"EZSP Protocol version 13 protocol handler.""" from __future__ import annotations +from collections.abc import AsyncGenerator, Iterable import logging -from typing import AsyncGenerator, Iterable import voluptuous as vol from zigpy.exceptions import NetworkNotFormed diff --git a/bellows/ezsp/v14/__init__.py b/bellows/ezsp/v14/__init__.py index 475c0fd4..16dbeec7 100644 --- a/bellows/ezsp/v14/__init__.py +++ b/bellows/ezsp/v14/__init__.py @@ -1,7 +1,7 @@ """"EZSP Protocol version 14 protocol handler.""" from __future__ import annotations -from typing import AsyncGenerator +from collections.abc import AsyncGenerator import voluptuous as vol from zigpy.exceptions import NetworkNotFormed diff --git a/bellows/ezsp/v4/__init__.py b/bellows/ezsp/v4/__init__.py index 534c842a..3b454ecd 100644 --- a/bellows/ezsp/v4/__init__.py +++ b/bellows/ezsp/v4/__init__.py @@ -1,9 +1,9 @@ """"EZSP Protocol version 4 command.""" from __future__ import annotations +from collections.abc import AsyncGenerator, Iterable import logging import random -from typing import AsyncGenerator, Iterable import voluptuous as vol import zigpy.state @@ -189,11 +189,11 @@ async def set_source_route(self, nwk: t.NWK, relays: list[t.NWK]) -> t.sl_Status async def read_counters(self) -> dict[t.EmberCounterType, t.uint16_t]: (res,) = await self.readCounters() - return dict(zip(t.EmberCounterType, res)) + return dict(zip(t.EmberCounterType, res, strict=False)) async def read_and_clear_counters(self) -> dict[t.EmberCounterType, t.uint16_t]: (res,) = await self.readAndClearCounters() - return dict(zip(t.EmberCounterType, res)) + return dict(zip(t.EmberCounterType, res, strict=False)) async def set_extended_timeout( self, nwk: t.NWK, ieee: t.EUI64, extended_timeout: bool = True diff --git a/bellows/ezsp/v5/__init__.py b/bellows/ezsp/v5/__init__.py index 53731e34..4f668667 100644 --- a/bellows/ezsp/v5/__init__.py +++ b/bellows/ezsp/v5/__init__.py @@ -1,8 +1,8 @@ """"EZSP Protocol version 5 protocol handler.""" from __future__ import annotations +from collections.abc import AsyncGenerator import logging -from typing import AsyncGenerator import voluptuous as vol diff --git a/bellows/ezsp/v7/__init__.py b/bellows/ezsp/v7/__init__.py index e812aba6..4938a31c 100644 --- a/bellows/ezsp/v7/__init__.py +++ b/bellows/ezsp/v7/__init__.py @@ -1,8 +1,8 @@ """"EZSP Protocol version 7 protocol handler.""" from __future__ import annotations +from collections.abc import AsyncGenerator import logging -from typing import AsyncGenerator import voluptuous diff --git a/bellows/ezsp/v8/__init__.py b/bellows/ezsp/v8/__init__.py index 6aaaf59b..b2b55dde 100644 --- a/bellows/ezsp/v8/__init__.py +++ b/bellows/ezsp/v8/__init__.py @@ -1,7 +1,6 @@ """"EZSP Protocol version 8 protocol handler.""" import asyncio import logging -from typing import Tuple import voluptuous @@ -30,7 +29,7 @@ def _ezsp_frame_tx(self, name: str) -> bytes: hdr = [self._seq, 0x00, 0x01] return bytes(hdr) + t.uint16_t(cmd_id).serialize() - def _ezsp_frame_rx(self, data: bytes) -> Tuple[int, int, bytes]: + def _ezsp_frame_rx(self, data: bytes) -> tuple[int, int, bytes]: """Handler for received data frame.""" seq, data = data[0], data[3:] frame_id, data = t.uint16_t.deserialize(data) diff --git a/bellows/ezsp/xncp.py b/bellows/ezsp/xncp.py index 61cf6a80..ab878f4a 100644 --- a/bellows/ezsp/xncp.py +++ b/bellows/ezsp/xncp.py @@ -1,9 +1,9 @@ """Custom EZSP commands.""" from __future__ import annotations +from collections.abc import Callable import dataclasses import logging -from typing import Callable import zigpy.types as t diff --git a/bellows/thread.py b/bellows/thread.py index 6d8c1309..4311768d 100644 --- a/bellows/thread.py +++ b/bellows/thread.py @@ -2,7 +2,6 @@ from concurrent.futures import ThreadPoolExecutor import functools import logging -import sys LOGGER = logging.getLogger(__name__) @@ -35,10 +34,7 @@ async def start(self): if self.loop is not None and not self.loop.is_closed(): return - executor_opts = {"max_workers": 1} - if sys.version_info[:2] >= (3, 6): - executor_opts["thread_name_prefix"] = __name__ - executor = ThreadPoolExecutor(**executor_opts) + executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix=__name__) thread_started_future = current_loop.create_future() diff --git a/bellows/types/__init__.py b/bellows/types/__init__.py index 75d16ae4..577de976 100644 --- a/bellows/types/__init__.py +++ b/bellows/types/__init__.py @@ -13,7 +13,7 @@ def deserialize_dict(data, schema): def serialize_dict(args, kwargs, schema): params = { - **dict(zip(schema.keys(), args)), + **dict(zip(schema.keys(), args, strict=False)), **kwargs, } diff --git a/bellows/uart.py b/bellows/uart.py index e2dd3095..ba377a0f 100644 --- a/bellows/uart.py +++ b/bellows/uart.py @@ -1,11 +1,6 @@ import asyncio +from asyncio import timeout as asyncio_timeout import logging -import sys - -if sys.version_info[:2] < (3, 11): - from async_timeout import timeout as asyncio_timeout # pragma: no cover -else: - from asyncio import timeout as asyncio_timeout # pragma: no cover import zigpy.config import zigpy.serial diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index ba5dfd91..c63d7627 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -1,19 +1,13 @@ from __future__ import annotations import asyncio -from datetime import datetime, timezone +from asyncio import timeout as asyncio_timeout +from collections.abc import AsyncGenerator +from datetime import UTC, datetime +import importlib.metadata import logging import os import statistics -import sys -from typing import AsyncGenerator - -if sys.version_info[:2] < (3, 11): - from async_timeout import timeout as asyncio_timeout # pragma: no cover -else: - from asyncio import timeout as asyncio_timeout # pragma: no cover - -import importlib.metadata import zigpy.application import zigpy.config @@ -843,7 +837,7 @@ async def _packet_capture(self, channel: int): with self._ezsp.callback_for_commands( {"mfglibRxHandler"}, callback=lambda _, response: queue.put_nowait( - (datetime.now(timezone.utc), response) + (datetime.now(UTC), response) ), ): while True: @@ -1096,7 +1090,7 @@ async def _watchdog_feed(self): cnt._last_reset_value = 0 LOGGER.debug("%s", counters) - except (asyncio.TimeoutError, EzspError) as exc: + except (TimeoutError, EzspError) as exc: # TODO: converted Silvercrest gateways break without this LOGGER.warning("Watchdog heartbeat timeout: %s", repr(exc)) self._watchdog_failures += 1 diff --git a/pyproject.toml b/pyproject.toml index 44161115..29603163 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,13 +12,12 @@ authors = [ ] readme = "README.md" license = {text = "GPL-3.0"} -requires-python = ">=3.8" +requires-python = ">=3.11" dependencies = [ "click", "click-log>=0.2.1", "voluptuous", "zigpy>=0.83.0", - 'async-timeout; python_version<"3.11"', ] [tool.setuptools.packages.find] diff --git a/ruff.toml b/ruff.toml index 9c27612c..19c2a7c3 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,4 +1,4 @@ -target-version = "py38" +target-version = "py311" select = [ "B007", # Loop control variable {name} not used within loop body diff --git a/tests/test_application.py b/tests/test_application.py index 76cdb910..353b1d3a 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -544,7 +544,7 @@ def test_frame_handler_ignored(app, aps_frame): 0xFF, ), ) -def test_send_failure(app, aps, ieee, msg_type): +async def test_send_failure(app, aps, ieee, msg_type): fut = app._pending_requests[(0xBEED, 254)] = asyncio.Future() app.ezsp_callback_handler( "messageSentHandler", [msg_type, 0xBEED, aps, 254, t.EmberStatus.SUCCESS, b""] @@ -583,7 +583,7 @@ def test_send_failure_unexpected(app, aps, ieee): ) -def test_send_success(app, aps, ieee): +async def test_send_success(app, aps, ieee): fut = app._pending_requests[(0xBEED, 253)] = asyncio.Future() app.ezsp_callback_handler( "messageSentHandler", @@ -1267,7 +1267,7 @@ async def nop_mock(): raise EzspError else: return ([0] * 10,) - raise asyncio.TimeoutError + raise TimeoutError app._ezsp._protocol.getValue.return_value = [t.EmberStatus.SUCCESS, b"\xFE"] app._ezsp._protocol.nop.side_effect = nop_mock @@ -1286,7 +1286,7 @@ async def nop_mock(): await app._watchdog_feed() # The last time will throw a real error - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(TimeoutError): await app._watchdog_feed() if ezsp_version == 4: @@ -1309,7 +1309,7 @@ async def counters_mock(): raise EzspError else: return ([0, 1, 2, 3],) - raise asyncio.TimeoutError + raise TimeoutError app._ezsp._protocol.getValue = AsyncMock( return_value=[t.EmberStatus.SUCCESS, b"\xFE"] @@ -1346,7 +1346,7 @@ async def counters_mock(): raise EzspError else: return {t.EmberCounterType(i): v for i, v in enumerate([0, 1, 2, 3])} - raise asyncio.TimeoutError + raise TimeoutError app._ezsp.read_counters = AsyncMock(side_effect=counters_mock) app._ezsp.nop = AsyncMock(side_effect=EzspError) @@ -1846,7 +1846,7 @@ async def test_startup_concurrency_setting( ) async def test_energy_scanning(app, scan_results): app._ezsp.startScan = AsyncMock( - return_value=list(zip(range(11, 26 + 1), scan_results)) + return_value=list(zip(range(11, 26 + 1), scan_results, strict=True)) ) results = await app.energy_scan( diff --git a/tests/test_ash.py b/tests/test_ash.py index 062e5356..af67fa02 100644 --- a/tests/test_ash.py +++ b/tests/test_ash.py @@ -70,7 +70,7 @@ def rst_frame_received(self, frame: ash.RstFrame) -> None: async def _send_data_frame(self, frame: ash.AshFrame) -> None: try: return await super()._send_data_frame(frame) - except asyncio.TimeoutError: + except TimeoutError: self._enter_ncp_error_state( t.NcpResetCode.ERROR_EXCEEDED_MAXIMUM_ACK_TIMEOUT_COUNT ) @@ -549,7 +549,7 @@ async def test_ash_end_to_end(transport_cls: type[FakeTransport]) -> None: send_task = asyncio.create_task(host.send_data(b"host failure")) await asyncio.sleep(host._t_rx_ack * 15) - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(TimeoutError): await send_task ncp_ezsp.data_received.reset_mock() @@ -575,7 +575,7 @@ async def test_ash_end_to_end(transport_cls: type[FakeTransport]) -> None: send_task = asyncio.create_task(ncp.send_data(b"ncp failure")) await asyncio.sleep(ncp._t_rx_ack * 15) - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(TimeoutError): await send_task assert ( diff --git a/tests/test_ezsp.py b/tests/test_ezsp.py index 440e1459..4ba29f93 100644 --- a/tests/test_ezsp.py +++ b/tests/test_ezsp.py @@ -1,9 +1,10 @@ from __future__ import annotations import asyncio +from asyncio import timeout as asyncio_timeout import functools import logging -import sys +from unittest.mock import ANY, AsyncMock, MagicMock, call, patch import pytest import zigpy.config @@ -12,16 +13,8 @@ from bellows.ash import NcpFailure from bellows.exception import EzspError, InvalidCommandError, InvalidCommandPayload from bellows.ezsp import EZSP, EZSP_LATEST, xncp -import bellows.types as t - -if sys.version_info[:2] < (3, 11): - from async_timeout import timeout as asyncio_timeout # pragma: no cover -else: - from asyncio import timeout as asyncio_timeout # pragma: no cover - -from unittest.mock import ANY, AsyncMock, MagicMock, call, patch - from bellows.ezsp.v9.commands import GetTokenDataRsp +import bellows.types as t DEVICE_CONFIG = { zigpy.config.CONF_DEVICE_PATH: "/dev/null", @@ -447,7 +440,7 @@ async def test_leave_network_no_stack_status(ezsp_f): with patch.object(ezsp_f, "_command", new_callable=AsyncMock) as cmd_mock: cmd_mock.return_value = [t.EmberStatus.SUCCESS] - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(TimeoutError): await ezsp_f.leaveNetwork(timeout=0.01) @@ -779,7 +772,7 @@ async def test_wait_for_stack_status(ezsp_f): # Cancellation clears handlers with ezsp_f.wait_for_stack_status(t.sl_Status.NETWORK_DOWN) as stack_status: - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(TimeoutError): async with asyncio_timeout(0.1): assert ezsp_f._stack_status_listeners[t.sl_Status.NETWORK_DOWN] await stack_status diff --git a/tests/test_thread.py b/tests/test_thread.py index a7c37723..72efa701 100644 --- a/tests/test_thread.py +++ b/tests/test_thread.py @@ -1,13 +1,8 @@ import asyncio -import sys +from asyncio import timeout as asyncio_timeout import threading from unittest import mock -if sys.version_info[:2] < (3, 11): - from async_timeout import timeout as asyncio_timeout # pragma: no cover -else: - from asyncio import timeout as asyncio_timeout # pragma: no cover - import pytest from bellows.thread import EventLoopThread, ThreadsafeProxy @@ -80,9 +75,8 @@ async def test_coroutine(): async def test_thread_double_start(thread): previous_loop = thread.loop await thread.start() - if sys.version_info[:2] >= (3, 6): - threads = [t for t in threading.enumerate() if "bellows" in t.name] - assert len(threads) == 1 + threads = [t for t in threading.enumerate() if "bellows" in t.name] + assert len(threads) == 1 assert thread.loop is previous_loop diff --git a/tests/test_uart.py b/tests/test_uart.py index 26ccb9ef..6908c539 100644 --- a/tests/test_uart.py +++ b/tests/test_uart.py @@ -152,7 +152,7 @@ def test_close(gw): async def test_reset_timeout(gw, monkeypatch): monkeypatch.setattr(uart, "RESET_TIMEOUT", 0.1) - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(TimeoutError): await gw.reset() @@ -223,7 +223,7 @@ async def test_wait_for_startup_reset(gw): async def test_wait_for_startup_reset_failure(gw): assert gw._startup_reset_future is None - with pytest.raises(asyncio.TimeoutError): + with pytest.raises(TimeoutError): await asyncio.wait_for(gw.wait_for_startup_reset(), 0.01) assert gw._startup_reset_future is None