Make typing checks more strict (#14429)

## Description:

Make typing checks more strict: add `--strict-optional` flag that forbids implicit None return type. This flag will become default in the next version of mypy (0.600)

Add `homeassistant/util/` to checked dirs.

## Checklist:
  - [x] The code change is tested and works locally.
  - [x] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass**
This commit is contained in:
Andrey 2018-07-13 13:24:51 +03:00 committed by GitHub
parent b6ca03ce47
commit c2fe0d0120
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 107 additions and 57 deletions

View File

@ -241,7 +241,7 @@ def cmdline() -> List[str]:
def setup_and_run_hass(config_dir: str, def setup_and_run_hass(config_dir: str,
args: argparse.Namespace) -> Optional[int]: args: argparse.Namespace) -> int:
"""Set up HASS and run.""" """Set up HASS and run."""
from homeassistant import bootstrap from homeassistant import bootstrap
@ -274,7 +274,7 @@ def setup_and_run_hass(config_dir: str,
log_no_color=args.log_no_color) log_no_color=args.log_no_color)
if hass is None: if hass is None:
return None return -1
if args.open_ui: if args.open_ui:
# Imported here to avoid importing asyncio before monkey patch # Imported here to avoid importing asyncio before monkey patch

View File

@ -28,9 +28,8 @@ ERROR_LOG_FILENAME = 'home-assistant.log'
# hass.data key for logging information. # hass.data key for logging information.
DATA_LOGGING = 'logging' DATA_LOGGING = 'logging'
FIRST_INIT_COMPONENT = set(( FIRST_INIT_COMPONENT = {'system_log', 'recorder', 'mqtt', 'mqtt_eventstream',
'system_log', 'recorder', 'mqtt', 'mqtt_eventstream', 'logger', 'logger', 'introduction', 'frontend', 'history'}
'introduction', 'frontend', 'history'))
def from_config_dict(config: Dict[str, Any], def from_config_dict(config: Dict[str, Any],
@ -95,7 +94,8 @@ async def async_from_config_dict(config: Dict[str, Any],
conf_util.async_log_exception(ex, 'homeassistant', core_config, hass) conf_util.async_log_exception(ex, 'homeassistant', core_config, hass)
return None return None
await hass.async_add_job(conf_util.process_ha_config_upgrade, hass) await hass.async_add_executor_job(
conf_util.process_ha_config_upgrade, hass)
hass.config.skip_pip = skip_pip hass.config.skip_pip = skip_pip
if skip_pip: if skip_pip:
@ -137,7 +137,7 @@ async def async_from_config_dict(config: Dict[str, Any],
for component in components: for component in components:
if component not in FIRST_INIT_COMPONENT: if component not in FIRST_INIT_COMPONENT:
continue continue
hass.async_add_job(async_setup_component(hass, component, config)) hass.async_create_task(async_setup_component(hass, component, config))
await hass.async_block_till_done() await hass.async_block_till_done()
@ -145,7 +145,7 @@ async def async_from_config_dict(config: Dict[str, Any],
for component in components: for component in components:
if component in FIRST_INIT_COMPONENT: if component in FIRST_INIT_COMPONENT:
continue continue
hass.async_add_job(async_setup_component(hass, component, config)) hass.async_create_task(async_setup_component(hass, component, config))
await hass.async_block_till_done() await hass.async_block_till_done()
@ -162,7 +162,8 @@ def from_config_file(config_path: str,
skip_pip: bool = True, skip_pip: bool = True,
log_rotate_days: Any = None, log_rotate_days: Any = None,
log_file: Any = None, log_file: Any = None,
log_no_color: bool = False): log_no_color: bool = False)\
-> Optional[core.HomeAssistant]:
"""Read the configuration file and try to start all the functionality. """Read the configuration file and try to start all the functionality.
Will add functionality to 'hass' parameter if given, Will add functionality to 'hass' parameter if given,
@ -187,7 +188,8 @@ async def async_from_config_file(config_path: str,
skip_pip: bool = True, skip_pip: bool = True,
log_rotate_days: Any = None, log_rotate_days: Any = None,
log_file: Any = None, log_file: Any = None,
log_no_color: bool = False): log_no_color: bool = False)\
-> Optional[core.HomeAssistant]:
"""Read the configuration file and try to start all the functionality. """Read the configuration file and try to start all the functionality.
Will add functionality to 'hass' parameter. Will add functionality to 'hass' parameter.
@ -204,7 +206,7 @@ async def async_from_config_file(config_path: str,
log_no_color) log_no_color)
try: try:
config_dict = await hass.async_add_job( config_dict = await hass.async_add_executor_job(
conf_util.load_yaml_config_file, config_path) conf_util.load_yaml_config_file, config_path)
except HomeAssistantError as err: except HomeAssistantError as err:
_LOGGER.error("Error loading %s: %s", config_path, err) _LOGGER.error("Error loading %s: %s", config_path, err)

View File

@ -83,7 +83,7 @@ def async_log_entry(hass, name, message, domain=None, entity_id=None):
hass.bus.async_fire(EVENT_LOGBOOK_ENTRY, data) hass.bus.async_fire(EVENT_LOGBOOK_ENTRY, data)
async def setup(hass, config): async def async_setup(hass, config):
"""Listen for download events to download files.""" """Listen for download events to download files."""
@callback @callback
def log_message(service): def log_message(service):

View File

@ -4,8 +4,6 @@ Register an iFrame front end panel.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/panel_iframe/ https://home-assistant.io/components/panel_iframe/
""" """
import asyncio
import voluptuous as vol import voluptuous as vol
from homeassistant.const import (CONF_ICON, CONF_URL) from homeassistant.const import (CONF_ICON, CONF_URL)
@ -34,11 +32,10 @@ CONFIG_SCHEMA = vol.Schema({
}})}, extra=vol.ALLOW_EXTRA) }})}, extra=vol.ALLOW_EXTRA)
@asyncio.coroutine async def async_setup(hass, config):
def setup(hass, config):
"""Set up the iFrame frontend panels.""" """Set up the iFrame frontend panels."""
for url_path, info in config[DOMAIN].items(): for url_path, info in config[DOMAIN].items():
yield from hass.components.frontend.async_register_built_in_panel( await hass.components.frontend.async_register_built_in_panel(
'iframe', info.get(CONF_TITLE), info.get(CONF_ICON), 'iframe', info.get(CONF_TITLE), info.get(CONF_ICON),
url_path, {'url': info[CONF_URL]}) url_path, {'url': info[CONF_URL]})

