From d80db0a855ad65943ec62804a19d5935e6cb11ec Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 12 Sep 2026 20:31:32 +0000 Subject: [PATCH] refactor: enable stricter ruff lints and broad exception checking --- pyproject.toml | 30 +++++++++++++++++++-- roborock/broadcast_protocol.py | 4 +-- roborock/cli.py | 22 +++++++-------- roborock/data/code_mappings.py | 2 +- roborock/data/containers.py | 10 +++---- roborock/data/v1/v1_code_mappings.py | 2 +- roborock/device_features.py | 2 +- roborock/devices/rpc/b01_q7_channel.py | 2 +- roborock/devices/traits/__init__.py | 2 +- roborock/devices/traits/v1/clean_summary.py | 2 +- roborock/devices/traits/v1/rooms.py | 2 +- roborock/devices/transport/local_channel.py | 2 +- roborock/map/b01_grid_layers.py | 2 +- roborock/map/b01_q10_map_parser.py | 4 +-- roborock/mqtt/roborock_session.py | 2 +- roborock/protocol.py | 6 ++--- roborock/protocols/a01_protocol.py | 8 +++--- roborock/protocols/b01_q7_protocol.py | 6 +++-- roborock/testing/v1_simulator.py | 4 +-- roborock/util.py | 2 +- tests/data/test_code_mappings.py | 6 ++--- tests/fixtures/local_async_fixtures.py | 2 +- tests/fixtures/pahomqtt_fixtures.py | 4 +-- tests/test_web_api.py | 1 - 24 files changed, 79 insertions(+), 50 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 94afa139a..3f364728d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,14 +106,40 @@ allowed_tags = [ major_tags= ["refactor"] [tool.ruff] -lint.ignore = ["F403", "E741"] -lint.select=["E", "F", "UP", "I"] +lint.ignore = [] +# TODO: Enable ASYNC (flake8-async) in a dedicated follow-up PR once async open() and timeout parameters are refactored. +lint.select = [ + "A", + "B", + "BLE", + "C4", + "E", + "F", + "FA", + "I", + "ICN", + "ISC", + "PGH", + "PIE", + "UP", + "W", + "YTT", +] line-length = 120 extend-exclude = ["roborock/map/proto/*_pb2.py"] [tool.ruff.lint.per-file-ignores] "*/__init__.py" = ["F401"] +"roborock/__init__.py" = ["F403"] +"roborock/data/__init__.py" = ["F403"] +"roborock/data/b01_q7/__init__.py" = ["F403"] +"roborock/data/b01_q10/__init__.py" = ["F403"] +"roborock/data/dyad/__init__.py" = ["F403"] +"roborock/data/mower/__init__.py" = ["F403"] +"roborock/data/v1/__init__.py" = ["F403"] +"roborock/data/zeo/__init__.py" = ["F403"] "roborock/map/proto/*_pb2.py" = ["E501", "I001", "UP009"] +"tests/*" = ["B010"] [[tool.mypy.overrides]] module = ["roborock.map.proto.*"] diff --git a/roborock/broadcast_protocol.py b/roborock/broadcast_protocol.py index e226a9cfe..ef8e303b0 100644 --- a/roborock/broadcast_protocol.py +++ b/roborock/broadcast_protocol.py @@ -4,7 +4,7 @@ import logging from asyncio import BaseTransport, Lock -from construct import ( # type: ignore +from construct import ( # type: ignore[import-untyped] Bytes, Checksum, GreedyBytes, @@ -63,7 +63,7 @@ def datagram_received(self, data: bytes, _): parsed_message = BroadcastMessage(duid=json_payload["duid"], ip=json_payload["ip"], version=version) _LOGGER.debug(f"Received broadcast: {parsed_message}") self.devices_found.append(parsed_message) - except Exception as e: + except Exception as e: # noqa: BLE001 _LOGGER.warning(f"Failed to decode message: {data!r}. Error: {e}") async def discover(self) -> list[BroadcastMessage]: diff --git a/roborock/cli.py b/roborock/cli.py index a83caa155..bb02116b0 100644 --- a/roborock/cli.py +++ b/roborock/cli.py @@ -22,6 +22,8 @@ ``` """ +# ruff: noqa: BLE001 + import asyncio import datetime import functools @@ -38,9 +40,9 @@ import click import click_shell import yaml - from pyshark import FileCapture # type: ignore - from pyshark.capture.live_capture import LiveCapture, UnknownInterfaceException # type: ignore - from pyshark.packet.packet import Packet # type: ignore + from pyshark import FileCapture # type: ignore[import-untyped] + from pyshark.capture.live_capture import LiveCapture, UnknownInterfaceException # type: ignore[import-untyped] + from pyshark.packet.packet import Packet # type: ignore[import-untyped] except ImportError as err: raise SystemExit( f"The 'roborock' command line tool requires extra dependencies that are not installed ({err.name}).\n" @@ -307,7 +309,7 @@ async def set(self, value: CacheData) -> None: @click.pass_context def cli(ctx, debug: int): logging_config: dict[str, Any] = {"level": logging.DEBUG if debug > 0 else logging.INFO} - logging.basicConfig(**logging_config) # type: ignore + logging.basicConfig(**logging_config) # type: ignore[call-overload] ctx.obj = RoborockContext() @@ -988,9 +990,8 @@ def on_package(packet: Packet): local_key, ) print(f"Received request: {f}") - except BaseException as e: + except Exception as e: print(e) - pass elif packet.ip.src == device_ip: try: f, buffer["data"] = MessageParser.parse( @@ -998,16 +999,15 @@ def on_package(packet: Packet): local_key, ) print(f"Received response: {f}") - except BaseException as e: + except Exception as e: print(e) - pass try: await capture.packets_from_tshark(on_package, close_tshark=not file_provided) - except UnknownInterfaceException: + except UnknownInterfaceException as err: raise RoborockException( "You need to run 'rvictl -s XXXXXXXX-XXXXXXXXXXXXXXXX' first, with an iPhone connected to usb port" - ) + ) from err def _parse_diagnostic_file(diagnostic_path: Path) -> dict[str, dict[str, Any]]: @@ -1319,7 +1319,7 @@ def write_markdown_table(product_features: dict[str, dict[str, any]], all_featur ] # Regular features are the remaining keys, sorted alphabetically # We filter out the special rows to avoid duplicating them. - sorted_features = sorted(list(all_features - set(special_rows))) + sorted_features = sorted(all_features - set(special_rows)) header = ["Feature"] + sorted_products diff --git a/roborock/data/code_mappings.py b/roborock/data/code_mappings.py index ff03e0e43..217cac235 100644 --- a/roborock/data/code_mappings.py +++ b/roborock/data/code_mappings.py @@ -33,7 +33,7 @@ def _missing_(cls: type[Self], key) -> Self: if warning not in completed_warnings: completed_warnings.add(warning) _LOGGER.warning(warning) - return cls.unknown # type: ignore + return cls.unknown # type: ignore[attr-defined] default_value = next(item for item in cls) warning = f"Missing {cls.__name__} code: {key} - defaulting to {default_value}" if warning not in completed_warnings: diff --git a/roborock/data/containers.py b/roborock/data/containers.py index cf0139908..cca9466d7 100644 --- a/roborock/data/containers.py +++ b/roborock/data/containers.py @@ -52,7 +52,7 @@ def _attr_repr(obj: Any) -> str: continue try: v = getattr(obj, k) - except (RuntimeError, Exception): + except Exception: # noqa: BLE001 continue if callable(v): continue @@ -210,7 +210,7 @@ class Reference(RoborockBase): r: str | None = None a: str | None = None m: str | None = None - l: str | None = None + l: str | None = None # noqa: E741 @dataclass @@ -379,9 +379,9 @@ class HomeDataSchedule(RoborockBase): class HomeData(RoborockBase): id: int name: str - products: list[HomeDataProduct] = field(default_factory=lambda: []) - devices: list[HomeDataDevice] = field(default_factory=lambda: []) - received_devices: list[HomeDataDevice] = field(default_factory=lambda: []) + products: list[HomeDataProduct] = field(default_factory=list) + devices: list[HomeDataDevice] = field(default_factory=list) + received_devices: list[HomeDataDevice] = field(default_factory=list) lon: Any | None = None lat: Any | None = None geo_name: Any | None = None diff --git a/roborock/data/v1/v1_code_mappings.py b/roborock/data/v1/v1_code_mappings.py index a346a8071..ce2d384ee 100644 --- a/roborock/data/v1/v1_code_mappings.py +++ b/roborock/data/v1/v1_code_mappings.py @@ -151,7 +151,7 @@ class RoborockDssCodes(RoborockEnum): def _missing_(cls: type[Self], key) -> Self: # If the calculated value is not provided, then it should be viewed as okay. # As the math will sometimes result in you getting numbers that don't matter. - return cls.okay # type: ignore + return cls.okay # type: ignore[attr-defined] class ClearWaterBoxStatus(RoborockDssCodes): diff --git a/roborock/device_features.py b/roborock/device_features.py index f92d6b3e8..160d14cc5 100644 --- a/roborock/device_features.py +++ b/roborock/device_features.py @@ -643,7 +643,7 @@ def from_feature_flags( elif (product_features := f.metadata.get("product_features")) is not None: if product_nickname is not None: available_features = PRODUCT_FEATURE_MAP.get(product_nickname, []) - if any(feat in available_features for feat in product_features): # type: ignore + if any(feat in available_features for feat in product_features): kwargs[f.name] = True # The app combines runtime shake-mop, model shake/spin, and roller-mop diff --git a/roborock/devices/rpc/b01_q7_channel.py b/roborock/devices/rpc/b01_q7_channel.py index d01257b38..a26611d22 100644 --- a/roborock/devices/rpc/b01_q7_channel.py +++ b/roborock/devices/rpc/b01_q7_channel.py @@ -84,7 +84,7 @@ def on_message(response_message: RoborockMessage) -> None: return try: response = response_matcher(response_message) - except Exception as ex: + except Exception as ex: # noqa: BLE001 future.set_exception(ex) return if response is not None: diff --git a/roborock/devices/traits/__init__.py b/roborock/devices/traits/__init__.py index 2dc86fcb6..a6190c23d 100644 --- a/roborock/devices/traits/__init__.py +++ b/roborock/devices/traits/__init__.py @@ -24,5 +24,5 @@ ] -class Trait(ABC): +class Trait(ABC): # noqa: B024 """Base class for all traits.""" diff --git a/roborock/devices/traits/v1/clean_summary.py b/roborock/devices/traits/v1/clean_summary.py index 1a2a4223d..7f1b234e0 100644 --- a/roborock/devices/traits/v1/clean_summary.py +++ b/roborock/devices/traits/v1/clean_summary.py @@ -56,7 +56,7 @@ def convert(self, response: common.V1ResponseData) -> CleanRecord: rec.square_meter_area or 0 ) return final_record - except Exception: + except Exception: # noqa: BLE001 # Return final record when an exception occurred return final_record # There are still a few unknown variables in this. diff --git a/roborock/devices/traits/v1/rooms.py b/roborock/devices/traits/v1/rooms.py index 0b838c749..021373dce 100644 --- a/roborock/devices/traits/v1/rooms.py +++ b/roborock/devices/traits/v1/rooms.py @@ -144,6 +144,6 @@ async def _refresh_rooms(self) -> list[HomeDataRoom]: rooms_by_id.update({room.iot_id: room for room in shared_rooms}) return list(rooms_by_id.values()) return await self._web_api.get_rooms() - except Exception: + except Exception: # noqa: BLE001 _LOGGER.debug("Failed to fetch rooms from web API", exc_info=True) return [] diff --git a/roborock/devices/transport/local_channel.py b/roborock/devices/transport/local_channel.py index 25d1380ae..413a90240 100644 --- a/roborock/devices/transport/local_channel.py +++ b/roborock/devices/transport/local_channel.py @@ -158,7 +158,7 @@ async def _keep_alive_loop(self) -> None: await self._ping() except asyncio.CancelledError: break - except Exception: + except Exception: # noqa: BLE001 self._logger.debug("Keep-alive ping failed", exc_info=True) # Retry next interval diff --git a/roborock/map/b01_grid_layers.py b/roborock/map/b01_grid_layers.py index bcc0f4571..494e48d75 100644 --- a/roborock/map/b01_grid_layers.py +++ b/roborock/map/b01_grid_layers.py @@ -198,7 +198,7 @@ def solve_calibration( min_sx, max_sx, min_sy, max_sy = min(sx), max(sx), min(sy), max(sy) if (max_sx - min_sx) >= w or (max_sy - min_sy) >= h: continue # path wider/taller than the map at this resolution - pts = list(zip(sx, sy)) + pts = list(zip(sx, sy, strict=True)) # Slide so every point stays in-bounds: px = px_f + ox in [0, w), py = oy - py_f in [0, h). for ox in range(ceil(-min_sx), ceil(w - max_sx)): for oy in range(ceil(max_sy), ceil(h + min_sy)): diff --git a/roborock/map/b01_q10_map_parser.py b/roborock/map/b01_q10_map_parser.py index 2f162c64f..81ac4036e 100644 --- a/roborock/map/b01_q10_map_parser.py +++ b/roborock/map/b01_q10_map_parser.py @@ -341,7 +341,7 @@ def _drop_stray_leading_point(points: list[Q10Point]) -> list[Q10Point]: """ if len(points) < 3: return points - steps = [math.hypot(b.x - a.x, b.y - a.y) for a, b in zip(points, points[1:])] + steps = [math.hypot(b.x - a.x, b.y - a.y) for a, b in zip(points, points[1:], strict=False)] median_rest = statistics.median(steps[1:]) if median_rest > 0 and steps[0] > _STRAY_POINT_STEP_RATIO * median_rest: return points[1:] @@ -425,7 +425,7 @@ def _infer_layout(decoded: bytes, width: int) -> tuple[int, bytes, bytes]: up with the marker. Used as a fallback when the header carries no usable height. """ - for room_count in range(0, _MAX_ROOMS + 1): + for room_count in range(_MAX_ROOMS + 1): room_data_length = 2 + room_count * _ROOM_RECORD_LENGTH area = len(decoded) - room_data_length if area <= 0 or area % width: diff --git a/roborock/mqtt/roborock_session.py b/roborock/mqtt/roborock_session.py index 15202372e..a64992718 100644 --- a/roborock/mqtt/roborock_session.py +++ b/roborock/mqtt/roborock_session.py @@ -165,7 +165,7 @@ async def _run_reconnect_loop(self, start_future: asyncio.Future[None] | None) - await self._connection_task except asyncio.CancelledError: _LOGGER.debug("MQTT connection task cancelled") - except Exception: + except Exception: # noqa: BLE001 # Exceptions are logged and handled in _run_connection. # There is a special case for exceptions on startup where we return # immediately. Otherwise, we let the reconnect loop retry with diff --git a/roborock/protocol.py b/roborock/protocol.py index fc3af8424..4b771b712 100644 --- a/roborock/protocol.py +++ b/roborock/protocol.py @@ -5,7 +5,7 @@ from collections.abc import Callable from urllib.parse import urlparse -from construct import ( # type: ignore +from construct import ( # type: ignore[import-untyped] Bytes, Checksum, ChecksumError, @@ -65,7 +65,7 @@ def ensure_bytes(msg: bytes | str) -> bytes: @staticmethod def encode_timestamp(_timestamp: int) -> bytes: hex_value = f"{_timestamp:x}".zfill(8) - return "".join(list(map(lambda idx: hex_value[idx], [5, 6, 3, 7, 1, 2, 0, 4]))).encode() + return "".join([hex_value[idx] for idx in [5, 6, 3, 7, 1, 2, 0, 4]]).encode() @staticmethod def md5(data: bytes) -> bytes: @@ -470,7 +470,7 @@ def build( } ) return self.con.build( - {"messages": [message for message in messages], "remaining": b""}, + {"messages": list(messages), "remaining": b""}, local_key=local_key, prefixed=prefixed, connect_nonce=connect_nonce, diff --git a/roborock/protocols/a01_protocol.py b/roborock/protocols/a01_protocol.py index 46db6ef4a..27889752d 100644 --- a/roborock/protocols/a01_protocol.py +++ b/roborock/protocols/a01_protocol.py @@ -62,7 +62,7 @@ def decode_rpc_response(message: RoborockMessage) -> dict[int, Any]: try: unpadded = unpad(message.payload, AES.block_size) except ValueError as err: - raise RoborockException(f"Unable to unpad A01 payload: {err}") + raise RoborockException(f"Unable to unpad A01 payload: {err}") from err try: payload = json.loads(unpadded.decode()) @@ -74,5 +74,7 @@ def decode_rpc_response(message: RoborockMessage) -> dict[int, Any]: raise RoborockException(f"Invalid A01 message format: 'dps' should be a dictionary for {message.payload!r}") try: return {int(key): value for key, value in datapoints.items()} - except ValueError: - raise RoborockException(f"Invalid A01 message format: 'dps' key should be an integer for {message.payload!r}") + except ValueError as err: + raise RoborockException( + f"Invalid A01 message format: 'dps' key should be an integer for {message.payload!r}" + ) from err diff --git a/roborock/protocols/b01_q7_protocol.py b/roborock/protocols/b01_q7_protocol.py index be4a01229..1eac3dcb7 100644 --- a/roborock/protocols/b01_q7_protocol.py +++ b/roborock/protocols/b01_q7_protocol.py @@ -83,8 +83,10 @@ def decode_rpc_response(message: RoborockMessage) -> dict[int, Any]: raise RoborockException(f"Invalid B01 message format: 'dps' should be a dictionary for {message.payload!r}") try: return {int(key): value for key, value in datapoints.items()} - except ValueError: - raise RoborockException(f"Invalid B01 message format: 'dps' key should be an integer for {message.payload!r}") + except ValueError as err: + raise RoborockException( + f"Invalid B01 message format: 'dps' key should be an integer for {message.payload!r}" + ) from err @dataclass diff --git a/roborock/testing/v1_simulator.py b/roborock/testing/v1_simulator.py index 83bbba99e..8dfd36091 100644 --- a/roborock/testing/v1_simulator.py +++ b/roborock/testing/v1_simulator.py @@ -346,7 +346,7 @@ async def _handle_publish(self, message: RoborockMessage, channel: FakeChannel) msg_id = inner["id"] method = inner["method"] params = inner.get("params", []) - except Exception as e: + except Exception as e: # noqa: BLE001 _LOGGER.debug("Failed to parse plaintext JSON RPC payload: %s", e, exc_info=True) return @@ -358,7 +358,7 @@ async def _handle_publish(self, message: RoborockMessage, channel: FakeChannel) if handler: try: result = handler(params) - except Exception as e: + except Exception as e: # noqa: BLE001 error = str(e) _LOGGER.debug("Error executing command handler for %s: %s", method, e, exc_info=True) else: diff --git a/roborock/util.py b/roborock/util.py index 481759ec7..09ab9088d 100644 --- a/roborock/util.py +++ b/roborock/util.py @@ -10,7 +10,7 @@ def unpack_list(value: list[T], size: int) -> list[T | None]: - return (value + [None] * size)[:size] # type: ignore + return (value + [None] * size)[:size] class RoborockLoggerAdapter(logging.LoggerAdapter): diff --git a/tests/data/test_code_mappings.py b/tests/data/test_code_mappings.py index 735d87ac4..40a2e8abc 100644 --- a/tests/data/test_code_mappings.py +++ b/tests/data/test_code_mappings.py @@ -85,7 +85,7 @@ def test_invalid_from_value() -> None: @pytest.mark.parametrize( - "input, expected", + "input_value, expected", [ ("START_CLEAN", B01_Q10_DP.START_CLEAN), ("start_clean", B01_Q10_DP.START_CLEAN), @@ -103,9 +103,9 @@ def test_invalid_from_value() -> None: (999999, None), ], ) -def test_from_any_optional(input: str | int, expected: B01_Q10_DP | None) -> None: +def test_from_any_optional(input_value: str | int, expected: B01_Q10_DP | None) -> None: """Test from_any_optional method.""" - assert B01_Q10_DP.from_any_optional(input) == expected + assert B01_Q10_DP.from_any_optional(input_value) == expected def test_homedata_product_unknown_category(): diff --git a/tests/fixtures/local_async_fixtures.py b/tests/fixtures/local_async_fixtures.py index 1edef1e10..812999acb 100644 --- a/tests/fixtures/local_async_fixtures.py +++ b/tests/fixtures/local_async_fixtures.py @@ -26,7 +26,7 @@ def response_queue_fixture() -> Generator[asyncio.Queue[bytes], None, None]: response_queue: asyncio.Queue[bytes] = asyncio.Queue() yield response_queue if not response_queue.empty(): - warnings.warn("Some enqueued local device responses were not consumed during the test") + warnings.warn("Some enqueued local device responses were not consumed during the test", stacklevel=2) @pytest.fixture(name="local_async_request_handler") diff --git a/tests/fixtures/pahomqtt_fixtures.py b/tests/fixtures/pahomqtt_fixtures.py index ecdfe69b8..260d6e26b 100644 --- a/tests/fixtures/pahomqtt_fixtures.py +++ b/tests/fixtures/pahomqtt_fixtures.py @@ -56,7 +56,7 @@ def fake_mqtt_socket_handler_fixture( socket_handler = FakeMqttSocketHandler(mqtt_request_handler, mqtt_response_queue, log) yield socket_handler if len(socket_handler.response_buf.getvalue()) > 0: - warnings.warn("Some enqueued MQTT responses were not consumed during the test") + warnings.warn("Some enqueued MQTT responses were not consumed during the test", stacklevel=2) @pytest.fixture(name="mock_sock") @@ -81,7 +81,7 @@ def response_queue_fixture() -> Generator[Queue[bytes], None, None]: response_queue: Queue[bytes] = Queue() yield response_queue if not response_queue.empty(): - warnings.warn("Some enqueued MQTT responses were not consumed during the test") + warnings.warn("Some enqueued MQTT responses were not consumed during the test", stacklevel=2) @pytest.fixture(name="mqtt_request_handler") diff --git a/tests/test_web_api.py b/tests/test_web_api.py index 15bdf5aff..cb6d0cb34 100644 --- a/tests/test_web_api.py +++ b/tests/test_web_api.py @@ -26,7 +26,6 @@ @pytest.fixture(autouse=True) def auto_mock_rest_fixture(mock_rest: Any) -> None: """Auto use the mock rest fixture for all tests in this module.""" - pass async def test_pass_login_flow() -> None: