tests: add type annotations to Smart tests (#1686)
Some checks are pending
CI / Perform Lint Checks (3.14) (push) Waiting to run
CI / Python 3.11 on macos-latest (push) Blocked by required conditions
CI / Python 3.12 on macos-latest (push) Blocked by required conditions
CI / Python 3.13 on macos-latest (push) Blocked by required conditions
CI / Python 3.14 on macos-latest (push) Blocked by required conditions
CI / Python 3.11 on ubuntu-latest (push) Blocked by required conditions
CI / Python 3.12 on ubuntu-latest (push) Blocked by required conditions
CI / Python 3.13 on ubuntu-latest (push) Blocked by required conditions
CI / Python 3.14 on ubuntu-latest (push) Blocked by required conditions
CI / Python 3.11 on windows-latest (push) Blocked by required conditions
CI / Python 3.12 on windows-latest (push) Blocked by required conditions
CI / Python 3.13 on windows-latest (push) Blocked by required conditions
CI / Python 3.14 on windows-latest (push) Blocked by required conditions
CodeQL Checks / Analyze (python) (push) Waiting to run

Add type annotations across all 'smart' tests, enabling mypy to check test function bodies, catching type errors that were previously hidden.
This commit is contained in:
ZeliardM
2026-07-07 18:11:59 -04:00
committed by GitHub
parent 8840fc395e
commit 88e1c27bd0
30 changed files with 192 additions and 125 deletions

View File

@@ -9,7 +9,7 @@ brightness = parametrize("brightness smart", component_filter="brightness")
@brightness
async def test_brightness_component(dev: SmartDevice):
async def test_brightness_component(dev: SmartDevice) -> None:
"""Test brightness feature."""
brightness = next(get_parent_and_child_modules(dev, "Brightness"))
assert brightness
@@ -35,7 +35,7 @@ async def test_brightness_component(dev: SmartDevice):
@dimmable_iot
async def test_brightness_dimmable(dev: IotDevice):
async def test_brightness_dimmable(dev: IotDevice) -> None:
"""Test brightness feature."""
assert isinstance(dev, IotDevice)
assert "brightness" in dev.sys_info or bool(dev.sys_info["is_dimmable"])

View File

@@ -6,7 +6,7 @@ from ...conftest import variable_temp_smart
@variable_temp_smart
async def test_colortemp_component(dev: SmartDevice):
async def test_colortemp_component(dev: SmartDevice) -> None:
"""Test brightness feature."""
assert isinstance(dev, SmartDevice)
assert "color_temperature" in dev._components

View File

@@ -1,17 +1,32 @@
from __future__ import annotations
from typing import TypedDict
import pytest
from pytest_mock import MockerFixture
from kasa import Module
from kasa.smart import SmartDevice
from kasa.smart.modules import Alarm
from kasa.smart.modules.alarm import AlarmVolume
from ...device_fixtures import get_parent_and_child_modules, parametrize
alarm = parametrize("has alarm", component_filter="alarm", protocol_filter={"SMART"})
class PlayAlarmKwargs(TypedDict, total=False):
duration: int
volume: AlarmVolume | int
sound: str
class PlayAlarmRequestParams(TypedDict, total=False):
alarm_duration: int
alarm_volume: AlarmVolume
alarm_type: str
@alarm
@pytest.mark.parametrize(
("feature", "prop_name", "type"),
@@ -23,7 +38,9 @@ alarm = parametrize("has alarm", component_filter="alarm", protocol_filter={"SMA
("alarm_volume_level", "alarm_volume", int),
],
)
async def test_features(dev: SmartDevice, feature: str, prop_name: str, type: type):
async def test_features(
dev: SmartDevice, feature: str, prop_name: str, type: type
) -> None:
"""Test that features are registered and work as expected."""
alarm = next(get_parent_and_child_modules(dev, Module.Alarm))
assert alarm is not None
@@ -37,7 +54,7 @@ async def test_features(dev: SmartDevice, feature: str, prop_name: str, type: ty
@alarm
async def test_volume_feature(dev: SmartDevice):
async def test_volume_feature(dev: SmartDevice) -> None:
"""Test that volume features have correct choices and range."""
alarm = next(get_parent_and_child_modules(dev, Module.Alarm))
assert alarm is not None
@@ -64,7 +81,12 @@ async def test_volume_feature(dev: SmartDevice):
),
],
)
async def test_play(dev: SmartDevice, kwargs, request_params, mocker: MockerFixture):
async def test_play(
dev: SmartDevice,
kwargs: PlayAlarmKwargs,
request_params: PlayAlarmRequestParams,
mocker: MockerFixture,
) -> None:
"""Test that play parameters are handled correctly."""
alarm: Alarm = next(get_parent_and_child_modules(dev, Module.Alarm))
call_spy = mocker.spy(alarm, "call")
@@ -86,7 +108,7 @@ async def test_play(dev: SmartDevice, kwargs, request_params, mocker: MockerFixt
@alarm
async def test_stop(dev: SmartDevice, mocker: MockerFixture):
async def test_stop(dev: SmartDevice, mocker: MockerFixture) -> None:
"""Test that stop creates the correct call."""
alarm: Alarm = next(get_parent_and_child_modules(dev, Module.Alarm))
call_spy = mocker.spy(alarm, "call")
@@ -112,7 +134,7 @@ async def test_set_alarm_configure(
method: str,
value: str | int,
target_key: str,
):
) -> None:
"""Test that set_alarm_sound creates the correct call."""
alarm: Alarm = next(get_parent_and_child_modules(dev, Module.Alarm))
call_spy = mocker.spy(alarm, "call")