View File

@ -17,7 +17,8 @@ import threading
from time import monotonic from time import monotonic
from types import MappingProxyType from types import MappingProxyType
from typing import Optional, Any, Callable, List, TypeVar, Dict # NOQA from typing import ( # NOQA
Optional, Any, Callable, List, TypeVar, Dict, Coroutine)
from async_timeout import timeout from async_timeout import timeout
import voluptuous as vol import voluptuous as vol
@ -205,8 +206,8 @@ class HomeAssistant(object):
def async_add_job( def async_add_job(
self, self,
target: Callable[..., Any], target: Callable[..., Any],
*args: Any) -> Optional[asyncio.tasks.Task]: *args: Any) -> Optional[asyncio.Future]:
"""Add a job from within the eventloop. """Add a job from within the event loop.
This method must be run in the event loop. This method must be run in the event loop.
@ -230,11 +231,26 @@ class HomeAssistant(object):
return task return task
@callback
def async_create_task(self, target: Coroutine) -> asyncio.tasks.Task:
"""Create a task from within the eventloop.
This method must be run in the event loop.
target: target to call.
"""
task = self.loop.create_task(target)
if self._track_task:
self._pending_tasks.append(task)
return task
@callback @callback
def async_add_executor_job( def async_add_executor_job(
self, self,
target: Callable[..., Any], target: Callable[..., Any],
*args: Any) -> asyncio.tasks.Task: *args: Any) -> asyncio.Future:
"""Add an executor job from within the event loop.""" """Add an executor job from within the event loop."""
task = self.loop.run_in_executor(None, target, *args) task = self.loop.run_in_executor(None, target, *args)

View File

