diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 2e946b53e5e..c229409a8d3 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -1,6 +1,5 @@ """Start Home Assistant.""" import argparse -import asyncio import os import platform import subprocess @@ -8,32 +7,9 @@ import sys import threading from typing import List -import yarl - from homeassistant.const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__ -def set_loop() -> None: - """Attempt to use different loop.""" - # pylint: disable=import-outside-toplevel - from asyncio.events import BaseDefaultEventLoopPolicy - - if sys.platform == "win32": - if hasattr(asyncio, "WindowsProactorEventLoopPolicy"): - # pylint: disable=no-member - policy = asyncio.WindowsProactorEventLoopPolicy() - else: - - class ProactorPolicy(BaseDefaultEventLoopPolicy): - """Event loop policy to create proactor loops.""" - - _loop_factory = asyncio.ProactorEventLoop - - policy = ProactorPolicy() - - asyncio.set_event_loop_policy(policy) - - def validate_python() -> None: """Validate that the right Python version is running.""" if sys.version_info[:3] < REQUIRED_PYTHON_VER: @@ -240,39 +216,6 @@ def cmdline() -> List[str]: return [arg for arg in sys.argv if arg != "--daemon"] -async def setup_and_run_hass(config_dir: str, args: argparse.Namespace) -> int: - """Set up Home Assistant and run.""" - # pylint: disable=import-outside-toplevel - from homeassistant import bootstrap - - hass = await bootstrap.async_setup_hass( - config_dir=config_dir, - verbose=args.verbose, - log_rotate_days=args.log_rotate_days, - log_file=args.log_file, - log_no_color=args.log_no_color, - skip_pip=args.skip_pip, - safe_mode=args.safe_mode, - ) - - if hass is None: - return 1 - - if args.open_ui: - import webbrowser # pylint: disable=import-outside-toplevel - - if hass.config.api is not None: - scheme = "https" if hass.config.api.use_ssl else "http" - url = str( - yarl.URL.build( - scheme=scheme, host="127.0.0.1", port=hass.config.api.port - ) - ) - hass.add_job(webbrowser.open, url) - - return await hass.async_run() - - def try_to_restart() -> None: """Attempt to clean up state and start a new Home Assistant instance.""" # Things should be mostly shut down already at this point, now just try @@ -319,8 +262,6 @@ def main() -> int: """Start Home Assistant.""" validate_python() - set_loop() - # Run a simple daemon runner process on Windows to handle restarts if os.name == "nt" and "--runner" not in sys.argv: nt_args = cmdline() + ["--runner"] @@ -353,7 +294,22 @@ def main() -> int: if args.pid_file: write_pid(args.pid_file) - exit_code = asyncio.run(setup_and_run_hass(config_dir, args), debug=args.debug) + # pylint: disable=import-outside-toplevel + from homeassistant import runner + + runtime_conf = runner.RuntimeConfig( + config_dir=config_dir, + verbose=args.verbose, + log_rotate_days=args.log_rotate_days, + log_file=args.log_file, + log_no_color=args.log_no_color, + skip_pip=args.skip_pip, + safe_mode=args.safe_mode, + debug=args.debug, + open_ui=args.open_ui, + ) + + exit_code = runner.run(runtime_conf) if exit_code == RESTART_EXIT_CODE and not args.runner: try_to_restart() diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 94ec33f4e1a..6e1d140b20e 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -7,10 +7,11 @@ import logging.handlers import os import sys from time import monotonic -from typing import Any, Dict, Optional, Set +from typing import TYPE_CHECKING, Any, Dict, Optional, Set from async_timeout import timeout import voluptuous as vol +import yarl from homeassistant import config as conf_util, config_entries, core, loader from homeassistant.components import http @@ -31,6 +32,9 @@ from homeassistant.util.logging import async_activate_log_queue_handler from homeassistant.util.package import async_get_user_site, is_virtual_env from homeassistant.util.yaml import clear_secret_cache +if TYPE_CHECKING: + from .runner import RuntimeConfig + _LOGGER = logging.getLogger(__name__) ERROR_LOG_FILENAME = "home-assistant.log" @@ -66,23 +70,22 @@ STAGE_1_INTEGRATIONS = { async def async_setup_hass( - *, - config_dir: str, - verbose: bool, - log_rotate_days: int, - log_file: str, - log_no_color: bool, - skip_pip: bool, - safe_mode: bool, + runtime_config: "RuntimeConfig", ) -> Optional[core.HomeAssistant]: """Set up Home Assistant.""" hass = core.HomeAssistant() - hass.config.config_dir = config_dir + hass.config.config_dir = runtime_config.config_dir - async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color) + async_enable_logging( + hass, + runtime_config.verbose, + runtime_config.log_rotate_days, + runtime_config.log_file, + runtime_config.log_no_color, + ) - hass.config.skip_pip = skip_pip - if skip_pip: + hass.config.skip_pip = runtime_config.skip_pip + if runtime_config.skip_pip: _LOGGER.warning( "Skipping pip installation of required modules. This may cause issues" ) @@ -91,10 +94,11 @@ async def async_setup_hass( _LOGGER.error("Error getting configuration path") return None - _LOGGER.info("Config directory: %s", config_dir) + _LOGGER.info("Config directory: %s", runtime_config.config_dir) config_dict = None basic_setup_success = False + safe_mode = runtime_config.safe_mode if not safe_mode: await hass.async_add_executor_job(conf_util.process_ha_config_upgrade, hass) @@ -107,7 +111,7 @@ async def async_setup_hass( ) else: if not is_virtual_env(): - await async_mount_local_lib_path(config_dir) + await async_mount_local_lib_path(runtime_config.config_dir) basic_setup_success = ( await async_from_config_dict(config_dict, hass) is not None @@ -153,9 +157,32 @@ async def async_setup_hass( {"safe_mode": {}, "http": http_conf}, hass, ) + if runtime_config.open_ui: + hass.add_job(open_hass_ui, hass) + return hass +def open_hass_ui(hass: core.HomeAssistant) -> None: + """Open the UI.""" + import webbrowser # pylint: disable=import-outside-toplevel + + if hass.config.api is None or "frontend" not in hass.config.components: + _LOGGER.warning("Cannot launch the UI because frontend not loaded") + return + + scheme = "https" if hass.config.api.use_ssl else "http" + url = str( + yarl.URL.build(scheme=scheme, host="127.0.0.1", port=hass.config.api.port) + ) + + if not webbrowser.open(url): + _LOGGER.warning( + "Unable to open the Home Assistant UI in a browser. Open it yourself at %s", + url, + ) + + async def async_from_config_dict( config: ConfigType, hass: core.HomeAssistant ) -> Optional[core.HomeAssistant]: diff --git a/homeassistant/components/bsblan/config_flow.py b/homeassistant/components/bsblan/config_flow.py index ba5e7468832..1ecfc239af6 100644 --- a/homeassistant/components/bsblan/config_flow.py +++ b/homeassistant/components/bsblan/config_flow.py @@ -7,8 +7,8 @@ import voluptuous as vol from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.helpers import ConfigType from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import ConfigType from .const import ( # pylint:disable=unused-import CONF_DEVICE_IDENT, diff --git a/homeassistant/components/plum_lightpad/config_flow.py b/homeassistant/components/plum_lightpad/config_flow.py index acf9380bf71..6645aef02a2 100644 --- a/homeassistant/components/plum_lightpad/config_flow.py +++ b/homeassistant/components/plum_lightpad/config_flow.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers import ConfigType +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN # pylint: disable=unused-import from .utils import load_plum diff --git a/homeassistant/core.py b/homeassistant/core.py index 60579f43e28..7c870d5139f 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -5,7 +5,6 @@ Home Assistant is a Home Automation framework for observing the state of entities and react to changes. """ import asyncio -from concurrent.futures import ThreadPoolExecutor import datetime import enum import functools @@ -145,19 +144,6 @@ def is_callback(func: Callable[..., Any]) -> bool: return getattr(func, "_hass_callback", False) is True -@callback -def async_loop_exception_handler(_: Any, context: Dict) -> None: - """Handle all exception inside the core loop.""" - kwargs = {} - exception = context.get("exception") - if exception: - kwargs["exc_info"] = (type(exception), exception, exception.__traceback__) - - _LOGGER.error( - "Error doing job: %s", context["message"], **kwargs # type: ignore - ) - - class CoreState(enum.Enum): """Represent the current state of Home Assistant.""" @@ -179,18 +165,9 @@ class HomeAssistant: http: "HomeAssistantHTTP" = None # type: ignore config_entries: "ConfigEntries" = None # type: ignore - def __init__(self, loop: Optional[asyncio.events.AbstractEventLoop] = None) -> None: + def __init__(self) -> None: """Initialize new Home Assistant object.""" - self.loop: asyncio.events.AbstractEventLoop = (loop or asyncio.get_event_loop()) - - executor_opts: Dict[str, Any] = { - "max_workers": None, - "thread_name_prefix": "SyncWorker", - } - - self.executor = ThreadPoolExecutor(**executor_opts) - self.loop.set_default_executor(self.executor) - self.loop.set_exception_handler(async_loop_exception_handler) + self.loop = asyncio.get_running_loop() self._pending_tasks: list = [] self._track_task = True self.bus = EventBus(self) @@ -461,7 +438,9 @@ class HomeAssistant: self.state = CoreState.not_running self.bus.async_fire(EVENT_HOMEASSISTANT_CLOSE) await self.async_block_till_done() - self.executor.shutdown() + + # Python 3.9+ and backported in runner.py + await self.loop.shutdown_default_executor() # type: ignore self.exit_code = exit_code diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index 7189f519724..6f22be8d323 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -1,13 +1,14 @@ """Helper methods for components within Home Assistant.""" import re -from typing import Any, Iterable, Sequence, Tuple +from typing import TYPE_CHECKING, Any, Iterable, Sequence, Tuple from homeassistant.const import CONF_PLATFORM -from .typing import ConfigType +if TYPE_CHECKING: + from .typing import ConfigType -def config_per_platform(config: ConfigType, domain: str) -> Iterable[Tuple[Any, Any]]: +def config_per_platform(config: "ConfigType", domain: str) -> Iterable[Tuple[Any, Any]]: """Break a component config into different platforms. For example, will find 'switch', 'switch 2', 'switch 3', .. etc @@ -31,7 +32,7 @@ def config_per_platform(config: ConfigType, domain: str) -> Iterable[Tuple[Any, yield platform, item -def extract_domain_configs(config: ConfigType, domain: str) -> Sequence[str]: +def extract_domain_configs(config: "ConfigType", domain: str) -> Sequence[str]: """Extract keys from config for given domain name. Async friendly. diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index fbe8ee62812..316c02a82f0 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -13,7 +13,7 @@ import async_timeout from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, __version__ from homeassistant.core import Event, callback -from homeassistant.helpers.frame import MissingIntegrationFrame, get_integration_frame +from homeassistant.helpers.frame import warn_use from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import bind_hass from homeassistant.util import ssl as ssl_util @@ -71,34 +71,9 @@ def async_create_clientsession( connector=connector, headers={USER_AGENT: SERVER_SOFTWARE}, **kwargs, ) - async def patched_close() -> None: - """Mock close to avoid integrations closing our session.""" - try: - found_frame, integration, path = get_integration_frame() - except MissingIntegrationFrame: - # Did not source from an integration? Hard error. - raise RuntimeError( - "Detected closing of the Home Assistant aiohttp session in the Home Assistant core. " - "Please report this issue." - ) - - index = found_frame.filename.index(path) - if path == "custom_components/": - extra = " to the custom component author" - else: - extra = "" - - _LOGGER.warning( - "Detected integration that closes the Home Assistant aiohttp session. " - "Please report issue%s for %s using this method at %s, line %s: %s", - extra, - integration, - found_frame.filename[index:], - found_frame.lineno, - found_frame.line.strip(), - ) - - clientsession.close = patched_close # type: ignore + clientsession.close = warn_use( # type: ignore + clientsession.close, "closes the Home Assistant aiohttp session" + ) if auto_cleanup: _async_register_clientsession_shutdown(hass, clientsession) diff --git a/homeassistant/helpers/frame.py b/homeassistant/helpers/frame.py index 057152e4def..63d7cba4ec5 100644 --- a/homeassistant/helpers/frame.py +++ b/homeassistant/helpers/frame.py @@ -1,9 +1,16 @@ """Provide frame helper for finding the current frame context.""" +import asyncio +import functools +import logging from traceback import FrameSummary, extract_stack -from typing import Tuple +from typing import Any, Callable, Tuple, TypeVar, cast from homeassistant.exceptions import HomeAssistantError +_LOGGER = logging.getLogger(__name__) + +CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-name + def get_integration_frame() -> Tuple[FrameSummary, str, str]: """Return the frame, integration and integration path of the current stack frame.""" @@ -34,3 +41,49 @@ def get_integration_frame() -> Tuple[FrameSummary, str, str]: class MissingIntegrationFrame(HomeAssistantError): """Raised when no integration is found in the frame.""" + + +def report(what: str) -> None: + """Report incorrect usage. + + Async friendly. + """ + try: + found_frame, integration, path = get_integration_frame() + except MissingIntegrationFrame: + # Did not source from an integration? Hard error. + raise RuntimeError(f"Detected code that {what}. Please report this issue.") + + index = found_frame.filename.index(path) + if path == "custom_components/": + extra = " to the custom component author" + else: + extra = "" + + _LOGGER.warning( + "Detected integration that %s. " + "Please report issue%s for %s using this method at %s, line %s: %s", + what, + extra, + integration, + found_frame.filename[index:], + found_frame.lineno, + found_frame.line.strip(), + ) + + +def warn_use(func: CALLABLE_T, what: str) -> CALLABLE_T: + """Mock a function to warn when it was about to be used.""" + if asyncio.iscoroutinefunction(func): + + @functools.wraps(func) + async def report_use(*args: Any, **kwargs: Any) -> None: + report(what) + + else: + + @functools.wraps(func) + def report_use(*args: Any, **kwargs: Any) -> None: + report(what) + + return cast(CALLABLE_T, report_use) diff --git a/homeassistant/runner.py b/homeassistant/runner.py new file mode 100644 index 00000000000..876ce5d694b --- /dev/null +++ b/homeassistant/runner.py @@ -0,0 +1,119 @@ +"""Run Home Assistant.""" +import asyncio +from concurrent.futures import ThreadPoolExecutor +import dataclasses +import logging +import sys +import threading +from typing import Any, Dict, Optional + +from homeassistant import bootstrap +from homeassistant.core import callback +from homeassistant.helpers.frame import warn_use + + +@dataclasses.dataclass +class RuntimeConfig: + """Class to hold the information for running Home Assistant.""" + + config_dir: str + skip_pip: bool = False + safe_mode: bool = False + + verbose: bool = False + + log_rotate_days: Optional[int] = None + log_file: Optional[str] = None + log_no_color: bool = False + + debug: bool = False + open_ui: bool = False + + +# In Python 3.8+ proactor policy is the default on Windows +if sys.platform == "win32" and sys.version_info[:2] < (3, 8): + PolicyBase = asyncio.WindowsProactorEventLoopPolicy +else: + PolicyBase = asyncio.DefaultEventLoopPolicy # pylint: disable=invalid-name + + +class HassEventLoopPolicy(PolicyBase): + """Event loop policy for Home Assistant.""" + + def __init__(self, debug: bool) -> None: + """Init the event loop policy.""" + super().__init__() + self.debug = debug + + @property + def loop_name(self) -> str: + """Return name of the loop.""" + return self._loop_factory.__name__ + + def new_event_loop(self): + """Get the event loop.""" + loop = super().new_event_loop() + loop.set_exception_handler(_async_loop_exception_handler) + if self.debug: + loop.set_debug(True) + + executor = ThreadPoolExecutor(thread_name_prefix="SyncWorker") + loop.set_default_executor(executor) + loop.set_default_executor = warn_use( # type: ignore + loop.set_default_executor, "sets default executor on the event loop" + ) + + # Python 3.9+ + if hasattr(loop, "shutdown_default_executor"): + return loop + + # Copied from Python 3.9 source + def _do_shutdown(future): + try: + executor.shutdown(wait=True) + loop.call_soon_threadsafe(future.set_result, None) + except Exception as ex: # pylint: disable=broad-except + loop.call_soon_threadsafe(future.set_exception, ex) + + async def shutdown_default_executor(): + """Schedule the shutdown of the default executor.""" + future = loop.create_future() + thread = threading.Thread(target=_do_shutdown, args=(future,)) + thread.start() + try: + await future + finally: + thread.join() + + loop.shutdown_default_executor = shutdown_default_executor + + return loop + + +@callback +def _async_loop_exception_handler(self, _: Any, context: Dict) -> None: + """Handle all exception inside the core loop.""" + kwargs = {} + exception = context.get("exception") + if exception: + kwargs["exc_info"] = (type(exception), exception, exception.__traceback__) + + logging.getLogger(__package__).error( + "Error doing job: %s", context["message"], **kwargs # type: ignore + ) + + +async def setup_and_run_hass(runtime_config: RuntimeConfig,) -> int: + """Set up Home Assistant and run.""" + hass = await bootstrap.async_setup_hass(runtime_config) + + if hass is None: + return 1 + + return await hass.async_run() + + +def run(runtime_config: RuntimeConfig) -> int: + """Run Home Assistant.""" + asyncio.set_event_loop_policy(HassEventLoopPolicy(runtime_config.debug)) + return asyncio.run(setup_and_run_hass(runtime_config)) diff --git a/homeassistant/scripts/__init__.py b/homeassistant/scripts/__init__.py index 1cfd6a4e0fc..8042e546884 100644 --- a/homeassistant/scripts/__init__.py +++ b/homeassistant/scripts/__init__.py @@ -7,6 +7,7 @@ import os import sys from typing import List, Optional, Sequence, Text +from homeassistant import runner from homeassistant.bootstrap import async_mount_local_lib_path from homeassistant.config import get_default_config_dir from homeassistant.requirements import pip_kwargs @@ -59,6 +60,8 @@ def run(args: List) -> int: print("Aborting script, could not install dependency", req) return 1 + asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(False)) + return script.run(args[1:]) # type: ignore diff --git a/homeassistant/scripts/auth.py b/homeassistant/scripts/auth.py index 66baf555306..11ab6aadfbf 100644 --- a/homeassistant/scripts/auth.py +++ b/homeassistant/scripts/auth.py @@ -4,6 +4,7 @@ import asyncio import logging import os +from homeassistant import runner from homeassistant.auth import auth_manager_from_config from homeassistant.auth.providers import homeassistant as hass_auth from homeassistant.config import get_default_config_dir @@ -43,24 +44,24 @@ def run(args): parser_change_pw.add_argument("new_password", type=str) parser_change_pw.set_defaults(func=change_password) - args = parser.parse_args(args) - loop = asyncio.get_event_loop() - hass = HomeAssistant(loop=loop) - loop.run_until_complete(run_command(hass, args)) - - # Triggers save on used storage helpers with delay (core auth) - logging.getLogger("homeassistant.core").setLevel(logging.WARNING) - loop.run_until_complete(hass.async_stop()) + asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(False)) + asyncio.run(run_command(parser.parse_args(args))) -async def run_command(hass, args): +async def run_command(args): """Run the command.""" + hass = HomeAssistant() hass.config.config_dir = os.path.join(os.getcwd(), args.config) hass.auth = await auth_manager_from_config(hass, [{"type": "homeassistant"}], []) provider = hass.auth.auth_providers[0] await provider.async_initialize() await args.func(hass, provider, args) + # Triggers save on used storage helpers with delay (core auth) + logging.getLogger("homeassistant.core").setLevel(logging.WARNING) + + await hass.async_stop() + async def list_users(hass, provider, args): """List the users.""" diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py index 1827004a099..d2091bb6003 100644 --- a/homeassistant/scripts/benchmark/__init__.py +++ b/homeassistant/scripts/benchmark/__init__.py @@ -36,18 +36,19 @@ def run(args): args = parser.parse_args() bench = BENCHMARKS[args.name] - - print("Using event loop:", asyncio.get_event_loop_policy().__module__) + print("Using event loop:", asyncio.get_event_loop_policy().loop_name) with suppress(KeyboardInterrupt): while True: - loop = asyncio.new_event_loop() - hass = core.HomeAssistant(loop) - hass.async_stop_track_tasks() - runtime = loop.run_until_complete(bench(hass)) - print(f"Benchmark {bench.__name__} done in {runtime}s") - loop.run_until_complete(hass.async_stop()) - loop.close() + asyncio.run(run_benchmark(bench)) + + +async def run_benchmark(bench): + """Run a benchmark.""" + hass = core.HomeAssistant() + runtime = await bench(hass) + print(f"Benchmark {bench.__name__} done in {runtime}s") + await hass.async_stop() def benchmark(func: CALLABLE_T) -> CALLABLE_T: diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 8b4c6a446f2..e96bc57624f 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -1,5 +1,6 @@ """Script to check the configuration file.""" import argparse +import asyncio from collections import OrderedDict from collections.abc import Mapping, Sequence from glob import glob @@ -199,12 +200,7 @@ def check(config_dir, secrets=False): yaml_loader.yaml.SafeLoader.add_constructor("!secret", yaml_loader.secret_yaml) try: - hass = core.HomeAssistant() - hass.config.config_dir = config_dir - - res["components"] = hass.loop.run_until_complete( - async_check_ha_config_file(hass) - ) + res["components"] = asyncio.run(async_check_config(config_dir)) res["secret_cache"] = OrderedDict(yaml_loader.__SECRET_CACHE) for err in res["components"].errors: domain = err.domain or ERROR_STR @@ -213,7 +209,6 @@ def check(config_dir, secrets=False): res["except"].setdefault(domain, []).append(err.config) except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("BURB") print(color("red", "Fatal error while loading config:"), str(err)) res["except"].setdefault(ERROR_STR, []).append(str(err)) finally: @@ -230,6 +225,15 @@ def check(config_dir, secrets=False): return res +async def async_check_config(config_dir): + """Check the HA config.""" + hass = core.HomeAssistant() + hass.config.config_dir = config_dir + components = await async_check_ha_config_file(hass) + await hass.async_stop(force=True) + return components + + def line_info(obj, **kwargs): """Display line config source.""" if hasattr(obj, "__config_file__"): diff --git a/homeassistant/scripts/ensure_config.py b/homeassistant/scripts/ensure_config.py index 10026127511..b78b38735e1 100644 --- a/homeassistant/scripts/ensure_config.py +++ b/homeassistant/scripts/ensure_config.py @@ -1,5 +1,6 @@ """Script to ensure a configuration file exists.""" import argparse +import asyncio import os import homeassistant.config as config_util @@ -31,15 +32,15 @@ def run(args): print("Creating directory", config_dir) os.makedirs(config_dir) - hass = HomeAssistant() - hass.config.config_dir = config_dir - config_path = hass.loop.run_until_complete(async_run(hass)) + config_path = asyncio.run(async_run(config_dir)) print("Configuration file:", config_path) return 0 -async def async_run(hass): +async def async_run(config_dir): """Make sure config exists.""" + hass = HomeAssistant() + hass.config.config_dir = config_dir path = await config_util.async_ensure_config_exists(hass) await hass.async_stop(force=True) return path diff --git a/tests/common.py b/tests/common.py index 03ab5b0e0a0..1436b0f5a8a 100644 --- a/tests/common.py +++ b/tests/common.py @@ -149,7 +149,7 @@ def get_test_home_assistant(): # pylint: disable=protected-access async def async_test_home_assistant(loop): """Return a Home Assistant object pointing at test config dir.""" - hass = ha.HomeAssistant(loop) + hass = ha.HomeAssistant() store = auth_store.AuthStore(hass) hass.auth = auth.AuthManager(hass, store, {}, {}) ensure_auth_manager_loaded(hass.auth) diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py index 2ee2e5849f7..79c15344b17 100644 --- a/tests/components/homekit/conftest.py +++ b/tests/components/homekit/conftest.py @@ -8,8 +8,8 @@ from homeassistant.core import callback as ha_callback from tests.async_mock import patch -@pytest.fixture(scope="session") -def hk_driver(): +@pytest.fixture +def hk_driver(loop): """Return a custom AccessoryDriver instance for HomeKit accessory init.""" with patch("pyhap.accessory_driver.Zeroconf"), patch( "pyhap.accessory_driver.AccessoryEncoder" @@ -18,7 +18,7 @@ def hk_driver(): ), patch( "pyhap.accessory_driver.AccessoryDriver.persist" ): - yield AccessoryDriver(pincode=b"123-45-678", address="127.0.0.1") + yield AccessoryDriver(pincode=b"123-45-678", address="127.0.0.1", loop=loop) @pytest.fixture diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index f281cda0c02..4ee25051547 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -68,6 +68,11 @@ from tests.components.homekit.common import patch_debounce IP_ADDRESS = "127.0.0.1" +@pytest.fixture(autouse=True) +def always_patch_driver(hk_driver): + """Load the hk_driver fixture.""" + + @pytest.fixture(name="device_reg") def device_reg_fixture(hass): """Return an empty, loaded, registry.""" diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 48f0a6d270e..c6845779313 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -215,7 +215,7 @@ def test_density_to_air_quality(): assert density_to_air_quality(300) == 5 -async def test_show_setup_msg(hass): +async def test_show_setup_msg(hass, hk_driver): """Test show setup message as persistence notification.""" pincode = b"123-45-678" diff --git a/tests/conftest.py b/tests/conftest.py index 0d100afc275..b86b1719d72 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,11 +1,12 @@ """Set up some common test helper things.""" +import asyncio import functools import logging import pytest import requests_mock as _requests_mock -from homeassistant import core as ha, loader, util +from homeassistant import core as ha, loader, runner, util from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY from homeassistant.auth.providers import homeassistant, legacy_api_password from homeassistant.components import mqtt @@ -40,6 +41,10 @@ from tests.test_util.aiohttp import mock_aiohttp_client # noqa: E402, isort:ski logging.basicConfig(level=logging.DEBUG) logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) +asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(False)) +# Disable fixtures overriding our beautiful policy +asyncio.set_event_loop_policy = lambda policy: None + def pytest_configure(config): """Register marker for tests that log exceptions.""" diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index f08ed5746b5..f283475b336 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -7,17 +7,15 @@ from unittest.mock import Mock import pytest -from homeassistant import bootstrap, core +from homeassistant import bootstrap, core, runner import homeassistant.config as config_util from homeassistant.exceptions import HomeAssistantError import homeassistant.util.dt as dt_util from tests.async_mock import patch from tests.common import ( - MockConfigEntry, MockModule, MockPlatform, - flush_store, get_test_config_dir, mock_coro, mock_entity_platform, @@ -351,6 +349,7 @@ async def test_setup_hass( mock_ensure_config_exists, mock_process_ha_config_upgrade, caplog, + loop, ): """Test it works.""" verbose = Mock() @@ -365,13 +364,15 @@ async def test_setup_hass( "homeassistant.components.http.start_http_server_and_save_config" ): hass = await bootstrap.async_setup_hass( - config_dir=get_test_config_dir(), - verbose=verbose, - log_rotate_days=log_rotate_days, - log_file=log_file, - log_no_color=log_no_color, - skip_pip=True, - safe_mode=False, + runner.RuntimeConfig( + config_dir=get_test_config_dir(), + verbose=verbose, + log_rotate_days=log_rotate_days, + log_file=log_file, + log_no_color=log_no_color, + skip_pip=True, + safe_mode=False, + ), ) assert "Waiting on integrations to complete setup" not in caplog.text @@ -399,6 +400,7 @@ async def test_setup_hass_takes_longer_than_log_slow_startup( mock_ensure_config_exists, mock_process_ha_config_upgrade, caplog, + loop, ): """Test it works.""" verbose = Mock() @@ -420,13 +422,15 @@ async def test_setup_hass_takes_longer_than_log_slow_startup( "homeassistant.components.http.start_http_server_and_save_config" ): await bootstrap.async_setup_hass( - config_dir=get_test_config_dir(), - verbose=verbose, - log_rotate_days=log_rotate_days, - log_file=log_file, - log_no_color=log_no_color, - skip_pip=True, - safe_mode=False, + runner.RuntimeConfig( + config_dir=get_test_config_dir(), + verbose=verbose, + log_rotate_days=log_rotate_days, + log_file=log_file, + log_no_color=log_no_color, + skip_pip=True, + safe_mode=False, + ), ) assert "Waiting on integrations to complete setup" in caplog.text @@ -438,19 +442,22 @@ async def test_setup_hass_invalid_yaml( mock_mount_local_lib_path, mock_ensure_config_exists, mock_process_ha_config_upgrade, + loop, ): """Test it works.""" with patch( "homeassistant.config.async_hass_config_yaml", side_effect=HomeAssistantError ), patch("homeassistant.components.http.start_http_server_and_save_config"): hass = await bootstrap.async_setup_hass( - config_dir=get_test_config_dir(), - verbose=False, - log_rotate_days=10, - log_file="", - log_no_color=False, - skip_pip=True, - safe_mode=False, + runner.RuntimeConfig( + config_dir=get_test_config_dir(), + verbose=False, + log_rotate_days=10, + log_file="", + log_no_color=False, + skip_pip=True, + safe_mode=False, + ), ) assert "safe_mode" in hass.config.components @@ -463,49 +470,52 @@ async def test_setup_hass_config_dir_nonexistent( mock_mount_local_lib_path, mock_ensure_config_exists, mock_process_ha_config_upgrade, + loop, ): """Test it works.""" mock_ensure_config_exists.return_value = False assert ( await bootstrap.async_setup_hass( - config_dir=get_test_config_dir(), - verbose=False, - log_rotate_days=10, - log_file="", - log_no_color=False, - skip_pip=True, - safe_mode=False, + runner.RuntimeConfig( + config_dir=get_test_config_dir(), + verbose=False, + log_rotate_days=10, + log_file="", + log_no_color=False, + skip_pip=True, + safe_mode=False, + ), ) is None ) async def test_setup_hass_safe_mode( - hass, mock_enable_logging, mock_is_virtual_env, mock_mount_local_lib_path, mock_ensure_config_exists, mock_process_ha_config_upgrade, + loop, ): """Test it works.""" - # Add a config entry to storage. - MockConfigEntry(domain="browser").add_to_hass(hass) - hass.config_entries._async_schedule_save() - await flush_store(hass.config_entries._store) - with patch("homeassistant.components.browser.setup") as browser_setup, patch( "homeassistant.components.http.start_http_server_and_save_config" + ), patch( + "homeassistant.config_entries.ConfigEntries.async_domains", + return_value=["browser"], ): hass = await bootstrap.async_setup_hass( - config_dir=get_test_config_dir(), - verbose=False, - log_rotate_days=10, - log_file="", - log_no_color=False, - skip_pip=True, - safe_mode=True, + runner.RuntimeConfig( + config_dir=get_test_config_dir(), + verbose=False, + log_rotate_days=10, + log_file="", + log_no_color=False, + skip_pip=True, + safe_mode=True, + ), ) assert "safe_mode" in hass.config.components @@ -522,6 +532,7 @@ async def test_setup_hass_invalid_core_config( mock_mount_local_lib_path, mock_ensure_config_exists, mock_process_ha_config_upgrade, + loop, ): """Test it works.""" with patch( @@ -529,13 +540,15 @@ async def test_setup_hass_invalid_core_config( return_value={"homeassistant": {"non-existing": 1}}, ), patch("homeassistant.components.http.start_http_server_and_save_config"): hass = await bootstrap.async_setup_hass( - config_dir=get_test_config_dir(), - verbose=False, - log_rotate_days=10, - log_file="", - log_no_color=False, - skip_pip=True, - safe_mode=False, + runner.RuntimeConfig( + config_dir=get_test_config_dir(), + verbose=False, + log_rotate_days=10, + log_file="", + log_no_color=False, + skip_pip=True, + safe_mode=False, + ), ) assert "safe_mode" in hass.config.components @@ -547,6 +560,7 @@ async def test_setup_safe_mode_if_no_frontend( mock_mount_local_lib_path, mock_ensure_config_exists, mock_process_ha_config_upgrade, + loop, ): """Test we setup safe mode if frontend didn't load.""" verbose = Mock() @@ -566,13 +580,15 @@ async def test_setup_safe_mode_if_no_frontend( }, ), patch("homeassistant.components.http.start_http_server_and_save_config"): hass = await bootstrap.async_setup_hass( - config_dir=get_test_config_dir(), - verbose=verbose, - log_rotate_days=log_rotate_days, - log_file=log_file, - log_no_color=log_no_color, - skip_pip=True, - safe_mode=False, + runner.RuntimeConfig( + config_dir=get_test_config_dir(), + verbose=verbose, + log_rotate_days=log_rotate_days, + log_file=log_file, + log_no_color=log_no_color, + skip_pip=True, + safe_mode=False, + ), ) assert "safe_mode" in hass.config.components diff --git a/tests/test_core.py b/tests/test_core.py index 32634313a48..49ddb1d3bf4 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1104,14 +1104,13 @@ def test_timer_out_of_sync(mock_monotonic, loop): assert abs(target - 14.2) < 0.001 -@asyncio.coroutine -def test_hass_start_starts_the_timer(loop): +async def test_hass_start_starts_the_timer(loop): """Test when hass starts, it starts the timer.""" - hass = ha.HomeAssistant(loop=loop) + hass = ha.HomeAssistant() try: with patch("homeassistant.core._async_create_timer") as mock_timer: - yield from hass.async_start() + await hass.async_start() assert hass.state == ha.CoreState.running assert not hass._track_task @@ -1119,21 +1118,20 @@ def test_hass_start_starts_the_timer(loop): assert mock_timer.mock_calls[0][1][0] is hass finally: - yield from hass.async_stop() + await hass.async_stop() assert hass.state == ha.CoreState.not_running -@asyncio.coroutine -def test_start_taking_too_long(loop, caplog): +async def test_start_taking_too_long(loop, caplog): """Test when async_start takes too long.""" - hass = ha.HomeAssistant(loop=loop) + hass = ha.HomeAssistant() caplog.set_level(logging.WARNING) try: with patch( "homeassistant.core.timeout", side_effect=asyncio.TimeoutError ), patch("homeassistant.core._async_create_timer") as mock_timer: - yield from hass.async_start() + await hass.async_start() assert hass.state == ha.CoreState.running assert len(mock_timer.mock_calls) == 1 @@ -1141,14 +1139,13 @@ def test_start_taking_too_long(loop, caplog): assert "Something is blocking Home Assistant" in caplog.text finally: - yield from hass.async_stop() + await hass.async_stop() assert hass.state == ha.CoreState.not_running -@asyncio.coroutine -def test_track_task_functions(loop): +async def test_track_task_functions(loop): """Test function to start/stop track task and initial state.""" - hass = ha.HomeAssistant(loop=loop) + hass = ha.HomeAssistant() try: assert hass._track_task @@ -1158,7 +1155,7 @@ def test_track_task_functions(loop): hass.async_track_tasks() assert hass._track_task finally: - yield from hass.async_stop() + await hass.async_stop() async def test_service_executed_with_subservices(hass):