mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-08-09 20:24:02 +00:00
Configure mypy to run in virtual environment and fix resulting issues (#989)
For some time I've noticed that my IDE is reporting mypy errors that the pre-commit hook is not picking up. This is because [mypy mirror](https://github.com/pre-commit/mirrors-mypy) runs in an isolated pre-commit environment which does not have dependencies installed and it enables `--ignore-missing-imports` to avoid errors. This is [advised against by mypy](https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-library-stubs-or-py-typed-marker) for obvious reasons: > We recommend avoiding --ignore-missing-imports if possible: it’s equivalent to adding a # type: ignore to all unresolved imports in your codebase. This PR configures the mypy pre-commit hook to run in the virtual environment and addresses the additional errors identified as a result. It also introduces a minimal mypy config into the `pyproject.toml` [mypy errors identified without the fixes in this PR](https://github.com/user-attachments/files/15896693/mypyerrors.txt)
This commit is contained in:
@@ -371,7 +371,10 @@ class AesEncyptionSession:
|
||||
handshake_key_bytes: bytes = base64.b64decode(handshake_key.encode("UTF-8"))
|
||||
private_key_data = base64.b64decode(keypair.get_private_key().encode("UTF-8"))
|
||||
|
||||
private_key = serialization.load_der_private_key(private_key_data, None, None)
|
||||
private_key = cast(
|
||||
rsa.RSAPrivateKey,
|
||||
serialization.load_der_private_key(private_key_data, None, None),
|
||||
)
|
||||
key_and_iv = private_key.decrypt(
|
||||
handshake_key_bytes, asymmetric_padding.PKCS1v15()
|
||||
)
|
||||
|
14
kasa/cli.py
14
kasa/cli.py
@@ -101,9 +101,7 @@ DEVICE_FAMILY_TYPES = [device_family_type.value for device_family_type in Device
|
||||
# Block list of commands which require no update
|
||||
SKIP_UPDATE_COMMANDS = ["raw-command", "command"]
|
||||
|
||||
click.anyio_backend = "asyncio"
|
||||
|
||||
pass_dev = click.make_pass_decorator(Device)
|
||||
pass_dev = click.make_pass_decorator(Device) # type: ignore[type-abstract]
|
||||
|
||||
|
||||
def CatchAllExceptions(cls):
|
||||
@@ -1005,7 +1003,7 @@ async def time_get(dev: Device):
|
||||
|
||||
@time.command(name="sync")
|
||||
@pass_dev
|
||||
async def time_sync(dev: SmartDevice):
|
||||
async def time_sync(dev: Device):
|
||||
"""Set the device time to current time."""
|
||||
if not isinstance(dev, SmartDevice):
|
||||
raise NotImplementedError("setting time currently only implemented on smart")
|
||||
@@ -1143,7 +1141,7 @@ async def presets(ctx):
|
||||
|
||||
@presets.command(name="list")
|
||||
@pass_dev
|
||||
def presets_list(dev: IotBulb):
|
||||
def presets_list(dev: Device):
|
||||
"""List presets."""
|
||||
if not dev.is_bulb or not isinstance(dev, IotBulb):
|
||||
error("Presets only supported on iot bulbs")
|
||||
@@ -1162,7 +1160,7 @@ def presets_list(dev: IotBulb):
|
||||
@click.option("--saturation", type=int)
|
||||
@click.option("--temperature", type=int)
|
||||
@pass_dev
|
||||
async def presets_modify(dev: IotBulb, index, brightness, hue, saturation, temperature):
|
||||
async def presets_modify(dev: Device, index, brightness, hue, saturation, temperature):
|
||||
"""Modify a preset."""
|
||||
for preset in dev.presets:
|
||||
if preset.index == index:
|
||||
@@ -1190,7 +1188,7 @@ async def presets_modify(dev: IotBulb, index, brightness, hue, saturation, tempe
|
||||
@click.option("--type", type=click.Choice(["soft", "hard"], case_sensitive=False))
|
||||
@click.option("--last", is_flag=True)
|
||||
@click.option("--preset", type=int)
|
||||
async def turn_on_behavior(dev: IotBulb, type, last, preset):
|
||||
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):
|
||||
error("Presets only supported on iot bulbs")
|
||||
@@ -1248,7 +1246,7 @@ async def shell(dev: Device):
|
||||
logging.getLogger("asyncio").setLevel(logging.WARNING)
|
||||
loop = asyncio.get_event_loop()
|
||||
try:
|
||||
await embed(
|
||||
await embed( # type: ignore[func-returns-value]
|
||||
globals=globals(),
|
||||
locals=locals(),
|
||||
return_asyncio_coroutine=True,
|
||||
|
@@ -36,7 +36,7 @@ class HttpClient:
|
||||
|
||||
def __init__(self, config: DeviceConfig) -> None:
|
||||
self._config = config
|
||||
self._client_session: aiohttp.ClientSession = None
|
||||
self._client_session: aiohttp.ClientSession | None = None
|
||||
self._jar = aiohttp.CookieJar(unsafe=True, quote_cookie=False)
|
||||
self._last_url = URL(f"http://{self._config.host}/")
|
||||
|
||||
|
@@ -231,7 +231,9 @@ def discovery_data(request, mocker):
|
||||
return {"system": {"get_sysinfo": fixture_data["system"]["get_sysinfo"]}}
|
||||
|
||||
|
||||
@pytest.fixture(params=UNSUPPORTED_DEVICES.values(), ids=UNSUPPORTED_DEVICES.keys())
|
||||
@pytest.fixture(
|
||||
params=UNSUPPORTED_DEVICES.values(), ids=list(UNSUPPORTED_DEVICES.keys())
|
||||
)
|
||||
def unsupported_device_info(request, mocker):
|
||||
"""Return unsupported devices for cli and discovery tests."""
|
||||
discovery_data = request.param
|
||||
|
@@ -276,7 +276,7 @@ class FakeSmartTransport(BaseTransport):
|
||||
):
|
||||
result["sum"] = len(result[list_key])
|
||||
if self.warn_fixture_missing_methods:
|
||||
pytest.fixtures_missing_methods.setdefault(
|
||||
pytest.fixtures_missing_methods.setdefault( # type: ignore[attr-defined]
|
||||
self.fixture_name, set()
|
||||
).add(f"{method} (incomplete '{list_key}' list)")
|
||||
|
||||
@@ -305,7 +305,7 @@ class FakeSmartTransport(BaseTransport):
|
||||
}
|
||||
# Reduce warning spam by consolidating and reporting at the end of the run
|
||||
if self.warn_fixture_missing_methods:
|
||||
pytest.fixtures_missing_methods.setdefault(
|
||||
pytest.fixtures_missing_methods.setdefault( # type: ignore[attr-defined]
|
||||
self.fixture_name, set()
|
||||
).add(method)
|
||||
return retval
|
||||
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import TypedDict
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
@@ -71,7 +72,17 @@ async def test_firmware_update(
|
||||
assert fw
|
||||
|
||||
upgrade_time = 5
|
||||
extras = {"reboot_time": 5, "upgrade_time": upgrade_time, "auto_upgrade": False}
|
||||
|
||||
class Extras(TypedDict):
|
||||
reboot_time: int
|
||||
upgrade_time: int
|
||||
auto_upgrade: bool
|
||||
|
||||
extras: Extras = {
|
||||
"reboot_time": 5,
|
||||
"upgrade_time": upgrade_time,
|
||||
"auto_upgrade": False,
|
||||
}
|
||||
update_states = [
|
||||
# Unknown 1
|
||||
DownloadState(status=1, download_progress=0, **extras),
|
||||
|
@@ -6,6 +6,7 @@ import importlib
|
||||
import inspect
|
||||
import pkgutil
|
||||
import sys
|
||||
from contextlib import AbstractContextManager
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
@@ -161,7 +162,7 @@ async def _test_attribute(
|
||||
dev: Device, attribute_name, is_expected, module_name, *args, will_raise=False
|
||||
):
|
||||
if is_expected and will_raise:
|
||||
ctx = pytest.raises(will_raise)
|
||||
ctx: AbstractContextManager = pytest.raises(will_raise)
|
||||
elif is_expected:
|
||||
ctx = pytest.deprecated_call(match=(f"{attribute_name} is deprecated, use:"))
|
||||
else:
|
||||
|
@@ -5,7 +5,7 @@ import pytest
|
||||
from voluptuous import (
|
||||
All,
|
||||
Any,
|
||||
Coerce, # type: ignore
|
||||
Coerce,
|
||||
Range,
|
||||
Schema,
|
||||
)
|
||||
@@ -21,14 +21,14 @@ CURRENT_CONSUMPTION_SCHEMA = Schema(
|
||||
Any(
|
||||
{
|
||||
"voltage": Any(All(float, Range(min=0, max=300)), None),
|
||||
"power": Any(Coerce(float, Range(min=0)), None),
|
||||
"total": Any(Coerce(float, Range(min=0)), None),
|
||||
"current": Any(All(float, Range(min=0)), None),
|
||||
"power": Any(Coerce(float), None),
|
||||
"total": Any(Coerce(float), None),
|
||||
"current": Any(All(float), None),
|
||||
"voltage_mv": Any(All(float, Range(min=0, max=300000)), int, None),
|
||||
"power_mw": Any(Coerce(float, Range(min=0)), None),
|
||||
"total_wh": Any(Coerce(float, Range(min=0)), None),
|
||||
"current_ma": Any(All(float, Range(min=0)), int, None),
|
||||
"slot_id": Any(Coerce(int, Range(min=0)), None),
|
||||
"power_mw": Any(Coerce(float), None),
|
||||
"total_wh": Any(Coerce(float), None),
|
||||
"current_ma": Any(All(float), int, None),
|
||||
"slot_id": Any(Coerce(int), None),
|
||||
},
|
||||
None,
|
||||
)
|
||||
|
@@ -38,7 +38,7 @@ from ..httpclient import HttpClient
|
||||
),
|
||||
(Exception(), KasaException, "Unable to query the device: "),
|
||||
(
|
||||
aiohttp.ServerFingerprintMismatch("exp", "got", "host", 1),
|
||||
aiohttp.ServerFingerprintMismatch(b"exp", b"got", "host", 1),
|
||||
KasaException,
|
||||
"Unable to query the device: ",
|
||||
),
|
||||
@@ -84,7 +84,7 @@ async def test_httpclient_errors(mocker, error, error_raises, error_message, moc
|
||||
client = HttpClient(DeviceConfig(host))
|
||||
# Exceptions with parameters print with double quotes, without use single quotes
|
||||
full_msg = (
|
||||
"\(" # type: ignore
|
||||
re.escape("(")
|
||||
+ "['\"]"
|
||||
+ re.escape(f"{error_message}{host}: {error}")
|
||||
+ "['\"]"
|
||||
|
@@ -207,7 +207,7 @@ async def test_mac(dev):
|
||||
|
||||
@device_iot
|
||||
async def test_representation(dev):
|
||||
pattern = re.compile("<DeviceType\..+ at .+? - .*? \(.+?\)>")
|
||||
pattern = re.compile(r"<DeviceType\..+ at .+? - .*? \(.+?\)>")
|
||||
assert pattern.match(str(dev))
|
||||
|
||||
|
||||
|
@@ -229,7 +229,7 @@ class XorEncryption:
|
||||
try:
|
||||
from kasa_crypt import decrypt, encrypt
|
||||
|
||||
XorEncryption.decrypt = decrypt # type: ignore[method-assign]
|
||||
XorEncryption.encrypt = encrypt # type: ignore[method-assign]
|
||||
XorEncryption.decrypt = decrypt # type: ignore[assignment]
|
||||
XorEncryption.encrypt = encrypt # type: ignore[assignment]
|
||||
except ImportError:
|
||||
pass
|
||||
|
Reference in New Issue
Block a user