mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
Adjust pylint plugin to enforce device_tracker type hints (#64903)
* Adjust pylint plugin to enforce device_tracker type hints * Use a constant for the type hint matchers * Add tests * Add x_of_y match * Adjust bluetooth_tracker * Adjust mysensors * Adjust tile Co-authored-by: epenet <epenet@users.noreply.github.com>
This commit is contained in:
parent
037621b796
commit
367521e369
@ -5,7 +5,7 @@ import asyncio
|
|||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Final
|
from typing import Final
|
||||||
|
|
||||||
import bluetooth # pylint: disable=import-error
|
import bluetooth # pylint: disable=import-error
|
||||||
from bt_proximity import BluetoothRSSI
|
from bt_proximity import BluetoothRSSI
|
||||||
@ -30,7 +30,7 @@ from homeassistant.const import CONF_DEVICE_ID
|
|||||||
from homeassistant.core import HomeAssistant, ServiceCall
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.event import async_track_time_interval
|
from homeassistant.helpers.event import async_track_time_interval
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
BT_PREFIX,
|
BT_PREFIX,
|
||||||
@ -131,7 +131,7 @@ async def async_setup_scanner(
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config: ConfigType,
|
config: ConfigType,
|
||||||
async_see: Callable[..., Awaitable[None]],
|
async_see: Callable[..., Awaitable[None]],
|
||||||
discovery_info: dict[str, Any] | None = None,
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Set up the Bluetooth Scanner."""
|
"""Set up the Bluetooth Scanner."""
|
||||||
device_id: int = config[CONF_DEVICE_ID]
|
device_id: int = config[CONF_DEVICE_ID]
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
"""Support for tracking MySensors devices."""
|
"""Support for tracking MySensors devices."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Awaitable, Callable
|
||||||
from typing import Any
|
from typing import Any, cast
|
||||||
|
|
||||||
from homeassistant.components import mysensors
|
from homeassistant.components import mysensors
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
from homeassistant.util import slugify
|
from homeassistant.util import slugify
|
||||||
|
|
||||||
from .const import ATTR_GATEWAY_ID, DevId, DiscoveryInfo, GatewayId
|
from .const import ATTR_GATEWAY_ID, DevId, DiscoveryInfo, GatewayId
|
||||||
@ -16,9 +17,9 @@ from .helpers import on_unload
|
|||||||
|
|
||||||
async def async_setup_scanner(
|
async def async_setup_scanner(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config: dict[str, Any],
|
config: ConfigType,
|
||||||
async_see: Callable,
|
async_see: Callable[..., Awaitable[None]],
|
||||||
discovery_info: DiscoveryInfo | None = None,
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Set up the MySensors device scanner."""
|
"""Set up the MySensors device scanner."""
|
||||||
if not discovery_info:
|
if not discovery_info:
|
||||||
@ -27,7 +28,7 @@ async def async_setup_scanner(
|
|||||||
new_devices = mysensors.setup_mysensors_platform(
|
new_devices = mysensors.setup_mysensors_platform(
|
||||||
hass,
|
hass,
|
||||||
Platform.DEVICE_TRACKER,
|
Platform.DEVICE_TRACKER,
|
||||||
discovery_info,
|
cast(DiscoveryInfo, discovery_info),
|
||||||
MySensorsDeviceScanner,
|
MySensorsDeviceScanner,
|
||||||
device_args=(hass, async_see),
|
device_args=(hass, async_see),
|
||||||
)
|
)
|
||||||
|
@ -3,7 +3,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from pytile.tile import Tile
|
from pytile.tile import Tile
|
||||||
|
|
||||||
@ -13,7 +12,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
|||||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
from homeassistant.helpers.update_coordinator import (
|
from homeassistant.helpers.update_coordinator import (
|
||||||
CoordinatorEntity,
|
CoordinatorEntity,
|
||||||
DataUpdateCoordinator,
|
DataUpdateCoordinator,
|
||||||
@ -55,7 +54,7 @@ async def async_setup_scanner(
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config: ConfigType,
|
config: ConfigType,
|
||||||
async_see: Callable[..., Awaitable[None]],
|
async_see: Callable[..., Awaitable[None]],
|
||||||
discovery_info: dict[str, Any] | None = None,
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Detect a legacy configuration and import it."""
|
"""Detect a legacy configuration and import it."""
|
||||||
hass.async_create_task(
|
hass.async_create_task(
|
||||||
|
@ -19,15 +19,29 @@ class TypeHintMatch:
|
|||||||
module_filter: re.Pattern
|
module_filter: re.Pattern
|
||||||
function_name: str
|
function_name: str
|
||||||
arg_types: dict[int, str]
|
arg_types: dict[int, str]
|
||||||
return_type: str | None
|
return_type: list[str] | str | None
|
||||||
|
|
||||||
|
|
||||||
|
_TYPE_HINT_MATCHERS: dict[str, re.Pattern] = {
|
||||||
|
# a_or_b matches items such as "DiscoveryInfoType | None"
|
||||||
|
"a_or_b": re.compile(r"^(\w+) \| (\w+)$"),
|
||||||
|
# x_of_y matches items such as "Awaitable[None]"
|
||||||
|
"x_of_y": re.compile(r"^(\w+)\[(.*?]*)\]$"),
|
||||||
|
# x_of_y_comma_z matches items such as "Callable[..., Awaitable[None]]"
|
||||||
|
"x_of_y_comma_z": re.compile(r"^(\w+)\[(.*?]*), (.*?]*)\]$"),
|
||||||
|
}
|
||||||
|
|
||||||
_MODULE_FILTERS: dict[str, re.Pattern] = {
|
_MODULE_FILTERS: dict[str, re.Pattern] = {
|
||||||
# init matches only in the package root (__init__.py)
|
# init matches only in the package root (__init__.py)
|
||||||
"init": re.compile(r"^homeassistant\.components\.\w+$"),
|
"init": re.compile(r"^homeassistant\.components\.\w+$"),
|
||||||
|
# any_platform matches any platform in the package root ({platform}.py)
|
||||||
"any_platform": re.compile(
|
"any_platform": re.compile(
|
||||||
f"^homeassistant\\.components\\.\\w+\\.({'|'.join([platform.value for platform in Platform])})$"
|
f"^homeassistant\\.components\\.\\w+\\.({'|'.join([platform.value for platform in Platform])})$"
|
||||||
),
|
),
|
||||||
|
# device_tracker matches only in the package root (device_tracker.py)
|
||||||
|
"device_tracker": re.compile(
|
||||||
|
f"^homeassistant\\.components\\.\\w+\\.({Platform.DEVICE_TRACKER.value})$"
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
_METHOD_MATCH: list[TypeHintMatch] = [
|
_METHOD_MATCH: list[TypeHintMatch] = [
|
||||||
@ -117,21 +131,89 @@ _METHOD_MATCH: list[TypeHintMatch] = [
|
|||||||
},
|
},
|
||||||
return_type=None,
|
return_type=None,
|
||||||
),
|
),
|
||||||
|
TypeHintMatch(
|
||||||
|
module_filter=_MODULE_FILTERS["device_tracker"],
|
||||||
|
function_name="setup_scanner",
|
||||||
|
arg_types={
|
||||||
|
0: "HomeAssistant",
|
||||||
|
1: "ConfigType",
|
||||||
|
2: "Callable[..., None]",
|
||||||
|
3: "DiscoveryInfoType | None",
|
||||||
|
},
|
||||||
|
return_type="bool",
|
||||||
|
),
|
||||||
|
TypeHintMatch(
|
||||||
|
module_filter=_MODULE_FILTERS["device_tracker"],
|
||||||
|
function_name="async_setup_scanner",
|
||||||
|
arg_types={
|
||||||
|
0: "HomeAssistant",
|
||||||
|
1: "ConfigType",
|
||||||
|
2: "Callable[..., Awaitable[None]]",
|
||||||
|
3: "DiscoveryInfoType | None",
|
||||||
|
},
|
||||||
|
return_type="bool",
|
||||||
|
),
|
||||||
|
TypeHintMatch(
|
||||||
|
module_filter=_MODULE_FILTERS["device_tracker"],
|
||||||
|
function_name="get_scanner",
|
||||||
|
arg_types={
|
||||||
|
0: "HomeAssistant",
|
||||||
|
1: "ConfigType",
|
||||||
|
},
|
||||||
|
return_type=["DeviceScanner", "DeviceScanner | None"],
|
||||||
|
),
|
||||||
|
TypeHintMatch(
|
||||||
|
module_filter=_MODULE_FILTERS["device_tracker"],
|
||||||
|
function_name="async_get_scanner",
|
||||||
|
arg_types={
|
||||||
|
0: "HomeAssistant",
|
||||||
|
1: "ConfigType",
|
||||||
|
},
|
||||||
|
return_type=["DeviceScanner", "DeviceScanner | None"],
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def _is_valid_type(expected_type: str | None, node: astroid.NodeNG) -> bool:
|
def _is_valid_type(expected_type: list[str] | str | None, node: astroid.NodeNG) -> bool:
|
||||||
"""Check the argument node against the expected type."""
|
"""Check the argument node against the expected type."""
|
||||||
|
if isinstance(expected_type, list):
|
||||||
|
for expected_type_item in expected_type:
|
||||||
|
if _is_valid_type(expected_type_item, node):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
# Const occurs when the type is None
|
# Const occurs when the type is None
|
||||||
if expected_type is None:
|
if expected_type is None or expected_type == "None":
|
||||||
return isinstance(node, astroid.Const) and node.value is None
|
return isinstance(node, astroid.Const) and node.value is None
|
||||||
|
|
||||||
# Special case for DiscoveryInfoType | None"
|
# Const occurs when the type is an Ellipsis
|
||||||
if expected_type == "DiscoveryInfoType | None":
|
if expected_type == "...":
|
||||||
|
return isinstance(node, astroid.Const) and node.value == Ellipsis
|
||||||
|
|
||||||
|
# Special case for `xxx | yyy`
|
||||||
|
if match := _TYPE_HINT_MATCHERS["a_or_b"].match(expected_type):
|
||||||
return (
|
return (
|
||||||
isinstance(node, astroid.BinOp)
|
isinstance(node, astroid.BinOp)
|
||||||
and _is_valid_type("DiscoveryInfoType", node.left)
|
and _is_valid_type(match.group(1), node.left)
|
||||||
and _is_valid_type(None, node.right)
|
and _is_valid_type(match.group(2), node.right)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Special case for xxx[yyy, zzz]`
|
||||||
|
if match := _TYPE_HINT_MATCHERS["x_of_y_comma_z"].match(expected_type):
|
||||||
|
return (
|
||||||
|
isinstance(node, astroid.Subscript)
|
||||||
|
and _is_valid_type(match.group(1), node.value)
|
||||||
|
and isinstance(node.slice, astroid.Tuple)
|
||||||
|
and _is_valid_type(match.group(2), node.slice.elts[0])
|
||||||
|
and _is_valid_type(match.group(3), node.slice.elts[1])
|
||||||
|
)
|
||||||
|
|
||||||
|
# Special case for xxx[yyy]`
|
||||||
|
if match := _TYPE_HINT_MATCHERS["x_of_y"].match(expected_type):
|
||||||
|
return (
|
||||||
|
isinstance(node, astroid.Subscript)
|
||||||
|
and _is_valid_type(match.group(1), node.value)
|
||||||
|
and _is_valid_type(match.group(2), node.slice)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Name occurs when a namespace is not used, eg. "HomeAssistant"
|
# Name occurs when a namespace is not used, eg. "HomeAssistant"
|
||||||
|
1
tests/pylint/__init__.py
Normal file
1
tests/pylint/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Tests for pylint."""
|
41
tests/pylint/test_enforce_type_hints.py
Normal file
41
tests/pylint/test_enforce_type_hints.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
"""Tests for pylint hass_enforce_type_hints plugin."""
|
||||||
|
# pylint:disable=protected-access
|
||||||
|
|
||||||
|
from importlib.machinery import SourceFileLoader
|
||||||
|
import re
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
loader = SourceFileLoader(
|
||||||
|
"hass_enforce_type_hints", "pylint/plugins/hass_enforce_type_hints.py"
|
||||||
|
)
|
||||||
|
hass_enforce_type_hints = loader.load_module(None)
|
||||||
|
_TYPE_HINT_MATCHERS: dict[str, re.Pattern] = hass_enforce_type_hints._TYPE_HINT_MATCHERS
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("string", "expected_x", "expected_y", "expected_z"),
|
||||||
|
[
|
||||||
|
("Callable[..., None]", "Callable", "...", "None"),
|
||||||
|
("Callable[..., Awaitable[None]]", "Callable", "...", "Awaitable[None]"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_regex_x_of_y_comma_z(string, expected_x, expected_y, expected_z):
|
||||||
|
"""Test x_of_y_comma_z regexes."""
|
||||||
|
assert (match := _TYPE_HINT_MATCHERS["x_of_y_comma_z"].match(string))
|
||||||
|
assert match.group(0) == string
|
||||||
|
assert match.group(1) == expected_x
|
||||||
|
assert match.group(2) == expected_y
|
||||||
|
assert match.group(3) == expected_z
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("string", "expected_a", "expected_b"),
|
||||||
|
[("DiscoveryInfoType | None", "DiscoveryInfoType", "None")],
|
||||||
|
)
|
||||||
|
def test_regex_a_or_b(string, expected_a, expected_b):
|
||||||
|
"""Test a_or_b regexes."""
|
||||||
|
assert (match := _TYPE_HINT_MATCHERS["a_or_b"].match(string))
|
||||||
|
assert match.group(0) == string
|
||||||
|
assert match.group(1) == expected_a
|
||||||
|
assert match.group(2) == expected_b
|
Loading…
x
Reference in New Issue
Block a user