Exclude __getattr__ for deprecated attributes from type checkers (#1294)

This commit is contained in:
Steven B. 2024-11-21 18:40:13 +00:00 committed by GitHub
parent 652b4e0bd7
commit cae9decb02
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 73 additions and 50 deletions

View File

@ -97,28 +97,29 @@ deprecated_classes = {
"DeviceFamilyType": DeviceFamily, "DeviceFamilyType": DeviceFamily,
} }
if not TYPE_CHECKING:
def __getattr__(name: str) -> Any: def __getattr__(name: str) -> Any:
if name in deprecated_names: if name in deprecated_names:
warn(f"{name} is deprecated", DeprecationWarning, stacklevel=2) warn(f"{name} is deprecated", DeprecationWarning, stacklevel=2)
return globals()[f"_deprecated_{name}"] return globals()[f"_deprecated_{name}"]
if name in deprecated_smart_devices: if name in deprecated_smart_devices:
new_class = deprecated_smart_devices[name] new_class = deprecated_smart_devices[name]
package_name = ".".join(new_class.__module__.split(".")[:-1]) package_name = ".".join(new_class.__module__.split(".")[:-1])
warn( warn(
f"{name} is deprecated, use {new_class.__name__} " f"{name} is deprecated, use {new_class.__name__} from "
+ f"from package {package_name} instead or use Discover.discover_single()" + f"package {package_name} instead or use Discover.discover_single()"
+ " and Device.connect() to support new protocols", + " and Device.connect() to support new protocols",
DeprecationWarning, DeprecationWarning,
stacklevel=2, stacklevel=2,
) )
return new_class return new_class
if name in deprecated_classes: if name in deprecated_classes:
new_class = deprecated_classes[name] # type: ignore[assignment] new_class = deprecated_classes[name] # type: ignore[assignment]
msg = f"{name} is deprecated, use {new_class.__name__} instead" msg = f"{name} is deprecated, use {new_class.__name__} instead"
warn(msg, DeprecationWarning, stacklevel=2) warn(msg, DeprecationWarning, stacklevel=2)
return new_class return new_class
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
if TYPE_CHECKING: if TYPE_CHECKING:

View File

@ -190,7 +190,7 @@ async def presets_modify(dev: Device, index, brightness, hue, saturation, temper
@click.option("--preset", type=int) @click.option("--preset", type=int)
async def turn_on_behavior(dev: Device, type, last, preset): async def turn_on_behavior(dev: Device, type, last, preset):
"""Modify bulb turn-on behavior.""" """Modify bulb turn-on behavior."""
if not dev.is_bulb or not isinstance(dev, IotBulb): if dev.device_type is not Device.Type.Bulb or not isinstance(dev, IotBulb):
error("Presets only supported on iot bulbs") error("Presets only supported on iot bulbs")
return return
settings = await dev.get_turn_on_behavior() settings = await dev.get_turn_on_behavior()

View File

@ -566,21 +566,25 @@ class Device(ABC):
"supported_modules": (None, ["modules"]), "supported_modules": (None, ["modules"]),
} }
def __getattr__(self, name: str) -> Any: if not TYPE_CHECKING:
# is_device_type
if dep_device_type_attr := self._deprecated_device_type_attributes.get(name): def __getattr__(self, name: str) -> Any:
msg = f"{name} is deprecated, use device_type property instead" # is_device_type
warn(msg, DeprecationWarning, stacklevel=2) if dep_device_type_attr := self._deprecated_device_type_attributes.get(
return self.device_type == dep_device_type_attr[1] name
# Other deprecated attributes ):
if (dep_attr := self._deprecated_other_attributes.get(name)) and ( msg = f"{name} is deprecated, use device_type property instead"
(replacing_attr := self._get_replacing_attr(dep_attr[0], *dep_attr[1])) warn(msg, DeprecationWarning, stacklevel=2)
is not None return self.device_type == dep_device_type_attr[1]
): # Other deprecated attributes
mod = dep_attr[0] if (dep_attr := self._deprecated_other_attributes.get(name)) and (
dev_or_mod = self.modules[mod] if mod else self (replacing_attr := self._get_replacing_attr(dep_attr[0], *dep_attr[1]))
replacing = f"Module.{mod} in device.modules" if mod else replacing_attr is not None
msg = f"{name} is deprecated, use: {replacing} instead" ):
warn(msg, DeprecationWarning, stacklevel=2) mod = dep_attr[0]
return getattr(dev_or_mod, replacing_attr) dev_or_mod = self.modules[mod] if mod else self
raise AttributeError(f"Device has no attribute {name!r}") replacing = f"Module.{mod} in device.modules" if mod else replacing_attr
msg = f"{name} is deprecated, use: {replacing} instead"
warn(msg, DeprecationWarning, stacklevel=2)
return getattr(dev_or_mod, replacing_attr)
raise AttributeError(f"Device has no attribute {name!r}")

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from enum import IntFlag, auto from enum import IntFlag, auto
from typing import Any from typing import TYPE_CHECKING, Any
from warnings import warn from warnings import warn
from ..emeterstatus import EmeterStatus from ..emeterstatus import EmeterStatus
@ -184,9 +184,11 @@ class Energy(Module, ABC):
"get_monthstat": "get_monthly_stats", "get_monthstat": "get_monthly_stats",
} }
def __getattr__(self, name: str) -> Any: if not TYPE_CHECKING:
if attr := self._deprecated_attributes.get(name):
msg = f"{name} is deprecated, use {attr} instead" def __getattr__(self, name: str) -> Any:
warn(msg, DeprecationWarning, stacklevel=2) if attr := self._deprecated_attributes.get(name):
return getattr(self, attr) msg = f"{name} is deprecated, use {attr} instead"
raise AttributeError(f"Energy module has no attribute {name!r}") warn(msg, DeprecationWarning, stacklevel=2)
return getattr(self, attr)
raise AttributeError(f"Energy module has no attribute {name!r}")

