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:
Steven B
2024-06-19 14:07:59 +01:00
committed by GitHub
parent 5b7e59056c
commit 416d3118bf
17 changed files with 138 additions and 42 deletions

View File

@@ -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()
)

View File

@@ -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,

View File

@@ -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}/")

View File

@@ -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

View File

@@ -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

View File

@@ -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),

View File

@@ -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:

View File

@@ -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,
)

View File

@@ -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}")
+ "['\"]"

View File

@@ -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))

View File

@@ -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