From f4988b001a7d2fa0b64b9f2052789cdb9ca85074 Mon Sep 17 00:00:00 2001 From: sdb9696 Date: Tue, 14 May 2024 12:53:57 +0100 Subject: [PATCH 1/2] Deprecate device level light, effect and led attributes --- kasa/device.py | 120 ++++++++++++++++++++++++- kasa/iot/iotbulb.py | 16 ++-- kasa/iot/iotdimmer.py | 14 ++- kasa/iot/iotlightstrip.py | 68 +------------- kasa/iot/iotplug.py | 10 --- kasa/iot/iotstrip.py | 11 --- kasa/iot/modules/light.py | 22 ++--- kasa/iot/modules/lighteffect.py | 26 ++++++ kasa/smart/modules/lightstripeffect.py | 26 ++++++ kasa/tests/test_device.py | 79 +++++++++++++++- 10 files changed, 278 insertions(+), 114 deletions(-) diff --git a/kasa/device.py b/kasa/device.py index 0f88f3a13..b156eb220 100644 --- a/kasa/device.py +++ b/kasa/device.py @@ -369,13 +369,125 @@ def __repr__(self): ), } - def __getattr__(self, name) -> bool: - if name in self._deprecated_attributes: + _deprecated_effect_attributes = { + # Light Effects + # The return values for effect is a str instead of dict so the lightstrip + # modules have a _deprecated method to return the value as before. + "effect": ( + Module.LightEffect, + lambda self: self.modules[Module.LightEffect]._deprecated_effect + if Module.LightEffect in self.modules + and hasattr(self.modules[Module.LightEffect], "_deprecated_effect") + else self.modules[Module.LightEffect].effect + if Module.LightEffect in self.modules + else None, + ), + # The return values for effect_list includes the Off effect so the lightstrip + # modules have a _deprecated method to return the values as before. + "effect_list": ( + Module.LightEffect, + lambda self: self.modules[Module.LightEffect]._deprecated_effect_list + if Module.LightEffect in self.modules + and hasattr(self.modules[Module.LightEffect], "_deprecated_effect_list") + else self.modules[Module.LightEffect].effect_list + if Module.LightEffect in self.modules + else None, + ), + "set_effect": ( + Module.LightEffect, + lambda self: self.modules[Module.LightEffect].set_effect + if Module.LightEffect in self.modules + else None, + ), + "set_custom_effect": ( + Module.LightEffect, + lambda self: self.modules[Module.LightEffect].set_custom_effect + if Module.LightEffect in self.modules + else None, + ), + } + + _deprecated_light_attributes = { + # Light device methods + "brightness": lambda self: self.modules[Module.Light].brightness + if Module.Light in self.modules + else None, + "set_brightness": lambda self: self.modules[Module.Light].set_brightness + if Module.Light in self.modules + else None, + "hsv": lambda self: self.modules[Module.Light].hsv + if Module.Light in self.modules + else None, + "set_hsv": lambda self: self.modules[Module.Light].set_hsv + if Module.Light in self.modules + else None, + "color_temp": lambda self: self.modules[Module.Light].color_temp + if Module.Light in self.modules + else None, + "set_color_temp": lambda self: self.modules[Module.Light].set_color_temp + if Module.Light in self.modules + else None, + "valid_temperature_range": lambda self: self.modules[ + Module.Light + ].valid_temperature_range + if Module.Light in self.modules + else None, + "has_effects": lambda self: self.modules[Module.Light].has_effects + if Module.Light in self.modules + else None, + } + + _deprecated_other_attributes = { + "led": ( + Module.Led, + lambda self: self.modules[Module.Led].led + if Module.Led in self.modules + else None, + ), + "set_led": ( + Module.Led, + lambda self: self.modules[Module.Led].set_led + if Module.Led in self.modules + else None, + ), + } + + def __getattr__(self, name): + # All devices + if (check_func := self._deprecated_attributes.get(name)) and ( + (func := check_func[1](self)) is not None + ): module = self._deprecated_attributes[name][0] - func = self._deprecated_attributes[name][1] msg = f"{name} is deprecated" if module: msg += f", use: {module} in device.modules instead" warn(msg, DeprecationWarning, stacklevel=1) - return func(self) + return func + # Light effects + if (effect_check_func := self._deprecated_effect_attributes.get(name)) and ( + (func := effect_check_func[1](self)) is not None + ): + msg = ( + f"{name} is deprecated, use: Module.LightEffect" + + " in device.modules instead" + ) + warn(msg, DeprecationWarning, stacklevel=1) + return func + # Bulb only + if (light_check_func := self._deprecated_light_attributes.get(name)) and ( + (light_func := light_check_func(self)) is not None + ): + msg = f"{name} is deprecated, use: Module.Light in device.modules instead" + warn(msg, DeprecationWarning, stacklevel=1) + return light_func + # Other misc attributes + if (other_check_func := self._deprecated_other_attributes.get(name)) and ( + (other_func := other_check_func[1](self)) is not None + ): + module = self._deprecated_other_attributes[name][0] + msg = ( + f"{name} is deprecated, use: Module.{module} in device.modules instead" + ) + warn(msg, DeprecationWarning, stacklevel=1) + return other_func raise AttributeError(f"Device has no attribute {name!r}") diff --git a/kasa/iot/iotbulb.py b/kasa/iot/iotbulb.py index 51df94d17..ffeac2801 100644 --- a/kasa/iot/iotbulb.py +++ b/kasa/iot/iotbulb.py @@ -247,7 +247,7 @@ def _is_variable_color_temp(self) -> bool: @property # type: ignore @requires_update - def valid_temperature_range(self) -> ColorTempRange: + def _valid_temperature_range(self) -> ColorTempRange: """Return the device-specific white temperature range (in Kelvin). :return: White temperature range in Kelvin (minimum, maximum) @@ -284,7 +284,7 @@ def light_state(self) -> dict[str, str]: @property # type: ignore @requires_update - def has_effects(self) -> bool: + def _has_effects(self) -> bool: """Return True if the device supports effects.""" return "lighting_effect_state" in self.sys_info @@ -347,7 +347,7 @@ async def set_light_state( @property # type: ignore @requires_update - def hsv(self) -> HSV: + def _hsv(self) -> HSV: """Return the current HSV state of the bulb. :return: hue, saturation and value (degrees, %, %) @@ -364,7 +364,7 @@ def hsv(self) -> HSV: return HSV(hue, saturation, value) @requires_update - async def set_hsv( + async def _set_hsv( self, hue: int, saturation: int, @@ -404,7 +404,7 @@ async def set_hsv( @property # type: ignore @requires_update - def color_temp(self) -> int: + def _color_temp(self) -> int: """Return color temperature of the device in kelvin.""" if not self._is_variable_color_temp: raise KasaException("Bulb does not support colortemp.") @@ -413,7 +413,7 @@ def color_temp(self) -> int: return int(light_state["color_temp"]) @requires_update - async def set_color_temp( + async def _set_color_temp( self, temp: int, *, brightness=None, transition: int | None = None ) -> dict: """Set the color temperature of the device in kelvin. @@ -444,7 +444,7 @@ def _raise_for_invalid_brightness(self, value): @property # type: ignore @requires_update - def brightness(self) -> int: + def _brightness(self) -> int: """Return the current brightness in percentage.""" if not self._is_dimmable: # pragma: no cover raise KasaException("Bulb is not dimmable.") @@ -453,7 +453,7 @@ def brightness(self) -> int: return int(light_state["brightness"]) @requires_update - async def set_brightness( + async def _set_brightness( self, brightness: int, *, transition: int | None = None ) -> dict: """Set the brightness in percentage. diff --git a/kasa/iot/iotdimmer.py b/kasa/iot/iotdimmer.py index ef99f7496..740d9bb5a 100644 --- a/kasa/iot/iotdimmer.py +++ b/kasa/iot/iotdimmer.py @@ -91,7 +91,7 @@ async def _initialize_modules(self): @property # type: ignore @requires_update - def brightness(self) -> int: + def _brightness(self) -> int: """Return current brightness on dimmers. Will return a range between 0 - 100. @@ -103,7 +103,7 @@ def brightness(self) -> int: return int(sys_info["brightness"]) @requires_update - async def set_brightness(self, brightness: int, *, transition: int | None = None): + async def _set_brightness(self, brightness: int, *, transition: int | None = None): """Set the new dimmer brightness level in percentage. :param int transition: transition duration in milliseconds. @@ -222,3 +222,13 @@ def _is_dimmable(self) -> bool: """Whether the switch supports brightness changes.""" sys_info = self.sys_info return "brightness" in sys_info + + @property + def _is_variable_color_temp(self) -> bool: + """Whether the device supports variable color temp.""" + return False + + @property + def _is_color(self) -> bool: + """Whether the device supports color.""" + return False diff --git a/kasa/iot/iotlightstrip.py b/kasa/iot/iotlightstrip.py index 6bc562583..f6a9719db 100644 --- a/kasa/iot/iotlightstrip.py +++ b/kasa/iot/iotlightstrip.py @@ -6,9 +6,8 @@ from ..deviceconfig import DeviceConfig from ..module import Module from ..protocol import BaseProtocol -from .effects import EFFECT_NAMES_V1 from .iotbulb import IotBulb -from .iotdevice import KasaException, requires_update +from .iotdevice import requires_update from .modules.lighteffect import LightEffect @@ -70,68 +69,3 @@ async def _initialize_modules(self): def length(self) -> int: """Return length of the strip.""" return self.sys_info["length"] - - @property # type: ignore - @requires_update - def effect(self) -> dict: - """Return effect state. - - Example: - {'brightness': 50, - 'custom': 0, - 'enable': 0, - 'id': '', - 'name': ''} - """ - # LightEffectModule returns the current effect name - # so return the dict here for backwards compatibility - return self.sys_info["lighting_effect_state"] - - @property # type: ignore - @requires_update - def effect_list(self) -> list[str] | None: - """Return built-in effects list. - - Example: - ['Aurora', 'Bubbling Cauldron', ...] - """ - # LightEffectModule returns effect names along with a LIGHT_EFFECTS_OFF value - # so return the original effect names here for backwards compatibility - return EFFECT_NAMES_V1 if self.has_effects else None - - @requires_update - async def set_effect( - self, - effect: str, - *, - brightness: int | None = None, - transition: int | None = None, - ) -> None: - """Set an effect on the device. - - If brightness or transition is defined, - its value will be used instead of the effect-specific default. - - See :meth:`effect_list` for available effects, - or use :meth:`set_custom_effect` for custom effects. - - :param str effect: The effect to set - :param int brightness: The wanted brightness - :param int transition: The wanted transition time - """ - await self.modules[Module.LightEffect].set_effect( - effect, brightness=brightness, transition=transition - ) - - @requires_update - async def set_custom_effect( - self, - effect_dict: dict, - ) -> None: - """Set a custom effect on the device. - - :param str effect_dict: The custom effect dict to set - """ - if not self.has_effects: - raise KasaException("Bulb does not support effects.") - await self.modules[Module.LightEffect].set_custom_effect(effect_dict) diff --git a/kasa/iot/iotplug.py b/kasa/iot/iotplug.py index 072261783..8651bf9a4 100644 --- a/kasa/iot/iotplug.py +++ b/kasa/iot/iotplug.py @@ -79,16 +79,6 @@ async def turn_off(self, **kwargs): """Turn the switch off.""" return await self._query_helper("system", "set_relay_state", {"state": 0}) - @property # type: ignore - @requires_update - def led(self) -> bool: - """Return the state of the led.""" - return self.modules[Module.Led].led - - async def set_led(self, state: bool): - """Set the state of the led (night mode).""" - return await self.modules[Module.Led].set_led(state) - class IotWallSwitch(IotPlug): """Representation of a TP-Link Smart Wall Switch.""" diff --git a/kasa/iot/iotstrip.py b/kasa/iot/iotstrip.py index c4dcc57f5..619046bd2 100755 --- a/kasa/iot/iotstrip.py +++ b/kasa/iot/iotstrip.py @@ -147,17 +147,6 @@ def on_since(self) -> datetime | None: return max(plug.on_since for plug in self.children if plug.on_since is not None) - @property # type: ignore - @requires_update - def led(self) -> bool: - """Return the state of the led.""" - sys_info = self.sys_info - return bool(1 - sys_info["led_off"]) - - async def set_led(self, state: bool): - """Set the state of the led (night mode).""" - await self._query_helper("system", "set_led_off", {"off": int(not state)}) - async def current_consumption(self) -> float: """Get the current power consumption in watts.""" return sum([await plug.current_consumption() for plug in self.children]) diff --git a/kasa/iot/modules/light.py b/kasa/iot/modules/light.py index 1bebf8175..833709df5 100644 --- a/kasa/iot/modules/light.py +++ b/kasa/iot/modules/light.py @@ -30,7 +30,7 @@ def _initialize_features(self): super()._initialize_features() device = self._device - if self._device.is_dimmable: + if self._device._is_dimmable: self._add_feature( Feature( device, @@ -45,7 +45,7 @@ def _initialize_features(self): category=Feature.Category.Primary, ) ) - if self._device.is_variable_color_temp: + if self._device._is_variable_color_temp: self._add_feature( Feature( device=device, @@ -59,7 +59,7 @@ def _initialize_features(self): type=Feature.Type.Number, ) ) - if self._device.is_color: + if self._device._is_color: self._add_feature( Feature( device=device, @@ -96,7 +96,7 @@ def is_dimmable(self) -> int: @property # type: ignore def brightness(self) -> int: """Return the current brightness in percentage.""" - return self._device.brightness + return self._device._brightness async def set_brightness( self, brightness: int, *, transition: int | None = None @@ -106,7 +106,7 @@ async def set_brightness( :param int brightness: brightness in percent :param int transition: transition in milliseconds. """ - return await self._device.set_brightness(brightness, transition=transition) + return await self._device._set_brightness(brightness, transition=transition) @property def is_color(self) -> bool: @@ -127,7 +127,7 @@ def has_effects(self) -> bool: """Return True if the device supports effects.""" if (bulb := self._get_bulb_device()) is None: return False - return bulb.has_effects + return bulb._has_effects @property def hsv(self) -> HSV: @@ -137,7 +137,7 @@ def hsv(self) -> HSV: """ if (bulb := self._get_bulb_device()) is None or not bulb._is_color: raise KasaException("Light does not support color.") - return bulb.hsv + return bulb._hsv async def set_hsv( self, @@ -158,7 +158,7 @@ async def set_hsv( """ if (bulb := self._get_bulb_device()) is None or not bulb._is_color: raise KasaException("Light does not support color.") - return await bulb.set_hsv(hue, saturation, value, transition=transition) + return await bulb._set_hsv(hue, saturation, value, transition=transition) @property def valid_temperature_range(self) -> ColorTempRange: @@ -170,7 +170,7 @@ def valid_temperature_range(self) -> ColorTempRange: bulb := self._get_bulb_device() ) is None or not bulb._is_variable_color_temp: raise KasaException("Light does not support colortemp.") - return bulb.valid_temperature_range + return bulb._valid_temperature_range @property def color_temp(self) -> int: @@ -179,7 +179,7 @@ def color_temp(self) -> int: bulb := self._get_bulb_device() ) is None or not bulb._is_variable_color_temp: raise KasaException("Light does not support colortemp.") - return bulb.color_temp + return bulb._color_temp async def set_color_temp( self, temp: int, *, brightness=None, transition: int | None = None @@ -195,6 +195,6 @@ async def set_color_temp( bulb := self._get_bulb_device() ) is None or not bulb._is_variable_color_temp: raise KasaException("Light does not support colortemp.") - return await bulb.set_color_temp( + return await bulb._set_color_temp( temp, brightness=brightness, transition=transition ) diff --git a/kasa/iot/modules/lighteffect.py b/kasa/iot/modules/lighteffect.py index de12fabb6..54b4725bc 100644 --- a/kasa/iot/modules/lighteffect.py +++ b/kasa/iot/modules/lighteffect.py @@ -94,3 +94,29 @@ def has_custom_effects(self) -> bool: def query(self): """Return the base query.""" return {} + + @property # type: ignore + def _deprecated_effect(self) -> dict: + """Return effect state. + + Example: + {'brightness': 50, + 'custom': 0, + 'enable': 0, + 'id': '', + 'name': ''} + """ + # LightEffectModule returns the current effect name + # so return the dict here for backwards compatibility + return self.data["lighting_effect_state"] + + @property # type: ignore + def _deprecated_effect_list(self) -> list[str] | None: + """Return built-in effects list. + + Example: + ['Aurora', 'Bubbling Cauldron', ...] + """ + # LightEffectModule returns effect names along with a LIGHT_EFFECTS_OFF value + # so return the original effect names here for backwards compatibility + return EFFECT_NAMES_V1 diff --git a/kasa/smart/modules/lightstripeffect.py b/kasa/smart/modules/lightstripeffect.py index b47f3fde2..854cf4813 100644 --- a/kasa/smart/modules/lightstripeffect.py +++ b/kasa/smart/modules/lightstripeffect.py @@ -107,3 +107,29 @@ def has_custom_effects(self) -> bool: def query(self): """Return the base query.""" return {} + + @property # type: ignore + def _deprecated_effect(self) -> dict: + """Return effect state. + + Example: + {'brightness': 50, + 'custom': 0, + 'enable': 0, + 'id': '', + 'name': ''} + """ + # LightEffectModule returns the current effect name + # so return the dict here for backwards compatibility + return self.data["lighting_effect"] + + @property # type: ignore + def _deprecated_effect_list(self) -> list[str] | None: + """Return built-in effects list. + + Example: + ['Aurora', 'Bubbling Cauldron', ...] + """ + # LightEffectModule returns effect names along with a LIGHT_EFFECTS_OFF value + # so return the original effect names here for backwards compatibility + return EFFECT_NAMES diff --git a/kasa/tests/test_device.py b/kasa/tests/test_device.py index 6fd63d15f..539748533 100644 --- a/kasa/tests/test_device.py +++ b/kasa/tests/test_device.py @@ -9,7 +9,7 @@ import pytest import kasa -from kasa import Credentials, Device, DeviceConfig, DeviceType +from kasa import Credentials, Device, DeviceConfig, DeviceType, KasaException, Module from kasa.iot import IotDevice from kasa.smart import SmartChildDevice, SmartDevice @@ -174,3 +174,80 @@ def _test_attr(attribute): key for key in Device._deprecated_attributes if key not in tested_keys ] assert len(untested_keys) == 0 + + +async def _test_attribute( + dev: Device, attribute_name, is_expected, module_name, *args, will_raise=False +): + if is_expected and will_raise: + ctx = pytest.raises(will_raise) + elif is_expected: + ctx = pytest.deprecated_call( + match=f"{attribute_name} is deprecated, use: Module.{module_name} in device.modules instead" + ) + else: + ctx = pytest.raises( + AttributeError, match=f"Device has no attribute '{attribute_name}'" + ) + + with ctx: + if args: + await getattr(dev, attribute_name)(*args) + else: + attribute_val = getattr(dev, attribute_name) + assert attribute_val is not None + + +async def test_deprecated_light_effect_attributes(dev: Device): + light_effect = dev.modules.get(Module.LightEffect) + + await _test_attribute(dev, "effect", bool(light_effect), "LightEffect") + await _test_attribute(dev, "effect_list", bool(light_effect), "LightEffect") + await _test_attribute(dev, "set_effect", bool(light_effect), "LightEffect", "Off") + exc = ( + NotImplementedError + if light_effect and not light_effect.has_custom_effects + else None + ) + await _test_attribute( + dev, + "set_custom_effect", + bool(light_effect), + "LightEffect", + {"enable": 0, "name": "foo", "id": "bar"}, + will_raise=exc, + ) + + +async def test_deprecated_light_attributes(dev: Device): + light = dev.modules.get(Module.Light) + + exc = KasaException if light and not light.is_dimmable else None + await _test_attribute(dev, "brightness", bool(light), "Light", will_raise=exc) + await _test_attribute( + dev, "set_brightness", bool(light), "Light", 50, will_raise=exc + ) + + exc = KasaException if light and not light.is_color else None + await _test_attribute(dev, "hsv", bool(light), "Light", will_raise=exc) + await _test_attribute( + dev, "set_hsv", bool(light), "Light", 50, 50, 50, will_raise=exc + ) + + exc = KasaException if light and not light.is_variable_color_temp else None + await _test_attribute(dev, "color_temp", bool(light), "Light", will_raise=exc) + await _test_attribute( + dev, "set_color_temp", bool(light), "Light", 2700, will_raise=exc + ) + await _test_attribute( + dev, "valid_temperature_range", bool(light), "Light", will_raise=exc + ) + + await _test_attribute(dev, "has_effects", bool(light), "Light") + + +async def test_deprecated_other_attributes(dev: Device): + led_module = dev.modules.get(Module.Led) + + await _test_attribute(dev, "led", bool(led_module), "Led") + await _test_attribute(dev, "set_led", bool(led_module), "Led", True) From 1e1d673d562747f790a32226abac812a016cba2a Mon Sep 17 00:00:00 2001 From: sdb9696 Date: Wed, 15 May 2024 09:09:42 +0100 Subject: [PATCH 2/2] Simplify getting deprecated attributes --- kasa/device.py | 195 ++++++++++---------------------------- kasa/tests/test_device.py | 33 ++----- 2 files changed, 62 insertions(+), 166 deletions(-) diff --git a/kasa/device.py b/kasa/device.py index b156eb220..052abc4ce 100644 --- a/kasa/device.py +++ b/kasa/device.py @@ -21,7 +21,7 @@ from .xortransport import XorTransport if TYPE_CHECKING: - from .modulemapping import ModuleMapping + from .modulemapping import ModuleMapping, ModuleName @dataclass @@ -330,164 +330,73 @@ def __repr__(self): return f"<{self.device_type} at {self.host} - update() needed>" return f"<{self.device_type} at {self.host} - {self.alias} ({self.model})>" - _deprecated_attributes = { + _deprecated_device_type_attributes = { # is_type - "is_bulb": (Module.Light, lambda self: self.device_type == DeviceType.Bulb), - "is_dimmer": ( - Module.Light, - lambda self: self.device_type == DeviceType.Dimmer, - ), - "is_light_strip": ( - Module.LightEffect, - lambda self: self.device_type == DeviceType.LightStrip, - ), - "is_plug": (Module.Led, lambda self: self.device_type == DeviceType.Plug), - "is_wallswitch": ( - Module.Led, - lambda self: self.device_type == DeviceType.WallSwitch, - ), - "is_strip": (None, lambda self: self.device_type == DeviceType.Strip), - "is_strip_socket": ( - None, - lambda self: self.device_type == DeviceType.StripSocket, - ), # TODO - # is_light_function - "is_color": ( - Module.Light, - lambda self: Module.Light in self.modules - and self.modules[Module.Light].is_color, - ), - "is_dimmable": ( - Module.Light, - lambda self: Module.Light in self.modules - and self.modules[Module.Light].is_dimmable, - ), - "is_variable_color_temp": ( - Module.Light, - lambda self: Module.Light in self.modules - and self.modules[Module.Light].is_variable_color_temp, - ), + "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_strip": (None, DeviceType.Strip), + "is_strip_socket": (None, DeviceType.StripSocket), } - _deprecated_effect_attributes = { - # Light Effects + def _get_replacing_attr(self, module_name: ModuleName, *attrs): + if module_name not in self.modules: + return None + + for attr in attrs: + if hasattr(self.modules[module_name], attr): + return getattr(self.modules[module_name], attr) + + return None + + _deprecated_other_attributes = { + # light attributes + "is_color": (Module.Light, ["is_color"]), + "is_dimmable": (Module.Light, ["is_dimmable"]), + "is_variable_color_temp": (Module.Light, ["is_variable_color_temp"]), + "brightness": (Module.Light, ["brightness"]), + "set_brightness": (Module.Light, ["set_brightness"]), + "hsv": (Module.Light, ["hsv"]), + "set_hsv": (Module.Light, ["set_hsv"]), + "color_temp": (Module.Light, ["color_temp"]), + "set_color_temp": (Module.Light, ["set_color_temp"]), + "valid_temperature_range": (Module.Light, ["valid_temperature_range"]), + "has_effects": (Module.Light, ["has_effects"]), + # led attributes + "led": (Module.Led, ["led"]), + "set_led": (Module.Led, ["set_led"]), + # light effect attributes # The return values for effect is a str instead of dict so the lightstrip # modules have a _deprecated method to return the value as before. - "effect": ( - Module.LightEffect, - lambda self: self.modules[Module.LightEffect]._deprecated_effect - if Module.LightEffect in self.modules - and hasattr(self.modules[Module.LightEffect], "_deprecated_effect") - else self.modules[Module.LightEffect].effect - if Module.LightEffect in self.modules - else None, - ), + "effect": (Module.LightEffect, ["_deprecated_effect", "effect"]), # The return values for effect_list includes the Off effect so the lightstrip # modules have a _deprecated method to return the values as before. - "effect_list": ( - Module.LightEffect, - lambda self: self.modules[Module.LightEffect]._deprecated_effect_list - if Module.LightEffect in self.modules - and hasattr(self.modules[Module.LightEffect], "_deprecated_effect_list") - else self.modules[Module.LightEffect].effect_list - if Module.LightEffect in self.modules - else None, - ), - "set_effect": ( - Module.LightEffect, - lambda self: self.modules[Module.LightEffect].set_effect - if Module.LightEffect in self.modules - else None, - ), - "set_custom_effect": ( - Module.LightEffect, - lambda self: self.modules[Module.LightEffect].set_custom_effect - if Module.LightEffect in self.modules - else None, - ), - } - - _deprecated_light_attributes = { - # Light device methods - "brightness": lambda self: self.modules[Module.Light].brightness - if Module.Light in self.modules - else None, - "set_brightness": lambda self: self.modules[Module.Light].set_brightness - if Module.Light in self.modules - else None, - "hsv": lambda self: self.modules[Module.Light].hsv - if Module.Light in self.modules - else None, - "set_hsv": lambda self: self.modules[Module.Light].set_hsv - if Module.Light in self.modules - else None, - "color_temp": lambda self: self.modules[Module.Light].color_temp - if Module.Light in self.modules - else None, - "set_color_temp": lambda self: self.modules[Module.Light].set_color_temp - if Module.Light in self.modules - else None, - "valid_temperature_range": lambda self: self.modules[ - Module.Light - ].valid_temperature_range - if Module.Light in self.modules - else None, - "has_effects": lambda self: self.modules[Module.Light].has_effects - if Module.Light in self.modules - else None, - } - - _deprecated_other_attributes = { - "led": ( - Module.Led, - lambda self: self.modules[Module.Led].led - if Module.Led in self.modules - else None, - ), - "set_led": ( - Module.Led, - lambda self: self.modules[Module.Led].set_led - if Module.Led in self.modules - else None, - ), + "effect_list": (Module.LightEffect, ["_deprecated_effect_list", "effect_list"]), + "set_effect": (Module.LightEffect, ["set_effect"]), + "set_custom_effect": (Module.LightEffect, ["set_custom_effect"]), } def __getattr__(self, name): - # All devices - if (check_func := self._deprecated_attributes.get(name)) and ( - (func := check_func[1](self)) is not None - ): - module = self._deprecated_attributes[name][0] + # 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" warn(msg, DeprecationWarning, stacklevel=1) - return func - # Light effects - if (effect_check_func := self._deprecated_effect_attributes.get(name)) and ( - (func := effect_check_func[1](self)) is not None - ): - msg = ( - f"{name} is deprecated, use: Module.LightEffect" - + " in device.modules instead" - ) - warn(msg, DeprecationWarning, stacklevel=1) - return func - # Bulb only - if (light_check_func := self._deprecated_light_attributes.get(name)) and ( - (light_func := light_check_func(self)) is not None - ): - msg = f"{name} is deprecated, use: Module.Light in device.modules instead" - warn(msg, DeprecationWarning, stacklevel=1) - return light_func - # Other misc attributes - if (other_check_func := self._deprecated_other_attributes.get(name)) and ( - (other_func := other_check_func[1](self)) is not None + return self.device_type == dep_device_type_attr[1] + # Other deprecated attributes + if (dep_attr := self._deprecated_other_attributes.get(name)) and ( + (replacing_attr := self._get_replacing_attr(dep_attr[0], *dep_attr[1])) + is not None ): - module = self._deprecated_other_attributes[name][0] + module_name = dep_attr[0] msg = ( - f"{name} is deprecated, use: Module.{module} in device.modules instead" + f"{name} is deprecated, use: " + + f"Module.{module_name} in device.modules instead" ) warn(msg, DeprecationWarning, stacklevel=1) - return other_func + return replacing_attr raise AttributeError(f"Device has no attribute {name!r}") diff --git a/kasa/tests/test_device.py b/kasa/tests/test_device.py index 539748533..d8f28d1bc 100644 --- a/kasa/tests/test_device.py +++ b/kasa/tests/test_device.py @@ -139,14 +139,12 @@ def test_deprecated_exceptions(exceptions_class, use_class): } -def test_deprecated_attributes(dev: SmartDevice): +def test_deprecated_device_type_attributes(dev: SmartDevice): """Test deprecated attributes on all devices.""" - tested_keys = set() def _test_attr(attribute): - tested_keys.add(attribute) msg = f"{attribute} is deprecated" - if module := Device._deprecated_attributes[attribute][0]: + if module := Device._deprecated_device_type_attributes[attribute][0]: msg += f", use: {module} in device.modules instead" with pytest.deprecated_call(match=msg): val = getattr(dev, attribute) @@ -157,24 +155,6 @@ def _test_attr(attribute): expected_val = dev.device_type == deprecated_is_device_type[attribute] assert val == expected_val - for attribute in deprecated_is_light_function_smart_module: - val = _test_attr(attribute) - if isinstance(dev, SmartDevice): - expected_val = ( - deprecated_is_light_function_smart_module[attribute] in dev.modules - ) - elif hasattr(dev, f"_{attribute}"): - expected_val = getattr(dev, f"_{attribute}") - else: - expected_val = False - assert val == expected_val - - assert len(tested_keys) == len(Device._deprecated_attributes) - untested_keys = [ - key for key in Device._deprecated_attributes if key not in tested_keys - ] - assert len(untested_keys) == 0 - async def _test_attribute( dev: Device, attribute_name, is_expected, module_name, *args, will_raise=False @@ -183,7 +163,10 @@ async def _test_attribute( ctx = pytest.raises(will_raise) elif is_expected: ctx = pytest.deprecated_call( - match=f"{attribute_name} is deprecated, use: Module.{module_name} in device.modules instead" + match=( + f"{attribute_name} is deprecated, use: Module." + + f"{module_name} in device.modules instead" + ) ) else: ctx = pytest.raises( @@ -222,6 +205,10 @@ async def test_deprecated_light_effect_attributes(dev: Device): async def test_deprecated_light_attributes(dev: Device): light = dev.modules.get(Module.Light) + await _test_attribute(dev, "is_dimmable", bool(light), "Light") + await _test_attribute(dev, "is_color", bool(light), "Light") + await _test_attribute(dev, "is_variable_color_temp", bool(light), "Light") + exc = KasaException if light and not light.is_dimmable else None await _test_attribute(dev, "brightness", bool(light), "Light", will_raise=exc) await _test_attribute(