From f6c76372ce67df4928f6f0fe9b691819ca074081 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 4 Feb 2023 18:52:59 +0100 Subject: [PATCH] Ensure hass is typed (#87068) * Ensure hass is typed * Adjust pilight * Adjust homeassistant scene * Adjust hassio * Adjust gree * Adjust google_maps * Adjust energyzero * Adjust harmony * Adjust mobile_app --- .../components/energyzero/coordinator.py | 3 +- .../components/google_maps/device_tracker.py | 4 +- homeassistant/components/gree/bridge.py | 2 +- homeassistant/components/harmony/data.py | 5 +- homeassistant/components/hassio/http.py | 3 +- .../components/homeassistant/scene.py | 2 +- homeassistant/components/mobile_app/util.py | 8 +-- homeassistant/components/pilight/__init__.py | 2 +- pylint/plugins/hass_enforce_type_hints.py | 72 ++++++++++++++----- 9 files changed, 72 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/energyzero/coordinator.py b/homeassistant/components/energyzero/coordinator.py index 284ae37ce22..232f14e8f8d 100644 --- a/homeassistant/components/energyzero/coordinator.py +++ b/homeassistant/components/energyzero/coordinator.py @@ -13,6 +13,7 @@ from energyzero import ( ) from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt @@ -33,7 +34,7 @@ class EnergyZeroDataUpdateCoordinator(DataUpdateCoordinator[EnergyZeroData]): config_entry: ConfigEntry - def __init__(self, hass) -> None: + def __init__(self, hass: HomeAssistant) -> None: """Initialize global EnergyZero data updater.""" super().__init__( hass, diff --git a/homeassistant/components/google_maps/device_tracker.py b/homeassistant/components/google_maps/device_tracker.py index e1927aa81c5..2ee12f0154c 100644 --- a/homeassistant/components/google_maps/device_tracker.py +++ b/homeassistant/components/google_maps/device_tracker.py @@ -61,7 +61,9 @@ def setup_scanner( class GoogleMapsScanner: """Representation of an Google Maps location sharing account.""" - def __init__(self, hass, config: ConfigType, see: SeeCallback) -> None: + def __init__( + self, hass: HomeAssistant, config: ConfigType, see: SeeCallback + ) -> None: """Initialize the scanner.""" self.see = see self.username = config[CONF_USERNAME] diff --git a/homeassistant/components/gree/bridge.py b/homeassistant/components/gree/bridge.py index 41ba4bd9842..6628f7fc32c 100644 --- a/homeassistant/components/gree/bridge.py +++ b/homeassistant/components/gree/bridge.py @@ -71,7 +71,7 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator): class DiscoveryService(Listener): """Discovery event handler for gree devices.""" - def __init__(self, hass) -> None: + def __init__(self, hass: HomeAssistant) -> None: """Initialize discovery service.""" super().__init__() self.hass = hass diff --git a/homeassistant/components/harmony/data.py b/homeassistant/components/harmony/data.py index 3cb87323c0b..a73f0822d77 100644 --- a/homeassistant/components/harmony/data.py +++ b/homeassistant/components/harmony/data.py @@ -9,6 +9,7 @@ from aioharmony.const import ClientCallbackType, SendCommandDevice import aioharmony.exceptions as aioexc from aioharmony.harmonyapi import HarmonyAPI as HarmonyClient +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.entity import DeviceInfo @@ -23,7 +24,9 @@ class HarmonyData(HarmonySubscriberMixin): _client: HarmonyClient - def __init__(self, hass, address: str, name: str, unique_id: str | None) -> None: + def __init__( + self, hass: HomeAssistant, address: str, name: str, unique_id: str | None + ) -> None: """Initialize a data object.""" super().__init__(hass) self._name = name diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 8b99b4075ee..2b7145bdcaa 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -23,6 +23,7 @@ from multidict import istr from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView from homeassistant.components.onboarding import async_is_onboarded +from homeassistant.core import HomeAssistant from .const import X_HASS_IS_ADMIN, X_HASS_USER_ID @@ -165,7 +166,7 @@ def _get_timeout(path: str) -> ClientTimeout: return ClientTimeout(connect=10, total=300) -def _need_auth(hass, path: str) -> bool: +def _need_auth(hass: HomeAssistant, path: str) -> bool: """Return if a path need authentication.""" if not async_is_onboarded(hass) and NO_AUTH_ONBOARDING.match(path): return False diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index 6cf480b19a3..4b694d2b97a 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -273,7 +273,7 @@ async def async_setup_platform( def _process_scenes_config( - hass, async_add_entities: AddEntitiesCallback, config: dict[str, Any] + hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: dict[str, Any] ) -> None: """Process multiple scenes and add them.""" # Check empty list diff --git a/homeassistant/components/mobile_app/util.py b/homeassistant/components/mobile_app/util.py index 1f1715eab67..45641861e5c 100644 --- a/homeassistant/components/mobile_app/util.py +++ b/homeassistant/components/mobile_app/util.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import TYPE_CHECKING -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from .const import ( ATTR_APP_DATA, @@ -21,7 +21,7 @@ if TYPE_CHECKING: @callback -def webhook_id_from_device_id(hass, device_id: str) -> str | None: +def webhook_id_from_device_id(hass: HomeAssistant, device_id: str) -> str | None: """Get webhook ID from device ID.""" if DOMAIN not in hass.data: return None @@ -34,7 +34,7 @@ def webhook_id_from_device_id(hass, device_id: str) -> str | None: @callback -def supports_push(hass, webhook_id: str) -> bool: +def supports_push(hass: HomeAssistant, webhook_id: str) -> bool: """Return if push notifications is supported.""" config_entry = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][webhook_id] app_data = config_entry.data[ATTR_APP_DATA] @@ -44,7 +44,7 @@ def supports_push(hass, webhook_id: str) -> bool: @callback -def get_notify_service(hass, webhook_id: str) -> str | None: +def get_notify_service(hass: HomeAssistant, webhook_id: str) -> str | None: """Return the notify service for this webhook ID.""" notify_service: MobileAppNotificationService = hass.data[DOMAIN][DATA_NOTIFY] diff --git a/homeassistant/components/pilight/__init__.py b/homeassistant/components/pilight/__init__.py index bcf43a0b735..60568e722ef 100644 --- a/homeassistant/components/pilight/__init__.py +++ b/homeassistant/components/pilight/__init__.py @@ -141,7 +141,7 @@ class CallRateDelayThrottle: it should not block the mainloop. """ - def __init__(self, hass, delay_seconds: float) -> None: + def __init__(self, hass: HomeAssistant, delay_seconds: float) -> None: """Initialize the delay handler.""" self._delay = timedelta(seconds=max(0.0, delay_seconds)) self._queue: list[Callable[[Any], None]] = [] diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index d3c9e090a23..15f5890c0f7 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -18,6 +18,11 @@ if TYPE_CHECKING: # pre-commit should still work on out of date environments from astroid.typing import InferenceResult +_COMMON_ARGUMENTS: dict[str, list[str]] = { + "hass": ["HomeAssistant", "HomeAssistant | None"] +} +_PLATFORMS: set[str] = {platform.value for platform in Platform} + class _Special(Enum): """Sentinel values.""" @@ -25,9 +30,6 @@ class _Special(Enum): UNDEFINED = 1 -_PLATFORMS: set[str] = {platform.value for platform in Platform} - - @dataclass class TypeHintMatch: """Class for pattern matching.""" @@ -2911,6 +2913,16 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] self._class_matchers.reverse() + def _ignore_function( + self, node: nodes.FunctionDef, annotations: list[nodes.NodeNG | None] + ) -> bool: + """Check if we can skip the function validation.""" + return ( + self.linter.config.ignore_missing_annotations + and node.returns is None + and not _has_valid_annotations(annotations) + ) + def visit_classdef(self, node: nodes.ClassDef) -> None: """Apply relevant type hint checks on a ClassDef node.""" ancestor: nodes.ClassDef @@ -2932,34 +2944,55 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] cached_methods: list[nodes.FunctionDef] = list(node.mymethods()) for match in matches: for function_node in cached_methods: - if function_node.name in checked_class_methods: + if ( + function_node.name in checked_class_methods + or not match.need_to_check_function(function_node) + ): continue - if match.need_to_check_function(function_node): - self._check_function(function_node, match) - checked_class_methods.add(function_node.name) + + annotations = _get_all_annotations(function_node) + if self._ignore_function(function_node, annotations): + continue + + self._check_function(function_node, match, annotations) + checked_class_methods.add(function_node.name) def visit_functiondef(self, node: nodes.FunctionDef) -> None: """Apply relevant type hint checks on a FunctionDef node.""" + annotations = _get_all_annotations(node) + if self._ignore_function(node, annotations): + return + + # Check that common arguments are correctly typed. + for arg_name, expected_type in _COMMON_ARGUMENTS.items(): + arg_node, annotation = _get_named_annotation(node, arg_name) + if arg_node and not _is_valid_type(expected_type, annotation): + self.add_message( + "hass-argument-type", + node=arg_node, + args=(arg_name, expected_type, node.name), + ) + + # Check function matchers. for match in self._function_matchers: if not match.need_to_check_function(node) or node.is_method(): continue - self._check_function(node, match) + self._check_function(node, match, annotations) visit_asyncfunctiondef = visit_functiondef - def _check_function(self, node: nodes.FunctionDef, match: TypeHintMatch) -> None: - # Check that at least one argument is annotated. - annotations = _get_all_annotations(node) - if ( - self.linter.config.ignore_missing_annotations - and node.returns is None - and not _has_valid_annotations(annotations) - ): - return - + def _check_function( + self, + node: nodes.FunctionDef, + match: TypeHintMatch, + annotations: list[nodes.NodeNG | None], + ) -> None: # Check that all positional arguments are correctly annotated. if match.arg_types: for key, expected_type in match.arg_types.items(): + if node.args.args[key].name in _COMMON_ARGUMENTS: + # It has already been checked, avoid double-message + continue if not _is_valid_type(expected_type, annotations[key]): self.add_message( "hass-argument-type", @@ -2970,6 +3003,9 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] # Check that all keyword arguments are correctly annotated. if match.named_arg_types is not None: for arg_name, expected_type in match.named_arg_types.items(): + if arg_name in _COMMON_ARGUMENTS: + # It has already been checked, avoid double-message + continue arg_node, annotation = _get_named_annotation(node, arg_name) if arg_node and not _is_valid_type(expected_type, annotation): self.add_message(