diff --git a/pyproject.toml b/pyproject.toml index 0ec6791..96840e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ dev = [ "mypy>=2.1.0", "pytest>=9.0.3", "pytest-cov>=7.1.0", - "ruff>=0.15.17", + "ruff>=0.16.0", "types-pyserial>=3.5.0", "vulture>=2.16", ] @@ -32,8 +32,8 @@ dev = [ # https://github.com/pydantic/pydantic/blob/main/pyproject.toml # https://github.com/pydantic/pydantic-ai/blob/main/pyproject.toml # https://github.com/encode/httpx/blob/master/pyproject.toml -select = [ - # Core correctness and safety. +extend-select = [ + # Baseline correctness, validation, and security. "E", # Pycodestyle errors "W", # Pycodestyle warnings "F", # Pyflakes correctness @@ -44,82 +44,37 @@ select = [ "PLE", # Pylint error-level correctness checks "PLW", # Pylint warning-level correctness checks "S", # Bandit-derived security checks - "TRY004", # Use TypeError for invalid argument types - - # Typing and dataclass correctness. - "PYI034", # Use Self for self-returning special methods - "RUF008", # Reject mutable dataclass defaults - "RUF009", # Reject function calls in dataclass defaults - "RUF012", # Require ClassVar for mutable class attributes - "RUF033", # Reject defaults on __post_init__ parameters - "RUF049", # Reject dataclass-decorated Enum classes - - # Targeted runtime and data correctness. - "PTH124", # Reject legacy py.path usage - "PTH210", # Reject invalid Path.with_suffix() arguments - "RUF024", # Reject mutable values passed to dict.fromkeys() - "RUF026", # Pass defaultdict factories positionally - "RUF032", # Avoid constructing Decimal from float literals "RUF064", # Require octal literals for file-permission modes - # Asynchronous and FastAPI correctness. + # Runtime, asynchronous, framework, and control-flow correctness. "ASYNC", # Detect blocking and unsafe async patterns - "FAST", # FastAPI-specific correctness checks "RUF006", # Retain references to created asyncio tasks + "FAST", # FastAPI-specific correctness checks + "RUF021", # Parenthesize mixed and/or expressions + "RUF060", # Reject membership tests against empty collections + "SIM105", # Use contextlib.suppress for intentional suppression + "TRY300", # Keep success-only statements outside try blocks - # Imports, modernization, and unnecessary code. + # Imports, modernization, performance, and maintainability. "I", # Import ordering - "RUF022", # Sort __all__ exports "UP", # Modernize syntax for Python 3.13 - "YTT", # Validate Python-version checks "FURB", # Stable Refurb modernization checks "C4", # Improve comprehensions "RUF005", # Prefer unpacking over collection concatenation - "PIE", # Remove unnecessary code - "RUF059", # Mark intentionally unused unpacked variables explicitly - - # Control-flow and resource safety. - "RUF021", # Parenthesize mixed and/or expressions - "RUF060", # Reject membership tests against empty collections - "SIM105", # Use contextlib.suppress for intentional suppression - "SIM107", # Prevent finally returns from masking exceptions - "SIM114", # Combine branches with identical bodies - "SIM115", # Require context managers for locally opened files - "SIM220", # Reject expressions equivalent to x and not x - "SIM221", # Reject expressions equivalent to x or not x - "TRY300", # Keep success-only statements outside try blocks - - # Documentation, maintainability, and performance. - "D", # Docstring conventions + "PERF", # Performance anti-patterns "N", # PEP 8 naming conventions + "D", # Docstring conventions "C90", # McCabe complexity "PLR0913", # Excessive function arguments - "PERF", # Performance anti-patterns - "RUF017", # Prevent quadratic list flattening with sum() - # Logging and development hygiene. + # Logging, tests, and development hygiene. "LOG", # Logging API correctness "G", # Logging format-string correctness - "T10", # Debugger calls "T20", # Print statements - - # Test and assertion correctness. "PT", # Pytest correctness and style checks - "RUF018", # Reject assignments inside assertions - "RUF030", # Reject print() calls used as assert messages - "RUF040", # Reject non-string literal assert messages "RUF043", # Require unambiguous pytest.raises() patterns - - # Suppression hygiene. "PGH", # Detect blanket suppressions and invalid mock access - "RUF028", # Validate Ruff formatter-suppression comments "RUF10", # Validate noqa and suppression directives - - # Project configuration correctness. - "RUF200", # Validate pyproject.toml -] -ignore = [ - "E501", # Line length is handled by the Ruff formatter ] [tool.ruff.lint.pydocstyle] diff --git a/src/xkc_kl200_python/_serial_manager.py b/src/xkc_kl200_python/_serial_manager.py index 70838dc..70da4d3 100644 --- a/src/xkc_kl200_python/_serial_manager.py +++ b/src/xkc_kl200_python/_serial_manager.py @@ -205,7 +205,7 @@ def _extract_frame( expected_header: int | None, expected_command: int | None, ) -> tuple[bytes | None, XkcKl200Status | None, bool]: - """Return the next valid frame, one protocol error, and whether data was consumed.""" + """Return a valid frame, one protocol error, and whether data was consumed.""" if not self._buffer: return None, None, False diff --git a/tests/conftest.py b/tests/conftest.py index d58766c..55f6fbe 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -46,7 +46,7 @@ def write(self, data: bytes) -> int: def flush(self) -> None: """Accept flush requests without additional behavior.""" - return None + return def reset_input_buffer(self) -> None: """Record an input-buffer reset request.""" diff --git a/tests/test_sensor_misc.py b/tests/test_sensor_misc.py index d9da63d..f7b9ee7 100644 --- a/tests/test_sensor_misc.py +++ b/tests/test_sensor_misc.py @@ -64,7 +64,7 @@ def test_reset_input_buffer_clears_serial_input( def test_reset_output_buffer_clears_serial_output( serial_factory: FakeSerialFactory, ) -> None: - """Verify that callers can clear the output buffer directly when aborting recovery.""" + """Verify callers can clear the output buffer when aborting recovery.""" sensor = make_sensor(serial_factory) serial_port = serial_factory.holder["serial"] @@ -93,7 +93,7 @@ def test_hard_and_soft_reset(serial_factory: FakeSerialFactory) -> None: def test_change_address_success_updates_config_and_state( serial_factory: FakeSerialFactory, ) -> None: - """Verify that address changes update both config intent and cached runtime state.""" + """Verify address changes update config intent and cached runtime state.""" sensor = make_sensor(serial_factory) serial_port = serial_factory.holder["serial"] serial_port.queue_read( @@ -230,7 +230,7 @@ def test_communication_mode_success_uses_system_header( def test_set_communication_mode_requires_system_header_ack( serial_factory: FakeSerialFactory, ) -> None: - """Verify that set_communication_mode ignores a stale baud-rate ACK with the same command byte.""" + """Verify communication mode ignores a stale ACK with the same command byte.""" sensor = make_sensor(serial_factory, timeout=0.0) serial_port = serial_factory.holder["serial"] serial_port.queue_read( diff --git a/tests/test_sensor_reading.py b/tests/test_sensor_reading.py index 52d3383..fa14edf 100644 --- a/tests/test_sensor_reading.py +++ b/tests/test_sensor_reading.py @@ -47,7 +47,7 @@ def test_read_distance_timeout_returns_last_distance( def test_read_distance_invalid_frame_returns_last_distance( serial_factory: FakeSerialFactory, ) -> None: - """Verify that corrupted expected frames fail explicitly and preserve the last good value.""" + """Verify corrupt frames fail explicitly and preserve the last good value.""" sensor = XkcKl200(port="/dev/ttyUSB0", timeout=0.01, serial_factory=serial_factory) serial_port = serial_factory.holder["serial"] sensor._last_received_distance_mm = 55 @@ -156,7 +156,7 @@ def test_read_distance_skips_stale_valid_frame( def test_read_distance_skips_stale_valid_frame_in_same_chunk( serial_factory: FakeSerialFactory, ) -> None: - """Verify that a coalesced stale frame and measurement still succeed at zero timeout.""" + """Verify a stale frame and measurement still succeed at zero timeout.""" sensor = XkcKl200(port="/dev/ttyUSB0", timeout=0.0, serial_factory=serial_factory) serial_port = serial_factory.holder["serial"] serial_port.queue_read( @@ -178,7 +178,7 @@ def test_read_distance_skips_stale_valid_frame_in_same_chunk( def test_read_distance_resynchronizes_within_checksum_valid_overlap( serial_factory: FakeSerialFactory, ) -> None: - """Verify that an overlapped checksum-valid window still recovers the buffered measurement.""" + """Verify an overlapped valid window recovers the buffered measurement.""" sensor = XkcKl200(port="/dev/ttyUSB0", timeout=0.0, serial_factory=serial_factory) serial_port = serial_factory.holder["serial"] measurement_frame = build_command_frame( @@ -221,7 +221,7 @@ def test_change_baud_rate_accepts_real_baudrate( def test_change_baud_rate_requires_command_header_ack( serial_factory: FakeSerialFactory, ) -> None: - """Verify that change_baud_rate ignores a stale system-header ACK sharing command 0x30.""" + """Verify baud changes ignore a stale system ACK sharing command 0x30.""" sensor = XkcKl200(port="/dev/ttyUSB0", timeout=0.0, serial_factory=serial_factory) serial_port = serial_factory.holder["serial"] serial_port.queue_read( diff --git a/tests/test_serial_manager.py b/tests/test_serial_manager.py index 90e19b9..e01f125 100644 --- a/tests/test_serial_manager.py +++ b/tests/test_serial_manager.py @@ -144,7 +144,7 @@ def test_read_frame_skips_leading_junk(serial_factory: FakeSerialFactory) -> Non def test_extract_frame_keeps_partial_header_after_leading_junk( serial_factory: FakeSerialFactory, ) -> None: - """Verify that helper resynchronization keeps a partial header candidate for later bytes.""" + """Verify resynchronization keeps a partial header for later bytes.""" manager = SerialManager( config=SensorConfig(port="/dev/ttyUSB0"), serial_factory=serial_factory, @@ -249,7 +249,7 @@ def test_read_frame_skips_invalid_length_then_recovers( def test_read_frame_invalid_length_reports_response_error( serial_factory: FakeSerialFactory, ) -> None: - """Verify that a complete malformed frame reports a response error after resync fails.""" + """Verify a malformed frame reports an error after resync fails.""" manager = SerialManager( config=SensorConfig(port="/dev/ttyUSB0"), serial_factory=serial_factory, @@ -283,7 +283,7 @@ def test_read_frame_checksum_error(serial_factory: FakeSerialFactory) -> None: def test_read_frame_recovers_from_checksum_error_with_buffered_valid_reply( serial_factory: FakeSerialFactory, ) -> None: - """Verify that a bad frame does not discard a valid reply already buffered behind it.""" + """Verify a bad frame retains a valid reply buffered behind it.""" manager = SerialManager( config=SensorConfig(port="/dev/ttyUSB0"), serial_factory=serial_factory, @@ -308,7 +308,7 @@ def test_read_frame_recovers_from_checksum_error_with_buffered_valid_reply( def test_read_frame_waits_through_interframe_gap_after_checksum_error( serial_factory: FakeSerialFactory, monkeypatch: pytest.MonkeyPatch ) -> None: - """Verify that a malformed frame does not fail early while a valid reply is still in flight.""" + """Verify a malformed frame waits while a valid reply is still in flight.""" manager = SerialManager( config=SensorConfig(port="/dev/ttyUSB0"), serial_factory=serial_factory, @@ -343,7 +343,7 @@ def fake_sleep(duration: float) -> None: def test_read_frame_recovers_from_checksum_error_with_next_ready_chunk( serial_factory: FakeSerialFactory, ) -> None: - """Verify that a bad frame can recover using a valid reply read immediately afterward.""" + """Verify a bad frame can recover with a valid reply read afterward.""" manager = SerialManager( config=SensorConfig(port="/dev/ttyUSB0"), serial_factory=serial_factory, @@ -369,7 +369,7 @@ def test_read_frame_recovers_from_checksum_error_with_next_ready_chunk( def test_read_frame_returns_deferred_checksum_error_at_timeout( serial_factory: FakeSerialFactory, monkeypatch: pytest.MonkeyPatch ) -> None: - """Verify that malformed frames still surface as errors once the timeout window closes.""" + """Verify malformed frames surface as errors after the timeout closes.""" manager = SerialManager( config=SensorConfig(port="/dev/ttyUSB0"), serial_factory=serial_factory, @@ -399,7 +399,7 @@ def test_read_frame_returns_deferred_checksum_error_at_timeout( def test_read_frame_invalid_header_reports_response_error( serial_factory: FakeSerialFactory, ) -> None: - """Verify that a complete reply with a corrupted header is not misreported as a timeout.""" + """Verify a reply with a corrupt header is not reported as a timeout.""" manager = SerialManager( config=SensorConfig(port="/dev/ttyUSB0"), serial_factory=serial_factory, @@ -418,7 +418,7 @@ def test_read_frame_invalid_header_reports_response_error( def test_read_frame_wrong_header_for_unique_command_reports_response_error( serial_factory: FakeSerialFactory, ) -> None: - """Verify that a wrong-header reply for a unique command is reported as malformed.""" + """Verify a wrong-header reply for a unique command is malformed.""" manager = SerialManager( config=SensorConfig(port="/dev/ttyUSB0"), serial_factory=serial_factory, @@ -441,7 +441,7 @@ def test_read_frame_wrong_header_for_unique_command_reports_response_error( def test_read_frame_matches_expected_header_and_command_for_ambiguous_opcode( serial_factory: FakeSerialFactory, ) -> None: - """Verify that replies with the right command byte but wrong header are skipped only when requested.""" + """Verify wrong-header replies are skipped only when requested.""" manager = SerialManager( config=SensorConfig(port="/dev/ttyUSB0"), serial_factory=serial_factory, @@ -467,7 +467,7 @@ def test_read_frame_matches_expected_header_and_command_for_ambiguous_opcode( def test_read_frame_resynchronizes_within_checksum_valid_mismatched_candidate( serial_factory: FakeSerialFactory, ) -> None: - """Verify that a checksum-valid overlapped candidate does not discard the real reply.""" + """Verify a valid overlapped candidate does not discard the real reply.""" manager = SerialManager( config=SensorConfig(port="/dev/ttyUSB0"), serial_factory=serial_factory, @@ -490,7 +490,7 @@ def test_read_frame_resynchronizes_within_checksum_valid_mismatched_candidate( def test_read_frame_zero_timeout_rescans_buffer_after_skipping_stale_frame( serial_factory: FakeSerialFactory, ) -> None: - """Verify that a stale frame consumed after timeout does not hide a buffered valid reply.""" + """Verify a stale frame after timeout does not hide a valid reply.""" manager = SerialManager( config=SensorConfig(port="/dev/ttyUSB0"), serial_factory=serial_factory, @@ -510,7 +510,7 @@ def test_read_frame_zero_timeout_rescans_buffer_after_skipping_stale_frame( def test_read_frame_reports_checksum_error_for_corrupted_command( serial_factory: FakeSerialFactory, ) -> None: - """Verify that a bad checksum is reported even when the command byte is corrupted.""" + """Verify a bad checksum is reported despite a corrupt command byte.""" manager = SerialManager( config=SensorConfig(port="/dev/ttyUSB0"), serial_factory=serial_factory, diff --git a/uv.lock b/uv.lock index 4cd8fe8..f9eeeb0 100644 --- a/uv.lock +++ b/uv.lock @@ -368,7 +368,7 @@ dev = [ { name = "mypy", specifier = ">=2.1.0" }, { name = "pytest", specifier = ">=9.0.3" }, { name = "pytest-cov", specifier = ">=7.1.0" }, - { name = "ruff", specifier = ">=0.15.17" }, + { name = "ruff", specifier = ">=0.16.0" }, { name = "types-pyserial", specifier = ">=3.5.0" }, { name = "vulture", specifier = ">=2.16" }, ]