Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 13 additions & 58 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
Expand All @@ -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
Expand All @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion src/xkc_kl200_python/_serial_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
6 changes: 3 additions & 3 deletions tests/test_sensor_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
8 changes: 4 additions & 4 deletions tests/test_sensor_reading.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
24 changes: 12 additions & 12 deletions tests/test_serial_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.