8000 Exclude __getattr__ for deprecated attributes from type checkers (#1294) · python-kasa/python-kasa@cae9dec · GitHub
[go: up one dir, main page]

Skip to content

Commit cae9dec

Browse files
authored
Exclude __getattr__ for deprecated attributes from type checkers (#1294)
1 parent 652b4e0 commit cae9dec

File tree

10 files changed

+74
-51
lines changed

10 files changed

+74
-51
lines changed

kasa/__init__.py

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -97,28 +97,29 @@
9797
"DeviceFamilyType": DeviceFamily,
9898
}
9999

100-
101-
def __getattr__(name: str) -> Any:
102-
if name in deprecated_names:
103-
warn(f"{name} is deprecated", DeprecationWarning, stacklevel=2)
104-
return globals()[f"_deprecated_{name}"]
105-
if name in deprecated_smart_devices:
106-
new_class = deprecated_smart_devices[name]
107-
package_name = ".".join(new_class.__module__.split(".")[:-1])
108-
warn(
109-
f"{name} is deprecated, use {new_class.__name__} "
110-
+ f"from package {package_name} instead or use Discover.discover_single()"
111-
+ " and Device.connect() to support new protocols",
112-
DeprecationWarning,
113-
stacklevel=2,
114-
)
115-
return new_class
116-
if name in deprecated_classes:
117-
new_class = deprecated_classes[name] # type: ignore[assignment]
118-
msg = f"{name} is deprecated, use {new_class.__name__} instead"
119-
warn(msg, DeprecationWarning, stacklevel=2)
120-
return new_class
121-
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
100+
if not TYPE_CHECKING:
101+
102+
def __getattr__(name: str) -> Any:
103+
if name in deprecated_names:
104+
warn(f"{name} is deprecated", DeprecationWarning, stacklevel=2)
105+
return globals()[f"_deprecated_{name}"]
106+
if name in deprecated_smart_devices:
107+
new_class = deprecated_smart_devices[name]
108+
package_name = ".".join(new_class.__module__.split(".")[:-1])
109+
warn(
110+
f"{name} is deprecated, use {new_class.__name__} from "
111+
+ f"package {package_name} instead or use Discover.discover_single()"
112+
+ " and Device.connect() to support new protocols",
113+
DeprecationWarning,
114+
stacklevel=2,
115+
)
116+
return new_class
117+
if name in deprecated_classes:
118+
new_class = deprecated_classes[name] # type: ignore[assignment]
119+
msg = f"{name} is deprecated, use {new_class.__name__} instead"
120+
warn(msg, DeprecationWarning, stacklevel=2)
121+
return new_class
122+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
122123

123124

124125
if TYPE_CHECKING:

kasa/cli/light.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ async def presets_modify(dev: Device, index, brightness, hue, saturation, temper
190190
@click.option("--preset", type=int)
191191
async def turn_on_behavior(dev: Device, type, last, preset):
192192
"""Modify bulb turn-on behavior."""
193-
if not dev.is_bulb or not isinstance(dev, IotBulb):
193+
if dev.device_type is not Device.Type.Bulb or not isinstance(dev, IotBulb):
194194
error("Presets only supported on iot bulbs")
195195
return
196196
settings = await dev.get_turn_on_behavior()

kasa/device.py

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -566,21 +566,25 @@ def _get_replacing_attr(
566566
"supported_modules": (None, ["modules"]),
567567
}
568568

569-
def __getattr__(self, name: str) -> Any:
570-
# is_device_type
571-
if dep_device_type_attr := self._deprecated_device_type_attributes.get(name):
572-
msg = f"{name} is deprecated, use device_type property instead"
573-
warn(msg, DeprecationWarning, stacklevel=2)
574-
return self.device_type == dep_device_type_attr[1]
575-
# Other deprecated attributes
576-
if (dep_attr := self._deprecated_other_attributes.get(name)) and (
577-
(replacing_attr := self._get_replacing_attr(dep_attr[0], *dep_attr[1]))
578-
is not None
579-
):
580-
mod = dep_attr[0]
581-
dev_or_mod = self.modules[mod] if mod else self
582-
replacing = f"Module.{mod} in device.modules" if mod else replacing_attr
583-
msg = f"{name} is deprecated, use: {replacing} instead"
584-
warn(msg, DeprecationWarning, stacklevel=2)
585-
return getattr(dev_or_mod, replacing_attr)
586-
raise AttributeError(f"Device has no attribute {name!r}")
569+
if not TYPE_CHECKING:
570+
571+
def __getattr__(self, name: str) -> Any:
572+
# is_device_type
573+
if dep_device_type_attr := self._deprecated_device_type_attributes.get(
574+
name
575+
):
576+
msg = f"{name} is deprecated, use device_type property instead"
577+
warn(msg, DeprecationWarning, stacklevel=2)
578+
return self.device_type == dep_device_type_attr[1]
579+
# Other deprecated attributes
580+
if (dep_attr := self._deprecated_other_attributes.get(name)) and (
581+
10000 (replacing_attr := self._get_replacing_attr(dep_attr[0], *dep_attr[1]))
582+
is not None
583+
):
584+
mod = dep_attr[0]
585+
dev_or_mod = self.modules[mod] if mod else self
586+
replacing = f"Module.{mod} in device.modules" if mod else replacing_attr
587+
msg = f"{name} is deprecated, use: {replacing} instead"
588+
warn(msg, DeprecationWarning, stacklevel=2)
589+
return getattr(dev_or_mod, replacing_attr)
590+
raise AttributeError(f"Device has no attribute {name!r}")

kasa/interfaces/energy.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from abc import ABC, abstractmethod
66
from enum import IntFlag, auto
7-
from typing import Any
7+
from typing import TYPE_CHECKING, Any
88
from warnings import warn
99

1010
from ..emeterstatus import EmeterStatus
@@ -184,9 +184,11 @@ async def get_monthly_stats(
184184
"get_monthstat": "get_monthly_stats",
185185
}
186186

187-
def __getattr__(self, name: str) -> Any:
188-
if attr := self._deprecated_attributes.get(name):
189-
msg = f"{name} is deprecated, use {attr} instead"
190-
warn(msg, DeprecationWarning, stacklevel=2)
191-
return getattr(self, attr)
192-
raise AttributeError(f"Energy module has no attribute {name!r}")
187+
if not TYPE_CHECKING:
188+
189+
def __getattr__(self, name: str) -> Any:
190+
if attr := self._deprecated_attributes.get(name):
191+
msg = f"{name} is deprecated, use {attr} instead"
192+
warn(msg, DeprecationWarning, stacklevel=2)
193+
return getattr(self, attr)
194+
raise AttributeError(f"Energy module has no attribute {name!r}")

kasa/iot/iotdimmer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ async def turn_on(self, *, transition: int | None = None, **kwargs) -> dict:
154154
"""
155155
if transition is not None:
156156
return await self.set_dimmer_transition(
157-
brightness=self.brightness, transition=transition
157+
brightness=self._brightness, transition=transition
158158
)
159159

160160
return await super().turn_on()

kasa/iot/iotmodule.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33
from __future__ import annotations
44

55
import logging
6-
from typing import Any
6+
from typing import TYPE_CHECKING, Any
77

88
from ..exceptions import KasaException
99
from ..module import Module
1010

1111
_LOGGER = logging.getLogger(__name__)
1212

13+
if TYPE_CHECKING:
14+
from .iotdevice import IotDevice
15+
1316

1417
def _merge_dict(dest: dict, source: dict) -> dict:
1518
"""Update dict recursively."""
@@ -27,6 +30,8 @@ def _merge_dict(dest: dict, source: dict) -> dict:
2730
class IotModule(Module):
2831
"""Base class implemention for all IOT modules."""
2932

33+
_device: IotDevice
34+
3035
async def call(self, method: str, params: dict | None = None) -> dict:
3136
"""Call the given method with the given parameters."""
3237
return await self._device._query_helper(self._module, method, params)

kasa/iot/iotstrip.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import logging
66
from collections import defaultdict
77
from datetime import datetime, timedelta
8-
from typing import Any
8+
from typing import TYPE_CHECKING, Any
99

1010
from ..device_type import DeviceType
1111
from ..deviceconfig import DeviceConfig
@@ -145,6 +145,8 @@ async def update(self, update_children: bool = True) -> None:
145145

146146
if update_children:
147147
for plug in self.children:
148+
if TYPE_CHECKING:
149+
assert isinstance(plug, IotStripPlug)
148150
await plug._update()
149151

150152
if not self.features:

kasa/iot/modules/light.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,8 @@ async def set_state(self, state: LightState) -> dict:
207207
# iot protocol Dimmers and smart protocol devices do not support
208208
# brightness of 0 so 0 will turn off all devices for consistency
209209
if (bulb := self._get_bulb_device()) is None: # Dimmer
210+
if TYPE_CHECKING:
211+
assert isinstance(self._device, IotDimmer)
210212
if state.brightness == 0 or state.light_on is False:
211213
return await self._device.turn_off(transition=state.transition)
212214
elif state.brightness:

kasa/smart/smartdevice.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
)
2929
from .smartmodule import SmartModule
3030

31+
if TYPE_CHECKING:
32+
from .smartchilddevice import SmartChildDevice
3133
_LOGGER = logging.getLogger(__name__)
3234

3335

@@ -196,6 +198,8 @@ async def update(self, update_children: bool = False) -> None:
196198
# child modules have access to their sysinfo.
197199
if update_children or self.device_type != DeviceType.Hub:
198200
for child in self._children.values():
201+
if TYPE_CHECKING:
202+
assert isinstance(child, SmartChildDevice)
199203
await child._update()
200204

201205
# We can first initialize the features after the first update.

tests/test_emeter.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from kasa.iot.modules.emeter import Emeter
1717
from kasa.smart import SmartDevice
1818
from kasa.smart.modules import Energy as SmartEnergyModule
19+
from kasa.smart.smartmodule import SmartModule
1920

2021
from .conftest import has_emeter, has_emeter_iot, no_emeter
2122

@@ -192,6 +193,7 @@ async def test_supported(dev: Device):
192193
pytest.skip(f"Energy module not supported for {dev}.")
193194
energy_module = dev.modules.get(Module.Energy)
194195
assert energy_module
196+
195197
if isinstance(dev, IotDevice):
196198
info = (
197199
dev._last_update
@@ -210,6 +212,7 @@ async def test_supported(dev: Device):
210212
)
211213
assert energy_module.supports(Energy.ModuleFeature.PERIODIC_STATS) is True
212214
else:
215+
assert isinstance(energy_module, SmartModule)
213216
assert energy_module.supports(Energy.ModuleFeature.CONSUMPTION_TOTAL) is False
214217
assert energy_module.supports(Energy.ModuleFeature.PERIODIC_STATS) is False
215218
if energy_module.supported_version < 2:

0 commit comments

Comments
 (0)
0