diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 7d3d2d2af88..496308598dc 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -241,7 +241,7 @@ def cmdline() -> List[str]: def setup_and_run_hass(config_dir: str, - args: argparse.Namespace) -> Optional[int]: + args: argparse.Namespace) -> int: """Set up HASS and run.""" from homeassistant import bootstrap @@ -274,7 +274,7 @@ def setup_and_run_hass(config_dir: str, log_no_color=args.log_no_color) if hass is None: - return None + return -1 if args.open_ui: # Imported here to avoid importing asyncio before monkey patch diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 0a71c2887b1..a190aea9fa8 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -28,9 +28,8 @@ ERROR_LOG_FILENAME = 'home-assistant.log' # hass.data key for logging information. DATA_LOGGING = 'logging' -FIRST_INIT_COMPONENT = set(( - 'system_log', 'recorder', 'mqtt', 'mqtt_eventstream', 'logger', - 'introduction', 'frontend', 'history')) +FIRST_INIT_COMPONENT = {'system_log', 'recorder', 'mqtt', 'mqtt_eventstream', + 'logger', 'introduction', 'frontend', 'history'} 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) 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 if skip_pip: @@ -137,7 +137,7 @@ async def async_from_config_dict(config: Dict[str, Any], for component in components: if component not in FIRST_INIT_COMPONENT: 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() @@ -145,7 +145,7 @@ async def async_from_config_dict(config: Dict[str, Any], for component in components: if component in FIRST_INIT_COMPONENT: 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() @@ -162,7 +162,8 @@ def from_config_file(config_path: str, skip_pip: bool = True, log_rotate_days: 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. 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, log_rotate_days: 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. Will add functionality to 'hass' parameter. @@ -204,7 +206,7 @@ async def async_from_config_file(config_path: str, log_no_color) try: - config_dict = await hass.async_add_job( + config_dict = await hass.async_add_executor_job( conf_util.load_yaml_config_file, config_path) except HomeAssistantError as err: _LOGGER.error("Error loading %s: %s", config_path, err) diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index e2d02acc61c..eb2e8391221 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -83,7 +83,7 @@ def async_log_entry(hass, name, message, domain=None, entity_id=None): 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.""" @callback def log_message(service): diff --git a/homeassistant/components/panel_iframe.py b/homeassistant/components/panel_iframe.py index 4574437bac9..86594b74995 100644 --- a/homeassistant/components/panel_iframe.py +++ b/homeassistant/components/panel_iframe.py @@ -4,8 +4,6 @@ Register an iFrame front end panel. For more details about this component, please refer to the documentation at https://home-assistant.io/components/panel_iframe/ """ -import asyncio - import voluptuous as vol from homeassistant.const import (CONF_ICON, CONF_URL) @@ -34,11 +32,10 @@ CONFIG_SCHEMA = vol.Schema({ }})}, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def setup(hass, config): +async def async_setup(hass, config): """Set up the iFrame frontend panels.""" 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), url_path, {'url': info[CONF_URL]}) diff --git a/homeassistant/core.py b/homeassistant/core.py index e0950172913..c7aa04910bd 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -17,7 +17,8 @@ import threading from time import monotonic 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 import voluptuous as vol @@ -205,8 +206,8 @@ class HomeAssistant(object): def async_add_job( self, target: Callable[..., Any], - *args: Any) -> Optional[asyncio.tasks.Task]: - """Add a job from within the eventloop. + *args: Any) -> Optional[asyncio.Future]: + """Add a job from within the event loop. This method must be run in the event loop. @@ -230,11 +231,26 @@ class HomeAssistant(object): 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 def async_add_executor_job( self, target: Callable[..., Any], - *args: Any) -> asyncio.tasks.Task: + *args: Any) -> asyncio.Future: """Add an executor job from within the event loop.""" task = self.loop.run_in_executor(None, target, *args) diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 962074ec3af..a68b489868d 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -80,11 +80,10 @@ class Store: data = self._data else: 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 - if data['version'] == self.version: stored = data['data'] else: diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 153d00f92fc..b22271d6eb5 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -16,14 +16,20 @@ import logging import sys 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.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 -DEPENDENCY_BLACKLIST = set(('config',)) +DEPENDENCY_BLACKLIST = {'config'} _LOGGER = logging.getLogger(__name__) @@ -33,7 +39,8 @@ PATH_CUSTOM_COMPONENTS = 'custom_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. Async friendly. diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 1664653f2a7..5398cfde963 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -50,7 +50,7 @@ async def async_setup_component(hass: core.HomeAssistant, domain: str, if setup_tasks is None: 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)) return await task @@ -142,7 +142,7 @@ async def _async_setup_component(hass: core.HomeAssistant, result = await component.async_setup( # type: ignore hass, processed_config) else: - result = await hass.async_add_job( + result = await hass.async_add_executor_job( component.setup, hass, processed_config) # type: ignore except Exception: # pylint: disable=broad-except _LOGGER.exception("Error during setup of component %s", domain) diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index d2138f4293c..a26f7014444 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -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]: """Convert a hsb into its rgb representation.""" if fS == 0: - fV = fB * 255 - return (fV, fV, fV) + fV = int(fB * 255) + return fV, fV, fV r = g = b = 0 h = fH / 60 diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 37b917baa2e..0f07a90e9bb 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -6,9 +6,11 @@ import re from typing import Any, Dict, Union, Optional, Tuple # NOQA import pytz +import pytz.exceptions as pytzexceptions 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. @@ -42,7 +44,7 @@ def get_time_zone(time_zone_str: str) -> Optional[dt.tzinfo]: """ try: return pytz.timezone(time_zone_str) - except pytz.exceptions.UnknownTimeZoneError: + except pytzexceptions.UnknownTimeZoneError: return None @@ -64,7 +66,7 @@ def as_utc(dattim: dt.datetime) -> dt.datetime: if dattim.tzinfo == UTC: return dattim elif dattim.tzinfo is None: - dattim = DEFAULT_TIME_ZONE.localize(dattim) + dattim = DEFAULT_TIME_ZONE.localize(dattim) # type: ignore return dattim.astimezone(UTC) @@ -92,7 +94,7 @@ def as_local(dattim: dt.datetime) -> dt.datetime: def utc_from_timestamp(timestamp: float) -> dt.datetime: """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: @@ -102,13 +104,14 @@ def start_of_local_day(dt_or_d: date = now().date() # type: dt.date elif isinstance(dt_or_d, dt.datetime): 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. # All rights reserved. # 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. 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] == '-': offset = -offset tzinfo = dt.timezone(offset) - else: - tzinfo = None kws = {k: int(v) for k, v in kws.items() if v is not None} kws['tzinfo'] = tzinfo 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.""" try: 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: """Add "unit" if it's plural.""" if number == 1: - return "1 %s" % unit - elif number > 1: - return "%d %ss" % (number, unit) + return '1 {}'.format(unit) + return '{:d} {}s'.format(number, unit) def q_n_r(first: int, second: int) -> Tuple[int, int]: """Return quotient and remaining.""" @@ -210,4 +210,4 @@ def get_age(date: dt.datetime) -> str: if minute > 0: return formatn(minute, 'minute') - return formatn(second, 'second') if second > 0 else "0 seconds" + return formatn(second, 'second') diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index 0e53342b0ca..74feb779dcd 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -8,8 +8,6 @@ from homeassistant.exceptions import HomeAssistantError _LOGGER = logging.getLogger(__name__) -_UNDEFINED = object() - class SerializationError(HomeAssistantError): """Error serializing the data to JSON.""" @@ -19,7 +17,7 @@ class WriteError(HomeAssistantError): """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]: """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: _LOGGER.exception('JSON file reading failed: %s', filename) 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]): @@ -46,9 +44,9 @@ def save_json(filename: str, data: Union[List, Dict]): Returns True on success. """ 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: - fdesc.write(data) + fdesc.write(json_data) except TypeError as error: _LOGGER.exception('Failed to serialize to JSON: %s', filename) diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py index ecef1087747..4cc0fff96b9 100644 --- a/homeassistant/util/unit_system.py +++ b/homeassistant/util/unit_system.py @@ -86,11 +86,11 @@ class UnitSystem(object): self.volume_unit = volume @property - def is_metric(self: object) -> bool: + def is_metric(self) -> bool: """Determine if this is the metric unit system.""" 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.""" if not isinstance(temperature, Number): raise TypeError( @@ -99,7 +99,7 @@ class UnitSystem(object): return temperature_util.convert(temperature, 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.""" if not isinstance(length, Number): raise TypeError('{} is not a numeric value.'.format(str(length))) diff --git a/homeassistant/util/yaml.py b/homeassistant/util/yaml.py index 0e7befd5e9e..298d52722a5 100644 --- a/homeassistant/util/yaml.py +++ b/homeassistant/util/yaml.py @@ -57,7 +57,7 @@ class SafeLineLoader(yaml.SafeLoader): last_line = self.line # type: int node = super(SafeLineLoader, self).compose_node(parent, index) # type: yaml.nodes.Node - node.__line__ = last_line + 1 + node.__line__ = last_line + 1 # type: ignore return node @@ -69,7 +69,7 @@ def load_yaml(fname: str) -> Union[List, Dict]: # We convert that to an empty dict return yaml.load(conf_file, Loader=SafeLineLoader) or OrderedDict() except yaml.YAMLError as exc: - _LOGGER.error(exc) + _LOGGER.error(str(exc)) raise HomeAssistantError(exc) except UnicodeDecodeError as 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) try: secrets = load_yaml(secret_path) + if not isinstance(secrets, dict): + raise HomeAssistantError('Secrets is not a dictionary') if 'logger' in secrets: logger = str(secrets['logger']).lower() if logger == 'debug': diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index e329f835f84..4f258bc2b09 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -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): """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), \ patch('homeassistant.bootstrap.async_enable_logging', diff --git a/tests/test_core.py b/tests/test_core.py index 4abce180093..7633c820d2d 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -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 +@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(): """Test that the callback annotation is respected.""" hass = MagicMock() diff --git a/tests/util/test_yaml.py b/tests/util/test_yaml.py index 734f4b548b9..d08915b348b 100644 --- a/tests/util/test_yaml.py +++ b/tests/util/test_yaml.py @@ -411,6 +411,22 @@ class TestSecrets(unittest.TestCase): assert mock_error.call_count == 1, \ "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(): """Test we can represent YAML loaded data.""" diff --git a/tox.ini b/tox.ini index 4ed68fddf37..6e22f2a5e95 100644 --- a/tox.ini +++ b/tox.ini @@ -42,4 +42,4 @@ whitelist_externals=/bin/bash deps = -r{toxinidir}/requirements_test.txt 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/'