Type check all helpers (#25373)

* Type check all helpers, add inline exclusions for work in progress

* Remove unused Script._template_cache

* Add some missing type hints

* Remove unneeded type: ignore

* Type hint fixes

* Mypy assistance tweaks

* Don't look for None in deprecated config "at most once" check

* Avoid None name slugify attempt when generating entity id

* Avoid None state store attempt on entity remove
This commit is contained in:
Ville Skyttä 2019-07-21 19:59:02 +03:00 committed by Paulus Schoutsen
parent 0653f57fb4
commit d64f1e767c
19 changed files with 119 additions and 69 deletions

View file

@ -1,6 +1,6 @@
"""Helper to check the configuration file."""
from collections import OrderedDict, namedtuple
# from typing import Dict, List, Sequence
from typing import List
import attr
import voluptuous as vol
@ -17,6 +17,9 @@ import homeassistant.util.yaml.loader as yaml_loader
from homeassistant.exceptions import HomeAssistantError
# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs
# mypy: no-warn-return-any
CheckConfigError = namedtuple(
'CheckConfigError', "message domain config")
@ -25,7 +28,8 @@ CheckConfigError = namedtuple(
class HomeAssistantConfig(OrderedDict):
"""Configuration result with errors attribute."""
errors = attr.ib(default=attr.Factory(list))
errors = attr.ib(
default=attr.Factory(list)) # type: List[CheckConfigError]
def add_error(self, message, domain=None, config=None):
"""Add a single error."""
@ -114,9 +118,10 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> \
result.add_error("Component not found: {}".format(domain))
continue
if hasattr(component, 'CONFIG_SCHEMA'):
config_schema = getattr(component, 'CONFIG_SCHEMA', None)
if config_schema is not None:
try:
config = component.CONFIG_SCHEMA(config)
config = config_schema(config)
result[domain] = config[domain]
except vol.Invalid as ex:
_comp_error(ex, domain, config)
@ -133,8 +138,7 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> \
for p_name, p_config in config_per_platform(config, domain):
# Validate component specific platform schema
try:
p_validated = component_platform_schema( # type: ignore
p_config)
p_validated = component_platform_schema(p_config)
except vol.Invalid as ex:
_comp_error(ex, domain, config)
continue
@ -163,9 +167,10 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> \
continue
# Validate platform specific schema
if hasattr(platform, 'PLATFORM_SCHEMA'):
platform_schema = getattr(platform, 'PLATFORM_SCHEMA', None)
if platform_schema is not None:
try:
p_validated = platform.PLATFORM_SCHEMA(p_validated)
p_validated = platform_schema(p_validated)
except vol.Invalid as ex:
_comp_error(
ex, '{}.{}'.format(domain, p_name), p_validated)

View file

@ -4,6 +4,8 @@ from functools import partial
from homeassistant import config_entries
# mypy: allow-incomplete-defs, allow-untyped-defs
def register_discovery_flow(domain, title, discovery_function,
connection_class):
"""Register flow for discovered integrations that not require auth."""

View file

@ -27,6 +27,9 @@ from homeassistant.exceptions import TemplateError
from homeassistant.helpers.logging import KeywordStyleAdapter
from homeassistant.util import slugify as util_slugify
# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs
# mypy: no-check-untyped-defs, no-warn-return-any
# pylint: disable=invalid-name
TIME_PERIOD_ERROR = "offset {} should be format 'HH:MM' or 'HH:MM:SS'"
@ -92,7 +95,8 @@ def boolean(value: Any) -> bool:
if value in ('0', 'false', 'no', 'off', 'disable'):
return False
elif isinstance(value, Number):
return value != 0
# type ignore: https://github.com/python/mypy/issues/3186
return value != 0 # type: ignore
raise vol.Invalid('invalid boolean value {}'.format(value))
@ -161,7 +165,7 @@ def isdir(value: Any) -> str:
return dir_in
def ensure_list(value: Union[T, Sequence[T]]) -> Sequence[T]:
def ensure_list(value: Union[T, Sequence[T], None]) -> Sequence[T]:
"""Wrap value in list if it is not one."""
if value is None:
return []
@ -556,7 +560,8 @@ def deprecated(key: str,
else:
# Unclear when it is None, but it happens, so let's guard.
# https://github.com/home-assistant/home-assistant/issues/24982
module_name = __name__
# type ignore/unreachable: https://github.com/python/typeshed/pull/3137
module_name = __name__ # type: ignore
if replacement_key and invalidation_version:
warning = ("The '{key}' option (with value '{value}') is"
@ -606,13 +611,15 @@ def deprecated(key: str,
config.pop(key)
else:
value = default
if (replacement_key
and (replacement_key not in config
or default == config.get(replacement_key))
and value is not None):
config[replacement_key] = value
keys = [key]
if replacement_key:
keys.append(replacement_key)
if value is not None and (
replacement_key not in config or
default == config.get(replacement_key)):
config[replacement_key] = value
return has_at_most_one_key(key, replacement_key)(config)
return has_at_most_one_key(*keys)(config)
return validator
@ -739,7 +746,7 @@ CONDITION_SCHEMA = vol.Any(
ZONE_CONDITION_SCHEMA,
AND_CONDITION_SCHEMA,
OR_CONDITION_SCHEMA,
)
) # type: vol.Schema
_SCRIPT_DELAY_SCHEMA = vol.Schema({
vol.Optional(CONF_ALIAS): string,

View file

@ -7,6 +7,8 @@ from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http.data_validator import RequestDataValidator
# mypy: allow-untyped-calls, allow-untyped-defs
class _BaseFlowManagerView(HomeAssistantView):
"""Foundation for flow manager views."""

View file

@ -12,6 +12,10 @@ from homeassistant.loader import bind_hass
from .typing import HomeAssistantType
# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs
# mypy: no-check-untyped-defs, no-warn-return-any
_LOGGER = logging.getLogger(__name__)
_UNDEF = object()

View file

@ -13,6 +13,9 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.loader import DEPENDENCY_BLACKLIST
from homeassistant.util.async_ import run_callback_threadsafe
# mypy: allow-untyped-defs, no-check-untyped-defs
EVENT_LOAD_PLATFORM = 'load_platform.{}'
ATTR_PLATFORM = 'platform'

View file

@ -10,14 +10,19 @@ from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, DEVICE_DEFAULT_NAME, STATE_OFF, STATE_ON,
STATE_UNAVAILABLE, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT,
ATTR_ENTITY_PICTURE, ATTR_SUPPORTED_FEATURES, ATTR_DEVICE_CLASS)
from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_registry import (
EVENT_ENTITY_REGISTRY_UPDATED, RegistryEntry)
from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE
from homeassistant.config import DATA_CUSTOMIZE
from homeassistant.exceptions import NoEntitySpecifiedError
from homeassistant.util import ensure_unique_string, slugify
from homeassistant.util.async_ import run_callback_threadsafe
from homeassistant.util import dt as dt_util
# mypy: allow-incomplete-defs, allow-untyped-defs, no-check-untyped-defs
# mypy: no-warn-return-any
_LOGGER = logging.getLogger(__name__)
SLOW_UPDATE_WARNING = 10
@ -34,7 +39,7 @@ def generate_entity_id(entity_id_format: str, name: Optional[str],
current_ids, hass
).result()
name = (slugify(name) or slugify(DEVICE_DEFAULT_NAME)).lower()
name = (slugify(name or "") or slugify(DEVICE_DEFAULT_NAME)).lower()
return ensure_unique_string(
entity_id_format.format(name), current_ids)
@ -80,10 +85,10 @@ class Entity:
parallel_updates = None
# Entry in the entity registry
registry_entry = None
registry_entry = None # type: Optional[RegistryEntry]
# Hold list for functions to call on remove.
_on_remove = None
_on_remove = None # type: Optional[List[CALLBACK_TYPE]]
# Context
_context = None
@ -98,7 +103,7 @@ class Entity:
return True
@property
def unique_id(self) -> str:
def unique_id(self) -> Optional[str]:
"""Return a unique ID."""
return None
@ -137,7 +142,7 @@ class Entity:
return None
@property
def device_class(self) -> str:
def device_class(self) -> Optional[str]:
"""Return the class of this device, from component DEVICE_CLASSES."""
return None
@ -181,7 +186,7 @@ class Entity:
return False
@property
def supported_features(self) -> int:
def supported_features(self) -> Optional[int]:
"""Flag supported features."""
return None
@ -386,7 +391,7 @@ class Entity:
self.parallel_updates.release()
@callback
def async_on_remove(self, func):
def async_on_remove(self, func: CALLBACK_TYPE) -> None:
"""Add a function to call when entity removed."""
if self._on_remove is None:
self._on_remove = []
@ -421,6 +426,7 @@ class Entity:
Not to be extended by integrations.
"""
if self.registry_entry is not None:
assert self.hass is not None
self.async_on_remove(self.hass.bus.async_listen(
EVENT_ENTITY_REGISTRY_UPDATED, self._async_registry_updated))

View file

@ -17,6 +17,9 @@ from homeassistant.loader import bind_hass, async_get_integration
from homeassistant.util import slugify
from .entity_platform import EntityPlatform
# mypy: allow-untyped-defs, no-check-untyped-defs
DEFAULT_SCAN_INTERVAL = timedelta(seconds=15)
DATA_INSTANCES = 'entity_components'

View file

@ -9,6 +9,9 @@ from homeassistant.util.async_ import (
from .event import async_track_time_interval, async_call_later
# mypy: allow-untyped-defs, no-check-untyped-defs
SLOW_SETUP_WARNING = 10
SLOW_SETUP_MAX_WAIT = 60
PLATFORM_NOT_READY_RETRIES = 10

View file

@ -23,6 +23,10 @@ from homeassistant.util.yaml import load_yaml
from .typing import HomeAssistantType
# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs
# mypy: no-check-untyped-defs, no-warn-return-any
PATH_REGISTRY = 'entity_registry.yaml'
DATA_REGISTRY = 'entity_registry'
EVENT_ENTITY_REGISTRY_UPDATED = 'entity_registry_updated'
@ -48,7 +52,8 @@ class RegistryEntry:
config_entry_id = attr.ib(type=str, default=None)
disabled_by = attr.ib(
type=str, default=None,
validator=attr.validators.in_((DISABLED_HASS, DISABLED_USER, None)))
validator=attr.validators.in_((DISABLED_HASS, DISABLED_USER, None))
) # type: Optional[str]
domain = attr.ib(type=str, init=False, repr=False)
@domain.default

View file

@ -7,13 +7,16 @@ import attr
from homeassistant.loader import bind_hass
from homeassistant.helpers.sun import get_astral_event_next
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE
from homeassistant.const import (
ATTR_NOW, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL,
SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, EVENT_CORE_CONFIG_UPDATE)
from homeassistant.util import dt as dt_util
from homeassistant.util.async_ import run_callback_threadsafe
# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs
# mypy: no-check-untyped-defs, no-warn-return-any
# PyLint does not like the use of threaded_listener_factory
# pylint: disable=invalid-name
@ -172,7 +175,7 @@ track_same_state = threaded_listener_factory(async_track_same_state)
@callback
@bind_hass
def async_track_point_in_time(hass, action, point_in_time):
def async_track_point_in_time(hass, action, point_in_time) -> CALLBACK_TYPE:
"""Add a listener that fires once after a specific point in time."""
utc_point_in_time = dt_util.as_utc(point_in_time)
@ -190,7 +193,8 @@ track_point_in_time = threaded_listener_factory(async_track_point_in_time)
@callback
@bind_hass
def async_track_point_in_utc_time(hass, action, point_in_time):
def async_track_point_in_utc_time(
hass, action, point_in_time) -> CALLBACK_TYPE:
"""Add a listener that fires once after a specific point in UTC time."""
# Ensure point_in_time is UTC
point_in_time = dt_util.as_utc(point_in_time)

View file

@ -3,6 +3,8 @@ import inspect
import logging
# mypy: allow-untyped-defs, no-check-untyped-defs
class KeywordMessage:
"""
Represents a logging message with keyword arguments.

View file

@ -15,6 +15,10 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.json import JSONEncoder
from homeassistant.helpers.storage import Store # noqa pylint_disable=unused-import
# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs
# mypy: no-warn-return-any
DATA_RESTORE_STATE_TASK = 'restore_state_task'
_LOGGER = logging.getLogger(__name__)
@ -182,8 +186,8 @@ class RestoreStateData():
# we're going to serialize it to JSON and then re-load it.
if state is not None:
state = State.from_dict(_encode_complex(state.as_dict()))
self.last_states[entity_id] = StoredState(state, dt_util.utcnow())
if state is not None:
self.last_states[entity_id] = StoredState(state, dt_util.utcnow())
self.entity_ids.remove(entity_id)
@ -219,6 +223,7 @@ class RestoreEntity(Entity):
async def async_internal_added_to_hass(self) -> None:
"""Register this entity as a restorable entity."""
assert self.hass is not None
_, data = await asyncio.gather(
super().async_internal_added_to_hass(),
RestoreStateData.async_get_instance(self.hass),
@ -227,6 +232,7 @@ class RestoreEntity(Entity):
async def async_internal_will_remove_from_hass(self) -> None:
"""Run when entity will be removed from hass."""
assert self.hass is not None
_, data = await asyncio.gather(
super().async_internal_will_remove_from_hass(),
RestoreStateData.async_get_instance(self.hass),

View file

@ -2,12 +2,13 @@
import logging
from contextlib import suppress
from datetime import datetime
from itertools import islice
from typing import Optional, Sequence
from typing import Optional, Sequence, Callable, Dict, List, Set, Tuple
import voluptuous as vol
from homeassistant.core import HomeAssistant, Context, callback
from homeassistant.core import HomeAssistant, Context, callback, CALLBACK_TYPE
from homeassistant.const import CONF_CONDITION, CONF_TIMEOUT
from homeassistant import exceptions
from homeassistant.helpers import (
@ -20,6 +21,10 @@ import homeassistant.util.dt as date_util
from homeassistant.util.async_ import (
run_coroutine_threadsafe, run_callback_threadsafe)
# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs
# mypy: no-check-untyped-defs
_LOGGER = logging.getLogger(__name__)
CONF_ALIAS = 'alias'
@ -76,7 +81,8 @@ class _SuspendScript(Exception):
class Script():
"""Representation of a script."""
def __init__(self, hass: HomeAssistant, sequence, name: str = None,
def __init__(self, hass: HomeAssistant, sequence,
name: Optional[str] = None,
change_listener=None) -> None:
"""Initialize the script."""
self.hass = hass
@ -85,14 +91,13 @@ class Script():
self.name = name
self._change_listener = change_listener
self._cur = -1
self._exception_step = None
self._exception_step = None # type: Optional[int]
self.last_action = None
self.last_triggered = None
self.last_triggered = None # type: Optional[datetime]
self.can_cancel = any(CONF_DELAY in action or CONF_WAIT_TEMPLATE
in action for action in self.sequence)
self._async_listener = []
self._template_cache = {}
self._config_cache = {}
self._async_listener = [] # type: List[CALLBACK_TYPE]
self._config_cache = {} # type: Dict[Set[Tuple], Callable[..., bool]]
self._actions = {
ACTION_DELAY: self._async_delay,
ACTION_WAIT_TEMPLATE: self._async_wait_template,

View file

@ -19,6 +19,9 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.util.async_ import run_coroutine_threadsafe
from homeassistant.helpers.typing import HomeAssistantType
# mypy: allow-incomplete-defs, allow-untyped-defs, no-check-untyped-defs
CONF_SERVICE = 'service'
CONF_SERVICE_TEMPLATE = 'service_template'
CONF_SERVICE_ENTITY_ID = 'entity_id'

View file

@ -3,7 +3,7 @@ import asyncio
from json import JSONEncoder
import logging
import os
from typing import Dict, List, Optional, Callable, Union
from typing import Dict, List, Optional, Callable, Union, Any, Type
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import callback
@ -11,6 +11,10 @@ from homeassistant.loader import bind_hass
from homeassistant.util import json as json_util
from homeassistant.helpers.event import async_call_later
# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs
# mypy: no-warn-return-any
STORAGE_DIR = '.storage'
_LOGGER = logging.getLogger(__name__)
@ -48,17 +52,17 @@ class Store:
"""Class to help storing data."""
def __init__(self, hass, version: int, key: str, private: bool = False, *,
encoder: JSONEncoder = None):
encoder: Optional[Type[JSONEncoder]] = None):
"""Initialize storage class."""
self.version = version
self.key = key
self.hass = hass
self._private = private
self._data = None
self._data = None # type: Optional[Dict[str, Any]]
self._unsub_delay_listener = None
self._unsub_stop_listener = None
self._write_lock = asyncio.Lock()
self._load_task = None
self._load_task = None # type: Optional[asyncio.Future]
self._encoder = encoder
@property

View file

@ -12,7 +12,7 @@ from typing import Iterable
import jinja2
from jinja2 import contextfilter, contextfunction
from jinja2.sandbox import ImmutableSandboxedEnvironment
from jinja2.utils import Namespace
from jinja2.utils import Namespace # type: ignore
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_UNIT_OF_MEASUREMENT,
@ -26,6 +26,10 @@ from homeassistant.loader import bind_hass
from homeassistant.util import convert, dt as dt_util, location as loc_util
from homeassistant.util.async_ import run_callback_threadsafe
# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs
# mypy: no-check-untyped-defs, no-warn-return-any
_LOGGER = logging.getLogger(__name__)
_SENTINEL = object()
DATE_STR_FORMAT = "%Y-%m-%d %H:%M:%S"
@ -192,14 +196,13 @@ class Template:
This method must be run in the event loop.
"""
if self._compiled is None:
self._ensure_compiled()
compiled = self._compiled or self._ensure_compiled()
if variables is not None:
kwargs.update(variables)
try:
return self._compiled.render(kwargs).strip()
return compiled.render(kwargs).strip()
except jinja2.TemplateError as err:
raise TemplateError(err)

View file

@ -1,6 +1,6 @@
"""JSON utility functions."""
import logging
from typing import Union, List, Dict, Optional
from typing import Union, List, Dict, Optional, Type
import json
import os
@ -42,7 +42,7 @@ def load_json(filename: str, default: Union[List, Dict, None] = None) \
def save_json(filename: str, data: Union[List, Dict],
private: bool = False, *,
encoder: Optional[json.JSONEncoder] = None) -> None:
encoder: Optional[Type[json.JSONEncoder]] = None) -> None:
"""Save JSON data to a file.
Returns True on success.

19
mypyrc
View file

@ -1,21 +1,4 @@
homeassistant/*.py
homeassistant/auth/
homeassistant/helpers/
homeassistant/util/
homeassistant/helpers/__init__.py
homeassistant/helpers/aiohttp_client.py
homeassistant/helpers/area_registry.py
homeassistant/helpers/condition.py
homeassistant/helpers/deprecation.py
homeassistant/helpers/dispatcher.py
homeassistant/helpers/entity_values.py
homeassistant/helpers/entityfilter.py
homeassistant/helpers/icon.py
homeassistant/helpers/intent.py
homeassistant/helpers/json.py
homeassistant/helpers/location.py
homeassistant/helpers/signal.py
homeassistant/helpers/state.py
homeassistant/helpers/sun.py
homeassistant/helpers/temperature.py
homeassistant/helpers/translation.py
homeassistant/helpers/typing.py