@ -80,11 +80,10 @@ class Store:
data = self._data data = self._data
else: else:
data = await self.hass.async_add_executor_job( data = await self.hass.async_add_executor_job(
json.load_json, self.path, None) json.load_json, self.path)
if data is None: if data == {}:
return None return None
if data['version'] == self.version: if data['version'] == self.version:
stored = data['data'] stored = data['data']
else: else:

View File

@ -16,14 +16,20 @@ import logging
import sys import sys
from types import ModuleType from types import ModuleType
from typing import Optional, Set # pylint: disable=unused-import
from typing import Dict, List, Optional, Sequence, Set, TYPE_CHECKING # NOQA
from homeassistant.const import PLATFORM_FORMAT from homeassistant.const import PLATFORM_FORMAT
from homeassistant.util import OrderedSet from homeassistant.util import OrderedSet
# Typing imports that create a circular dependency
# pylint: disable=using-constant-test,unused-import
if TYPE_CHECKING:
from homeassistant.core import HomeAssistant # NOQA
PREPARED = False PREPARED = False
DEPENDENCY_BLACKLIST = set(('config',)) DEPENDENCY_BLACKLIST = {'config'}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -33,7 +39,8 @@ PATH_CUSTOM_COMPONENTS = 'custom_components'
PACKAGE_COMPONENTS = 'homeassistant.components' PACKAGE_COMPONENTS = 'homeassistant.components'
def set_component(hass, comp_name: str, component: ModuleType) -> None: def set_component(hass, # type: HomeAssistant
comp_name: str, component: Optional[ModuleType]) -> None:
"""Set a component in the cache. """Set a component in the cache.
Async friendly. Async friendly.

View File