View File

@ -154,7 +154,7 @@ class IotDimmer(IotPlug):
""" """
if transition is not None: if transition is not None:
return await self.set_dimmer_transition( return await self.set_dimmer_transition(
brightness=self.brightness, transition=transition brightness=self._brightness, transition=transition
) )
return await super().turn_on() return await super().turn_on()

View File

@ -3,13 +3,16 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from typing import Any from typing import TYPE_CHECKING, Any
from ..exceptions import KasaException from ..exceptions import KasaException
from ..module import Module from ..module import Module
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
if TYPE_CHECKING:
from .iotdevice import IotDevice
def _merge_dict(dest: dict, source: dict) -> dict: def _merge_dict(dest: dict, source: dict) -> dict:
"""Update dict recursively.""" """Update dict recursively."""
@ -27,6 +30,8 @@ merge = _merge_dict
class IotModule(Module): class IotModule(Module):
"""Base class implemention for all IOT modules.""" """Base class implemention for all IOT modules."""
_device: IotDevice
async def call(self, method: str, params: dict | None = None) -> dict: async def call(self, method: str, params: dict | None = None) -> dict:
"""Call the given method with the given parameters.""" """Call the given method with the given parameters."""
return await self._device._query_helper(self._module, method, params) return await self._device._query_helper(self._module, method, params)

View File

@ -5,7 +5,7 @@ from __future__ import annotations
import logging import logging
from collections import defaultdict from collections import defaultdict
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any from typing import TYPE_CHECKING, Any
from ..device_type import DeviceType from ..device_type import DeviceType
from ..deviceconfig import DeviceConfig from ..deviceconfig import DeviceConfig
@ -145,6 +145,8 @@ class IotStrip(IotDevice):
if update_children: if update_children:
for plug in self.children: for plug in self.children:
if TYPE_CHECKING:
assert isinstance(plug, IotStripPlug)
await plug._update() await plug._update()
if not self.features: if not self.features:

View File

@ -207,6 +207,8 @@ class Light(IotModule, LightInterface):
# iot protocol Dimmers and smart protocol devices do not support # iot protocol Dimmers and smart protocol devices do not support
# brightness of 0 so 0 will turn off all devices for consistency # brightness of 0 so 0 will turn off all devices for consistency
if (bulb := self._get_bulb_device()) is None: # Dimmer if (bulb := self._get_bulb_device()) is None: # Dimmer
if TYPE_CHECKING:
assert isinstance(self._device, IotDimmer)
if state.brightness == 0 or state.light_on is False: if state.brightness == 0 or state.light_on is False:
return await self._device.turn_off(transition=state.transition) return await self._device.turn_off(transition=state.transition)
elif state.brightness: elif state.brightness:

View File

@ -28,6 +28,8 @@ from .modules import (
) )
from .smartmodule import SmartModule from .smartmodule import SmartModule
if TYPE_CHECKING:
from .smartchilddevice import SmartChildDevice
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -196,6 +198,8 @@ class SmartDevice(Device):
# child modules have access to their sysinfo. # child modules have access to their sysinfo.
if update_children or self.device_type != DeviceType.Hub: if update_children or self.device_type != DeviceType.Hub:
for child in self._children.values(): for child in self._children.values():
if TYPE_CHECKING:
assert isinstance(child, SmartChildDevice)
await child._update() await child._update()
# We can first initialize the features after the first update. # We can first initialize the features after the first update.

View File

@ -16,6 +16,7 @@ from kasa.iot import IotDevice, IotStrip
from kasa.iot.modules.emeter import Emeter from kasa.iot.modules.emeter import Emeter
from kasa.smart import SmartDevice from kasa.smart import SmartDevice
from kasa.smart.modules import Energy as SmartEnergyModule from kasa.smart.modules import Energy as SmartEnergyModule
from kasa.smart.smartmodule import SmartModule
from .conftest import has_emeter, has_emeter_iot, no_emeter from .conftest import has_emeter, has_emeter_iot, no_emeter
@ -192,6 +193,7 @@ async def test_supported(dev: Device):
pytest.skip(f"Energy module not supported for {dev}.") pytest.skip(f"Energy module not supported for {dev}.")
energy_module = dev.modules.get(Module.Energy) energy_module = dev.modules.get(Module.Energy)
assert energy_module assert energy_module
if isinstance(dev, IotDevice): if isinstance(dev, IotDevice):
info = ( info = (
dev._last_update dev._last_update
@ -210,6 +212,7 @@ async def test_supported(dev: Device):
) )
assert energy_module.supports(Energy.ModuleFeature.PERIODIC_STATS) is True assert energy_module.supports(Energy.ModuleFeature.PERIODIC_STATS) is True
else: else:
assert isinstance(energy_module, SmartModule)
assert energy_module.supports(Energy.ModuleFeature.CONSUMPTION_TOTAL) is False assert energy_module.supports(Energy.ModuleFeature.CONSUMPTION_TOTAL) is False
assert energy_module.supports(Energy.ModuleFeature.PERIODIC_STATS) is False assert energy_module.supports(Energy.ModuleFeature.PERIODIC_STATS) is False
if energy_module.supported_version < 2: if energy_module.supported_version < 2: