Remove support for python <3.11 (#1273)

Python 3.11 ships with latest Debian Bookworm. 
pypy is not that widely used with this library based on statistics. It could be added back when pypy supports python 3.11.
This commit is contained in:
Steven B.
2024-11-18 18:46:36 +00:00
committed by GitHub
parent 0c40939624
commit a01247d48f
55 changed files with 176 additions and 620 deletions

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
import asyncio
from zoneinfo import ZoneInfo

View File

@@ -5,9 +5,10 @@ from __future__ import annotations
import json
import re
import sys
from collections.abc import Callable
from contextlib import contextmanager
from functools import singledispatch, update_wrapper, wraps
from typing import TYPE_CHECKING, Any, Callable, Final
from typing import TYPE_CHECKING, Any, Final
import asyncclick as click

View File

@@ -2,10 +2,10 @@
from __future__ import annotations
import zoneinfo
from datetime import datetime
import asyncclick as click
import zoneinfo
from kasa import (
Device,

View File

@@ -110,11 +110,9 @@ from abc import ABC, abstractmethod
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from datetime import datetime, tzinfo
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, TypeAlias
from warnings import warn
from typing_extensions import TypeAlias
from .credentials import Credentials as _Credentials
from .device_type import DeviceType
from .deviceconfig import (
@@ -213,7 +211,7 @@ class Device(ABC):
self._last_update: Any = None
_LOGGER.debug("Initializing %s of type %s", host, type(self))
self._device_type = DeviceType.Unknown
# TODO: typing Any is just as using Optional[Dict] would require separate
# TODO: typing Any is just as using dict | None would require separate
# checks in accessors. the @updated_required decorator does not ensure
# mypy that these are not accessed incorrectly.
self._discovery_info: dict[str, Any] | None = None

View File

@@ -27,14 +27,12 @@ Living Room Bulb
"""
# Note that this module does not work with from __future__ import annotations
# due to it's use of type returned by fields() which becomes a string with the import.
# https://bugs.python.org/issue39442
# ruff: noqa: FA100
# Module cannot use from __future__ import annotations until migrated to mashumaru
# as dataclass.fields() will not resolve the type.
import logging
from dataclasses import asdict, dataclass, field, fields, is_dataclass
from enum import Enum
from typing import TYPE_CHECKING, Any, Dict, Optional, TypedDict, Union
from typing import TYPE_CHECKING, Any, Optional, TypedDict
from .credentials import Credentials
from .exceptions import KasaException
@@ -118,15 +116,15 @@ class DeviceConnectionParameters:
device_family: DeviceFamily
encryption_type: DeviceEncryptionType
login_version: Optional[int] = None
login_version: int | None = None
https: bool = False
@staticmethod
def from_values(
device_family: str,
encryption_type: str,
login_version: Optional[int] = None,
https: Optional[bool] = None,
login_version: int | None = None,
https: bool | None = None,
) -> "DeviceConnectionParameters":
"""Return connection parameters from string values."""
try:
@@ -145,7 +143,7 @@ class DeviceConnectionParameters:
) from ex
@staticmethod
def from_dict(connection_type_dict: Dict[str, Any]) -> "DeviceConnectionParameters":
def from_dict(connection_type_dict: dict[str, Any]) -> "DeviceConnectionParameters":
"""Return connection parameters from dict."""
if (
isinstance(connection_type_dict, dict)
@@ -163,9 +161,9 @@ class DeviceConnectionParameters:
raise KasaException(f"Invalid connection type data for {connection_type_dict}")
def to_dict(self) -> Dict[str, Union[str, int, bool]]:
def to_dict(self) -> dict[str, str | int | bool]:
"""Convert connection params to dict."""
result: Dict[str, Union[str, int]] = {
result: dict[str, str | int] = {
"device_family": self.device_family.value,
"encryption_type": self.encryption_type.value,
"https": self.https,
@@ -183,17 +181,17 @@ class DeviceConfig:
#: IP address or hostname
host: str
#: Timeout for querying the device
timeout: Optional[int] = DEFAULT_TIMEOUT
timeout: int | None = DEFAULT_TIMEOUT
#: Override the default 9999 port to support port forwarding
port_override: Optional[int] = None
port_override: int | None = None
#: Credentials for devices requiring authentication
credentials: Optional[Credentials] = None
credentials: Credentials | None = None
#: Credentials hash for devices requiring authentication.
#: If credentials are also supplied they take precendence over credentials_hash.
#: Credentials hash can be retrieved from :attr:`Device.credentials_hash`
credentials_hash: Optional[str] = None
credentials_hash: str | None = None
#: The protocol specific type of connection. Defaults to the legacy type.
batch_size: Optional[int] = None
batch_size: int | None = None
#: The batch size for protoools supporting multiple request batches.
connection_type: DeviceConnectionParameters = field(
default_factory=lambda: DeviceConnectionParameters(
@@ -208,7 +206,7 @@ class DeviceConfig:
#: Set a custom http_client for the device to use.
http_client: Optional["ClientSession"] = field(default=None, compare=False)
aes_keys: Optional[KeyPairDict] = None
aes_keys: KeyPairDict | None = None
def __post_init__(self) -> None:
if self.connection_type is None:
@@ -219,9 +217,9 @@ class DeviceConfig:
def to_dict(
self,
*,
credentials_hash: Optional[str] = None,
credentials_hash: str | None = None,
exclude_credentials: bool = False,
) -> Dict[str, Dict[str, str]]:
) -> dict[str, dict[str, str]]:
"""Convert device config to dict."""
if credentials_hash is not None or exclude_credentials:
self.credentials = None
@@ -230,7 +228,7 @@ class DeviceConfig:
return _dataclass_to_dict(self)
@staticmethod
def from_dict(config_dict: Dict[str, Dict[str, str]]) -> "DeviceConfig":
def from_dict(config_dict: dict[str, dict[str, str]]) -> "DeviceConfig":
"""Return device config from dict."""
if isinstance(config_dict, dict):
return _dataclass_from_dict(DeviceConfig, config_dict)

View File

@@ -89,26 +89,19 @@ import logging
import secrets
import socket
import struct
from asyncio import timeout as asyncio_timeout
from asyncio.transports import DatagramTransport
from collections.abc import Callable, Coroutine
from dataclasses import dataclass, field
from pprint import pformat as pf
from typing import (
TYPE_CHECKING,
Any,
Callable,
Coroutine,
Dict,
NamedTuple,
Optional,
Type,
cast,
)
from aiohttp import ClientSession
# When support for cpython older than 3.11 is dropped
# async_timeout can be replaced with asyncio.timeout
from async_timeout import timeout as asyncio_timeout
from mashumaro import field_options
from mashumaro.config import BaseConfig
@@ -156,7 +149,7 @@ class ConnectAttempt(NamedTuple):
OnDiscoveredCallable = Callable[[Device], Coroutine]
OnUnsupportedCallable = Callable[[UnsupportedDeviceError], Coroutine]
OnConnectAttemptCallable = Callable[[ConnectAttempt, bool], None]
DeviceDict = Dict[str, Device]
DeviceDict = dict[str, Device]
NEW_DISCOVERY_REDACTORS: dict[str, Callable[[Any], Any] | None] = {
"device_id": lambda x: "REDACTED_" + x[9::],
@@ -676,7 +669,7 @@ class Discover:
data = redact_data(info, IOT_REDACTORS) if Discover._redact_data else info
_LOGGER.debug("[DISCOVERY] %s << %s", config.host, pf(data))
device_class = cast(Type[IotDevice], Discover._get_device_class(info))
device_class = cast(type[IotDevice], Discover._get_device_class(info))
device = device_class(config.host, config=config)
sys_info = info["system"]["get_sysinfo"]
if device_type := sys_info.get("mic_type", sys_info.get("type")):
@@ -830,9 +823,9 @@ class EncryptionScheme(_DiscoveryBaseMixin):
"""Base model for encryption scheme of discovery result."""
is_support_https: bool
encrypt_type: Optional[str] = None # noqa: UP007
http_port: Optional[int] = None # noqa: UP007
lv: Optional[int] = None # noqa: UP007
encrypt_type: str | None = None
http_port: int | None = None
lv: int | None = None
@dataclass
@@ -854,18 +847,18 @@ class DiscoveryResult(_DiscoveryBaseMixin):
ip: str
mac: str
mgt_encrypt_schm: EncryptionScheme
device_name: Optional[str] = None # noqa: UP007
encrypt_info: Optional[EncryptionInfo] = None # noqa: UP007
encrypt_type: Optional[list[str]] = None # noqa: UP007
decrypted_data: Optional[dict] = None # noqa: UP007
is_reset_wifi: Optional[bool] = field( # noqa: UP007
device_name: str | None = None
encrypt_info: EncryptionInfo | None = None
encrypt_type: list[str] | None = None
decrypted_data: dict | None = None
is_reset_wifi: bool | None = field(
metadata=field_options(alias="isResetWiFi"), default=None
)
firmware_version: Optional[str] = None # noqa: UP007
hardware_version: Optional[str] = None # noqa: UP007
hw_ver: Optional[str] = None # noqa: UP007
owner: Optional[str] = None # noqa: UP007
is_support_iot_cloud: Optional[bool] = None # noqa: UP007
obd_src: Optional[str] = None # noqa: UP007
factory_default: Optional[bool] = None # noqa: UP007
firmware_version: str | None = None
hardware_version: str | None = None
hw_ver: str | None = None
owner: str | None = None
is_support_iot_cloud: bool | None = None
obd_src: str | None = None
factory_default: bool | None = None

View File

@@ -39,7 +39,7 @@ class DeviceError(KasaException):
"""Base exception for device errors."""
def __init__(self, *args: Any, **kwargs: Any) -> None:
self.error_code: SmartErrorCode | None = kwargs.get("error_code", None)
self.error_code: SmartErrorCode | None = kwargs.get("error_code")
super().__init__(*args)
def __repr__(self) -> str:

View File

@@ -68,10 +68,11 @@ Type.Choice
from __future__ import annotations
import logging
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from enum import Enum, auto
from functools import cached_property
from typing import TYPE_CHECKING, Any, Callable, Coroutine
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from .device import Device
@@ -244,7 +245,7 @@ class Feature:
if self.attribute_setter is None:
raise ValueError("Tried to set read-only feature.")
if self.type == Feature.Type.Number: # noqa: SIM102
if not isinstance(value, (int, float)):
if not isinstance(value, int | float):
raise ValueError("value must be a number")
if value < self.minimum_value or value > self.maximum_value:
raise ValueError(

View File

@@ -6,7 +6,7 @@ import asyncio
import logging
import ssl
import time
from typing import Any, Dict
from typing import Any
import aiohttp
from yarl import URL
@@ -98,7 +98,7 @@ class HttpClient:
# This allows the json parameter to be used to pass other
# types of data such as async_generator and still have json
# returned.
if json and not isinstance(json, Dict):
if json and not isinstance(json, dict):
data = json
json = None
try:
@@ -131,7 +131,7 @@ class HttpClient:
raise _ConnectionError(
f"Device connection error: {self._config.host}: {ex}", ex
) from ex
except (aiohttp.ServerTimeoutError, asyncio.TimeoutError) as ex:
except (aiohttp.ServerTimeoutError, TimeoutError) as ex:
raise TimeoutError(
"Unable to query the device, "
+ f"timed out: {self._config.host}: {ex}",

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
import logging
import re
from enum import Enum
from typing import Optional, cast
from typing import cast
from pydantic.v1 import BaseModel, Field, root_validator
@@ -49,7 +49,7 @@ class TurnOnBehavior(BaseModel):
"""
#: Index of preset to use, or ``None`` for the last known state.
preset: Optional[int] = Field(alias="index", default=None) # noqa: UP007
preset: int | None = Field(alias="index", default=None)
#: Wanted behavior
mode: BehaviorMode

View File

@@ -17,9 +17,9 @@ from __future__ import annotations
import functools
import inspect
import logging
from collections.abc import Mapping, Sequence
from collections.abc import Callable, Mapping, Sequence
from datetime import datetime, timedelta, tzinfo
from typing import TYPE_CHECKING, Any, Callable, cast
from typing import TYPE_CHECKING, Any, cast
from warnings import warn
from ..device import Device, WifiNetwork, _DeviceInfo

View File

@@ -5,7 +5,6 @@ from __future__ import annotations
import logging
from datetime import datetime, timedelta, tzinfo
from typing import cast
from zoneinfo import ZoneInfo
from ..cachedzoneinfo import CachedZoneInfo

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from collections.abc import Sequence
from dataclasses import asdict
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING
from pydantic.v1 import BaseModel, Field
@@ -17,22 +17,25 @@ from ..iotmodule import IotModule
if TYPE_CHECKING:
pass
# type ignore can be removed after migration mashumaro:
# error: Signature of "__replace__" incompatible with supertype "LightState"
class IotLightPreset(BaseModel, LightState):
class IotLightPreset(BaseModel, LightState): # type: ignore[override]
"""Light configuration preset."""
index: int = Field(kw_only=True)
brightness: int = Field(kw_only=True)
# These are not available for effect mode presets on light strips
hue: Optional[int] = Field(kw_only=True, default=None) # noqa: UP007
saturation: Optional[int] = Field(kw_only=True, default=None) # noqa: UP007
color_temp: Optional[int] = Field(kw_only=True, default=None) # noqa: UP007
hue: int | None = Field(kw_only=True, default=None)
saturation: int | None = Field(kw_only=True, default=None)
color_temp: int | None = Field(kw_only=True, default=None)
# Variables for effect mode presets
custom: Optional[int] = Field(kw_only=True, default=None) # noqa: UP007
id: Optional[str] = Field(kw_only=True, default=None) # noqa: UP007
mode: Optional[int] = Field(kw_only=True, default=None) # noqa: UP007
custom: int | None = Field(kw_only=True, default=None)
id: str | None = Field(kw_only=True, default=None)
mode: int | None = Field(kw_only=True, default=None)
class LightPreset(IotModule, LightPresetInterface):

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
import logging
from enum import Enum
from typing import Dict, List, Optional
from pydantic.v1 import BaseModel
@@ -35,20 +34,20 @@ class Rule(BaseModel):
id: str
name: str
enable: bool
wday: List[int] # noqa: UP006
wday: list[int]
repeat: bool
# start action
sact: Optional[Action] # noqa: UP007
sact: Action | None
stime_opt: TimeOption
smin: int
eact: Optional[Action] # noqa: UP007
eact: Action | None
etime_opt: TimeOption
emin: int
# Only on bulbs
s_light: Optional[Dict] # noqa: UP006,UP007
s_light: dict | None
_LOGGER = logging.getLogger(__name__)

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import datetime, timezone, tzinfo
from datetime import UTC, datetime, tzinfo
from ...exceptions import KasaException
from ...interfaces import Time as TimeInterface
@@ -13,7 +13,7 @@ from ..iottimezone import get_timezone, get_timezone_index
class Time(IotModule, TimeInterface):
"""Implements the timezone settings."""
_timezone: tzinfo = timezone.utc
_timezone: tzinfo = UTC
def query(self) -> dict:
"""Request time and timezone."""

View File

@@ -2,7 +2,8 @@
from __future__ import annotations
from typing import Any, Callable
from collections.abc import Callable
from typing import Any
try:
import orjson

View File

@@ -29,7 +29,7 @@ If you know or expect the module to exist you can access by index:
Modules support typing via the Module names in Module:
>>> from typing_extensions import reveal_type, TYPE_CHECKING
>>> from typing import reveal_type, TYPE_CHECKING
>>> light_effect = dev.modules.get("LightEffect")
>>> light_effect_typed = dev.modules.get(Module.LightEffect)
>>> if TYPE_CHECKING:

View File

@@ -50,9 +50,7 @@ def _test_module_mapping_typing() -> None:
This is tested during the mypy run and needs to be in this file.
"""
from typing import Any, NewType, cast
from typing_extensions import assert_type
from typing import Any, NewType, assert_type, cast
from .iot.iotmodule import IotModule
from .module import Module

View File

@@ -4,8 +4,9 @@ from __future__ import annotations
import asyncio
import logging
from collections.abc import Callable
from pprint import pformat as pf
from typing import TYPE_CHECKING, Any, Callable
from typing import TYPE_CHECKING, Any
from ..deviceconfig import DeviceConfig
from ..exceptions import (

View File

@@ -17,10 +17,9 @@ import hashlib
import logging
import struct
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast
from collections.abc import Callable
from typing import TYPE_CHECKING, Any, TypeVar, cast
# When support for cpython older than 3.11 is dropped
# async_timeout can be replaced with asyncio.timeout
from ..deviceconfig import DeviceConfig
_LOGGER = logging.getLogger(__name__)
@@ -36,7 +35,7 @@ if TYPE_CHECKING:
def redact_data(data: _T, redactors: dict[str, Callable[[Any], Any] | None]) -> _T:
"""Redact sensitive data for logging."""
if not isinstance(data, (dict, list)):
if not isinstance(data, dict | list):
return data
if isinstance(data, list):

View File

@@ -11,8 +11,9 @@ import base64
import logging
import time
import uuid
from collections.abc import Callable
from pprint import pformat as pf
from typing import TYPE_CHECKING, Any, Callable
from typing import TYPE_CHECKING, Any
from ..exceptions import (
SMART_AUTHENTICATION_ERRORS,

View File

@@ -4,13 +4,11 @@ from __future__ import annotations
import asyncio
import logging
from collections.abc import Coroutine
from asyncio import timeout as asyncio_timeout
from collections.abc import Callable, Coroutine
from datetime import date
from typing import TYPE_CHECKING, Callable, Optional
from typing import TYPE_CHECKING
# When support for cpython older than 3.11 is dropped
# async_timeout can be replaced with asyncio.timeout
from async_timeout import timeout as asyncio_timeout
from pydantic.v1 import BaseModel, Field, validator
from ...exceptions import KasaException
@@ -41,11 +39,11 @@ class UpdateInfo(BaseModel):
"""Update info status object."""
status: int = Field(alias="type")
version: Optional[str] = Field(alias="fw_ver", default=None) # noqa: UP007
release_date: Optional[date] = None # noqa: UP007
release_notes: Optional[str] = Field(alias="release_note", default=None) # noqa: UP007
fw_size: Optional[int] = None # noqa: UP007
oem_id: Optional[str] = None # noqa: UP007
version: str | None = Field(alias="fw_ver", default=None)
release_date: date | None = None
release_notes: str | None = Field(alias="release_note", default=None)
fw_size: int | None = None
oem_id: str | None = None
needs_upgrade: bool = Field(alias="need_to_upgrade")
@validator("release_date", pre=True)
@@ -58,9 +56,7 @@ class UpdateInfo(BaseModel):
@property
def update_available(self) -> bool:
"""Return True if update available."""
if self.status != 0:
return True
return False
return self.status != 0
class Firmware(SmartModule):

View File

@@ -2,9 +2,8 @@
from __future__ import annotations
from datetime import datetime, timedelta, timezone, tzinfo
from datetime import UTC, datetime, timedelta, timezone, tzinfo
from typing import cast
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from ...cachedzoneinfo import CachedZoneInfo
@@ -19,7 +18,7 @@ class Time(SmartModule, TimeInterface):
REQUIRED_COMPONENT = "time"
QUERY_GETTER_NAME = "get_device_time"
_timezone: tzinfo = timezone.utc
_timezone: tzinfo = UTC
def _initialize_features(self) -> None:
"""Initialize features after the initial update."""

View File

@@ -6,7 +6,7 @@ import base64
import logging
import time
from collections.abc import Mapping, Sequence
from datetime import datetime, timedelta, timezone, tzinfo
from datetime import UTC, datetime, timedelta, tzinfo
from typing import TYPE_CHECKING, Any, cast
from ..device import Device, WifiNetwork, _DeviceInfo
@@ -520,7 +520,7 @@ class SmartDevice(Device):
return time_mod.time
# We have no device time, use current local time.
return datetime.now(timezone.utc).astimezone().replace(microsecond=0)
return datetime.now(UTC).astimezone().replace(microsecond=0)
@property
def on_since(self) -> datetime | None:

View File

@@ -4,9 +4,7 @@ from __future__ import annotations
import logging
from collections.abc import Awaitable, Callable, Coroutine
from typing import TYPE_CHECKING, Any
from typing_extensions import Concatenate, ParamSpec, TypeVar
from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar
from ..exceptions import DeviceError, KasaException, SmartErrorCode
from ..module import Module

View File

@@ -2,9 +2,8 @@
from __future__ import annotations
from datetime import datetime, timezone, tzinfo
from datetime import UTC, datetime, tzinfo
from typing import cast
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from ...cachedzoneinfo import CachedZoneInfo
@@ -20,7 +19,7 @@ class Time(SmartCameraModule, TimeInterface):
QUERY_MODULE_NAME = "system"
QUERY_SECTION_NAMES = "basic"
_timezone: tzinfo = timezone.utc
_timezone: tzinfo = UTC
_time: datetime
def _initialize_features(self) -> None:

View File

@@ -12,7 +12,7 @@ import logging
import time
from collections.abc import AsyncGenerator
from enum import Enum, auto
from typing import TYPE_CHECKING, Any, Dict, cast
from typing import TYPE_CHECKING, Any, cast
from cryptography.hazmat.primitives import hashes, padding, serialization
from cryptography.hazmat.primitives.asymmetric import padding as asymmetric_padding
@@ -193,7 +193,7 @@ class AesTransport(BaseTransport):
)
if TYPE_CHECKING:
resp_dict = cast(Dict[str, Any], resp_dict)
resp_dict = cast(dict[str, Any], resp_dict)
assert self._encryption_session is not None
self._handle_response_error_code(
@@ -326,7 +326,7 @@ class AesTransport(BaseTransport):
)
if TYPE_CHECKING:
resp_dict = cast(Dict[str, Any], resp_dict)
resp_dict = cast(dict[str, Any], resp_dict)
self._handle_response_error_code(resp_dict, "Unable to complete handshake")

View File

@@ -51,7 +51,8 @@ import secrets
import struct
import time
from asyncio import Future
from typing import TYPE_CHECKING, Any, Generator, cast
from collections.abc import Generator
from typing import TYPE_CHECKING, Any, cast
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

View File

@@ -9,7 +9,7 @@ import logging
import secrets
import ssl
from enum import Enum, auto
from typing import TYPE_CHECKING, Any, Dict, cast
from typing import TYPE_CHECKING, Any, cast
from yarl import URL
@@ -227,7 +227,7 @@ class SslAesTransport(BaseTransport):
)
if TYPE_CHECKING:
resp_dict = cast(Dict[str, Any], resp_dict)
resp_dict = cast(dict[str, Any], resp_dict)
assert self._encryption_session is not None
if "result" in resp_dict and "response" in resp_dict["result"]:
@@ -393,7 +393,7 @@ class SslAesTransport(BaseTransport):
raise AuthenticationError(f"Error trying handshake1: {resp_dict}")
if TYPE_CHECKING:
resp_dict = cast(Dict[str, Any], resp_dict)
resp_dict = cast(dict[str, Any], resp_dict)
server_nonce = resp_dict["result"]["data"]["nonce"]
device_confirm = resp_dict["result"]["data"]["device_confirm"]

View File

@@ -18,12 +18,9 @@ import errno
import logging
import socket
import struct
from asyncio import timeout as asyncio_timeout
from collections.abc import Generator
# When support for cpython older than 3.11 is dropped
# async_timeout can be replaced with asyncio.timeout
from async_timeout import timeout as asyncio_timeout
from kasa.deviceconfig import DeviceConfig
from kasa.exceptions import KasaException, _RetryableError
from kasa.json import loads as json_loads