View File

@@ -26,7 +26,7 @@ autooff = parametrize(
)
async def test_autooff_features(
dev: SmartDevice, feature: str, prop_name: str, type: type
):
) -> None:
"""Test that features are registered and work as expected."""
autooff = next(get_parent_and_child_modules(dev, Module.AutoOff))
assert autooff is not None
@@ -40,7 +40,7 @@ async def test_autooff_features(
@autooff
async def test_settings(dev: SmartDevice, mocker: MockerFixture):
async def test_settings(dev: SmartDevice, mocker: MockerFixture) -> None:
"""Test autooff settings."""
autooff = next(get_parent_and_child_modules(dev, Module.AutoOff))
assert autooff
@@ -79,7 +79,7 @@ async def test_settings(dev: SmartDevice, mocker: MockerFixture):
@pytest.mark.parametrize("is_timer_active", [True, False])
async def test_auto_off_at(
dev: SmartDevice, mocker: MockerFixture, is_timer_active: bool
):
) -> None:
"""Test auto-off at sensor."""
autooff = next(get_parent_and_child_modules(dev, Module.AutoOff))
assert autooff

View File

@@ -1,7 +1,7 @@
import pytest
from kasa import Module
from kasa.smart.modules import ChildLock
from kasa.smart import SmartDevice
from ...device_fixtures import parametrize
@@ -19,9 +19,11 @@ childlock = parametrize(
("child_lock", "enabled", bool),
],
)
async def test_features(dev, feature, prop_name, type):
async def test_features(
dev: SmartDevice, feature: str, prop_name: str, type: type
) -> None:
"""Test that features are registered and work as expected."""
protect: ChildLock = dev.modules[Module.ChildLock]
protect = dev.modules[Module.ChildLock]
assert protect is not None
prop = getattr(protect, prop_name)
@@ -33,9 +35,9 @@ async def test_features(dev, feature, prop_name, type):
@childlock
async def test_enabled(dev):
async def test_enabled(dev: SmartDevice) -> None:
"""Test the API."""
protect: ChildLock = dev.modules[Module.ChildLock]
protect = dev.modules[Module.ChildLock]
assert protect is not None
assert isinstance(protect.enabled, bool)

View File

@@ -1,7 +1,7 @@
import pytest
from kasa import Module
from kasa.smart.modules import ChildProtection
from kasa.smart import SmartDevice
from ...device_fixtures import parametrize
@@ -19,9 +19,11 @@ child_protection = parametrize(
("child_lock", "enabled", bool),
],
)
async def test_features(dev, feature, prop_name, type):
async def test_features(
dev: SmartDevice, feature: str, prop_name: str, type: type
) -> None:
"""Test that features are registered and work as expected."""
protect: ChildProtection = dev.modules[Module.ChildProtection]
protect = dev.modules[Module.ChildProtection]
assert protect is not None
prop = getattr(protect, prop_name)
@@ -33,9 +35,9 @@ async def test_features(dev, feature, prop_name, type):
@child_protection
async def test_enabled(dev):
async def test_enabled(dev: SmartDevice) -> None:
"""Test the API."""
protect: ChildProtection = dev.modules[Module.ChildProtection]
protect = dev.modules[Module.ChildProtection]
assert protect is not None
assert isinstance(protect.enabled, bool)

View File

@@ -15,7 +15,7 @@ childsetup = parametrize(
@childsetup
async def test_childsetup_features(dev: Device):
async def test_childsetup_features(dev: Device) -> None:
"""Test the exposed features."""
cs = dev.modules.get(Module.ChildSetup)
assert cs
@@ -28,7 +28,7 @@ async def test_childsetup_features(dev: Device):
@childsetup
async def test_childsetup_pair(
dev: Device, mocker: MockerFixture, caplog: pytest.LogCaptureFixture
):
) -> None:
"""Test device pairing."""
caplog.set_level(logging.INFO)
mock_query_helper = mocker.spy(dev, "_query_helper")
@@ -52,7 +52,7 @@ async def test_childsetup_pair(
@childsetup
async def test_childsetup_unpair(
dev: Device, mocker: MockerFixture, caplog: pytest.LogCaptureFixture
):
) -> None:
"""Test unpair."""
mock_query_helper = mocker.spy(dev, "_query_helper")
DUMMY_ID = "dummy_id"

View File