@ -50,7 +50,7 @@ async def async_setup_component(hass: core.HomeAssistant, domain: str,
if setup_tasks is None: if setup_tasks is None:
setup_tasks = hass.data[DATA_SETUP] = {} setup_tasks = hass.data[DATA_SETUP] = {}
task = setup_tasks[domain] = hass.async_add_job( task = setup_tasks[domain] = hass.async_create_task(
_async_setup_component(hass, domain, config)) _async_setup_component(hass, domain, config))
return await task return await task
@ -142,7 +142,7 @@ async def _async_setup_component(hass: core.HomeAssistant,
result = await component.async_setup( # type: ignore result = await component.async_setup( # type: ignore
hass, processed_config) hass, processed_config)
else: else:
result = await hass.async_add_job( result = await hass.async_add_executor_job(
component.setup, hass, processed_config) # type: ignore component.setup, hass, processed_config) # type: ignore
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
_LOGGER.exception("Error during setup of component %s", domain) _LOGGER.exception("Error during setup of component %s", domain)

View File

@ -267,8 +267,8 @@ def color_xy_brightness_to_RGB(vX: float, vY: float,
def color_hsb_to_RGB(fH: float, fS: float, fB: float) -> Tuple[int, int, int]: def color_hsb_to_RGB(fH: float, fS: float, fB: float) -> Tuple[int, int, int]:
"""Convert a hsb into its rgb representation.""" """Convert a hsb into its rgb representation."""
if fS == 0: if fS == 0:
fV = fB * 255 fV = int(fB * 255)
return (fV, fV, fV) return fV, fV, fV
r = g = b = 0 r = g = b = 0
h = fH / 60 h = fH / 60

View File

@ -6,9 +6,11 @@ import re
from typing import Any, Dict, Union, Optional, Tuple # NOQA from typing import Any, Dict, Union, Optional, Tuple # NOQA
import pytz import pytz
import pytz.exceptions as pytzexceptions
DATE_STR_FORMAT = "%Y-%m-%d" DATE_STR_FORMAT = "%Y-%m-%d"
UTC = DEFAULT_TIME_ZONE = pytz.utc # type: dt.tzinfo UTC = pytz.utc
DEFAULT_TIME_ZONE = pytz.utc # type: dt.tzinfo
# Copyright (c) Django Software Foundation and individual contributors. # Copyright (c) Django Software Foundation and individual contributors.
@ -42,7 +44,7 @@ def get_time_zone(time_zone_str: str) -> Optional[dt.tzinfo]:
""" """
try: try:
return pytz.timezone(time_zone_str) return pytz.timezone(time_zone_str)
except pytz.exceptions.UnknownTimeZoneError: except pytzexceptions.UnknownTimeZoneError:
return None return None
@ -64,7 +66,7 @@ def as_utc(dattim: dt.datetime) -> dt.datetime:
if dattim.tzinfo == UTC: if dattim.tzinfo == UTC:
return dattim return dattim
elif dattim.tzinfo is None: elif dattim.tzinfo is None:
dattim = DEFAULT_TIME_ZONE.localize(dattim) dattim = DEFAULT_TIME_ZONE.localize(dattim) # type: ignore
return dattim.astimezone(UTC) return dattim.astimezone(UTC)
@ -92,7 +94,7 @@ def as_local(dattim: dt.datetime) -> dt.datetime:
def utc_from_timestamp(timestamp: float) -> dt.datetime: def utc_from_timestamp(timestamp: float) -> dt.datetime:
"""Return a UTC time from a timestamp.""" """Return a UTC time from a timestamp."""
return dt.datetime.utcfromtimestamp(timestamp).replace(tzinfo=UTC) return UTC.localize(dt.datetime.utcfromtimestamp(timestamp))
def start_of_local_day(dt_or_d: def start_of_local_day(dt_or_d:
@ -102,13 +104,14 @@ def start_of_local_day(dt_or_d:
date = now().date() # type: dt.date date = now().date() # type: dt.date
elif isinstance(dt_or_d, dt.datetime): elif isinstance(dt_or_d, dt.datetime):
date = dt_or_d.date() date = dt_or_d.date()
return DEFAULT_TIME_ZONE.localize(dt.datetime.combine(date, dt.time())) return DEFAULT_TIME_ZONE.localize(dt.datetime.combine( # type: ignore
date, dt.time()))
# Copyright (c) Django Software Foundation and individual contributors. # Copyright (c) Django Software Foundation and individual contributors.
# All rights reserved. # All rights reserved.
# https://github.com/django/django/blob/master/LICENSE # https://github.com/django/django/blob/master/LICENSE
def parse_datetime(dt_str: str) -> dt.datetime: def parse_datetime(dt_str: str) -> Optional[dt.datetime]:
"""Parse a string and return a datetime.datetime. """Parse a string and return a datetime.datetime.
This function supports time zone offsets. When the input contains one, This function supports time zone offsets. When the input contains one,
@ -134,14 +137,12 @@ def parse_datetime(dt_str: str) -> dt.datetime:
if tzinfo_str[0] == '-': if tzinfo_str[0] == '-':
offset = -offset offset = -offset
tzinfo = dt.timezone(offset) tzinfo = dt.timezone(offset)
else:
tzinfo = None
kws = {k: int(v) for k, v in kws.items() if v is not None} kws = {k: int(v) for k, v in kws.items() if v is not None}
kws['tzinfo'] = tzinfo kws['tzinfo'] = tzinfo
return dt.datetime(**kws) return dt.datetime(**kws)
def parse_date(dt_str: str) -> dt.date: def parse_date(dt_str: str) -> Optional[dt.date]:
"""Convert a date string to a date object.""" """Convert a date string to a date object."""
try: try:
return dt.datetime.strptime(dt_str, DATE_STR_FORMAT).date() return dt.datetime.strptime(dt_str, DATE_STR_FORMAT).date()
@ -180,9 +181,8 @@ def get_age(date: dt.datetime) -> str:
def formatn(number: int, unit: str) -> str: def formatn(number: int, unit: str) -> str:
"""Add "unit" if it's plural.""" """Add "unit" if it's plural."""
if number == 1: if number == 1:
return "1 %s" % unit return '1 {}'.format(unit)
elif number > 1: return '{:d} {}s'.format(number, unit)
return "%d %ss" % (number, unit)
def q_n_r(first: int, second: int) -> Tuple[int, int]: def q_n_r(first: int, second: int) -> Tuple[int, int]:
"""Return quotient and remaining.""" """Return quotient and remaining."""
@ -210,4 +210,4 @@ def get_age(date: dt.datetime) -> str:
if minute > 0: if minute > 0:
return formatn(minute, 'minute') return formatn(minute, 'minute')
return formatn(second, 'second') if second > 0 else "0 seconds" return formatn(second, 'second')

View File

@ -8,8 +8,6 @@ from homeassistant.exceptions import HomeAssistantError
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_UNDEFINED = object()
class SerializationError(HomeAssistantError): class SerializationError(HomeAssistantError):
"""Error serializing the data to JSON.""" """Error serializing the data to JSON."""
@ -19,7 +17,7 @@ class WriteError(HomeAssistantError):
"""Error writing the data.""" """Error writing the data."""
def load_json(filename: str, default: Union[List, Dict] = _UNDEFINED) \ def load_json(filename: str, default: Union[List, Dict, None] = None) \
-> Union[List, Dict]: -> Union[List, Dict]:
"""Load JSON data from a file and return as dict or list. """Load JSON data from a file and return as dict or list.
@ -37,7 +35,7 @@ def load_json(filename: str, default: Union[List, Dict] = _UNDEFINED) \
except OSError as error: except OSError as error:
_LOGGER.exception('JSON file reading failed: %s', filename) _LOGGER.exception('JSON file reading failed: %s', filename)
raise HomeAssistantError(error) raise HomeAssistantError(error)
return {} if default is _UNDEFINED else default return {} if default is None else default
def save_json(filename: str, data: Union[List, Dict]): def save_json(filename: str, data: Union[List, Dict]):
@ -46,9 +44,9 @@ def save_json(filename: str, data: Union[List, Dict]):
Returns True on success. Returns True on success.
""" """
try: try:
data = json.dumps(data, sort_keys=True, indent=4) json_data = json.dumps(data, sort_keys=True, indent=4)
with open(filename, 'w', encoding='utf-8') as fdesc: with open(filename, 'w', encoding='utf-8') as fdesc:
fdesc.write(data) fdesc.write(json_data)
except TypeError as error: except TypeError as error:
_LOGGER.exception('Failed to serialize to JSON: %s', _LOGGER.exception('Failed to serialize to JSON: %s',
filename) filename)

View File

@ -86,11 +86,11 @@ class UnitSystem(object):
self.volume_unit = volume self.volume_unit = volume
@property @property
def is_metric(self: object) -> bool: def is_metric(self) -> bool:
"""Determine if this is the metric unit system.""" """Determine if this is the metric unit system."""
return self.name == CONF_UNIT_SYSTEM_METRIC return self.name == CONF_UNIT_SYSTEM_METRIC
def temperature(self: object, temperature: float, from_unit: str) -> float: def temperature(self, temperature: float, from_unit: str) -> float:
"""Convert the given temperature to this unit system.""" """Convert the given temperature to this unit system."""
if not isinstance(temperature, Number): if not isinstance(temperature, Number):
raise TypeError( raise TypeError(
@ -99,7 +99,7 @@ class UnitSystem(object):
return temperature_util.convert(temperature, return temperature_util.convert(temperature,
from_unit, self.temperature_unit) from_unit, self.temperature_unit)
def length(self: object, length: float, from_unit: str) -> float: def length(self, length: float, from_unit: str) -> float:
"""Convert the given length to this unit system.""" """Convert the given length to this unit system."""
if not isinstance(length, Number): if not isinstance(length, Number):
raise TypeError('{} is not a numeric value.'.format(str(length))) raise TypeError('{} is not a numeric value.'.format(str(length)))

View File

@ -57,7 +57,7 @@ class SafeLineLoader(yaml.SafeLoader):
last_line = self.line # type: int last_line = self.line # type: int
node = super(SafeLineLoader, node = super(SafeLineLoader,
self).compose_node(parent, index) # type: yaml.nodes.Node self).compose_node(parent, index) # type: yaml.nodes.Node
node.__line__ = last_line + 1 node.__line__ = last_line + 1 # type: ignore
return node return node
@ -69,7 +69,7 @@ def load_yaml(fname: str) -> Union[List, Dict]:
# We convert that to an empty dict # We convert that to an empty dict
return yaml.load(conf_file, Loader=SafeLineLoader) or OrderedDict() return yaml.load(conf_file, Loader=SafeLineLoader) or OrderedDict()
except yaml.YAMLError as exc: except yaml.YAMLError as exc:
_LOGGER.error(exc) _LOGGER.error(str(exc))
raise HomeAssistantError(exc) raise HomeAssistantError(exc)
except UnicodeDecodeError as exc: except UnicodeDecodeError as exc:
_LOGGER.error("Unable to read file %s: %s", fname, exc) _LOGGER.error("Unable to read file %s: %s", fname, exc)
@ -232,6 +232,8 @@ def _load_secret_yaml(secret_path: str) -> Dict:
_LOGGER.debug('Loading %s', secret_path) _LOGGER.debug('Loading %s', secret_path)
try: try:
secrets = load_yaml(secret_path) secrets = load_yaml(secret_path)
if not isinstance(secrets, dict):
raise HomeAssistantError('Secrets is not a dictionary')
if 'logger' in secrets: if 'logger' in secrets:
logger = str(secrets['logger']).lower() logger = str(secrets['logger']).lower()
if logger == 'debug': if logger == 'debug':

View File

@ -81,7 +81,8 @@ def test_from_config_dict_not_mount_deps_folder(loop):
async def test_async_from_config_file_not_mount_deps_folder(loop): async def test_async_from_config_file_not_mount_deps_folder(loop):
"""Test that we not mount the deps folder inside async_from_config_file.""" """Test that we not mount the deps folder inside async_from_config_file."""
hass = Mock(async_add_job=Mock(side_effect=lambda *args: mock_coro())) hass = Mock(
async_add_executor_job=Mock(side_effect=lambda *args: mock_coro()))
with patch('homeassistant.bootstrap.is_virtual_env', return_value=False), \ with patch('homeassistant.bootstrap.is_virtual_env', return_value=False), \
patch('homeassistant.bootstrap.async_enable_logging', patch('homeassistant.bootstrap.async_enable_logging',

View File

@ -67,6 +67,18 @@ def test_async_add_job_add_threaded_job_to_pool(mock_iscoro):
assert len(hass.loop.run_in_executor.mock_calls) == 1 assert len(hass.loop.run_in_executor.mock_calls) == 1
@patch('asyncio.iscoroutine', return_value=True)
def test_async_create_task_schedule_coroutine(mock_iscoro):
"""Test that we schedule coroutines and add jobs to the job pool."""
hass = MagicMock()
job = MagicMock()
ha.HomeAssistant.async_create_task(hass, job)
assert len(hass.loop.call_soon.mock_calls) == 0
assert len(hass.loop.create_task.mock_calls) == 1
assert len(hass.add_job.mock_calls) == 0
def test_async_run_job_calls_callback(): def test_async_run_job_calls_callback():
"""Test that the callback annotation is respected.""" """Test that the callback annotation is respected."""
hass = MagicMock() hass = MagicMock()

View File

@ -411,6 +411,22 @@ class TestSecrets(unittest.TestCase):
assert mock_error.call_count == 1, \ assert mock_error.call_count == 1, \
"Expected an error about logger: value" "Expected an error about logger: value"
def test_secrets_are_not_dict(self):
"""Did secrets handle non-dict file."""
FILES[self._secret_path] = (
'- http_pw: pwhttp\n'
' comp1_un: un1\n'
' comp1_pw: pw1\n')
yaml.clear_secret_cache()
with self.assertRaises(HomeAssistantError):
load_yaml(self._yaml_path,
'http:\n'
' api_password: !secret http_pw\n'
'component:\n'
' username: !secret comp1_un\n'
' password: !secret comp1_pw\n'
'')
def test_representing_yaml_loaded_data(): def test_representing_yaml_loaded_data():
"""Test we can represent YAML loaded data.""" """Test we can represent YAML loaded data."""

View File

@ -42,4 +42,4 @@ whitelist_externals=/bin/bash
deps = deps =
-r{toxinidir}/requirements_test.txt -r{toxinidir}/requirements_test.txt
commands = commands =
/bin/bash -c 'mypy --ignore-missing-imports --follow-imports=silent homeassistant/*.py' /bin/bash -c 'mypy --ignore-missing-imports --follow-imports=silent --strict-optional --warn-unused-ignores homeassistant/*.py homeassistant/util/'