8000 Deprecate legacy light module is_capability checks (#1297) · python-kasa/python-kasa@fa0f715 · GitHub
[go: up one dir, main page]

Skip to content

Commit fa0f715

Browse files
authored
Deprecate legacy light module is_capability checks (#1297)
Deprecate the `is_color`, `is_dimmable`, `is_variable_color_temp`, `valid_temperate_range`, and `has_effects` attributes from the `Light` module, as consumers should use `has_feature("hsv")`, `has_feature("brightness")`, `has_feature("color_temp")`, `get_feature("color_temp").range`, and `Module.LightEffect in dev.modules` respectively. Calling the deprecated attributes will emit a `DeprecationWarning` and type checkers will fail them.
1 parent a03a4b1 commit fa0f715

File tree

5 files changed

+161
-109
lines changed

5 files changed

+161
-109
lines changed

kasa/device.py

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@
107107

108108
import logging
109109
from abc import ABC, abstractmethod
110-
from collections.abc import Mapping, Sequence
110+
from collections.abc import Callable, Mapping, Sequence
111111
from dataclasses import dataclass
112112
from datetime import datetime, tzinfo
113113
from typing import TYPE_CHECKING, Any, TypeAlias
@@ -537,19 +537,52 @@ def _get_replacing_attr(
537537

538538
return None
539539

540+
def _get_deprecated_callable_attribute(self, name: str) -> Any | None:
541+
vals: dict[str, tuple[ModuleName, Callable[[Any], Any], str]] = {
542+
"is_dimmable": (
543+
Module.Light,
544+
lambda c: c.has_feature("brightness"),
545+
'light_module.has_feature("brightness")',
546+
),
547+
"is_color": (
548+
Module.Light,
549+
lambda c: c.has_feature("hsv"),
550+
'light_module.has_feature("hsv")',
551+
),
552+
"is_variable_color_temp": (
553+
Module.Light,
554+
lambda c: c.has_feature("color_temp"),
555+
'light_module.has_feature("color_temp")',
556+
),
557+
"valid_temperature_range": (
558+
Module.Light,
559+
lambda c: c._deprecated_valid_temperature_range(),
560+
'minimum and maximum value of get_feature("color_temp")',
561+
),
562+
"has_effects": (
563+
Module.Light,
564+
lambda c: Module.LightEffect in c._device.modules,
565+
"Module.LightEffect in device.modules",
566+
),
567+
}
568+
if mod_call_msg := vals.get(name):
569+
mod, call, msg = mod_call_msg
570+
msg = f"{name} is deprecated, use: {msg} instead"
571+
warn(msg, DeprecationWarning, stacklevel=2)
572+
if (module := self.modules.get(mod)) is None:
573+
raise AttributeError(f"Device has no attribute {name!r}")
574+
return call(module)
575+
576+
return None
577+
540578
_deprecated_other_attributes = {
541579
# light attributes
542-
"is_color": (Module.Light, ["is_color"]),
543-
"is_dimmable": (Module.Light, ["is_dimmable"]),
544-
"is_variable_color_temp": (Module.Light, ["is_variable_color_temp"]),
545580
"brightness": (Module.Light, ["brightness"]),
546581
"set_brightness": (Module.Light, ["set_brightness"]),
547582
"hsv": (Module.Light, ["hsv"]),
548583
"set_hsv": (Module.Light, ["set_hsv"]),
549584
"color_temp": (Module.Light, ["color_temp"]),
550585
"set_color_temp": (Module.Light, ["set_color_temp"]),
551-
"valid_temperature_range": (Module.Light, ["valid_temperature_range"]),
552-
"has_effects": (Module.Light, ["has_effects"]),
553586
"_deprecated_set_light_state": (Module.Light, ["has_effects"]),
554587
# led attributes
555588
"led": (Module.Led, ["led"]),
@@ -588,6 +621,9 @@ def __getattr__(self, name: str) -> Any:
588621
msg = f"{name} is deprecated, use device_type property instead"
589622
warn(msg, DeprecationWarning, stacklevel=2)
590623
return self.device_type == dep_device_type_attr[1]
624+
# callable
625+
if (result := self._get_deprecated_callable_attribute(name)) is not None:
626+
return result
591627
# Other deprecated attributes
592628
if (dep_attr := self._deprecated_other_attributes.get(name)) and (
593629
(replacing_attr := self._get_replacing_attr(dep_attr[0], *dep_attr[1]))

kasa/interfaces/light.py

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,10 @@
6565

6666
from abc import ABC, abstractmethod
6767
from dataclasses import dataclass
68-
from typing import Annotated, NamedTuple
68+
from typing import TYPE_CHECKING, Annotated, Any, NamedTuple
69+
from warnings import warn
6970

71+
from ..exceptions import KasaException
7072
from ..module import FeatureAttribute, Module
7173

7274

@@ -100,34 +102,6 @@ class HSV(NamedTuple):
100102
class Light(Module, ABC):
101103
"""Base class for TP-Link Light."""
102104

103-
@property
104-
@abstractmethod
105-
def is_dimmable(self) -> bool:
106-
"""Whether the light supports brightness changes."""
107-
108-
@property
109-
@abstractmethod
110-
def is_color(self) -> bool:
111-
"""Whether the bulb supports color changes."""
112-
113-
@property
114-
@abstractmethod
115-
def is_variable_color_temp(self) -> bool:
116-
"""Whether the bulb supports color temperature changes."""
117-
118-
@property
119-
@abstractmethod
120-
def valid_temperature_range(self) -> ColorTempRange:
121-
"""Return the device-specific white temperature range (in Kelvin).
122-
123-
:return: White temperature range in Kelvin (minimum, maximum)
124-
"""
125-
126-
@property
127-
@abstractmethod
128-
def has_effects(self) -> bool:
129-
"""Return True if the device supports effects."""
130-
131105
@property
132106
@abstractmethod
133107
def hsv(self) -> Annotated[HSV, FeatureAttribute()]:
@@ -197,3 +171,44 @@ def state(self) -> LightState:
197171
@abstractmethod
198172
async def set_state(self, state: LightState) -> dict:
199173
"""Set the light state."""
174+
175+
def _deprecated_valid_temperature_range(self) -> ColorTempRange:
176+
if not (temp := self.get_feature("color_temp")):
177+
raise KasaException("Color temperature not supported")
178+
return ColorTempRange(temp.minimum_value, temp.maximum_value)
179+
180+
def _deprecated_attributes(self, dep_name: str) -> str | None:
181+
map: dict[str, str] = {
182+
"is_color": "hsv",
183+
"is_dimmable": "brightness",
184+
"is_variable_color_temp": "color_temp",
185+
}
186+
return map.get(dep_name)
187+
188+
if not TYPE_CHECKING:
189+
190+
def __getattr__(self, name: str) -> Any:
191+
if name == "valid_temperature_range":
192+
msg = (
193+
"valid_temperature_range is deprecated, use "
194+
'get_feature("color_temp") minimum_value '
195+
" and maximum_value instead"
196+
)
197+
warn(msg, DeprecationWarning, stacklevel=2)
198+
res = self._deprecated_valid_temperature_range()
199+
return res
200+
201+
if name == "has_effects":
202+
msg = (
203+
"has_effects is deprecated, check `Module.LightEffect "
204+
"in device.modules` instead"
205+
)
206+
warn(msg, DeprecationWarning, stacklevel=2)
207+
return Module.LightEffect in self._device.modules
208+
209+
if attr := self._deprecated_attributes(name):
210+
msg = f'{name} is deprecated, use has_feature("{attr}") instead'
211+
warn(msg, DeprecationWarning, stacklevel=2)
212+
return self.has_feature(attr)
213+
214+
raise AttributeError(f"Energy module has no attribute {name!r}")

kasa/iot/modules/light.py

Lines changed: 4 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from ...device_type import DeviceType
99
from ...exceptions import KasaException
1010
from ...feature import Feature
11-
from ...interfaces.light import HSV, ColorTempRange, LightState
11+
from ...interfaces.light import HSV, LightState
1212
from ...interfaces.light import Light as LightInterface
1313
from ...module import FeatureAttribute
1414
from ..iotmodule import IotModule
@@ -48,6 +48,8 @@ def _initialize_features(self) -> None:
4848
)
4949
)
5050
if device._is_variable_color_temp:
51+
if TYPE_CHECKING:
52+
assert isinstance(device, IotBulb)
5153
self._add_feature(
5254
Feature(
5355
device=device,
@@ -56,7 +58,7 @@ def _initialize_features(self) -> None:
5658
container=self,
5759
attribute_getter="color_temp",
5860
attribute_setter="set_color_temp",
59-
range_getter="valid_temperature_range",
61+
range_getter=lambda: device._valid_temperature_range,
6062
category=Feature.Category.Primary,
6163
type=Feature.Type.Number,
6264
)
@@ -90,11 +92,6 @@ def _get_bulb_device(self) -> IotBulb | None:
9092
return cast("IotBulb", self._device)
9193
return None
9294

93-
@property # type: ignore
94-
def is_dimmable(self) -> int:
95-
"""Whether the bulb supports brightness changes."""
96-
return self._device._is_dimmable
97-
9895
@property # type: ignore
9996
def brightness(self) -> Annotated[int, FeatureAttribute()]:
10097
" F438 ""Return the current brightness in percentage."""
@@ -112,27 +109,6 @@ async def set_brightness(
112109
LightState(brightness=brightness, transition=transition)
113110
)
114111

115-
@property
116-
def is_color(self) -> bool:
117-
"""Whether the light supports color changes."""
118-
if (bulb := self._get_bulb_device()) is None:
119-
return False
120-
return bulb._is_color
121-
122-
@property
123-
def is_variable_color_temp(self) -> bool:
124-
"""Whether the bulb supports color temperature changes."""
125-
if (bulb := self._get_bulb_device()) is None:
126-
return False
127-
return bulb._is_variable_color_temp
128-
129-
@property
130-
def has_effects(self) -> bool:
131-
"""Return True if the device supports effects."""
132-
if (bulb := self._get_bulb_device()) is None:
133-
return False
134-
return bulb._has_effects
135-
136112
@property
137113
def hsv(self) -> Annotated[HSV, FeatureAttribute()]:
138114
"""Return the current HSV state of the bulb.
@@ -164,18 +140,6 @@ async def set_hsv(
164140
raise KasaException("Light does not support color.")
165141
return await bulb._set_hsv(hue, saturation, value, transition=transition)
166142

167-
@property
168-
def valid_temperature_range(self) -> ColorTempRange:
169-
"""Return the device-specific white temperature range (in Kelvin).
170-
171-
:return: White temperature range in Kelvin (minimum, maximum)
172-
"""
173-
if (
174-
bulb := self._get_bulb_device()
175-
) is None or not bulb._is_variable_color_temp:
176-
raise KasaException("Light does not support colortemp.")
177-
return bulb._valid_temperature_range
178-
179143
@property
180144
def color_temp(self) -> Annotated[int, FeatureAttribute()]:
181145
"""Whether the bulb supports color temperature changes."""

kasa/smart/modules/light.py

Lines changed: 3 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from ...exceptions import KasaException
99
from ...feature import Feature
10-
from ...interfaces.light import HSV, ColorTempRange, LightState
10+
from ...interfaces.light import HSV, LightState
1111
from ...interfaces.light import Light as LightInterface
1212
from ...module import FeatureAttribute, Module
1313
from ..smartmodule import SmartModule
@@ -34,32 +34,6 @@ def query(self) -> dict:
3434
"""Query to execute during the update cycle."""
3535
return {}
3636

37-
@property
38-
def is_color(self) -> bool:
39-
"""Whether the bulb supports color changes."""
40-
return Module.Color in self._device.modules
41-
42-
@property
43-
def is_dimmable(self) -> bool:
44-
"""Whether the bulb supports brightness changes."""
45-
return Module.Brightness in self._device.modules
46-
47-
@property
48-
def is_variable_color_temp(self) -> bool:
49-
"""Whether the bulb supports color temperature changes."""
50-
return Module.ColorTemperature in self._device.modules
51-
52-
@property
53-
def valid_temperature_range(self) -> ColorTempRange:
54-
"""Return the device-specific white temperature range (in Kelvin).
55-
56-
:return: White temperature range in Kelvin (minimum, maximum)
57-
"""
58-
if Module.ColorTemperature not in self._device.modules:
59-
raise KasaException("Color temperature not supported")
60-
61-
return self._device.modules[Module.ColorTemperature].valid_temperature_range
62-
6337
@property
6438
def hsv(self) -> Annotated[HSV, FeatureAttribute()]:
6539
"""Return the current HSV state of the bulb.
@@ -82,7 +56,7 @@ def color_temp(self) -> Annotated[int, FeatureAttribute()]:
8256
@property
8357
def brightness(self) -> Annotated[int, FeatureAttribute()]:
8458
"""Return the current brightness in percentage."""
85-
if Module.Brightness not in self._device.modules:
59+
if Module.Brightness not in self._device.modules: # pragma: no cover
8660
raise KasaException("Bulb is not dimmable.")
8761

8862
return self._device.modules[Module.Brightness].brightness
@@ -135,16 +109,11 @@ async def set_brightness(
135109
:param int brightness: brightness in percent
136110
:param int transition: transition in milliseconds.
137111
"""
138-
if Module.Brightness not in self._device.modules:
112+
if Module.Brightness not in self._device.modules: # pragma: no cover
139113
raise KasaException("Bulb is not dimmable.")
140114

141115
return await self._device.modules[Module.Brightness].set_brightness(brightness)
142116

143-
@property
144-
def has_effects(self) -> bool:
145-
"""Return True if the device supports effects."""
146-
return Module.LightEffect in self._device.modules
147-
148117
async def set_state(self, state: LightState) -> dict:
149118
"""Set the light state."""
150119
state_dict = asdict(state)

0 commit comments

Comments
 (0)
0