@@ -25,7 +25,9 @@ clean = parametrize("clean module", component_filter="clean", protocol_filter={"
("battery_level", "battery", int),
],
)
async def test_features(dev: SmartDevice, feature: str, prop_name: str, type: type):
async def test_features(
dev: SmartDevice, feature: str, prop_name: str, type: type
) -> None:
"""Test that features are registered and work as expected."""
clean = next(get_parent_and_child_modules(dev, Module.Clean))
assert clean is not None
@@ -94,7 +96,7 @@ async def test_actions(
value: str | int,
method: str,
params: dict,
):
) -> None:
"""Test the clean actions."""
clean = next(get_parent_and_child_modules(dev, Module.Clean))
call = mocker.spy(clean, "call")
@@ -130,7 +132,7 @@ async def test_post_update_hook(
error: ErrorCode,
warning_msg: str | None,
caplog: pytest.LogCaptureFixture,
):
) -> None:
"""Test that post update hook sets error states correctly."""
clean = next(get_parent_and_child_modules(dev, Module.Clean))
assert clean
@@ -160,7 +162,7 @@ async def test_post_update_hook(
@clean
async def test_resume(dev: SmartDevice, mocker: MockerFixture):
async def test_resume(dev: SmartDevice, mocker: MockerFixture) -> None:
"""Test that start calls resume if the state is paused."""
clean = next(get_parent_and_child_modules(dev, Module.Clean))
@@ -182,7 +184,7 @@ async def test_resume(dev: SmartDevice, mocker: MockerFixture):
@clean
async def test_unknown_status(
dev: SmartDevice, mocker: MockerFixture, caplog: pytest.LogCaptureFixture
):
) -> None:
"""Test that unknown status is logged."""
clean = next(get_parent_and_child_modules(dev, Module.Clean))
@@ -227,7 +229,7 @@ async def test_invalid_settings(
value: str,
exc: type[Exception],
exc_message: str,
):
) -> None:
"""Test invalid settings."""
clean = next(get_parent_and_child_modules(dev, Module.Clean))

View File

@@ -27,7 +27,9 @@ cleanrecords = parametrize(
("last_clean_timestamp", "last_clean_timestamp", datetime),
],
)
async def test_features(dev: SmartDevice, feature: str, prop_name: str, type: type):
async def test_features(
dev: SmartDevice, feature: str, prop_name: str, type: type
) -> None:
"""Test that features are registered and work as expected."""
records = next(get_parent_and_child_modules(dev, Module.CleanRecords))
assert records is not None
@@ -41,7 +43,7 @@ async def test_features(dev: SmartDevice, feature: str, prop_name: str, type: ty
@cleanrecords
async def test_timezone(dev: SmartDevice):
async def test_timezone(dev: SmartDevice) -> None:
"""Test that timezone is added to timestamps."""
clean_records = next(get_parent_and_child_modules(dev, Module.CleanRecords))
assert clean_records is not None

View File

@@ -21,7 +21,7 @@ consumables = parametrize(
"consumable_name", [consumable.id for consumable in CONSUMABLE_METAS]
)
@pytest.mark.parametrize("postfix", ["used", "remaining"])
async def test_features(dev: SmartDevice, consumable_name: str, postfix: str):
async def test_features(dev: SmartDevice, consumable_name: str, postfix: str) -> None:
"""Test that features are registered and work as expected."""
consumables = next(get_parent_and_child_modules(dev, Module.Consumables))
assert consumables is not None
@@ -39,7 +39,7 @@ async def test_features(dev: SmartDevice, consumable_name: str, postfix: str):
)
async def test_erase(
dev: SmartDevice, mocker: MockerFixture, consumable_name: str, data_key: str
):
) -> None:
"""Test autocollection switch."""
consumables = next(get_parent_and_child_modules(dev, Module.Consumables))
call = mocker.spy(consumables, "call")

View File

@@ -16,7 +16,7 @@ contact = parametrize(
("is_open", bool),
],
)
async def test_contact_features(dev: Device, feature, type):
async def test_contact_features(dev: Device, feature: str, type: type) -> None:
"""Test that features are registered and work as expected."""
contact = dev.modules.get(Module.ContactSensor)
assert contact is not None

View File

@@ -22,7 +22,9 @@ dustbin = parametrize(
("dustbin_mode", "mode", str),
],
)
async def test_features(dev: SmartDevice, feature: str, prop_name: str, type: type):
async def test_features(
dev: SmartDevice, feature: str, prop_name: str, type: type
) -> None:
"""Test that features are registered and work as expected."""
dustbin = next(get_parent_and_child_modules(dev, Module.Dustbin))
assert dustbin is not None
@@ -36,7 +38,7 @@ async def test_features(dev: SmartDevice, feature: str, prop_name: str, type: ty
@dustbin
async def test_dustbin_mode(dev: SmartDevice, mocker: MockerFixture):
async def test_dustbin_mode(dev: SmartDevice, mocker: MockerFixture) -> None:
"""Test dust mode."""
dustbin = next(get_parent_and_child_modules(dev, Module.Dustbin))
call = mocker.spy(dustbin, "call")
@@ -61,7 +63,7 @@ async def test_dustbin_mode(dev: SmartDevice, mocker: MockerFixture):
@dustbin
async def test_dustbin_mode_off(dev: SmartDevice, mocker: MockerFixture):
async def test_dustbin_mode_off(dev: SmartDevice, mocker: MockerFixture) -> None:
"""Test dustbin_mode == Off."""
dustbin = next(get_parent_and_child_modules(dev, Module.Dustbin))
call = mocker.spy(dustbin, "call")
@@ -80,7 +82,7 @@ async def test_dustbin_mode_off(dev: SmartDevice, mocker: MockerFixture):
@dustbin
async def test_autocollection(dev: SmartDevice, mocker: MockerFixture):
async def test_autocollection(dev: SmartDevice, mocker: MockerFixture) -> None:
"""Test autocollection switch."""
dustbin = next(get_parent_and_child_modules(dev, Module.Dustbin))
call = mocker.spy(dustbin, "call")
@@ -101,7 +103,7 @@ async def test_autocollection(dev: SmartDevice, mocker: MockerFixture):
@dustbin
async def test_empty_dustbin(dev: SmartDevice, mocker: MockerFixture):
async def test_empty_dustbin(dev: SmartDevice, mocker: MockerFixture) -> None:
"""Test the empty dustbin feature."""
dustbin = next(get_parent_and_child_modules(dev, Module.Dustbin))
call = mocker.spy(dustbin, "call")

View File

@@ -14,7 +14,7 @@ from tests.conftest import has_emeter_smart
@has_emeter_smart
async def test_supported(dev: SmartDevice):
async def test_supported(dev: SmartDevice) -> None:
energy_module = dev.modules.get(Module.Energy)
if not energy_module:
pytest.skip(f"Energy module not supported for {dev}.")
@@ -31,7 +31,7 @@ async def test_supported(dev: SmartDevice):
@has_emeter_smart
async def test_get_energy_usage_error(
dev: SmartDevice, caplog: pytest.LogCaptureFixture
):
) -> None:
"""Test errors on get_energy_usage."""
caplog.set_level(logging.DEBUG)

