From 1c566743794a3a51d7bdf3dd477de7cd9d4a82dc Mon Sep 17 00:00:00 2001 From: Casey Date: Fri, 5 Jun 2026 12:16:40 +0200 Subject: [PATCH] Clean up all remaining ruff errors; lint is now fully green Config: - .ruff.toml target-version py310 -> py312, matching pyproject's requires-python (>=3.12) and the 3.12 'type' aliases switch.py already ships. Unblocks two false invalid-syntax errors. - Ignore D102 in tests (test-class methods), consistent with the existing D100/D103 test ignores. Code (no behavior changes): - COM812 trailing commas + UP017 dt.UTC via ruff --fix, black re-wrap - RUF002: unicode minus/en-dash -> ASCII hyphen in docstrings - SIM105: contextlib.suppress for the lux-reading parse in config_flow - TRY300: move return out of try in _read_lux_sensor - PLC0415: hoist inline asyncio/logging imports to module top - E741: rename ambiguous 'l' comprehension variable to light_id - PT006/PT018: parametrize tuple + split compound assertion - D102: docstring for OptionsFlow.async_step_init - ARG001/ARG002: noqa with justification on HA-required signatures - PLR0912/PLR0915: noqa on prepare_adaptation_data and _update_attrs_and_maybe_adapt_lights (upstream-inherited complexity; splitting would hurt readability more than it helps) ./scripts/lint passes clean; 148 tests pass. --- .ruff.toml | 5 +- .../adaptive_lighting/__init__.py | 5 +- .../adaptive_lighting/color_and_brightness.py | 8 ++-- .../adaptive_lighting/config_flow.py | 6 +-- .../adaptive_lighting/hass_utils.py | 3 +- custom_components/adaptive_lighting/switch.py | 14 ++++-- tests/test_color_and_brightness.py | 4 +- tests/test_config_flow.py | 48 ++++++++++++++----- tests/test_init.py | 3 +- tests/test_number_platform.py | 7 ++- webapp/app.py | 2 +- 11 files changed, 68 insertions(+), 37 deletions(-) diff --git a/.ruff.toml b/.ruff.toml index fe94764b..3e6129f3 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -1,6 +1,8 @@ # The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml -target-version = "py310" +# Matches pyproject.toml's requires-python (>=3.12); switch.py already uses +# 3.12-only `type` alias statements. +target-version = "py312" [lint] select = ["ALL"] @@ -27,6 +29,7 @@ ignore = [ "tests/*.py" = [ "ARG001", # Unused function argument: `call` "D100", # Missing docstring in public module + "D102", # Missing docstring in public method (test-class methods) "D103", # Missing docstring in public function "D205", # 1 blank line required between summary line and description "D400", # First line should end with a period diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index dd1d0323..83ed5222 100644 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -123,7 +123,10 @@ def _remove_orphan_lux_sensors( registry.async_remove(entry.entity_id) -async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_migrate_entry( + hass: HomeAssistant, # noqa: ARG001 — signature required by HA + config_entry: ConfigEntry, +) -> bool: """Reject older config-entry versions with a friendly recreate message. Spec R8 + design D4: this fork deliberately does not migrate upstream diff --git a/custom_components/adaptive_lighting/color_and_brightness.py b/custom_components/adaptive_lighting/color_and_brightness.py index 223703eb..2b00b252 100644 --- a/custom_components/adaptive_lighting/color_and_brightness.py +++ b/custom_components/adaptive_lighting/color_and_brightness.py @@ -145,12 +145,12 @@ def _tanh_day_curve( """Piecewise tanh ramp from value_min to value_max around sunrise/sunset. Schema (specs/options-flow/spec.md R4): - - t ≤ t_sunrise − half_width: value_min - - t_sunrise − half_width < t < t_sunrise + half_width: + - t ≤ t_sunrise - half_width: value_min + - t_sunrise - half_width < t < t_sunrise + half_width: tanh ramp min → max - - t_sunrise + half_width ≤ t ≤ t_sunset − half_width: + - t_sunrise + half_width ≤ t ≤ t_sunset - half_width: value_max - - t_sunset − half_width < t < t_sunset + half_width: + - t_sunset - half_width < t < t_sunset + half_width: tanh ramp max → min - t ≥ t_sunset + half_width: value_min """ diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py index f87d447b..5923135d 100644 --- a/custom_components/adaptive_lighting/config_flow.py +++ b/custom_components/adaptive_lighting/config_flow.py @@ -6,6 +6,7 @@ Diagnostics) using HA's `section()` helper and native selectors throughout. See design.md decisions 1, 5, 6, 9, 14 and specs/options-flow/spec.md R1-R7. """ +import contextlib import logging from typing import Any @@ -464,6 +465,7 @@ class OptionsFlowHandler(OptionsFlowWithReload): continue async def async_step_init(self, user_input: dict[str, Any] | None = None): + """Render the single-page options form and handle its submission.""" conf = self.config_entry if conf.source == SOURCE_IMPORT: return self.async_abort(reason="yaml_managed") @@ -508,10 +510,8 @@ class OptionsFlowHandler(OptionsFlowWithReload): if lux_sensor_id: lux_state = self.hass.states.get(lux_sensor_id) if lux_state and lux_state.state not in ("unavailable", "unknown"): - try: + with contextlib.suppress(TypeError, ValueError): lux_reading = f"{float(lux_state.state):.0f} lx" - except (TypeError, ValueError): - pass return self.async_show_form( step_id="init", diff --git a/custom_components/adaptive_lighting/hass_utils.py b/custom_components/adaptive_lighting/hass_utils.py index 550ae350..1382c377 100644 --- a/custom_components/adaptive_lighting/hass_utils.py +++ b/custom_components/adaptive_lighting/hass_utils.py @@ -1,5 +1,6 @@ """Utility functions for HA core.""" +import asyncio import logging from collections.abc import Awaitable, Callable @@ -81,8 +82,6 @@ def setup_service_call_interceptor( call.data, ) # Call original service handler with processed data - import asyncio - target = existing_service.job.target if asyncio.iscoroutinefunction(target): await target(call) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 14474dad..ae07f687 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1112,9 +1112,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): return None try: value = float(state.state) - return value if value > 0 else None except (TypeError, ValueError): return None + return value if value > 0 else None def _call_on_remove_callbacks(self) -> None: """Call callbacks registered by async_on_remove.""" @@ -1211,7 +1211,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): force=False, ) - async def prepare_adaptation_data( + # Complexity inherited from upstream's per-light adaptation pipeline; + # splitting it would obscure the linear lux-gate -> service-data flow. + async def prepare_adaptation_data( # noqa: PLR0912, PLR0915 self, light: str, transition: int | None = None, @@ -1502,7 +1504,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): SIGNAL_OUTPUTS_UPDATED.format(entry_id=self._config_entry.entry_id), ) - async def _update_attrs_and_maybe_adapt_lights( + async def _update_attrs_and_maybe_adapt_lights( # noqa: PLR0912 self, *, context: Context, @@ -1542,7 +1544,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if self._lux_turned_off and self._target_lux > 0: current_lux = self._read_lux_sensor() if current_lux is not None and current_lux <= self._target_lux: - recovering = [l for l in self._lux_turned_off if l in lights] + recovering = [ + light_id for light_id in self._lux_turned_off if light_id in lights + ] self._lux_turned_off -= set(recovering) for light_id in recovering: _LOGGER.debug( @@ -1653,7 +1657,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): def fire_manual_control_event( self, light: str, - context: Context, + context: Context, # noqa: ARG002 — signature kept for manager compatibility ) -> None: """Deprecated no-op kept for compatibility with the manager bus listener. diff --git a/tests/test_color_and_brightness.py b/tests/test_color_and_brightness.py index a5ba8b3a..77f29e5b 100644 --- a/tests/test_color_and_brightness.py +++ b/tests/test_color_and_brightness.py @@ -42,12 +42,12 @@ class TestBrightnessCurve: """Spec R4: piecewise tanh ramp around the two sun events.""" def test_min_brightness_more_than_half_width_before_sunrise(self, settings): - """T = sunrise − 2h → still deep night, brightness at minimum.""" + """T = sunrise - 2h → still deep night, brightness at minimum.""" t = T_SUNRISE - timedelta(hours=2) assert settings.brightness_pct(t, T_SUNRISE, T_SUNSET) == 5 def test_min_brightness_exactly_at_clamp_boundary(self, settings): - """T = sunrise − half_width → exactly at the boundary, still min.""" + """T = sunrise - half_width → exactly at the boundary, still min.""" t = T_SUNRISE - timedelta(seconds=HALF_WIDTH) assert settings.brightness_pct(t, T_SUNRISE, T_SUNSET) == 5 diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index fa677cbc..f90f8586 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -101,7 +101,9 @@ def _section_inner_keys(schema_section) -> set[str]: def test_options_schema_has_all_seven_sections_in_order() -> None: """R1: the options form returns the seven named sections in order.""" schema = _build_options_schema( - {}, show_send_split_delay=False, show_target_lux=False + {}, + show_send_split_delay=False, + show_target_lux=False, ) keys = [ k.schema if hasattr(k, "schema") else k @@ -115,7 +117,9 @@ def test_each_section_contains_only_its_specified_fields() -> None: the layout table. """ schema = _build_options_schema( - {}, show_send_split_delay=True, show_target_lux=False + {}, + show_send_split_delay=True, + show_target_lux=False, ) for marker in schema.schema: # type: ignore[attr-defined] section_id = marker.schema if hasattr(marker, "schema") else marker @@ -137,7 +141,9 @@ def test_each_section_contains_only_its_specified_fields() -> None: def test_send_split_delay_hidden_when_driver_false() -> None: """R2: send_split_delay is absent when separate_turn_on_commands=False.""" schema = _build_options_schema( - {}, show_send_split_delay=False, show_target_lux=False + {}, + show_send_split_delay=False, + show_target_lux=False, ) advanced_marker = next( m @@ -151,7 +157,9 @@ def test_send_split_delay_hidden_when_driver_false() -> None: def test_send_split_delay_visible_when_driver_true() -> None: """R2: send_split_delay appears when separate_turn_on_commands=True.""" schema = _build_options_schema( - {}, show_send_split_delay=True, show_target_lux=False + {}, + show_send_split_delay=True, + show_target_lux=False, ) advanced_marker = next( m @@ -170,7 +178,9 @@ def test_send_split_delay_visible_when_driver_true() -> None: def test_default_sunrise_and_sunset_entities() -> None: """R3: default entities point at the built-in sun.sun sensors.""" schema = _build_options_schema( - {}, show_send_split_delay=False, show_target_lux=False + {}, + show_send_split_delay=False, + show_target_lux=False, ) sun_marker = next( m @@ -200,7 +210,9 @@ def test_sun_entity_selectors_are_strict_timestamp_sensors() -> None: device_class=timestamp. """ schema = _build_options_schema( - {}, show_send_split_delay=False, show_target_lux=False + {}, + show_send_split_delay=False, + show_target_lux=False, ) sun_marker = next( m @@ -229,9 +241,11 @@ def test_sun_entity_selectors_are_strict_timestamp_sensors() -> None: def test_brightness_uses_slider_number_selector() -> None: - """R5: brightness fields are NumberSelectors with slider mode, 1–100 %, step 1.""" + """R5: brightness fields are NumberSelectors with slider mode, 1-100 %, step 1.""" schema = _build_options_schema( - {}, show_send_split_delay=False, show_target_lux=False + {}, + show_send_split_delay=False, + show_target_lux=False, ) daytime_marker = next( m @@ -252,9 +266,11 @@ def test_brightness_uses_slider_number_selector() -> None: def test_color_temp_uses_box_number_selector() -> None: - """R5: color-temp fields are NumberSelectors, 1000–10000 K, step 100.""" + """R5: color-temp fields are NumberSelectors, 1000-10000 K, step 100.""" schema = _build_options_schema( - {}, show_send_split_delay=False, show_target_lux=False + {}, + show_send_split_delay=False, + show_target_lux=False, ) daytime_marker = next( m @@ -276,7 +292,9 @@ def test_color_temp_uses_box_number_selector() -> None: def test_booleans_use_boolean_selector() -> None: """R5: every boolean field renders as a BooleanSelector.""" schema = _build_options_schema( - {}, show_send_split_delay=True, show_target_lux=False + {}, + show_send_split_delay=True, + show_target_lux=False, ) boolean_fields = { CONF_PREFER_RGB_COLOR, @@ -368,7 +386,9 @@ async def test_options_flow_renders_sectioned_schema(hass) -> None: def test_target_lux_hidden_when_no_sensor() -> None: """target_lux should not appear when lux_sensor is empty.""" schema = _build_options_schema( - {}, show_send_split_delay=False, show_target_lux=False + {}, + show_send_split_delay=False, + show_target_lux=False, ) lux_marker = next( m @@ -400,7 +420,9 @@ def test_target_lux_visible_when_sensor_set() -> None: def test_lux_sensor_selector_filters_to_illuminance() -> None: """lux_sensor entity selector should filter to device_class=illuminance.""" schema = _build_options_schema( - {}, show_send_split_delay=False, show_target_lux=False + {}, + show_send_split_delay=False, + show_target_lux=False, ) lux_marker = next( m diff --git a/tests/test_init.py b/tests/test_init.py index 949b83a0..b6835ac8 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -56,7 +56,8 @@ async def test_stale_version_raises_config_entry_error(hass, caplog) -> None: # The full captured log (which includes the exception traceback) should # carry our friendly "delete and recreate" message so the user sees it. text = caplog.text.lower() - assert "delete" in text and "recreate" in text + assert "delete" in text + assert "recreate" in text async def test_unload_entry(hass) -> None: diff --git a/tests/test_number_platform.py b/tests/test_number_platform.py index d59c6534..9d66c52c 100644 --- a/tests/test_number_platform.py +++ b/tests/test_number_platform.py @@ -7,6 +7,7 @@ no-reload-on-slider write, and the curve-math read path. from __future__ import annotations import datetime +import logging from unittest.mock import patch import pytest @@ -138,7 +139,7 @@ async def test_number_entities_share_switch_device(hass) -> None: @pytest.mark.parametrize( - "field_key,expected", + ("field_key", "expected"), [ ("min_brightness", {"min": 1.0, "max": 100.0, "step": 1, "unit": "%"}), ("max_brightness", {"min": 1.0, "max": 100.0, "step": 1, "unit": "%"}), @@ -259,7 +260,7 @@ async def test_restore_state_survives_restart(hass) -> None: object.__setattr__( entry, "modified_at", - datetime.datetime(2020, 1, 1, tzinfo=datetime.timezone.utc), + datetime.datetime(2020, 1, 1, tzinfo=datetime.UTC), ) # Prime the restore cache with a saved native_value of 30. @@ -412,8 +413,6 @@ async def test_curve_math_falls_back_on_unavailable(hass, caplog) -> None: al_data = hass.data[DOMAIN][entry.entry_id] al_switch = al_data["switch"] - import logging - caplog.set_level(logging.DEBUG) settings = al_switch.sun_light_settings assert settings.max_brightness == 88 # fell back to options diff --git a/webapp/app.py b/webapp/app.py index 68dbb2f9..d3db44d9 100644 --- a/webapp/app.py +++ b/webapp/app.py @@ -298,7 +298,7 @@ def time_to_float(time: dt.time | dt.datetime) -> float: def _kw(input): - location = Location(LocationInfo(timezone=dt.timezone.utc)) + location = Location(LocationInfo(timezone=dt.UTC)) return { "name": "Adaptive Lighting Simulator", "adapt_until_sleep": input.adapt_until_sleep(),