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 e7b5df6..23accfa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,47 +1,14 @@ repos: - - repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.16.9 hooks: - - id: pyupgrade - args: [--py38-plus] - - - repo: https://github.com/PyCQA/autoflake - rev: v2.0.2 - hooks: - - id: autoflake - - - repo: https://github.com/psf/black - rev: 23.3.0 - hooks: - - id: black - args: - - --quiet - - --safe - - - repo: https://github.com/pycqa/flake8 - rev: 6.1.0 - hooks: - - id: flake8 - additional_dependencies: - - flake8-docstrings==1.5.0 - - pydocstyle==5.1.1 - - Flake8-pyproject==1.2.3 - - - 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.3.0 + rev: v2.4.3 hooks: - id: codespell additional_dependencies: [tomli] args: ["--toml", "pyproject.toml"] - - - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.261 - hooks: - - id: ruff - args: - - --fix \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 763c124..6a331d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,13 +34,49 @@ 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_deconz", "tests"] -forced_separate = "tests" -combine_as_imports = true +[tool.ruff] +target-version = "py38" +line-length = 88 + +[tool.ruff.lint] +select = [ + "B", # flake8-bugbear + "C", # complexity + "D", # pydocstyle + "E", # pycodestyle + "F", # pyflakes/autoflake + "I", # isort + "SIM", # flake8-simplify + "UP", # pyupgrade + "W", # pycodestyle +] +ignore = [ + "E501", # line too long + "D101", # Missing docstring in public class + "D102", # Missing docstring in public method + "D103", # Missing docstring in public function + "D202", # No blank lines allowed after function docstring +] + +[tool.ruff.lint.per-file-ignores] +"tests/*" = [ + "F811", + "F401", + "F403", + "SIM117", # merging needs parenthesized `with`, but requires-python is still >=3.8 +] + +[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_deconz", "tests"] +forced-separate = ["tests"] +combine-as-imports = true [tool.mypy] ignore_errors = true @@ -49,20 +85,6 @@ ignore_errors = true 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 ':' -# 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"] - [tool.codespell] ignore-words-list = ["IntStruct"] skip = ["./.*", "tests/*", "pyproject.toml"] 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 diff --git a/tests/async_mock.py b/tests/async_mock.py index 8257ddd..599efb5 100644 --- a/tests/async_mock.py +++ b/tests/async_mock.py @@ -1,9 +1,3 @@ """Mock utilities that are async aware.""" -import sys -if sys.version_info[:2] < (3, 8): - from asynctest.mock import * # noqa - - AsyncMock = CoroutineMock # noqa: F405 -else: - from unittest.mock import * # noqa +from unittest.mock import * # noqa diff --git a/tests/test_api.py b/tests/test_api.py index 55e4179..a2bbded 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -731,7 +731,7 @@ async def test_unknown_command(api, caplog): assert 0xFF not in deconz_api.COMMAND_SCHEMAS with caplog.at_level(logging.WARNING): - api.data_received(b"\xFF\xAA\xBB") + api.data_received(b"\xff\xaa\xbb") assert ( "Unknown command received: Command(command_id=," @@ -754,8 +754,7 @@ async def test_bad_command_parsing(api, caplog): ) assert ( - "Failed to parse command Command(command_id=" - "" + "Failed to parse command Command(command_id=" ) in caplog.text caplog.clear() @@ -1068,11 +1067,10 @@ async def test_firmware_responding_with_wrong_type_with_correct_seq( }, ) - with caplog.at_level(logging.DEBUG): - with pytest.raises(asyncio.TimeoutError): - # We wait beyond 500ms to make sure it triggers - async with asyncio_timeout(0.6): - await api.send_command(deconz_api.CommandId.aps_data_confirm) + with caplog.at_level(logging.DEBUG), pytest.raises(asyncio.TimeoutError): + # We wait beyond 500ms to make sure it triggers + async with asyncio_timeout(0.6): + await api.send_command(deconz_api.CommandId.aps_data_confirm) assert ( "Firmware responded incorrectly (Response is mismatched! Sent" diff --git a/tests/test_application.py b/tests/test_application.py index cd3da3c..90ea531 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -603,7 +603,7 @@ async def test_energy_scan_conbee_3(app): "ScannedChannels": Channels.ALL_CHANNELS, "TotalTransmissions": 0, "TransmissionFailures": 0, - "EnergyValues": [i for i in range(11, 26 + 1)], + "EnergyValues": list(range(11, 26 + 1)), }.values() ), ] diff --git a/tests/test_network_state.py b/tests/test_network_state.py index e1964e8..025b284 100644 --- a/tests/test_network_state.py +++ b/tests/test_network_state.py @@ -227,7 +227,7 @@ async def write_parameter(param, *args): elif channel_mask is not None: assert params["channel_mask"] == (network_info.channel_mask,) else: - assert False + raise AssertionError() assert params["use_predefined_nwk_panid"] == (True,) assert params["nwk_panid"] == (network_info.pan_id,) @@ -340,7 +340,7 @@ async def read_param(param, *args): except KeyError: raise zigpy_deconz.exception.CommandError( zigpy_deconz.api.Status.UNSUPPORTED, f"Unsupported: {param!r} {args!r}" - ) + ) from None if isinstance(value, Exception): raise value diff --git a/tests/test_types.py b/tests/test_types.py index 17ad0e5..a7ba2fe 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,6 +1,5 @@ """Tests for zigpy_deconz.types module.""" - import pytest import zigpy.types as zigpy_t diff --git a/tests/test_uart.py b/tests/test_uart.py index 17fc320..38a5b70 100644 --- a/tests/test_uart.py +++ b/tests/test_uart.py @@ -46,10 +46,10 @@ def test_send(gw): gw.send(data) packet = b"" - packet += b"\xC0" # END + packet += b"\xc0" # END packet += b"test" # data - packet += b"\x40\xFE" # checksum - packet += b"\xC0" # END + packet += b"\x40\xfe" # checksum + packet += b"\xc0" # END gw._transport.write.assert_called_once_with(packet) @@ -60,7 +60,7 @@ def test_close(gw): def test_data_received_chunk_frame(gw): - data = b"\x07\x01\x00\x08\x00\xaa\x00\x02\x44\xFF\xC0" + data = b"\x07\x01\x00\x08\x00\xaa\x00\x02\x44\xff\xc0" gw.data_received(data[:-4]) assert gw._api.data_received.call_count == 0 gw.data_received(data[-4:]) @@ -69,7 +69,7 @@ def test_data_received_chunk_frame(gw): def test_data_received_full_frame(gw): - data = b"\x07\x01\x00\x08\x00\xaa\x00\x02\x44\xFF\xC0" + data = b"\x07\x01\x00\x08\x00\xaa\x00\x02\x44\xff\xc0" gw.data_received(data) assert gw._api.data_received.call_count == 1 assert gw._api.data_received.call_args[0][0] == data[:-3] @@ -82,13 +82,13 @@ def test_data_received_incomplete_frame(gw): def test_data_received_runt_frame(gw): - data = b"\x02\x44\xC0" + data = b"\x02\x44\xc0" gw.data_received(data) assert gw._api.data_received.call_count == 0 def test_data_received_extra(gw): - data = b"\x07\x01\x00\x08\x00\xaa\x00\x02\x44\xFF\xC0\x00" + data = b"\x07\x01\x00\x08\x00\xaa\x00\x02\x44\xff\xc0\x00" gw.data_received(data) assert gw._api.data_received.call_count == 1 assert gw._api.data_received.call_args[0][0] == data[:-4] @@ -96,13 +96,13 @@ def test_data_received_extra(gw): def test_data_received_wrong_checksum(gw): - data = b"\x07\x01\x00\x08\x00\xaa\x00\x02\x44\xFE\xC0" + data = b"\x07\x01\x00\x08\x00\xaa\x00\x02\x44\xfe\xc0" gw.data_received(data) assert gw._api.data_received.call_count == 0 def test_data_received_error(gw, caplog): - data = b"\x07\x01\x00\x08\x00\xaa\x00\x02\x44\xFF\xC0" + data = b"\x07\x01\x00\x08\x00\xaa\x00\x02\x44\xff\xc0" gw._api.data_received.side_effect = [RuntimeError("error")] @@ -116,28 +116,28 @@ def test_data_received_error(gw, caplog): def test_unescape(gw): - data = b"\x00\xDB\xDC\x00\xDB\xDD\x00\x00\x00" - data_unescaped = b"\x00\xC0\x00\xDB\x00\x00\x00" + data = b"\x00\xdb\xdc\x00\xdb\xdd\x00\x00\x00" + data_unescaped = b"\x00\xc0\x00\xdb\x00\x00\x00" r = gw._unescape(data) assert r == data_unescaped def test_unescape_error(gw): - data = b"\x00\xDB\xDC\x00\xDB\xDD\x00\x00\x00\xDB" + data = b"\x00\xdb\xdc\x00\xdb\xdd\x00\x00\x00\xdb" r = gw._unescape(data) assert r is None def test_escape(gw): - data = b"\x00\xC0\x00\xDB\x00\x00\x00" - data_escaped = b"\x00\xDB\xDC\x00\xDB\xDD\x00\x00\x00" + data = b"\x00\xc0\x00\xdb\x00\x00\x00" + data_escaped = b"\x00\xdb\xdc\x00\xdb\xdd\x00\x00\x00" r = gw._escape(data) assert r == data_escaped def test_checksum(gw): data = b"\x07\x01\x00\x08\x00\xaa\x00\x02" - checksum = b"\x44\xFF" + checksum = b"\x44\xff" r = gw._checksum(data) assert r == checksum diff --git a/zigpy_deconz/api.py b/zigpy_deconz/api.py index 3ec9e3d..7de1905 100644 --- a/zigpy_deconz/api.py +++ b/zigpy_deconz/api.py @@ -664,19 +664,19 @@ def data_received(self, data: bytes) -> None: MISMATCHED_RESPONSE_TIMEOUT, ) # The firmware *sometimes* responds with the correct response later - self._mismatched_response_timers[ - command.seq - ] = asyncio.get_event_loop().call_later( - MISMATCHED_RESPONSE_TIMEOUT, - fut.set_exception, - MismatchedResponseError( - command.command_id, - params, - ( - f"Response is mismatched! Sent {wrong_fut_cmd_id}," - f" received {command.command_id}" + self._mismatched_response_timers[command.seq] = ( + asyncio.get_event_loop().call_later( + MISMATCHED_RESPONSE_TIMEOUT, + fut.set_exception, + MismatchedResponseError( + command.command_id, + params, + ( + f"Response is mismatched! Sent {wrong_fut_cmd_id}," + f" received {command.command_id}" + ), ), - ), + ) ) # Make sure we do not resolve the future diff --git a/zigpy_deconz/types.py b/zigpy_deconz/types.py index 632a333..42e44fb 100644 --- a/zigpy_deconz/types.py +++ b/zigpy_deconz/types.py @@ -1,6 +1,5 @@ """Data types module.""" - import zigpy.types as zigpy_t from zigpy.types import ( # noqa: F401 EUI64, @@ -203,9 +202,11 @@ def deserialize(cls, data): def serialize(self): r = uint8_t(self.address_mode).serialize() - if self.address_mode in (AddressMode.NWK, AddressMode.NWK_AND_IEEE): - r += NWK(self.address).serialize() - elif self.address_mode == AddressMode.GROUP: + if self.address_mode in ( + AddressMode.NWK, + AddressMode.NWK_AND_IEEE, + AddressMode.GROUP, + ): r += NWK(self.address).serialize() if self.address_mode in (AddressMode.IEEE, AddressMode.NWK_AND_IEEE): diff --git a/zigpy_deconz/uart.py b/zigpy_deconz/uart.py index 3481fb6..83a2ab3 100644 --- a/zigpy_deconz/uart.py +++ b/zigpy_deconz/uart.py @@ -14,10 +14,10 @@ class Gateway(zigpy.serial.SerialProtocol): - END = b"\xC0" - ESC = b"\xDB" - ESC_END = b"\xDC" - ESC_ESC = b"\xDD" + END = b"\xc0" + ESC = b"\xdb" + ESC_END = b"\xdc" + ESC_ESC = b"\xdd" def __init__(self, api): """Initialize instance of the UART gateway.""" diff --git a/zigpy_deconz/utils.py b/zigpy_deconz/utils.py index 9aa7330..75ac20b 100644 --- a/zigpy_deconz/utils.py +++ b/zigpy_deconz/utils.py @@ -31,9 +31,6 @@ def is_usb_serial_port(device_path: str) -> bool: """Check if a device path is a USB serial port.""" resolved_device = os.path.realpath(device_path) - # Platform serial ports (Raspbee) - if re.match(r"/dev/tty(S|AMA)\d+", resolved_device): - return False - - # Everything else is assumed to be USB serial - return True + # Platform serial ports (Raspbee) are not USB serial, everything else is assumed + # to be USB serial + return not re.match(r"/dev/tty(S|AMA)\d+", resolved_device) diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index 8fa2fdb..590e7eb 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -167,7 +167,7 @@ async def change_loop(): try: async with asyncio_timeout(timeout): await change_loop() - except asyncio.TimeoutError: + except asyncio.TimeoutError as exc: if target_state != NetworkState.CONNECTED: raise @@ -176,7 +176,7 @@ async def change_loop(): " Make sure your coordinator is on a USB 2.0 extension cable and" " away from any sources of interference, like USB 3.0 ports, SSDs," " 2.4GHz routers, motherboards, etc." - ) + ) from exc async def reset_network_info(self): # TODO: There does not appear to be a way to factory reset a Conbee @@ -223,7 +223,7 @@ async def write_network_info(self, *, network_info, node_info): f"Please upgrade your adapter firmware. Firmware version" f" {fw_version} does not support writing the network key frame" f" counter, which is required for migration to succeed." - ) + ) from ex if node_info.logical_type == zdo_t.LogicalType.Coordinator: await self._api.write_parameter( @@ -587,7 +587,7 @@ async def send_packet(self, packet): except zigpy_deconz.exception.CommandError as ex: raise zigpy.exceptions.DeliveryError( f"Failed to enqueue packet: {ex!r}", ex.status - ) + ) from ex async with asyncio_timeout(SEND_CONFIRM_TIMEOUT): status = await future