View File

@@ -11,7 +11,7 @@ fan = parametrize("has fan", component_filter="fan_control", protocol_filter={"S
@fan
async def test_fan_speed(dev: SmartDevice, mocker: MockerFixture):
async def test_fan_speed(dev: SmartDevice, mocker: MockerFixture) -> None:
"""Test fan speed feature."""
fan = next(get_parent_and_child_modules(dev, Module.Fan))
assert fan
@@ -35,7 +35,7 @@ async def test_fan_speed(dev: SmartDevice, mocker: MockerFixture):
@fan
async def test_sleep_mode(dev: SmartDevice, mocker: MockerFixture):
async def test_sleep_mode(dev: SmartDevice, mocker: MockerFixture) -> None:
"""Test sleep mode feature."""
fan = next(get_parent_and_child_modules(dev, Module.Fan))
assert fan
@@ -53,7 +53,7 @@ async def test_sleep_mode(dev: SmartDevice, mocker: MockerFixture):
@fan
async def test_fan_module(dev: SmartDevice, mocker: MockerFixture):
async def test_fan_module(dev: SmartDevice, mocker: MockerFixture) -> None:
"""Test fan speed on device interface."""
assert isinstance(dev, SmartDevice)
fan = next(get_parent_and_child_modules(dev, Module.Fan))
@@ -89,7 +89,7 @@ async def test_fan_module(dev: SmartDevice, mocker: MockerFixture):
@fan
async def test_fan_features(dev: SmartDevice, mocker: MockerFixture):
async def test_fan_features(dev: SmartDevice, mocker: MockerFixture) -> None:
"""Test fan speed on device interface."""
assert isinstance(dev, SmartDevice)
fan = next(get_parent_and_child_modules(dev, Module.Fan))

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import asyncio
import logging
from contextlib import nullcontext
from contextlib import AbstractContextManager, nullcontext
from datetime import date
from typing import TypedDict
@@ -31,9 +31,14 @@ firmware = parametrize(
],
)
async def test_firmware_features(
dev: SmartDevice, feature, prop_name, type, required_version, mocker: MockerFixture
):
"""Test light effect."""
dev: SmartDevice,
feature: str,
prop_name: str,
type: type,
required_version: int,
mocker: MockerFixture,
) -> None:
"""Test firmware features."""
fw = dev.modules.get(Module.Firmware)
assert fw
assert fw.firmware_update_info is None
@@ -54,7 +59,7 @@ async def test_firmware_features(
@firmware
async def test_firmware_update_info(dev: SmartDevice):
async def test_firmware_update_info(dev: SmartDevice) -> None:
"""Test that the firmware UpdateInfo object deserializes correctly."""
fw = dev.modules.get(Module.Firmware)
assert fw
@@ -68,7 +73,7 @@ async def test_firmware_update_info(dev: SmartDevice):
@firmware
async def test_update_available_without_cloud(dev: SmartDevice):
async def test_update_available_without_cloud(dev: SmartDevice) -> None:
"""Test that update_available returns None when disconnected."""
fw = dev.modules.get(Module.Firmware)
assert fw
@@ -95,9 +100,9 @@ async def test_firmware_update(
dev: SmartDevice,
mocker: MockerFixture,
caplog: pytest.LogCaptureFixture,
update_available,
expected_result,
):
update_available: bool,
expected_result: AbstractContextManager,
) -> None:
"""Test updating firmware."""
caplog.set_level(logging.INFO)

View File

@@ -9,7 +9,7 @@ homekit = parametrize(
@homekit
async def test_info(dev: SmartDevice):
async def test_info(dev: SmartDevice) -> None:
"""Test homekit info."""
homekit = dev.modules.get(Module.HomeKit)
assert homekit

View File

@@ -1,6 +1,7 @@
import pytest
from kasa.smart.modules import HumiditySensor
from kasa import Module
from kasa.smart import SmartDevice
from ...device_fixtures import parametrize
@@ -17,9 +18,9 @@ humidity = parametrize(
("humidity_warning", bool),
],
)
async def test_humidity_features(dev, feature, type):
async def test_humidity_features(dev: SmartDevice, feature: str, type: type) -> None:
"""Test that features are registered and work as expected."""
humidity: HumiditySensor = dev.modules["HumiditySensor"]
humidity = dev.modules[Module.HumiditySensor]
prop = getattr(humidity, feature)
assert isinstance(prop, type)

View File

