From ef89f008de19367051a12afee515a220b6761b5a 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 | 38 +++---------------- pyproject.toml | 52 +++++++++++++++----------- requirements_test.txt | 6 +-- 4 files changed, 38 insertions(+), 88 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 4e81b50..c1523cc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,43 +4,17 @@ repos: hooks: - id: debug-statements - - repo: https://github.com/psf/black - rev: 26.3.1 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.16.9 hooks: - - id: black - - - repo: https://github.com/PyCQA/flake8 - rev: 7.3.0 - hooks: - - id: flake8 - additional_dependencies: - - Flake8-pyproject - - flake8-bugbear==25.11.29 - - flake8-comprehensions==3.17.0 - - flake8_2020==1.8.1 - - mccabe==0.7.0 - - pycodestyle==2.14.0 - - pyflakes==3.4.0 - - - repo: https://github.com/PyCQA/isort - rev: 8.0.1 - hooks: - - id: isort + - id: ruff-check + args: ["--fix", "--exit-non-zero-on-fix"] + - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.20.1 + rev: v2.3.1 hooks: - id: mypy additional_dependencies: - zigpy - types-setuptools - - - repo: https://github.com/asottile/pyupgrade - rev: v3.21.2 - hooks: - - id: pyupgrade - - - repo: https://github.com/fsouza/autoflake8 - rev: v0.4.1 - hooks: - - id: autoflake8 diff --git a/pyproject.toml b/pyproject.toml index c7fd9af..56e920c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,30 +35,40 @@ 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_zigate", "tests"] -forced_separate = "tests" -combine_as_imports = true +[tool.ruff] +target-version = "py311" +line-length = 88 + +[tool.ruff.lint] +select = [ + "B", # flake8-bugbear + "C", # complexity + "E", # pycodestyle + "F", # pyflakes/autoflake + "I", # isort + "SIM", # flake8-simplify + "UP", # pyupgrade + "W", # pycodestyle + "YTT", # flake8-2020 +] +ignore = [ + "E501", # line too long +] + +[tool.ruff.lint.per-file-ignores] +"tests/*" = ["F811", "F401", "F403"] + +[tool.ruff.lint.mccabe] +max-complexity = 25 + +[tool.ruff.lint.isort] +force-sort-within-sections = true +known-first-party = ["zigpy_zigate", "tests"] +forced-separate = ["tests"] +combine-as-imports = true [tool.mypy] ignore_errors = true [tool.pytest.ini_options] asyncio_mode = "auto" - -[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 ':' -# E501: line too long -# D202 No blank lines allowed after function docstring -ignore = [ - "W503", "E203", "E501", "D202", - "D103", "D102", "D101", # TODO: remove these once docstrings are added -] -per-file-ignores = ["tests/*:F811,F401,F403"] \ No newline at end of file diff --git a/requirements_test.txt b/requirements_test.txt index 2f3dfb3..20dcbd3 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,9 +1,6 @@ # Test dependencies. asynctest -isort -black -flake8 codecov colorlog codespell @@ -16,5 +13,4 @@ pytest-timeout pytest-asyncio>=0.17 pytest>=7.1.3 zigpy>=0.54.1 -ruff==0.0.261 -Flake8-pyproject \ No newline at end of file +ruff==0.16.9 From 45082be3179c90b622882307c12eeecf7be58be8 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:57:21 +0200 Subject: [PATCH 2/2] Apply ruff fixes Mostly autofixes: import sorting, pyupgrade rewrites for the Python 3.11 floor (typing.Dict -> dict, timezone.utc -> UTC, str.format -> f-strings) and ruff format. By hand: remaining %-formatting (struct format strings and __str__ methods) -> f-strings, a %-formatted LOGGER.exception call -> lazy logging arguments, explicit zip(strict=...) keeping the current behaviour, and exception chaining for re-raised errors. --- tests/test_api.py | 5 ++- zigpy_zigate/api.py | 21 ++++++------ zigpy_zigate/tools/flasher.py | 55 +++++++++++++----------------- zigpy_zigate/types.py | 14 ++++---- zigpy_zigate/uart.py | 4 +-- zigpy_zigate/zigbee/application.py | 6 ++-- 6 files changed, 47 insertions(+), 58 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 38b8f56..f9716b9 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,4 +1,5 @@ import asyncio +import contextlib from unittest.mock import AsyncMock, MagicMock, patch, sentinel import pytest @@ -60,8 +61,6 @@ async def test_api_new(conn_mck): @patch.object(asyncio, "wait", return_value=([], [])) async def test_api_command(mock_command, api): """Test command method.""" - try: + with contextlib.suppress(zigate_api.NoResponseError): await api.set_raw_mode() - except zigate_api.NoResponseError: - pass assert mock_command.call_count == 3 diff --git a/zigpy_zigate/api.py b/zigpy_zigate/api.py index 515f8d8..7b7b7d2 100644 --- a/zigpy_zigate/api.py +++ b/zigpy_zigate/api.py @@ -1,10 +1,10 @@ import asyncio import binascii -from datetime import datetime, timezone +from datetime import UTC, datetime import enum import functools import logging -from typing import Any, Dict +from typing import Any from zigpy.datastructures import PriorityLock import zigpy.exceptions @@ -220,7 +220,7 @@ class CommandNotSupportedError(CommandError): class ZiGate: - def __init__(self, device_config: Dict[str, Any]): + def __init__(self, device_config: dict[str, Any]): self._app = None self._config = device_config self._uart = None @@ -231,7 +231,7 @@ def __init__(self, device_config: Dict[str, Any]): self.network_state = None @classmethod - async def new(cls, config: Dict[str, Any], application=None) -> "ZiGate": + async def new(cls, config: dict[str, Any], application=None) -> "ZiGate": api = cls(config) await api.connect() api.set_application(application) @@ -263,10 +263,9 @@ def data_received(self, cmd, data, lqi): cmd = ResponseId(cmd) data, rest = t.deserialize(data, RESPONSES[cmd]) LOGGER.debug("Response received: %s %s %s (LQI:%s)", cmd, data, rest, lqi) - if cmd == ResponseId.STATUS: - if data[2] in self._status_awaiting: - fut = self._status_awaiting.pop(data[2]) - fut.set_result((data, lqi)) + if cmd == ResponseId.STATUS and data[2] in self._status_awaiting: + fut = self._status_awaiting.pop(data[2]) + fut.set_result((data, lqi)) if cmd in self._awaiting: fut = self._awaiting.pop(cmd) fut.set_result((data, lqi)) @@ -386,8 +385,8 @@ async def version(self): async def version_str(self): version, lqi = await self.version() - version = "{:x}".format(version[1]) - version = "{}.{}".format(version[0], version[1:]) + version = f"{version[1]:x}" + version = f"{version[0]}.{version[1:]}" return version async def get_network_state(self): @@ -418,7 +417,7 @@ async def erase_persistent_data(self): async def set_time(self): """set internal time""" timestamp = ( - datetime.now(timezone.utc) - datetime(2000, 1, 1, tzinfo=timezone.utc) + datetime.now(UTC) - datetime(2000, 1, 1, tzinfo=UTC) ).total_seconds() data = t.serialize([int(timestamp)], COMMANDS[CommandId.SET_TIMESERVER]) diff --git a/zigpy_zigate/tools/flasher.py b/zigpy_zigate/tools/flasher.py index 8517865..2893650 100644 --- a/zigpy_zigate/tools/flasher.py +++ b/zigpy_zigate/tools/flasher.py @@ -35,7 +35,7 @@ class Command: def __init__(self, type_, fmt=None, raw=False): assert not (raw and fmt), "Raw commands cannot use built-in struct formatting" - LOGGER.debug("Command {} {} {}".format(type_, fmt, raw)) + LOGGER.debug(f"Command {type_} {fmt} {raw}") self.type = type_ self.raw = raw if fmt: @@ -65,7 +65,7 @@ def wrapper(*args, **kwargs): class Response: def __init__(self, type_, data, chksum): - LOGGER.debug("Response {} {} {}".format(type_, data, chksum)) + LOGGER.debug(f"Response {type_} {data} {chksum}") self.type = type_ self.data = data[1:] self.chksum = chksum @@ -76,15 +76,11 @@ def ok(self): return self.status == 0 def __str__(self): - return "Response(type=0x{:02x}, data=0x{}, checksum=0x{:02x})".format( - self.type, - self.data.hex(), - self.chksum, - ) + return f"Response(type=0x{self.type:02x}, data=0x{self.data.hex()}, checksum=0x{self.chksum:02x})" def register(type_): - assert type_ not in _responses, "Duplicate response type 0x%02x" % type_ + assert type_ not in _responses, f"Duplicate response type 0x{type_:02x}" def decorator(func): _responses[type_] = func @@ -102,7 +98,7 @@ def prepare(type_, data): 0, ) - message = struct.pack("!BB%dsB" % len(data), length, type_, data, checksum) + message = struct.pack(f"!BB{len(data)}sB", length, type_, data, checksum) # print('Prepared command 0x%s' % message.hex()) return message @@ -110,20 +106,20 @@ def prepare(type_, data): def read_response(ser): length = ser.read() length = int.from_bytes(length, "big") - LOGGER.debug("read_response length {}".format(length)) + LOGGER.debug(f"read_response length {length}") answer = ser.read(length) - LOGGER.debug("read_response answer {}".format(answer)) + LOGGER.debug(f"read_response answer {answer}") return _unpack_raw_message(length, answer) # type_, data, chksum = struct.unpack('!B%dsB' % (length - 2), answer) # return {'type': type_, 'data': data, 'chksum': chksum} def _unpack_raw_message(length, decoded): - LOGGER.debug("unpack raw message {} {}".format(length, decoded)) + LOGGER.debug(f"unpack raw message {length} {decoded}") if len(decoded) != length or length < 2: - LOGGER.exception("Unpack failed, length: %d, msg %s" % (length, decoded.hex())) + LOGGER.exception("Unpack failed, length: %d, msg %s", length, decoded.hex()) return - type_, data, chksum = struct.unpack("!B%dsB" % (length - 2), decoded) + type_, data, chksum = struct.unpack(f"!B{length - 2}sB", decoded) return _responses.get(type_, Response)(type_, data, chksum) @@ -134,7 +130,7 @@ def req_flash_erase(): @Command(0x09, raw=True) def req_flash_write(addr, data): - msg = struct.pack("