10000 Exclude __getattr__ for deprecated attributes from type checkers by sdb9696 · Pull Request #1294 · python-kasa/python-kasa · GitHub
[go: up one dir, main page]

Skip to content

Exclude __getattr__ for deprecated attributes from type checkers #1294

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 23 additions & 22 deletions kasa/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,28 +97,29 @@
"DeviceFamilyType": DeviceFamily,
}


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

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


if TYPE_CHECKING:
Expand Down
2 changes: 1 addition & 1 deletion kasa/cli/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ async def presets_modify(dev: Device, index, brightness, hue, saturation, temper
@click.option("--preset", type=int)
async def turn_on_behavior(dev: Device, type, last, preset):
"""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")
return
settings = await dev.get_turn_on_behavior()
Expand Down
40 changes: 22 additions & 18 deletions kasa/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -566,21 +566,25 @@ def _get_replacing_attr(
"supported_modules": (None, ["modules"]),
}

def __getattr__(self, name: str) -> Any:
# is_device_type
if dep_device_type_attr := self._deprecated_device_type_attributes.get(name):
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
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
):
mod = dep_attr[0]
dev_or_mod = self.modules[mod] if mod else self
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}")
if not TYPE_CHECKING:

def __getattr__(self, name: str) -> Any:
# is_device_type
if dep_device_type_attr := self._deprecated_device_type_attributes.get(
name
):
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
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
):
mod = dep_attr[0]
dev_or_mod = self.modules[mod] if mod else self
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}")
16 changes: 9 additions & 7 deletions kasa/interfaces/energy.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from abc import ABC, abstractmethod
from enum import IntFlag, auto
from typing import Any
from typing import TYPE_CHECKING, Any
from warnings import warn

from ..emeterstatus import EmeterStatus
Expand Down Expand Up @@ -184,9 +184,11 @@
"get_monthstat": "get_monthly_stats",
}

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

def __getattr__(self, name: str) -> Any:
if attr := self._deprecated_attributes.get(name):
msg = f"{name} is deprecated, use {attr} instead"
warn(msg, DeprecationWarning, stacklevel=2)
return getattr(self, attr)
raise AttributeError(f"Energy module has no attribute {name!r}")

Check warning on line 194 in kasa/interfaces/energy.py

View check run for this annotation

Codecov / codecov/patch

kasa/interfaces/energy.py#L191-L194

Added lines #L191 - L194 were not covered by tests
2 changes: 1 addition & 1 deletion kasa/iot/iotdimmer.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ async def turn_on(self, *, transition: int | None = None, **kwargs) -> dict:
"""
if transition is not None:
return await self.set_dimmer_transition(
brightness=self.brightness, transition=transition
brightness=self._brightness, transition=transition
)

return await super().turn_on()
Expand Down
7 changes: 6 additions & 1 deletion kasa/iot/iotmodule.py
1E0A
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
from __future__ import annotations

import logging
from typing import Any
from typing import TYPE_CHECKING, Any

from ..exceptions import KasaException
from ..module import Module

_LOGGER = logging.getLogger(__name__)

if TYPE_CHECKING:
from .iotdevice import IotDevice


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

_device: IotDevice

async def call(self, method: str, params: dict | None = None) -> dict:
"""Call the given method with the given parameters."""
return await self._device._query_helper(self._module, method, params)
Expand Down
4 changes: 3 additions & 1 deletion kasa/iot/iotstrip.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import logging
from collections import defaultdict
from datetime import datetime, timedelta
from typing import Any
from typing import TYPE_CHECKING, Any

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

if update_children:
for plug in self.children:
if TYPE_CHECKING:
assert isinstance(plug, IotStripPlug)
await plug._update()

if not self.features:
Expand Down
2 changes: 2 additions & 0 deletions kasa/iot/modules/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ async def set_state(self, state: LightState) -> dict:
# iot protocol Dimmers and smart protocol devices do not support
# brightness of 0 so 0 will turn off all devices for consistency
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:
return await self._device.turn_off(transition=state.transition)
elif state.brightness:
Expand Down
4 changes: 4 additions & 0 deletions kasa/smart/smartdevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
)
from .smartmodule import SmartModule

if TYPE_CHECKING:
from .smartchilddevice import SmartChildDevice
_LOGGER = logging.getLogger(__name__)


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

# We can first initialize the features after the first update.
Expand Down
3 changes: 3 additions & 0 deletions tests/test_emeter.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from kasa.iot.modules.emeter import Emeter
from kasa.smart import SmartDevice
from kasa.smart.modules import Energy as SmartEnergyModule
from kasa.smart.smartmodule import SmartModule

from .conftest import has_emeter, has_emeter_iot, no_emeter

Expand Down Expand Up @@ -192,6 +193,7 @@ async def test_supported(dev: Device):
pytest.skip(f"Energy module not supported for {dev}.")
energy_module = dev.modules.get(Module.Energy)
assert energy_module

if isinstance(dev, IotDevice):
info = (
dev._last_update
Expand All @@ -210,6 +212,7 @@ async def test_supported(dev: Device):
)
assert energy_module.supports(Energy.ModuleFeature.PERIODIC_STATS) is True
else:
assert isinstance(energy_module, SmartModule)
assert energy_module.supports(Energy.ModuleFeature.CONSUMPTION_TOTAL) is False
assert energy_module.supports(Energy.ModuleFeature.PERIODIC_STATS) is False
if energy_module.supported_version < 2:
Expand Down
Loading
0