@@ -16,7 +16,7 @@ light_effect = parametrize(
@light_effect
async def test_light_effect(dev: Device, mocker: MockerFixture):
async def test_light_effect(dev: Device, mocker: MockerFixture) -> None:
"""Test light effect."""
light_effect = dev.modules.get(Module.LightEffect)
assert isinstance(light_effect, LightEffect)
@@ -46,7 +46,7 @@ async def test_light_effect(dev: Device, mocker: MockerFixture):
@pytest.mark.parametrize("effect_active", [True, False])
async def test_light_effect_brightness(
dev: Device, effect_active: bool, mocker: MockerFixture
):
) -> None:
"""Test that light module uses light_effect for brightness when active."""
light_module = dev.modules[Module.Light]

View File

@@ -18,7 +18,7 @@ light_strip_effect = parametrize(
@light_strip_effect
async def test_light_strip_effect(dev: Device, mocker: MockerFixture):
async def test_light_strip_effect(dev: Device, mocker: MockerFixture) -> None:
"""Test light strip effect."""
light_effect = dev.modules.get(Module.LightEffect)
@@ -63,7 +63,7 @@ async def test_light_strip_effect(dev: Device, mocker: MockerFixture):
@pytest.mark.parametrize("effect_active", [True, False])
async def test_light_effect_brightness(
dev: Device, effect_active: bool, mocker: MockerFixture
):
) -> None:
"""Test that light module uses light_effect for brightness when active."""
light_module = dev.modules[Module.Light]

View File

@@ -23,7 +23,7 @@ light_transition_gt_v1 = parametrize(
@light_transition_v1
async def test_module_v1(dev: SmartDevice, mocker: MockerFixture):
async def test_module_v1(dev: SmartDevice, mocker: MockerFixture) -> None:
"""Test light transition module."""
assert isinstance(dev, SmartDevice)
light_transition = next(get_parent_and_child_modules(dev, Module.LightTransition))
@@ -42,7 +42,7 @@ async def test_module_v1(dev: SmartDevice, mocker: MockerFixture):
@light_transition_gt_v1
async def test_module_gt_v1(dev: SmartDevice, mocker: MockerFixture):
async def test_module_gt_v1(dev: SmartDevice, mocker: MockerFixture) -> None:
"""Test light transition module."""
assert isinstance(dev, SmartDevice)
light_transition = next(get_parent_and_child_modules(dev, Module.LightTransition))

View File

@@ -9,7 +9,7 @@ matter = parametrize(
@matter
async def test_info(dev: SmartDevice):
async def test_info(dev: SmartDevice) -> None:
"""Test matter info."""
matter = dev.modules.get(Module.Matter)
assert matter

View File

@@ -20,7 +20,9 @@ mop = parametrize("has mop", component_filter="mop", protocol_filter={"SMART"})
("mop_waterlevel", "waterlevel", str),
],
)
async def test_features(dev: SmartDevice, feature: str, prop_name: str, type: type):
async def test_features(
dev: SmartDevice, feature: str, prop_name: str, type: type
) -> None:
"""Test that features are registered and work as expected."""
mod = next(get_parent_and_child_modules(dev, Module.Mop))
assert mod is not None
@@ -34,7 +36,7 @@ async def test_features(dev: SmartDevice, feature: str, prop_name: str, type: ty
@mop
async def test_mop_waterlevel(dev: SmartDevice, mocker: MockerFixture):
async def test_mop_waterlevel(dev: SmartDevice, mocker: MockerFixture) -> None:
"""Test dust mode."""
mop_module = next(get_parent_and_child_modules(dev, Module.Mop))
call = mocker.spy(mop_module, "call")

View File

@@ -16,7 +16,7 @@ motion = parametrize(
("motion_detected", bool),
],
)
async def test_motion_features(dev: Device, feature, type):
async def test_motion_features(dev: Device, feature: str, type: type) -> None:
"""Test that features are registered and work as expected."""
motion = dev.modules.get(Module.MotionSensor)
assert motion is not None

View File

@@ -2,6 +2,7 @@ import pytest
from pytest_mock import MockerFixture
from kasa import Device, Module
from kasa.smart import SmartDevice
from ...device_fixtures import get_parent_and_child_modules, parametrize
@@ -20,7 +21,9 @@ powerprotection = parametrize(
("power_protection_threshold", "protection_threshold", int),
],
)
async def test_features(dev, feature, prop_name, type):
async def test_features(
dev: SmartDevice, feature: str, prop_name: str, type: type
) -> None:
"""Test that features are registered and work as expected."""
powerprot = next(get_parent_and_child_modules(dev, Module.PowerProtection))
assert powerprot
@@ -35,7 +38,7 @@ async def test_features(dev, feature, prop_name, type):
@powerprotection
async def test_set_enable(dev: Device, mocker: MockerFixture):
async def test_set_enable(dev: Device, mocker: MockerFixture) -> None:
"""Test enable."""
powerprot = next(get_parent_and_child_modules(dev, Module.PowerProtection))
assert powerprot
@@ -88,7 +91,7 @@ async def test_set_enable(dev: Device, mocker: MockerFixture):
@powerprotection
async def test_set_threshold(dev: Device, mocker: MockerFixture):
async def test_set_threshold(dev: Device, mocker: MockerFixture) -> None:
"""Test enable."""
powerprot = next(get_parent_and_child_modules(dev, Module.PowerProtection))
assert powerprot

View File

@@ -20,7 +20,9 @@ speaker = parametrize(
("volume", "volume", int),
],
)
async def test_features(dev: SmartDevice, feature: str, prop_name: str, type: type):
async def test_features(
dev: SmartDevice, feature: str, prop_name: str, type: type
) -> None:
"""Test that features are registered and work as expected."""
speaker = next(get_parent_and_child_modules(dev, Module.Speaker))
assert speaker is not None
@@ -34,7 +36,7 @@ async def test_features(dev: SmartDevice, feature: str, prop_name: str, type: ty
@speaker
async def test_set_volume(dev: SmartDevice, mocker: MockerFixture):
async def test_set_volume(dev: SmartDevice, mocker: MockerFixture) -> None:
"""Test speaker settings."""
speaker = next(get_parent_and_child_modules(dev, Module.Speaker))
assert speaker is not None
@@ -61,7 +63,7 @@ async def test_set_volume(dev: SmartDevice, mocker: MockerFixture):
@speaker
async def test_locate(dev: SmartDevice, mocker: MockerFixture):
async def test_locate(dev: SmartDevice, mocker: MockerFixture) -> None:
"""Test the locate method."""
speaker = next(get_parent_and_child_modules(dev, Module.Speaker))
call = mocker.spy(speaker, "call")

View File

@@ -1,6 +1,7 @@
import pytest
from kasa.smart.modules import TemperatureSensor
from kasa import Module
from kasa.smart import SmartDevice
from ...device_fixtures import parametrize
@@ -23,9 +24,9 @@ temperature_warning = parametrize(
("temperature_unit", str),
],
)
async def test_temperature_features(dev, feature, type):
async def test_temperature_features(dev: SmartDevice, feature: str, type: type) -> None:
"""Test that features are registered and work as expected."""
temp_module: TemperatureSensor = dev.modules["TemperatureSensor"]
temp_module = dev.modules[Module.TemperatureSensor]
prop = getattr(temp_module, feature)
assert isinstance(prop, type)
@@ -36,9 +37,9 @@ async def test_temperature_features(dev, feature, type):
@temperature_warning
async def test_temperature_warning(dev):
async def test_temperature_warning(dev: SmartDevice) -> None:
"""Test that features are registered and work as expected."""
temp_module: TemperatureSensor = dev.modules["TemperatureSensor"]
temp_module = dev.modules[Module.TemperatureSensor]
assert hasattr(temp_module, "temperature_warning")
assert isinstance(temp_module.temperature_warning, bool)

View File

@@ -3,7 +3,8 @@ import re
import pytest
from kasa.smart.modules import TemperatureControl
from kasa import Module
from kasa.smart import SmartDevice
from kasa.smart.modules.temperaturecontrol import ThermostatState
from ...device_fixtures import parametrize, thermostats_smart
@@ -23,9 +24,11 @@ temperature = parametrize(
("temperature_offset", int),
],
)
async def test_temperature_control_features(dev, feature, type):
async def test_temperature_control_features(
dev: SmartDevice, feature: str, type: type
) -> None:
"""Test that features are registered and work as expected."""
temp_module: TemperatureControl = dev.modules["TemperatureControl"]
temp_module = dev.modules[Module.TemperatureControl]
prop = getattr(temp_module, feature)
assert isinstance(prop, type)
@@ -40,9 +43,9 @@ async def test_temperature_control_features(dev, feature, type):
@thermostats_smart
async def test_set_temperature_turns_heating_on(dev):
async def test_set_temperature_turns_heating_on(dev: SmartDevice) -> None:
"""Test that set_temperature turns heating on."""
temp_module: TemperatureControl = dev.modules["TemperatureControl"]
temp_module = dev.modules[Module.TemperatureControl]
await temp_module.set_state(False)
await dev.update()
@@ -57,9 +60,9 @@ async def test_set_temperature_turns_heating_on(dev):
@thermostats_smart
async def test_set_temperature_invalid_values(dev):
async def test_set_temperature_invalid_values(dev: SmartDevice) -> None:
"""Test that out-of-bounds temperature values raise errors."""
temp_module: TemperatureControl = dev.modules["TemperatureControl"]
temp_module = dev.modules[Module.TemperatureControl]
with pytest.raises(
ValueError, match="Invalid target temperature -1, must be in range"
@@ -73,9 +76,9 @@ async def test_set_temperature_invalid_values(dev):
@thermostats_smart
async def test_temperature_offset(dev):
async def test_temperature_offset(dev: SmartDevice) -> None:
"""Test the temperature offset API."""
temp_module: TemperatureControl = dev.modules["TemperatureControl"]
temp_module = dev.modules[Module.TemperatureControl]
with pytest.raises(
ValueError, match=re.escape("Temperature offset must be [-10, 10]")
):
@@ -111,9 +114,11 @@ async def test_temperature_offset(dev):
pytest.param(ThermostatState.Unknown, ["invalid"], False, id="unknown state"),
],
)
async def test_thermostat_mode(dev, mode, states, frost_protection):
async def test_thermostat_mode(
dev: SmartDevice, mode: ThermostatState, states: list[str], frost_protection: bool
) -> None:
"""Test different thermostat modes."""
temp_module: TemperatureControl = dev.modules["TemperatureControl"]
temp_module = dev.modules[Module.TemperatureControl]
temp_module.data["frost_protection_on"] = frost_protection
temp_module.data["trv_states"] = states
@@ -138,9 +143,15 @@ async def test_thermostat_mode(dev, mode, states, frost_protection):
],
)
@pytest.mark.xdist_group(name="caplog")
async def test_thermostat_mode_warnings(dev, mode, states, msg, caplog):
async def test_thermostat_mode_warnings(
dev: SmartDevice,
mode: ThermostatState,
states: list[str],
msg: str,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test thermostat modes that should log a warning."""
temp_module: TemperatureControl = dev.modules["TemperatureControl"]
temp_module = dev.modules[Module.TemperatureControl]
caplog.set_level(logging.WARNING)
temp_module.data["trv_states"] = states
@@ -149,17 +160,19 @@ async def test_thermostat_mode_warnings(dev, mode, states, msg, caplog):
@thermostats_smart
async def test_thermostat_heating_with_low_battery(dev):
async def test_thermostat_heating_with_low_battery(dev: SmartDevice) -> None:
"""Test that mode is reported correctly with extra states."""
temp_module: TemperatureControl = dev.modules["TemperatureControl"]
temp_module = dev.modules[Module.TemperatureControl]
temp_module.data["trv_states"] = ["low_battery", "heating"]
assert temp_module.mode is ThermostatState.Heating
@thermostats_smart
async def test_thermostat_idle_with_low_battery(dev, caplog):
async def test_thermostat_idle_with_low_battery(
dev: SmartDevice, caplog: pytest.LogCaptureFixture
) -> None:
"""Test that mode is reported correctly with extra states."""
temp_module: TemperatureControl = dev.modules["TemperatureControl"]
temp_module = dev.modules[Module.TemperatureControl]
temp_module.data["trv_states"] = ["low_battery"]
with caplog.at_level(logging.WARNING):
assert temp_module.mode is ThermostatState.Idle

View File

@@ -10,7 +10,7 @@ triggerlogs = parametrize(
@triggerlogs
async def test_trigger_logs(dev: Device):
async def test_trigger_logs(dev: Device) -> None:
"""Test that features are registered and work as expected."""
triggerlogs = dev.modules.get(Module.TriggerLogs)
assert triggerlogs is not None

View File

@@ -3,7 +3,8 @@ from enum import Enum
import pytest
from kasa.smart.modules import WaterleakSensor
from kasa import Module
from kasa.smart import SmartDevice
from ...conftest import get_device_for_fixture_protocol
from ...device_fixtures import parametrize
@@ -28,10 +29,12 @@ async def parent(request):
("water_leak", "status", Enum),
],
)
async def test_waterleak_properties(dev, parent, feature, prop_name, type):
async def test_waterleak_properties(
dev: SmartDevice, parent: SmartDevice, feature: str, prop_name: str, type: type
) -> None:
"""Test that features are registered and work as expected."""
dev._parent = parent
waterleak: WaterleakSensor = dev.modules["WaterleakSensor"]
waterleak = dev.modules[Module.WaterleakSensor]
prop = getattr(waterleak, prop_name)
assert isinstance(prop, type)
@@ -42,10 +45,10 @@ async def test_waterleak_properties(dev, parent, feature, prop_name, type):
@waterleak
async def test_waterleak_features(dev, parent):
async def test_waterleak_features(dev: SmartDevice, parent: SmartDevice) -> None:
"""Test waterleak features."""
dev._parent = parent
waterleak: WaterleakSensor = dev.modules["WaterleakSensor"]
waterleak = dev.modules[Module.WaterleakSensor]
assert "water_leak" in dev.features
assert dev.features["water_leak"].value == waterleak.status

View File

@@ -44,7 +44,9 @@ hub_all = parametrize_combine([hubs_smart, hub_smartcam])
@device_smart
@pytest.mark.requires_dummy
@pytest.mark.xdist_group(name="caplog")
async def test_try_get_response(dev: SmartDevice, caplog):
async def test_try_get_response(
dev: SmartDevice, caplog: pytest.LogCaptureFixture
) -> None:
mock_response: dict = {
"get_device_info": SmartErrorCode.PARAMS_ERROR,
}
@@ -56,7 +58,7 @@ async def test_try_get_response(dev: SmartDevice, caplog):
@device_smart
@pytest.mark.requires_dummy
async def test_update_no_device_info(dev: SmartDevice, mocker: MockerFixture):
async def test_update_no_device_info(dev: SmartDevice, mocker: MockerFixture) -> None:
mock_response: dict = {
"get_device_usage": {},
"get_device_time": {},
@@ -68,7 +70,9 @@ async def test_update_no_device_info(dev: SmartDevice, mocker: MockerFixture):
@smart_discovery
async def test_device_type_no_update(discovery_mock, caplog: pytest.LogCaptureFixture):
async def test_device_type_no_update(
discovery_mock, caplog: pytest.LogCaptureFixture
) -> None:
"""Test device type and repr when device not updated."""
dev = SmartDevice(DISCOVERY_MOCK_IP)
assert dev.device_type is DeviceType.Unknown
@@ -96,7 +100,7 @@ async def test_device_type_no_update(discovery_mock, caplog: pytest.LogCaptureFi
@device_smart
async def test_initial_update(dev: SmartDevice, mocker: MockerFixture):
async def test_initial_update(dev: SmartDevice, mocker: MockerFixture) -> None:
"""Test the initial update cycle."""
# As the fixture data is already initialized, we reset the state for testing
dev._components_raw = None
@@ -124,7 +128,7 @@ async def test_initial_update(dev: SmartDevice, mocker: MockerFixture):
@device_smart
async def test_negotiate(dev: SmartDevice, mocker: MockerFixture):
async def test_negotiate(dev: SmartDevice, mocker: MockerFixture) -> None:
"""Test that the initial negotiation performs expected steps."""
# As the fixture data is already initialized, we reset the state for testing
dev._components_raw = None
@@ -158,7 +162,7 @@ async def test_negotiate(dev: SmartDevice, mocker: MockerFixture):
@device_smart
async def test_update_module_queries(dev: SmartDevice, mocker: MockerFixture):
async def test_update_module_queries(dev: SmartDevice, mocker: MockerFixture) -> None:
"""Test that the regular update uses queries from all supported modules."""
# We need to have some modules initialized by now
assert dev._modules
@@ -195,7 +199,7 @@ async def test_update_module_update_delays(
mocker: MockerFixture,
caplog: pytest.LogCaptureFixture,
freezer: FrozenDateTimeFactory,
):
) -> None:
"""Test that modules with minimum delays delay."""
# We need to have some modules initialized by now
assert dev._modules
@@ -253,7 +257,7 @@ async def test_hub_children_update_delays(
mocker: MockerFixture,
caplog: pytest.LogCaptureFixture,
freezer: FrozenDateTimeFactory,
):
) -> None:
"""Test that hub children use the correct delay."""
if not dev.children:
pytest.skip(f"Device {dev.model} does not have children.")
@@ -416,14 +420,15 @@ async def test_update_module_query_errors(
mocker: MockerFixture,
caplog: pytest.LogCaptureFixture,
freezer: FrozenDateTimeFactory,
first_update,
error_type,
recover,
):
first_update: bool,
error_type: SmartErrorCode | TimeoutError,
recover: bool,
) -> None:
"""Test that modules that disabled / removed on query failures.
i.e. the whole query times out rather than device returns an error.
"""
caplog.set_level(logging.DEBUG)
# We need to have some modules initialized by now
assert dev._modules
@@ -480,8 +485,6 @@ async def test_update_module_query_errors(
raise TimeoutError()
if len(child_requests) > 1:
raise TimeoutError()
if raise_error:
resp = {
"method": child_requests[0]["method"],
"error_code": error_type.value,
@@ -594,7 +597,7 @@ async def test_update_module_query_errors(
assert emod.status is not None
async def test_get_modules():
async def test_get_modules() -> None:
"""Test getting modules for child and parent modules."""
dummy_device = await get_device_for_fixture_protocol(
"KS240(US)_1.0_1.0.5.json", "SMART"
@@ -629,7 +632,9 @@ async def test_get_modules():
@device_smart
async def test_smartdevice_cloud_connection(dev: SmartDevice, mocker: MockerFixture):
async def test_smartdevice_cloud_connection(
dev: SmartDevice, mocker: MockerFixture
) -> None:
"""Test is_cloud_connected property."""
assert isinstance(dev, SmartDevice)
assert "cloud_connect" in dev._components
@@ -697,7 +702,7 @@ async def test_smartdevice_cloud_connection(dev: SmartDevice, mocker: MockerFixt
@variable_temp_smart
async def test_smart_temp_range(dev: Device):
async def test_smart_temp_range(dev: Device) -> None:
light = dev.modules.get(Module.Light)
assert light
color_temp_feat = light.get_feature("color_temp")
@@ -708,7 +713,7 @@ async def test_smart_temp_range(dev: Device):
@device_smart
async def test_initialize_modules_sysinfo_lookup_keys(
dev: SmartDevice, mocker: MockerFixture
):
) -> None:
"""Test that matching modules using SYSINFO_LOOKUP_KEYS are initialized correctly."""
class AvailableKey(SmartModule):
@@ -737,7 +742,7 @@ async def test_initialize_modules_sysinfo_lookup_keys(
@device_smart
async def test_initialize_modules_required_component(
dev: SmartDevice, mocker: MockerFixture
):
) -> None:
"""Test that matching modules using REQUIRED_COMPONENT are initialized correctly."""
class AvailableComponent(SmartModule):
@@ -763,7 +768,7 @@ async def test_initialize_modules_required_component(
assert "NonExistingComponent" not in dev.modules
async def test_smartmodule_query():
async def test_smartmodule_query() -> None:
"""Test that a module that doesn't set QUERY_GETTER_NAME has empty query."""
class DummyModule(SmartModule):
@@ -779,7 +784,7 @@ async def test_smartmodule_query():
@hub_all
@pytest.mark.xdist_group(name="caplog")
@pytest.mark.requires_dummy
async def test_dynamic_devices(dev: Device, caplog: pytest.LogCaptureFixture):
async def test_dynamic_devices(dev: Device, caplog: pytest.LogCaptureFixture) -> None:
"""Test dynamic child devices."""
if not dev.children:
pytest.skip(f"Device {dev.model} does not have children.")
@@ -992,7 +997,7 @@ async def test_dynamic_devices(dev: Device, caplog: pytest.LogCaptureFixture):
@hubs_smart
async def test_unpair(dev: SmartDevice, mocker: MockerFixture):
async def test_unpair(dev: SmartDevice, mocker: MockerFixture) -> None:
"""Verify that unpair calls childsetup module."""
if not dev.children:
pytest.skip("device has no children")