From 4b5c69405741f1589c3acd997eb258e649c3b59e Mon Sep 17 00:00:00 2001 From: zigpy-review-bot <286747149+zigpy-review-bot@users.noreply.github.com> Date: Mon, 28 Sep 2026 02:55:37 +0200 Subject: [PATCH 1/2] Replace black, isort, flake8 and pyupgrade with ruff --- .github/workflows/matchers/flake8.json | 30 -------------- .pre-commit-config.yaml | 54 ++++---------------------- pyproject.toml | 49 +++++++++++++++-------- requirements_test.txt | 6 +-- 4 files changed, 41 insertions(+), 98 deletions(-) delete mode 100644 .github/workflows/matchers/flake8.json diff --git a/.github/workflows/matchers/flake8.json b/.github/workflows/matchers/flake8.json deleted file mode 100644 index e059a1c..0000000 --- a/.github/workflows/matchers/flake8.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "problemMatcher": [ - { - "owner": "flake8-error", - "severity": "error", - "pattern": [ - { - "regexp": "^(.*):(\\d+):(\\d+):\\s([EF]\\d{3}\\s.*)$", - "file": 1, - "line": 2, - "column": 3, - "message": 4 - } - ] - }, - { - "owner": "flake8-warning", - "severity": "warning", - "pattern": [ - { - "regexp": "^(.*):(\\d+):(\\d+):\\s([CDNW]\\d{3}\\s.*)$", - "file": 1, - "line": 2, - "column": 3, - "message": 4 - } - ] - } - ] -} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 03ab0c2..56bdcd4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,46 +1,13 @@ repos: - - repo: https://github.com/asottile/pyupgrade - rev: v3.15.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.16.9 hooks: - - id: pyupgrade - args: [--py311-plus] - - - repo: https://github.com/PyCQA/autoflake - rev: v2.2.1 - hooks: - - id: autoflake - - - repo: https://github.com/psf/black - rev: 23.9.1 - hooks: - - id: black - args: - - --quiet - - --safe - - - repo: https://github.com/PyCQA/flake8 - rev: 6.1.0 - hooks: - - id: flake8 - additional_dependencies: - - flake8-docstrings==1.7.0 - - pydocstyle==6.3.0 - - Flake8-pyproject==1.2.3 - - flake8-bugbear==23.9.16 - - flake8-comprehensions==3.14.0 - - flake8_2020==1.8.1 - - mccabe==0.7.0 - - pycodestyle==2.11.0 - - pyflakes==3.1.0 - - flake8-async==22.11.14 - - - repo: https://github.com/PyCQA/isort - rev: 5.12.0 - hooks: - - id: isort + - id: ruff-check + args: ["--fix", "--exit-non-zero-on-fix"] + - id: ruff-format - repo: https://github.com/codespell-project/codespell - rev: v2.2.6 + rev: v2.4.3 hooks: - id: codespell args: @@ -49,15 +16,8 @@ repos: - --quiet-level=2 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.6.0 + rev: v2.3.1 hooks: - id: mypy additional_dependencies: - zigpy - - - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.291 - hooks: - - id: ruff - args: - - --fix diff --git a/pyproject.toml b/pyproject.toml index 752fbd5..9894f39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,13 +29,39 @@ testing = [ [tool.setuptools-git-versioning] enabled = true -[tool.isort] -profile = "black" -# will group `import x` and `from x import` of the same module. -force_sort_within_sections = true -known_first_party = ["zigpy_xbee", "tests"] -forced_separate = "tests" -combine_as_imports = true +[tool.ruff] +target-version = "py311" +line-length = 88 + +[tool.ruff.lint] +select = [ + "ASYNC", # flake8-async + "B", # flake8-bugbear + "C", # complexity + "D", # pydocstyle + "E", # pycodestyle + "F", # pyflakes/autoflake + "I", # isort + "SIM", # flake8-simplify + "UP", # pyupgrade + "W", # pycodestyle + "YTT", # flake8-2020 +] +ignore = [ + "D202", # No blank lines allowed after function docstring +] + +[tool.ruff.lint.mccabe] +max-complexity = 25 + +[tool.ruff.lint.pydocstyle] +convention = "pep257" + +[tool.ruff.lint.isort] +force-sort-within-sections = true +known-first-party = ["zigpy_xbee", "tests"] +forced-separate = ["tests"] +combine-as-imports = true [tool.mypy] ignore_errors = true @@ -43,12 +69,3 @@ ignore_errors = true [tool.pytest.ini_options] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" - -[tool.flake8] -exclude = [".venv", ".git", ".tox", "docs", "venv", "bin", "lib", "deps", "build"] -# To work with Black -max-line-length = 88 -# W503: Line break occurred before a binary operator -# E203: Whitespace before ':' -# D202 No blank lines allowed after function docstring -ignore = ["W503", "E203", "D202"] diff --git a/requirements_test.txt b/requirements_test.txt index a664180..e27e51e 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,8 +1,5 @@ # Test dependencies. -isort -black -flake8 codecov colorlog codespell @@ -15,5 +12,4 @@ pytest-timeout pytest-asyncio>=0.17 pytest>=7.1.3 zigpy>=2.0.0 -ruff>=0.0.291 -Flake8-pyproject +ruff==0.16.9 From 56353b03470b8ce43550e19d7fadd23d2cbb419d Mon Sep 17 00:00:00 2001 From: zigpy-review-bot <286747149+zigpy-review-bot@users.noreply.github.com> Date: Mon, 28 Sep 2026 03:00:19 +0200 Subject: [PATCH 2/2] Apply ruff fixes Autofixes: C420 (dict.fromkeys), SIM910 (dict.get default), SIM103, SIM105 (contextlib.suppress) and lowercase hex escapes from ruff format. By hand: exception chaining for re-raised errors, explicit zip(strict=...) (strict=False where the lengths may differ, keeping the current behaviour), a merged nested `with` statement, and a test that evaluated a property without asserting on it. --- tests/test_api.py | 16 +++++++--------- tests/test_application.py | 10 +++++----- tests/test_types.py | 4 ++-- tests/test_uart.py | 25 +++++++++++++------------ zigpy_xbee/api.py | 4 +++- zigpy_xbee/types.py | 2 +- zigpy_xbee/uart.py | 4 ++-- zigpy_xbee/zigbee/application.py | 18 ++++++++---------- 8 files changed, 41 insertions(+), 42 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index a366a81..5c15783 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -53,9 +53,11 @@ async def test_connect_initial_timeout_failure(): api._at_command = mock.AsyncMock(side_effect=asyncio.TimeoutError) api.init_api_mode = mock.AsyncMock(return_value=False) - with mock.patch("zigpy_xbee.uart.connect") as mock_connect: - with pytest.raises(zigpy.exceptions.APIException): - await api.connect() + with ( + mock.patch("zigpy_xbee.uart.connect") as mock_connect, + pytest.raises(zigpy.exceptions.APIException), + ): + await api.connect() assert mock_connect.return_value.disconnect.mock_calls == [mock.call()] @@ -565,9 +567,7 @@ async def test_api_mode_at_commands(api): assert res is True async def mock_at_cmd(cmd): - if cmd == "ATWR\r": - return False - return True + return cmd != "ATWR\r" api.command_mode_at_cmd = mock_at_cmd res = await api.api_mode_at_commands(57600) @@ -590,9 +590,7 @@ async def test_init_api_mode(api, monkeypatch): assert api.enter_at_command_mode.call_count == 10 async def enter_at_mode(): - if api._uart.baudrate == 9600: - return True - return False + return api._uart.baudrate == 9600 api._uart.baudrate = 57600 api.enter_at_command_mode = mock.MagicMock(side_effect=enter_at_mode) diff --git a/tests/test_application.py b/tests/test_application.py index 3bf58f4..f83872f 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -369,7 +369,7 @@ def _at_command_mock(cmd, *args): "SL": 0x04030201, "ZS": zs, "VR": 0x1234, - }.get(cmd, None) + }.get(cmd) def init_api_mode_mock(): nonlocal api_mode @@ -492,7 +492,7 @@ async def test_permit_with_link_key(app): app._api._command = mock.AsyncMock(return_value=xbee_t.TXStatus.SUCCESS) app._api._at_command = mock.AsyncMock(return_value="OK") node = t.EUI64(b"\x01\x02\x03\x04\x05\x06\x07\x08") - link_key = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F" + link_key = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" time_s = 500 await app.permit_with_link_key(node=node, link_key=link_key, time_s=time_s) app._api._at_command.assert_called_once_with("KT", time_s) @@ -735,7 +735,7 @@ async def test_move_network_to_channel(app): async def test_energy_scan(app): """Test channel energy scan.""" - rssi = b"\x0A\x0F\x14\x19\x1E\x23\x28\x2D\x32\x37\x3C\x41\x46\x4B\x50\x55" + rssi = b"\x0a\x0f\x14\x19\x1e\x23\x28\x2d\x32\x37\x3c\x41\x46\x4b\x50\x55" app._api._at_command = mock.AsyncMock(spec=XBee._at_command, return_value=rssi) time_s = 3 count = 3 @@ -774,7 +774,7 @@ async def test_energy_scan_legacy_module(app): channels=list(range(11, 27)), duration_exp=time_s, count=count ) app._api._at_command.assert_called_once_with("ED", bytes([time_s])) - assert energy == {c: 0 for c in range(11, 27)} + assert energy == dict.fromkeys(range(11, 27), 0) def test_neighbors_updated(app, device): @@ -819,7 +819,7 @@ def test_neighbors_updated(app, device): # Let's also include an unknown device zdo_t.Neighbor( extended_pan_id=pan_id, - ieee=t.EUI64(b"\x00\x0F\x0E\x0D\x0C\x0B\x0A\x09"), + ieee=t.EUI64(b"\x00\x0f\x0e\x0d\x0c\x0b\x0a\x09"), nwk=t.NWK(0x9999), device_type=0x2, rx_on_when_idle=0x0, diff --git a/tests/test_types.py b/tests/test_types.py index 9894184..4e85b7f 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -16,7 +16,7 @@ def test_bytes_serialize(): def test_bytes_deserialize(): """Test Bytes.deserialize().""" data, rest = xbee_t.Bytes.deserialize(0x89AB.to_bytes(3, "big")) - assert data == b"\x00\x89\xAB" + assert data == b"\x00\x89\xab" assert rest == b"" @@ -76,7 +76,7 @@ def test_nwk(): def test_eui64(): """Test EUI64 class.""" - extra = b"\xBE\xEF" + extra = b"\xbe\xef" data = b"01234567" result, rest = xbee_t.EUI64.deserialize(data + extra) diff --git a/tests/test_uart.py b/tests/test_uart.py index f844cac..06a9ee3 100644 --- a/tests/test_uart.py +++ b/tests/test_uart.py @@ -27,7 +27,8 @@ def gw(): def test_baudrate(gw): """Test setting baudrate.""" - gw.baudrate + gw._transport.serial.baudrate = 57600 + assert gw.baudrate == 57600 gw.baudrate = 19200 assert gw._transport.serial.baudrate == 19200 @@ -62,7 +63,7 @@ def test_command_mode_rsp(gw): def test_command_mode_send(gw): """Test command mode request.""" - data = b"ATAP2\x0D" + data = b"ATAP2\x0d" gw.command_mode_send(data) gw._transport.write.assert_called_once_with(data) @@ -105,7 +106,7 @@ def test_data_received_incomplete_frame(gw): def test_data_received_at_response_non_cmd_mode(gw): """Test command mode response while not in command mode.""" - data = b"OK\x0D" + data = b"OK\x0d" gw.frame_received = mock.MagicMock() gw.command_mode_rsp = mock.MagicMock() @@ -115,7 +116,7 @@ def test_data_received_at_response_non_cmd_mode(gw): def test_data_received_at_response_in_cmd_mode(gw): """Test command mode response in command mode.""" - data = b"OK\x0D" + data = b"OK\x0d" gw.frame_received = mock.MagicMock() gw.command_mode_rsp = mock.MagicMock() @@ -131,7 +132,7 @@ def test_data_received_at_response_in_cmd_mode(gw): def test_extract(gw): """Test handling extra chaining data.""" - gw._buffer = b"\x7E\x00\x02\x23\x7D\x31\xCBextra" + gw._buffer = b"\x7e\x00\x02\x23\x7d\x31\xcbextra" frame = gw._extract_frame() assert frame == b"\x23\x11" assert gw._buffer == b"extra" @@ -139,7 +140,7 @@ def test_extract(gw): def test_extract_wrong_checksum(gw): """Test API frame with wrong checksum and extra data.""" - gw._buffer = b"\x7E\x00\x02\x23\x7D\x31\xCEextra" + gw._buffer = b"\x7e\x00\x02\x23\x7d\x31\xceextra" frame = gw._extract_frame() assert frame is None assert gw._buffer == b"extra" @@ -147,7 +148,7 @@ def test_extract_wrong_checksum(gw): def test_extract_checksum_none(gw): """Test API frame with no checksum.""" - data = b"\x7E\x00\x02\x23\x7D\x31" + data = b"\x7e\x00\x02\x23\x7d\x31" gw._buffer = data gw._checksum = lambda x: None frame = gw._extract_frame() @@ -157,7 +158,7 @@ def test_extract_checksum_none(gw): def test_extract_frame_len_none(gw): """Test API frame with no length.""" - data = b"\x7E" + data = b"\x7e" gw._buffer = data frame = gw._extract_frame() assert frame is None @@ -166,7 +167,7 @@ def test_extract_frame_len_none(gw): def test_extract_frame_no_start(gw): """Test API frame without frame ID.""" - data = b"\x00\x02\x23\x7D\x31" + data = b"\x00\x02\x23\x7d\x31" gw._buffer = data frame = gw._extract_frame() assert frame is None @@ -184,7 +185,7 @@ def test_frame_received(gw): def test_send(gw): """Test data send.""" gw.send(b"\x23\x11") - data = b"\x7E\x00\x02\x23\x7D\x31\xCB" + data = b"\x7e\x00\x02\x23\x7d\x31\xcb" gw._transport.write.assert_called_once_with(data) @@ -193,7 +194,7 @@ def test_escape(gw): data = b"".join( [ a.to_bytes(1, "big") + b.to_bytes(1, "big") - for a, b in zip(gw.RESERVED, b"\x22\x33\x44\x55") + for a, b in zip(gw.RESERVED, b"\x22\x33\x44\x55", strict=True) ] ) escaped = gw._escape(data) @@ -210,7 +211,7 @@ def test_unescape(gw): chk = b"".join( [ a.to_bytes(1, "big") + b.to_bytes(1, "big") - for a, b in zip(gw.RESERVED, b"\x22\x33\x44\x55") + for a, b in zip(gw.RESERVED, b"\x22\x33\x44\x55", strict=True) ] ) unescaped, rest = gw._get_unescaped(escaped + extra, 8) diff --git a/zigpy_xbee/api.py b/zigpy_xbee/api.py index e2f881d..2052143 100644 --- a/zigpy_xbee/api.py +++ b/zigpy_xbee/api.py @@ -312,7 +312,9 @@ async def connect(self) -> None: await self._at_command("AP", 2) except TimeoutError: if not await self.init_api_mode(): - raise APIException("Failed to configure XBee for API mode") + raise APIException( + "Failed to configure XBee for API mode" + ) from None except Exception: await self.disconnect() raise diff --git a/zigpy_xbee/types.py b/zigpy_xbee/types.py index f4b6b23..9a7933f 100644 --- a/zigpy_xbee/types.py +++ b/zigpy_xbee/types.py @@ -57,7 +57,7 @@ def __call__(cls, value=None, *args, **kwargs): try: return super().__call__(cls._UNDEFINED) except AttributeError: - raise exc + raise exc from None class UndefinedEnum(enum.Enum, metaclass=UndefinedEnumMeta): diff --git a/zigpy_xbee/uart.py b/zigpy_xbee/uart.py index 98457eb..f9a7862 100644 --- a/zigpy_xbee/uart.py +++ b/zigpy_xbee/uart.py @@ -13,8 +13,8 @@ class Gateway(zigpy.serial.SerialProtocol): """Class implementing the UART protocol.""" - START = b"\x7E" - ESCAPE = b"\x7D" + START = b"\x7e" + ESCAPE = b"\x7d" XON = b"\x11" XOFF = b"\x13" diff --git a/zigpy_xbee/zigbee/application.py b/zigpy_xbee/zigbee/application.py index a068ff1..a09ca8a 100644 --- a/zigpy_xbee/zigbee/application.py +++ b/zigpy_xbee/zigbee/application.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +import contextlib from datetime import UTC, datetime import logging import math @@ -166,10 +167,8 @@ async def write_network_info(self, *, network_info, node_info): await self._api._queued_at("SP", CONF_CYCLIC_SLEEP_PERIOD) await self._api._queued_at("SN", CONF_POLL_TIMEOUT) - try: + with contextlib.suppress(InvalidCommand): await self._api._queued_at("CE", 1) - except InvalidCommand: - pass await self._api._at_command("WR") @@ -197,10 +196,11 @@ async def energy_scan( results = await self._api._at_command("ED", bytes([duration_exp])) except InvalidCommand: LOGGER.warning("Coordinator does not support energy scanning") - return {c: 0 for c in channels} + return dict.fromkeys(channels, 0) results = { - channel: -int(rssi) for channel, rssi in zip(range(11, 27), results) + channel: -int(rssi) + for channel, rssi in zip(range(11, 27), results, strict=False) } for channel, rssi in results.items(): @@ -305,10 +305,10 @@ async def send_packet(self, packet: zigpy.types.ZigbeePacket) -> None: try: v = await asyncio.wait_for(send_req, timeout=TIMEOUT_TX_STATUS) - except TimeoutError: + except TimeoutError as exc: raise zigpy.exceptions.DeliveryError( "Timeout waiting for ACK", status=TXStatus.NETWORK_ACK_FAILURE - ) + ) from exc if v != TXStatus.SUCCESS: raise zigpy.exceptions.DeliveryError( @@ -366,10 +366,8 @@ def handle_rx( if src == dst: LOGGER.info("handle_rx self addressed") - try: + with contextlib.suppress(KeyError): self._device.last_seen = datetime.now(UTC) - except KeyError: - pass self.packet_received( zigpy.types.ZigbeePacket(