From e843e314d3e7c16fadef53b1dc3bd244da978bd7 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Mon, 11 Nov 2024 16:11:07 +0100 Subject: [PATCH 01/12] WIP Make sure led/set_led logs correct deprecation warning --- tests/test_plug.py | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/tests/test_plug.py b/tests/test_plug.py index 8989c975f..a1554d5fd 100644 --- a/tests/test_plug.py +++ b/tests/test_plug.py @@ -1,3 +1,4 @@ +import pytest from kasa import DeviceType from .conftest import plug, plug_iot, plug_smart, switch_smart, wallswitch_iot @@ -32,32 +33,34 @@ async def test_switch_sysinfo(dev): @plug_iot async def test_plug_led(dev): - original = dev.led + with pytest.deprecated_call(match="use: Module.Led in device.modules instead"): + original = dev.led - await dev.set_led(False) - await dev.update() - assert not dev.led + await dev.set_led(False) + await dev.update() + assert not dev.led - await dev.set_led(True) - await dev.update() - assert dev.led + await dev.set_led(True) + await dev.update() + assert dev.led - await dev.set_led(original) + await dev.set_led(original) @wallswitch_iot async def test_switch_led(dev): - original = dev.led + with pytest.deprecated_call(match="use: Module.Led in device.modules instead"): + original = dev.led - await dev.set_led(False) - await dev.update() - assert not dev.led + await dev.set_led(False) + await dev.update() + assert not dev.led - await dev.set_led(True) - await dev.update() - assert dev.led + await dev.set_led(True) + await dev.update() + assert dev.led - await dev.set_led(original) + await dev.set_led(original) @plug_smart From 988069dd13d3940820a25d0f00e82744d0c72d69 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Mon, 11 Nov 2024 16:19:15 +0100 Subject: [PATCH 02/12] Fix is_ property warnings to tell to use device_type, capture in tests --- kasa/device.py | 15 ++++++--------- tests/test_plug.py | 7 +++++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/kasa/device.py b/kasa/device.py index 72c567175..3eeae6714 100644 --- a/kasa/device.py +++ b/kasa/device.py @@ -479,11 +479,11 @@ def __repr__(self) -> str: _deprecated_device_type_attributes = { # is_type - "is_bulb": (Module.Light, DeviceType.Bulb), - "is_dimmer": (Module.Light, DeviceType.Dimmer), - "is_light_strip": (Module.LightEffect, DeviceType.LightStrip), - "is_plug": (Module.Led, DeviceType.Plug), - "is_wallswitch": (Module.Led, DeviceType.WallSwitch), + "is_bulb": (None, DeviceType.Bulb), + "is_dimmer": (None, DeviceType.Dimmer), + "is_light_strip": (None, DeviceType.LightStrip), + "is_plug": (None, DeviceType.Plug), + "is_wallswitch": (None, DeviceType.WallSwitch), "is_strip": (None, DeviceType.Strip), "is_strip_socket": (None, DeviceType.StripSocket), } @@ -547,10 +547,7 @@ def _get_replacing_attr( def __getattr__(self, name: str) -> Any: # is_device_type if dep_device_type_attr := self._deprecated_device_type_attributes.get(name): - module = dep_device_type_attr[0] - msg = f"{name} is deprecated" - if module: - msg += f", use: {module} in device.modules instead" + msg = f"{name} is deprecated, use device_type property instead" warn(msg, DeprecationWarning, stacklevel=2) return self.device_type == dep_device_type_attr[1] # Other deprecated attributes diff --git a/tests/test_plug.py b/tests/test_plug.py index a1554d5fd..76169c2cf 100644 --- a/tests/test_plug.py +++ b/tests/test_plug.py @@ -1,4 +1,5 @@ import pytest + from kasa import DeviceType from .conftest import plug, plug_iot, plug_smart, switch_smart, wallswitch_iot @@ -17,7 +18,8 @@ async def test_plug_sysinfo(dev): assert dev.model is not None assert dev.device_type == DeviceType.Plug or dev.device_type == DeviceType.Strip - assert dev.is_plug or dev.is_strip + with pytest.deprecated_call(match="use device_type property instead"): + assert dev.is_plug or dev.is_strip @wallswitch_iot @@ -28,7 +30,8 @@ async def test_switch_sysinfo(dev): assert dev.model is not None assert dev.device_type == DeviceType.WallSwitch - assert dev.is_wallswitch + with pytest.deprecated_call(match="use device_type property instead"): + assert dev.is_wallswitch @plug_iot From 9777d7a6aff12c9293a3e0c27d9deab4dc030513 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Mon, 11 Nov 2024 16:38:16 +0100 Subject: [PATCH 03/12] Fix deprecated calls in test_lightstrip --- tests/test_lightstrip.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/tests/test_lightstrip.py b/tests/test_lightstrip.py index c72f10ed0..365d0163d 100644 --- a/tests/test_lightstrip.py +++ b/tests/test_lightstrip.py @@ -1,35 +1,37 @@ import pytest -from kasa import DeviceType +from kasa import DeviceType, Module from kasa.iot import IotLightStrip +from kasa.iot.modules import LightEffect from .conftest import lightstrip_iot @lightstrip_iot async def test_lightstrip_length(dev: IotLightStrip): - assert dev.is_light_strip assert dev.device_type == DeviceType.LightStrip assert dev.length == dev.sys_info["length"] @lightstrip_iot async def test_lightstrip_effect(dev: IotLightStrip): - assert isinstance(dev.effect, dict) + le: LightEffect = dev.modules[Module.LightEffect] + assert isinstance(le._deprecated_effect, dict) for k in ["brightness", "custom", "enable", "id", "name"]: - assert k in dev.effect + assert k in le._deprecated_effect @lightstrip_iot async def test_effects_lightstrip_set_effect(dev: IotLightStrip): + le: LightEffect = dev.modules[Module.LightEffect] with pytest.raises( ValueError, match="The effect Not real is not a built in effect" ): - await dev.set_effect("Not real") + await le.set_effect("Not real") - await dev.set_effect("Candy Cane") + await le.set_effect("Candy Cane") await dev.update() - assert dev.effect["name"] == "Candy Cane" + assert le.effect == "Candy Cane" @lightstrip_iot @@ -38,12 +40,13 @@ async def test_effects_lightstrip_set_effect_brightness( dev: IotLightStrip, brightness, mocker ): query_helper = mocker.patch("kasa.iot.IotLightStrip._query_helper") + le: LightEffect = dev.modules[Module.LightEffect] # test that default brightness works (100 for candy cane) if brightness == 100: - await dev.set_effect("Candy Cane") + await le.set_effect("Candy Cane") else: - await dev.set_effect("Candy Cane", brightness=brightness) + await le.set_effect("Candy Cane", brightness=brightness) args, kwargs = query_helper.call_args_list[0] payload = args[2] @@ -56,12 +59,13 @@ async def test_effects_lightstrip_set_effect_transition( dev: IotLightStrip, transition, mocker ): query_helper = mocker.patch("kasa.iot.IotLightStrip._query_helper") + le: LightEffect = dev.modules[Module.LightEffect] # test that default (500 for candy cane) transition works if transition == 500: - await dev.set_effect("Candy Cane") + await le.set_effect("Candy Cane") else: - await dev.set_effect("Candy Cane", transition=transition) + await le.set_effect("Candy Cane", transition=transition) args, kwargs = query_helper.call_args_list[0] payload = args[2] @@ -70,8 +74,9 @@ async def test_effects_lightstrip_set_effect_transition( @lightstrip_iot async def test_effects_lightstrip_has_effects(dev: IotLightStrip): - assert dev.has_effects is True - assert dev.effect_list + le: LightEffect = dev.modules[Module.LightEffect] + assert le is not None + assert le.effect_list @lightstrip_iot From ec01e37ee5a544c264b0cacf3709f1cce85f9caf Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Mon, 11 Nov 2024 16:39:49 +0100 Subject: [PATCH 04/12] Fix deprecated calls in test_iotdevice --- tests/test_iotdevice.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/test_iotdevice.py b/tests/test_iotdevice.py index dd401ac99..a22ed6cef 100644 --- a/tests/test_iotdevice.py +++ b/tests/test_iotdevice.py @@ -16,7 +16,7 @@ Schema, ) -from kasa import KasaException, Module +from kasa import DeviceType, KasaException, Module from kasa.iot import IotDevice from kasa.iot.iotmodule import _merge_dict @@ -92,10 +92,8 @@ async def test_state_info(dev): @pytest.mark.requires_dummy() @device_iot async def test_invalid_connection(mocker, dev): - with ( - mocker.patch.object(FakeIotProtocol, "query", side_effect=KasaException), - pytest.raises(KasaException), - ): + mocker.patch.object(FakeIotProtocol, "query", side_effect=KasaException) + with pytest.raises(KasaException): await dev.update() @@ -169,7 +167,7 @@ async def test_state(dev, turn_on): async def test_on_since(dev, turn_on): await handle_turn_on(dev, turn_on) orig_state = dev.is_on - if "on_time" not in dev.sys_info and not dev.is_strip: + if "on_time" not in dev.sys_info and dev.device_type is not DeviceType.Strip: assert dev.on_since is None elif orig_state: assert isinstance(dev.on_since, datetime) @@ -179,7 +177,7 @@ async def test_on_since(dev, turn_on): @device_iot async def test_time(dev): - assert isinstance(await dev.get_time(), datetime) + assert isinstance(dev.modules[Module.Time].time, datetime) @device_iot @@ -216,7 +214,7 @@ async def test_representation(dev): @device_iot async def test_children(dev): """Make sure that children property is exposed by every device.""" - if dev.is_strip: + if dev.device_type is DeviceType.Strip: assert len(dev.children) > 0 else: assert len(dev.children) == 0 From c61658069b60f6d2ad7c3637373065e3ec7b8518 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Mon, 11 Nov 2024 16:52:35 +0100 Subject: [PATCH 05/12] Fix deprecated calls in test_emeter --- tests/test_emeter.py | 49 +++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/tests/test_emeter.py b/tests/test_emeter.py index d5a35758d..fa1c04002 100644 --- a/tests/test_emeter.py +++ b/tests/test_emeter.py @@ -10,7 +10,7 @@ Schema, ) -from kasa import Device, EmeterStatus, Module +from kasa import Device, DeviceType, EmeterStatus, Module from kasa.interfaces.energy import Energy from kasa.iot import IotDevice, IotStrip from kasa.iot.modules.emeter import Emeter @@ -61,20 +61,20 @@ async def test_get_emeter_realtime(dev): if not await mod._check_supported(): pytest.skip(f"Energy module not supported for {dev}.") - assert dev.has_emeter + emeter = dev.modules[Module.Energy] - current_emeter = await dev.get_emeter_realtime() + current_emeter = await emeter.get_status() CURRENT_CONSUMPTION_SCHEMA(current_emeter) @has_emeter_iot @pytest.mark.requires_dummy() async def test_get_emeter_daily(dev): - assert dev.has_emeter + emeter = dev.modules[Module.Energy] - assert await dev.get_emeter_daily(year=1900, month=1) == {} + assert await emeter.get_daily_stats(year=1900, month=1) == {} - d = await dev.get_emeter_daily() + d = await emeter.get_daily_stats() assert len(d) > 0 k, v = d.popitem() @@ -82,7 +82,7 @@ async def test_get_emeter_daily(dev): assert isinstance(v, float) # Test kwh (energy, energy_wh) - d = await dev.get_emeter_daily(kwh=False) + d = await emeter.get_daily_stats(kwh=False) k2, v2 = d.popitem() assert v * 1000 == v2 @@ -90,11 +90,11 @@ async def test_get_emeter_daily(dev): @has_emeter_iot @pytest.mark.requires_dummy() async def test_get_emeter_monthly(dev): - assert dev.has_emeter + emeter = dev.modules[Module.Energy] - assert await dev.get_emeter_monthly(year=1900) == {} + assert await emeter.get_monthly_stats(year=1900) == {} - d = await dev.get_emeter_monthly() + d = await emeter.get_monthly_stats() assert len(d) > 0 k, v = d.popitem() @@ -102,23 +102,26 @@ async def test_get_emeter_monthly(dev): assert isinstance(v, float) # Test kwh (energy, energy_wh) - d = await dev.get_emeter_monthly(kwh=False) + d = await emeter.get_monthly_stats(kwh=False) k2, v2 = d.popitem() assert v * 1000 == v2 @has_emeter_iot async def test_emeter_status(dev): - assert dev.has_emeter + emeter = dev.modules[Module.Energy] - d = await dev.get_emeter_realtime() + d = await emeter.get_status() with pytest.raises(KeyError): assert d["foo"] assert d["power_mw"] == d["power"] * 1000 # bulbs have only power according to tplink simulator. - if not dev.is_bulb and not dev.is_light_strip: + if ( + dev.device_type is not DeviceType.Bulb + and dev.device_type is not DeviceType.LightStrip + ): assert d["voltage_mv"] == d["voltage"] * 1000 assert d["current_ma"] == d["current"] * 1000 @@ -128,19 +131,17 @@ async def test_emeter_status(dev): @pytest.mark.skip("not clearing your stats..") @has_emeter async def test_erase_emeter_stats(dev): - assert dev.has_emeter + emeter = dev.modules[Module.Energy] - await dev.erase_emeter() + await emeter.erase_emeter() @has_emeter_iot async def test_current_consumption(dev): - if dev.has_emeter: - x = dev.current_consumption - assert isinstance(x, float) - assert x >= 0.0 - else: - assert dev.current_consumption is None + emeter = dev.modules[Module.Energy] + x = emeter.current_consumption + assert isinstance(x, float) + assert x >= 0.0 async def test_emeterstatus_missing_current(): @@ -180,7 +181,9 @@ def data(self): emeter_data["get_daystat"]["day_list"].append( {"day": now.day, "energy_wh": 500, "month": now.month, "year": now.year} ) - assert emeter.emeter_today == 0.500 + # TODO: unclear how to fix this, or if it is even needed anymore, so using deprecated_call + with pytest.deprecated_call(match="use consumption_today instead"): + assert emeter.emeter_today == 0.500 @has_emeter From d6283baaea03d604e87bd15be8965477e7acb536 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Mon, 11 Nov 2024 16:57:16 +0100 Subject: [PATCH 06/12] Fix deprecated calls in test_discovery --- tests/test_discovery.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/tests/test_discovery.py b/tests/test_discovery.py index 0318de35c..34f38576b 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -78,14 +78,16 @@ @wallswitch_iot async def test_type_detection_switch(dev: Device): d = Discover._get_device_class(dev._last_update)("localhost") - assert d.is_wallswitch - assert d.device_type == DeviceType.WallSwitch + with pytest.deprecated_call(match="use device_type property instead"): + assert d.is_wallswitch + assert d.device_type is DeviceType.WallSwitch @plug_iot async def test_type_detection_plug(dev: Device): d = Discover._get_device_class(dev._last_update)("localhost") - assert d.is_plug + with pytest.deprecated_call(match="use device_type property instead"): + assert d.is_plug assert d.device_type == DeviceType.Plug @@ -93,29 +95,34 @@ async def test_type_detection_plug(dev: Device): async def test_type_detection_bulb(dev: Device): d = Discover._get_device_class(dev._last_update)("localhost") # TODO: light_strip is a special case for now to force bulb tests on it - if not d.is_light_strip: - assert d.is_bulb + + if d.device_type is not DeviceType.LightStrip: + with pytest.deprecated_call(match="use device_type property instead"): + assert d.is_bulb assert d.device_type == DeviceType.Bulb @strip_iot async def test_type_detection_strip(dev: Device): d = Discover._get_device_class(dev._last_update)("localhost") - assert d.is_strip + with pytest.deprecated_call(match="use device_type property instead"): + assert d.is_strip assert d.device_type == DeviceType.Strip @dimmer_iot async def test_type_detection_dimmer(dev: Device): d = Discover._get_device_class(dev._last_update)("localhost") - assert d.is_dimmer + with pytest.deprecated_call(match="use device_type property instead"): + assert d.is_dimmer assert d.device_type == DeviceType.Dimmer @lightstrip_iot async def test_type_detection_lightstrip(dev: Device): d = Discover._get_device_class(dev._last_update)("localhost") - assert d.is_light_strip + with pytest.deprecated_call(match="use device_type property instead"): + assert d.is_light_strip assert d.device_type == DeviceType.LightStrip From 25be3bc1464c8d3cc79f8cd9221da78f000705ea Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Mon, 11 Nov 2024 16:59:13 +0100 Subject: [PATCH 07/12] Fix warnings in test_smartdevice --- tests/test_smartdevice.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_smartdevice.py b/tests/test_smartdevice.py index d96542e5e..1465b60a0 100644 --- a/tests/test_smartdevice.py +++ b/tests/test_smartdevice.py @@ -43,10 +43,8 @@ async def test_update_no_device_info(dev: SmartDevice, mocker: MockerFixture): "get_device_time": {}, } msg = f"get_device_info not found in {mock_response} for device 127.0.0.123" - with ( - mocker.patch.object(dev.protocol, "query", return_value=mock_response), - pytest.raises(KasaException, match=msg), - ): + mocker.patch.object(dev.protocol, "query", return_value=mock_response) + with pytest.raises(KasaException, match=msg): await dev.update() From 7d59eac09c5f86e2a43fd39f232bb81b96295400 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Mon, 11 Nov 2024 16:02:57 +0000 Subject: [PATCH 08/12] Suppress child fixture creation warnings in test_smart_protocol_lists_multiple_request --- tests/fakeprotocol_smart.py | 8 +++++--- tests/test_smartprotocol.py | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/fakeprotocol_smart.py b/tests/fakeprotocol_smart.py index 842147f35..242336d3e 100644 --- a/tests/fakeprotocol_smart.py +++ b/tests/fakeprotocol_smart.py @@ -33,6 +33,7 @@ def __init__( warn_fixture_missing_methods=True, fix_incomplete_fixture_lists=True, is_child=False, + get_child_fixtures=True, ): super().__init__( config=DeviceConfig( @@ -48,9 +49,10 @@ def __init__( # child are then still reflected on the parent's lis of child device in if not is_child: self.info = copy.deepcopy(info) - self.child_protocols = self._get_child_protocols( - self.info, self.fixture_name, "get_child_device_list" - ) + if get_child_fixtures: + self.child_protocols = self._get_child_protocols( + self.info, self.fixture_name, "get_child_device_list" + ) else: self.info = info if not component_nego_not_included: diff --git a/tests/test_smartprotocol.py b/tests/test_smartprotocol.py index 19e62a3dd..f8aa307af 100644 --- a/tests/test_smartprotocol.py +++ b/tests/test_smartprotocol.py @@ -358,6 +358,7 @@ async def test_smart_protocol_lists_multiple_request(mocker, list_sum, batch_siz "foobar", list_return_size=batch_size, component_nego_not_included=True, + get_child_fixtures=False, ) protocol = SmartProtocol(transport=ft) query_spy = mocker.spy(protocol, "_execute_query") From b859752ecd8cd9c4db819bf201ed8a2bda02fe09 Mon Sep 17 00:00:00 2001 From: "Teemu R." Date: Tue, 12 Nov 2024 16:54:39 +0100 Subject: [PATCH 09/12] Apply suggestions from code review Co-authored-by: Steven B. <51370195+sdb9696@users.noreply.github.com> --- tests/test_discovery.py | 10 ---------- tests/test_emeter.py | 4 +--- tests/test_plug.py | 4 ---- 3 files changed, 1 insertion(+), 17 deletions(-) diff --git a/tests/test_discovery.py b/tests/test_discovery.py index 4b046eccf..a6e1f6172 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -89,8 +89,6 @@ async def test_type_detection_switch(dev: Device): @plug_iot async def test_type_detection_plug(dev: Device): d = Discover._get_device_class(dev._last_update)("localhost") - with pytest.deprecated_call(match="use device_type property instead"): - assert d.is_plug assert d.device_type == DeviceType.Plug @@ -100,32 +98,24 @@ async def test_type_detection_bulb(dev: Device): # TODO: light_strip is a special case for now to force bulb tests on it if d.device_type is not DeviceType.LightStrip: - with pytest.deprecated_call(match="use device_type property instead"): - assert d.is_bulb assert d.device_type == DeviceType.Bulb @strip_iot async def test_type_detection_strip(dev: Device): d = Discover._get_device_class(dev._last_update)("localhost") - with pytest.deprecated_call(match="use device_type property instead"): - assert d.is_strip assert d.device_type == DeviceType.Strip @dimmer_iot async def test_type_detection_dimmer(dev: Device): d = Discover._get_device_class(dev._last_update)("localhost") - with pytest.deprecated_call(match="use device_type property instead"): - assert d.is_dimmer assert d.device_type == DeviceType.Dimmer @lightstrip_iot async def test_type_detection_lightstrip(dev: Device): d = Discover._get_device_class(dev._last_update)("localhost") - with pytest.deprecated_call(match="use device_type property instead"): - assert d.is_light_strip assert d.device_type == DeviceType.LightStrip diff --git a/tests/test_emeter.py b/tests/test_emeter.py index fa1c04002..4829ff0ca 100644 --- a/tests/test_emeter.py +++ b/tests/test_emeter.py @@ -181,9 +181,7 @@ def data(self): emeter_data["get_daystat"]["day_list"].append( {"day": now.day, "energy_wh": 500, "month": now.month, "year": now.year} ) - # TODO: unclear how to fix this, or if it is even needed anymore, so using deprecated_call - with pytest.deprecated_call(match="use consumption_today instead"): - assert emeter.emeter_today == 0.500 + assert emeter.consumption_today == 0.500 @has_emeter diff --git a/tests/test_plug.py b/tests/test_plug.py index 76169c2cf..795ebe55b 100644 --- a/tests/test_plug.py +++ b/tests/test_plug.py @@ -18,8 +18,6 @@ async def test_plug_sysinfo(dev): assert dev.model is not None assert dev.device_type == DeviceType.Plug or dev.device_type == DeviceType.Strip - with pytest.deprecated_call(match="use device_type property instead"): - assert dev.is_plug or dev.is_strip @wallswitch_iot @@ -30,8 +28,6 @@ async def test_switch_sysinfo(dev): assert dev.model is not None assert dev.device_type == DeviceType.WallSwitch - with pytest.deprecated_call(match="use device_type property instead"): - assert dev.is_wallswitch @plug_iot From ee759320a733710304b6362d7f1aea4e3094b15e Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Wed, 13 Nov 2024 10:31:45 +0000 Subject: [PATCH 10/12] Fix test_smart_protocol_lists_single_request warnings --- tests/test_smartprotocol.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_smartprotocol.py b/tests/test_smartprotocol.py index 9579a75e2..c523fcdbc 100644 --- a/tests/test_smartprotocol.py +++ b/tests/test_smartprotocol.py @@ -325,6 +325,7 @@ async def test_smart_protocol_lists_single_request(mocker, list_sum, batch_size) "foobar", list_return_size=batch_size, component_nego_not_included=True, + get_child_fixtures=False, ) protocol = SmartProtocol(transport=ft) query_spy = mocker.spy(protocol, "_execute_query") From e09d073a01fdea8cf8ed37307affe8957bab7ab7 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Wed, 13 Nov 2024 12:39:21 +0000 Subject: [PATCH 11/12] Finish remove all deprecated attribute access in tests --- kasa/device.py | 4 +- tests/smart/modules/test_contact.py | 4 +- tests/smart/modules/test_light_effect.py | 2 +- .../smart/modules/test_light_strip_effect.py | 2 +- tests/smart/modules/test_motionsensor.py | 4 +- tests/test_bulb.py | 77 +++++++++++-------- tests/test_cli.py | 15 ++-- tests/test_device.py | 26 +++++-- tests/test_dimmer.py | 54 ++++++++----- 9 files changed, 117 insertions(+), 71 deletions(-) diff --git a/kasa/device.py b/kasa/device.py index cddf7ca6d..ad4efceaf 100644 --- a/kasa/device.py +++ b/kasa/device.py @@ -503,7 +503,9 @@ def _get_replacing_attr( return None for attr in attrs: - if hasattr(check, attr): + # Use dir() as opposed to hasattr() to avoid raising exceptions + # from properties + if attr in dir(check): return attr return None diff --git a/tests/smart/modules/test_contact.py b/tests/smart/modules/test_contact.py index 56287e2a3..c5c4c935f 100644 --- a/tests/smart/modules/test_contact.py +++ b/tests/smart/modules/test_contact.py @@ -1,6 +1,6 @@ import pytest -from kasa import Module, SmartDevice +from kasa import Device, Module from ...device_fixtures import parametrize @@ -16,7 +16,7 @@ ("is_open", bool), ], ) -async def test_contact_features(dev: SmartDevice, feature, type): +async def test_contact_features(dev: Device, feature, type): """Test that features are registered and work as expected.""" contact = dev.modules.get(Module.ContactSensor) assert contact is not None diff --git a/tests/smart/modules/test_light_effect.py b/tests/smart/modules/test_light_effect.py index a48b29add..e4475652c 100644 --- a/tests/smart/modules/test_light_effect.py +++ b/tests/smart/modules/test_light_effect.py @@ -71,7 +71,7 @@ async def test_light_effect_brightness( if effect_active: assert light_effect.is_active - assert light_effect.brightness == dev.brightness + assert light_effect.brightness == light_module.brightness light_effect_set_brightness.assert_called_with(10) mock_light_effect_call.assert_called_with( diff --git a/tests/smart/modules/test_light_strip_effect.py b/tests/smart/modules/test_light_strip_effect.py index a3db847e3..81bc35c83 100644 --- a/tests/smart/modules/test_light_strip_effect.py +++ b/tests/smart/modules/test_light_strip_effect.py @@ -86,7 +86,7 @@ async def test_light_effect_brightness( if effect_active: assert light_effect.is_active - assert light_effect.brightness == dev.brightness + assert light_effect.brightness == light_module.brightness light_effect_set_brightness.assert_called_with(10) mock_light_effect_call.assert_called_with( diff --git a/tests/smart/modules/test_motionsensor.py b/tests/smart/modules/test_motionsensor.py index 91119a759..418ad51a1 100644 --- a/tests/smart/modules/test_motionsensor.py +++ b/tests/smart/modules/test_motionsensor.py @@ -1,6 +1,6 @@ import pytest -from kasa import Module, SmartDevice +from kasa import Device, Module from ...device_fixtures import parametrize @@ -16,7 +16,7 @@ ("motion_detected", bool), ], ) -async def test_motion_features(dev: SmartDevice, feature, type): +async def test_motion_features(dev: Device, feature, type): """Test that features are registered and work as expected.""" motion = dev.modules.get(Module.MotionSensor) assert motion is not None diff --git a/tests/test_bulb.py b/tests/test_bulb.py index 64c012fd7..53a3542a3 100644 --- a/tests/test_bulb.py +++ b/tests/test_bulb.py @@ -13,6 +13,7 @@ from kasa import Device, DeviceType, IotLightPreset, KasaException, LightState, Module from kasa.iot import IotBulb, IotDimmer +from kasa.iot.modules import LightPreset as IotLightPresetModule from .conftest import ( bulb, @@ -39,11 +40,6 @@ async def test_bulb_sysinfo(dev: Device): assert dev.model is not None - # TODO: remove special handling for lightstrip - if not dev.is_light_strip: - assert dev.device_type == DeviceType.Bulb - assert dev.is_bulb - @bulb async def test_state_attributes(dev: Device): @@ -88,7 +84,9 @@ async def test_hsv(dev: Device, turn_on): @color_bulb_iot async def test_set_hsv_transition(dev: IotBulb, mocker): set_light_state = mocker.patch("kasa.iot.IotBulb._set_light_state") - await dev.set_hsv(10, 10, 100, transition=1000) + light = dev.modules.get(Module.Light) + assert light + await light.set_hsv(10, 10, 100, transition=1000) set_light_state.assert_called_with( {"hue": 10, "saturation": 10, "brightness": 100, "color_temp": 0}, @@ -226,7 +224,9 @@ async def test_try_set_colortemp(dev: Device, turn_on): @variable_temp_iot async def test_set_color_temp_transition(dev: IotBulb, mocker): set_light_state = mocker.patch("kasa.iot.IotBulb._set_light_state") - await dev.set_color_temp(2700, transition=100) + light = dev.modules.get(Module.Light) + assert light + await light.set_color_temp(2700, transition=100) set_light_state.assert_called_with({"color_temp": 2700}, transition=100) @@ -234,8 +234,9 @@ async def test_set_color_temp_transition(dev: IotBulb, mocker): @variable_temp_iot async def test_unknown_temp_range(dev: IotBulb, monkeypatch, caplog): monkeypatch.setitem(dev._sys_info, "model", "unknown bulb") - - assert dev.valid_temperature_range == (2700, 5000) + light = dev.modules.get(Module.Light) + assert light + assert light.valid_temperature_range == (2700, 5000) assert "Unknown color temperature range, fallback to 2700-5000" in caplog.text @@ -278,19 +279,21 @@ async def test_non_variable_temp(dev: Device): @turn_on async def test_dimmable_brightness(dev: IotBulb, turn_on): assert isinstance(dev, (IotBulb, IotDimmer)) + light = dev.modules.get(Module.Light) + assert light await handle_turn_on(dev, turn_on) assert dev._is_dimmable - await dev.set_brightness(50) + await light.set_brightness(50) await dev.update() - assert dev.brightness == 50 + assert light.brightness == 50 - await dev.set_brightness(10) + await light.set_brightness(10) await dev.update() - assert dev.brightness == 10 + assert light.brightness == 10 with pytest.raises(TypeError, match="Brightness must be an integer"): - await dev.set_brightness("foo") # type: ignore[arg-type] + await light.set_brightness("foo") # type: ignore[arg-type] @bulb_iot @@ -308,7 +311,9 @@ async def test_turn_on_transition(dev: IotBulb, mocker): @bulb_iot async def test_dimmable_brightness_transition(dev: IotBulb, mocker): set_light_state = mocker.patch("kasa.iot.IotBulb._set_light_state") - await dev.set_brightness(10, transition=1000) + light = dev.modules.get(Module.Light) + assert light + await light.set_brightness(10, transition=1000) set_light_state.assert_called_with({"brightness": 10, "on_off": 1}, transition=1000) @@ -316,28 +321,30 @@ async def test_dimmable_brightness_transition(dev: IotBulb, mocker): @dimmable_iot async def test_invalid_brightness(dev: IotBulb): assert dev._is_dimmable - + light = dev.modules.get(Module.Light) + assert light with pytest.raises( ValueError, match=re.escape("Invalid brightness value: 110 (valid range: 0-100%)"), ): - await dev.set_brightness(110) + await light.set_brightness(110) with pytest.raises( ValueError, match=re.escape("Invalid brightness value: -100 (valid range: 0-100%)"), ): - await dev.set_brightness(-100) + await light.set_brightness(-100) @non_dimmable_iot async def test_non_dimmable(dev: IotBulb): assert not dev._is_dimmable - + light = dev.modules.get(Module.Light) + assert light with pytest.raises(KasaException): - assert dev.brightness == 0 + assert light.brightness == 0 with pytest.raises(KasaException): - await dev.set_brightness(100) + await light.set_brightness(100) @bulb_iot @@ -357,7 +364,10 @@ async def test_ignore_default_not_set_without_color_mode_change_turn_on( @bulb_iot async def test_list_presets(dev: IotBulb): - presets = dev.presets + light_preset = dev.modules.get(Module.LightPreset) + assert light_preset + assert isinstance(light_preset, IotLightPresetModule) + presets = light_preset._deprecated_presets # Light strip devices may list some light effects along with normal presets but these # are handled by the LightEffect module so exclude preferred states with id raw_presets = [ @@ -376,9 +386,13 @@ async def test_list_presets(dev: IotBulb): @bulb_iot async def test_modify_preset(dev: IotBulb, mocker): """Verify that modifying preset calls the and exceptions are raised properly.""" - if not dev.presets: + if ( + not (light_preset := dev.modules.get(Module.LightPreset)) + or not light_preset._deprecated_presets + ): pytest.skip("Some strips do not support presets") + assert isinstance(light_preset, IotLightPresetModule) data: dict[str, int | None] = { "index": 0, "brightness": 10, @@ -394,12 +408,12 @@ async def test_modify_preset(dev: IotBulb, mocker): assert preset.saturation == 0 assert preset.color_temp == 0 - await dev.save_preset(preset) + await light_preset._deprecated_save_preset(preset) await dev.update() - assert dev.presets[0].brightness == 10 + assert light_preset._deprecated_presets[0].brightness == 10 with pytest.raises(KasaException): - await dev.save_preset( + await light_preset._deprecated_save_preset( IotLightPreset(index=5, hue=0, brightness=0, saturation=0, color_temp=0) # type: ignore[call-arg] ) @@ -420,11 +434,14 @@ async def test_modify_preset(dev: IotBulb, mocker): ) async def test_modify_preset_payloads(dev: IotBulb, preset, payload, mocker): """Test that modify preset payloads ignore none values.""" - if not dev.presets: + if ( + not (light_preset := dev.modules.get(Module.LightPreset)) + or not light_preset._deprecated_presets + ): pytest.skip("Some strips do not support presets") query_helper = mocker.patch("kasa.iot.IotBulb._query_helper") - await dev.save_preset(preset) + await light_preset._deprecated_save_preset(preset) query_helper.assert_called_with(dev.LIGHT_SERVICE, "set_preferred_state", payload) @@ -476,6 +493,4 @@ async def test_modify_preset_payloads(dev: IotBulb, preset, payload, mocker): @bulb def test_device_type_bulb(dev: Device): - if dev.is_light_strip: - pytest.skip("bulb has also lightstrips to test the api") - assert dev.device_type == DeviceType.Bulb + assert dev.device_type in {DeviceType.Bulb, DeviceType.LightStrip} diff --git a/tests/test_cli.py b/tests/test_cli.py index d22bb1129..6d16e8a3d 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -15,6 +15,7 @@ Credentials, Device, DeviceError, + DeviceType, EmeterStatus, KasaException, Module, @@ -424,20 +425,22 @@ async def test_time_set(dev: Device, mocker, runner): async def test_emeter(dev: Device, mocker, runner): res = await runner.invoke(emeter, obj=dev) - if not dev.has_emeter: + if not (energy := dev.modules.get(Module.Energy)): assert "Device has no emeter" in res.output return assert "== Emeter ==" in res.output - if not dev.is_strip: + if dev.device_type is not DeviceType.Strip: res = await runner.invoke(emeter, ["--index", "0"], obj=dev) assert f"Device: {dev.host} does not have children" in res.output res = await runner.invoke(emeter, ["--name", "mock"], obj=dev) assert f"Device: {dev.host} does not have children" in res.output - if dev.is_strip and len(dev.children) > 0: - realtime_emeter = mocker.patch.object(dev.children[0], "get_emeter_realtime") + if dev.device_type is DeviceType.Strip and len(dev.children) > 0: + child_energy = dev.children[0].modules.get(Module.Energy) + assert child_energy + realtime_emeter = mocker.patch.object(child_energy, "get_status") realtime_emeter.return_value = EmeterStatus({"voltage_mv": 122066}) res = await runner.invoke(emeter, ["--index", "0"], obj=dev) @@ -450,7 +453,7 @@ async def test_emeter(dev: Device, mocker, runner): assert realtime_emeter.call_count == 2 if isinstance(dev, IotDevice): - monthly = mocker.patch.object(dev, "get_emeter_monthly") + monthly = mocker.patch.object(energy, "get_monthly_stats") monthly.return_value = {1: 1234} res = await runner.invoke(emeter, ["--year", "1900"], obj=dev) if not isinstance(dev, IotDevice): @@ -461,7 +464,7 @@ async def test_emeter(dev: Device, mocker, runner): monthly.assert_called_with(year=1900) if isinstance(dev, IotDevice): - daily = mocker.patch.object(dev, "get_emeter_daily") + daily = mocker.patch.object(energy, "get_daily_stats") daily.return_value = {1: 1234} res = await runner.invoke(emeter, ["--month", "1900-12"], obj=dev) if not isinstance(dev, IotDevice): diff --git a/tests/test_device.py b/tests/test_device.py index 2b9d970a4..5f527287a 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -6,7 +6,7 @@ import inspect import pkgutil import sys -from contextlib import AbstractContextManager +from contextlib import AbstractContextManager, nullcontext from unittest.mock import AsyncMock, patch import pytest @@ -170,15 +170,22 @@ async def _test_attribute( dev: Device, attribute_name, is_expected, module_name, *args, will_raise=False ): if is_expected and will_raise: - ctx: AbstractContextManager = pytest.raises(will_raise) + ctx: AbstractContextManager | nullcontext = pytest.raises(will_raise) + dep_context: pytest.WarningsRecorder | nullcontext = pytest.deprecated_call( + match=(f"{attribute_name} is deprecated, use:") + ) elif is_expected: - ctx = pytest.deprecated_call(match=(f"{attribute_name} is deprecated, use:")) + ctx = nullcontext() + dep_context = pytest.deprecated_call( + match=(f"{attribute_name} is deprecated, use:") + ) else: ctx = pytest.raises( AttributeError, match=f"Device has no attribute '{attribute_name}'" ) + dep_context = nullcontext() - with ctx: + with dep_context, ctx: if args: await getattr(dev, attribute_name)(*args) else: @@ -267,16 +274,19 @@ async def test_deprecated_light_preset_attributes(dev: Device): await _test_attribute(dev, "presets", bool(preset), "LightPreset", will_raise=exc) exc = None + is_expected = bool(preset) # deprecated save_preset not implemented for smart devices as it's unlikely anyone # has an existing reliance on this for the newer devices. - if not preset or isinstance(dev, SmartDevice): - exc = AttributeError - elif len(preset.preset_states_list) == 0: + if isinstance(dev, SmartDevice): + is_expected = False + + if preset and len(preset.preset_states_list) == 0: exc = KasaException + await _test_attribute( dev, "save_preset", - bool(preset), + is_expected, "LightPreset", IotLightPreset(index=0, hue=100, brightness=100, saturation=0, color_temp=0), # type: ignore[call-arg] will_raise=exc, diff --git a/tests/test_dimmer.py b/tests/test_dimmer.py index 5d1d10e53..3505a7c1c 100644 --- a/tests/test_dimmer.py +++ b/tests/test_dimmer.py @@ -1,6 +1,6 @@ import pytest -from kasa import DeviceType +from kasa import DeviceType, Module from kasa.iot import IotDimmer from .conftest import dimmer_iot, handle_turn_on, turn_on @@ -8,28 +8,32 @@ @dimmer_iot async def test_set_brightness(dev): + light = dev.modules.get(Module.Light) + assert light await handle_turn_on(dev, False) await dev.update() assert dev.is_on is False - await dev.set_brightness(99) + await light.set_brightness(99) await dev.update() - assert dev.brightness == 99 + assert light.brightness == 99 assert dev.is_on is True - await dev.set_brightness(0) + await light.set_brightness(0) await dev.update() - assert dev.brightness == 99 + assert light.brightness == 99 assert dev.is_on is False @dimmer_iot @turn_on async def test_set_brightness_transition(dev, turn_on, mocker): + light = dev.modules.get(Module.Light) + assert light await handle_turn_on(dev, turn_on) query_helper = mocker.spy(IotDimmer, "_query_helper") - await dev.set_brightness(99, transition=1000) + await light.set_brightness(99, transition=1000) query_helper.assert_called_with( mocker.ANY, "smartlife.iot.dimmer", @@ -37,39 +41,45 @@ async def test_set_brightness_transition(dev, turn_on, mocker): {"brightness": 99, "duration": 1000}, ) await dev.update() - assert dev.brightness == 99 + assert light.brightness == 99 assert dev.is_on - await dev.set_brightness(0, transition=1000) + await light.set_brightness(0, transition=1000) await dev.update() assert dev.is_on is False @dimmer_iot async def test_set_brightness_invalid(dev): + light = dev.modules.get(Module.Light) + assert light for invalid_brightness in [-1, 101]: with pytest.raises(ValueError, match="Invalid brightness"): - await dev.set_brightness(invalid_brightness) + await light.set_brightness(invalid_brightness) for invalid_type in [0.5, "foo"]: with pytest.raises(TypeError, match="Brightness must be an integer"): - await dev.set_brightness(invalid_type) + await light.set_brightness(invalid_type) @dimmer_iot async def test_set_brightness_invalid_transition(dev): + light = dev.modules.get(Module.Light) + assert light for invalid_transition in [-1]: with pytest.raises(ValueError, match="Transition value .+? is not valid."): - await dev.set_brightness(1, transition=invalid_transition) + await light.set_brightness(1, transition=invalid_transition) for invalid_type in [0.5, "foo"]: with pytest.raises(TypeError, match="Transition must be integer"): - await dev.set_brightness(1, transition=invalid_type) + await light.set_brightness(1, transition=invalid_type) @dimmer_iot async def test_turn_on_transition(dev, mocker): + light = dev.modules.get(Module.Light) + assert light query_helper = mocker.spy(IotDimmer, "_query_helper") - original_brightness = dev.brightness + original_brightness = light.brightness await dev.turn_on(transition=1000) query_helper.assert_called_with( @@ -80,20 +90,22 @@ async def test_turn_on_transition(dev, mocker): ) await dev.update() assert dev.is_on - assert dev.brightness == original_brightness + assert light.brightness == original_brightness @dimmer_iot async def test_turn_off_transition(dev, mocker): + light = dev.modules.get(Module.Light) + assert light await handle_turn_on(dev, True) query_helper = mocker.spy(IotDimmer, "_query_helper") - original_brightness = dev.brightness + original_brightness = light.brightness await dev.turn_off(transition=1000) await dev.update() assert dev.is_off - assert dev.brightness == original_brightness + assert light.brightness == original_brightness query_helper.assert_called_with( mocker.ANY, "smartlife.iot.dimmer", @@ -105,6 +117,8 @@ async def test_turn_off_transition(dev, mocker): @dimmer_iot @turn_on async def test_set_dimmer_transition(dev, turn_on, mocker): + light = dev.modules.get(Module.Light) + assert light await handle_turn_on(dev, turn_on) query_helper = mocker.spy(IotDimmer, "_query_helper") @@ -117,21 +131,23 @@ async def test_set_dimmer_transition(dev, turn_on, mocker): ) await dev.update() assert dev.is_on - assert dev.brightness == 99 + assert light.brightness == 99 @dimmer_iot @turn_on async def test_set_dimmer_transition_to_off(dev, turn_on, mocker): + light = dev.modules.get(Module.Light) + assert light await handle_turn_on(dev, turn_on) - original_brightness = dev.brightness + original_brightness = light.brightness query_helper = mocker.spy(IotDimmer, "_query_helper") await dev.set_dimmer_transition(0, 1000) await dev.update() assert dev.is_off - assert dev.brightness == original_brightness + assert light.brightness == original_brightness query_helper.assert_called_with( mocker.ANY, "smartlife.iot.dimmer", From 48d62d91e83b19f27c5b2ed1af35f1e5c0c5a520 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Wed, 13 Nov 2024 16:00:13 +0000 Subject: [PATCH 12/12] Remove duplicate missing child warning --- tests/fakeprotocol_smart.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/fakeprotocol_smart.py b/tests/fakeprotocol_smart.py index 9adad37f3..bde908851 100644 --- a/tests/fakeprotocol_smart.py +++ b/tests/fakeprotocol_smart.py @@ -222,10 +222,7 @@ async def _handle_control_child(self, params: dict): """Handle control_child command.""" device_id = params.get("device_id") if device_id not in self.child_protocols: - warn( - f"Could not find child fixture {device_id} in {self.fixture_name}", - stacklevel=2, - ) + # no need to warn as the warning was raised during protocol init return self._handle_control_child_missing(params) child_protocol: SmartProtocol = self.child_protocols[device_id]