Skip to content
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
30 changes: 2 additions & 28 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:

strategy:
matrix:
python-version: ["3.12"]
python-version: ["3.13"]

steps:
- name: "Checkout source files"
Expand All @@ -39,37 +39,17 @@ jobs:
name: Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }}
needs: linting
runs-on: ${{ matrix.os }}
continue-on-error: ${{ startsWith(matrix.python-version, 'pypy') }}

strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "pypy-3.9", "pypy-3.10"]
python-version: ["3.11", "3.12", "3.13"]
os: [ubuntu-latest, macos-latest, windows-latest]
extras: [false, true]
exclude:
- os: macos-latest
extras: true
- os: windows-latest
extras: true
- os: ubuntu-latest
python-version: "pypy-3.9"
extras: true
- os: ubuntu-latest
python-version: "pypy-3.10"
extras: true
- os: ubuntu-latest
python-version: "3.9"
extras: true
- os: ubuntu-latest
python-version: "3.10"
extras: true
# Exclude pypy on windows due to significant performance issues
# running pytest requires ~12 min instead of 2 min on other platforms
- os: windows-latest
python-version: "pypy-3.9"
- os: windows-latest
python-version: "pypy-3.10"


steps:
- uses: "actions/checkout@v4"
Expand All @@ -79,16 +59,10 @@ jobs:
python-version: ${{ matrix.python-version }}
uv-version: ${{ env.UV_VERSION }}
uv-install-options: ${{ matrix.extras == true && '--all-extras' || '' }}
- name: "Run tests (no coverage)"
if: ${{ startsWith(matrix.python-version, 'pypy') }}
run: |
uv run pytest -n auto
- name: "Run tests (with coverage)"
if: ${{ !startsWith(matrix.python-version, 'pypy') }}
run: |
uv run pytest -n auto --cov kasa --cov-report xml
- name: "Upload coverage to Codecov"
if: ${{ !startsWith(matrix.python-version, 'pypy') }}
uses: "codecov/codecov-action@v4"
with:
token: ${{ secrets.CODECOV_TOKEN }}
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ repos:
- id: check-ast

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.7
rev: v0.7.4
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
Expand Down
2 changes: 1 addition & 1 deletion devtools/bench/utils/original.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Original implementation of the TP-Link Smart Home protocol."""

import struct
from typing import Generator
from collections.abc import Generator


class OriginalTPLinkSmartHomeProtocol:
Expand Down
14 changes: 7 additions & 7 deletions devtools/dump_devinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ async def cli(
credentials = Credentials(username=username, password=password)
if host is not None:
if discovery_info:
click.echo("Host and discovery info given, trying connect on %s." % host)
click.echo(f"Host and discovery info given, trying connect on {host}.")

di = json.loads(discovery_info)
dr = DiscoveryResult.from_dict(di)
Expand Down Expand Up @@ -358,7 +358,7 @@ async def cli(
"Could not find a protocol for the given parameters."
)
else:
click.echo("Host given, performing discovery on %s." % host)
click.echo(f"Host given, performing discovery on {host}.")
device = await Discover.discover_single(
host,
credentials=credentials,
Expand All @@ -374,13 +374,13 @@ async def cli(
)
else:
click.echo(
"No --host given, performing discovery on %s. Use --target to override."
% target
"No --host given, performing discovery on"
f" {target}. Use --target to override."
)
devices = await Discover.discover(
target=target, credentials=credentials, discovery_timeout=discovery_timeout
)
click.echo("Detected %s devices" % len(devices))
click.echo(f"Detected {len(devices)} devices")
for dev in devices.values():
await handle_device(
basedir,
Expand Down Expand Up @@ -446,7 +446,7 @@ async def get_legacy_fixture(
dr = DiscoveryResult.from_dict(discovery_info)
final["discovery_result"] = dr.to_dict()

click.echo("Got %s successes" % len(successes))
click.echo(f"Got {len(successes)} successes")
click.echo(click.style("## device info file ##", bold=True))

sysinfo = final["system"]["get_sysinfo"]
Expand Down Expand Up @@ -959,7 +959,7 @@ async def get_smart_fixtures(
dr = DiscoveryResult.from_dict(discovery_info) # type: ignore
final["discovery_result"] = dr.to_dict()

click.echo("Got %s successes" % len(successes))
click.echo(f"Got {len(successes)} successes")
click.echo(click.style("## device info file ##", bold=True))

if "get_device_info" in final:
Expand Down
2 changes: 1 addition & 1 deletion devtools/parse_pcap.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def parse_pcap(file):
for module, cmds in json_payload.items():
seen_items["modules"][module] += 1
if "err_code" in cmds:
echo("[red]Got error for module: %s[/red]" % cmds)
echo(f"[red]Got error for module: {cmds}[/red]")
continue

for cmd, response in cmds.items():
Expand Down
1 change: 0 additions & 1 deletion kasa/cachedzoneinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from __future__ import annotations

import asyncio

from zoneinfo import ZoneInfo


Expand Down
3 changes: 2 additions & 1 deletion kasa/cli/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
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

Expand Down
2 changes: 1 addition & 1 deletion kasa/cli/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

from __future__ import annotations

import zoneinfo
from datetime import datetime

import asyncclick as click
import zoneinfo

from kasa import (
Device,
Expand Down
6 changes: 2 additions & 4 deletions kasa/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,9 @@
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 (
Expand Down Expand Up @@ -213,7 +211,7 @@ def __init__(
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
Expand Down
38 changes: 18 additions & 20 deletions kasa/deviceconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,12 @@

"""

# 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
Expand Down Expand Up @@ -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:
Expand All @@ -145,7 +143,7 @@ def from_values(
) 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)
Expand All @@ -163,9 +161,9 @@ def from_dict(connection_type_dict: Dict[str, Any]) -> "DeviceConnectionParamete

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,
Expand All @@ -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(
Expand All @@ -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:
Expand All @@ -219,9 +217,9 @@ def __post_init__(self) -> None:
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
Expand All @@ -230,7 +228,7 @@ def to_dict(
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)
Expand Down
Loading
Loading