diff --git a/homeassistant/const.py b/homeassistant/const.py index a8449bf38c2..915ee5ac216 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,6 @@ __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) REQUIRED_PYTHON_VER_WIN = (3, 5, 2) -CONSTRAINT_FILE = 'package_constraints.txt' # Format for platforms PLATFORM_FORMAT = '{}.{}' diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py new file mode 100644 index 00000000000..aaf83870147 --- /dev/null +++ b/homeassistant/requirements.py @@ -0,0 +1,45 @@ +"""Module to handle installing requirements.""" +import asyncio +from functools import partial +import logging +import os + +import homeassistant.util.package as pkg_util + +DATA_PIP_LOCK = 'pip_lock' +CONSTRAINT_FILE = 'package_constraints.txt' +_LOGGER = logging.getLogger(__name__) + + +@asyncio.coroutine +def async_process_requirements(hass, name, requirements): + """Install the requirements for a component or platform. + + This method is a coroutine. + """ + pip_lock = hass.data.get(DATA_PIP_LOCK) + if pip_lock is None: + pip_lock = hass.data[DATA_PIP_LOCK] = asyncio.Lock(loop=hass.loop) + + pip_install = partial(pkg_util.install_package, + **pip_kwargs(hass.config.config_dir)) + + with (yield from pip_lock): + for req in requirements: + ret = yield from hass.async_add_job(pip_install, req) + if not ret: + _LOGGER.error("Not initializing %s because could not install " + "requirement %s", name, req) + return False + + return True + + +def pip_kwargs(config_dir): + """Return keyword arguments for PIP install.""" + kwargs = { + 'constraints': os.path.join(os.path.dirname(__file__), CONSTRAINT_FILE) + } + if not pkg_util.running_under_virtualenv(): + kwargs['target'] = os.path.join(config_dir, 'deps') + return kwargs diff --git a/homeassistant/scripts/__init__.py b/homeassistant/scripts/__init__.py index be39540682c..815a5c8e55f 100644 --- a/homeassistant/scripts/__init__.py +++ b/homeassistant/scripts/__init__.py @@ -9,9 +9,8 @@ from typing import List from homeassistant.bootstrap import mount_local_lib_path from homeassistant.config import get_default_config_dir -from homeassistant.const import CONSTRAINT_FILE -from homeassistant.util.package import ( - install_package, running_under_virtualenv) +from homeassistant import requirements +from homeassistant.util.package import install_package def run(args: List) -> int: @@ -39,17 +38,14 @@ def run(args: List) -> int: script = importlib.import_module('homeassistant.scripts.' + args[0]) config_dir = extract_config_dir() - deps_dir = mount_local_lib_path(config_dir) + mount_local_lib_path(config_dir) + pip_kwargs = requirements.pip_kwargs(config_dir) logging.basicConfig(stream=sys.stdout, level=logging.INFO) + for req in getattr(script, 'REQUIREMENTS', []): - if running_under_virtualenv(): - returncode = install_package(req, constraints=os.path.join( - os.path.dirname(__file__), os.pardir, CONSTRAINT_FILE)) - else: - returncode = install_package( - req, target=deps_dir, constraints=os.path.join( - os.path.dirname(__file__), os.pardir, CONSTRAINT_FILE)) + returncode = install_package(req, **pip_kwargs) + if not returncode: print('Aborting script, could not install dependency', req) return 1 diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 12a39e80517..3221ea35d48 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -1,27 +1,24 @@ """All methods needed to bootstrap a Home Assistant instance.""" import asyncio import logging.handlers -import os from timeit import default_timer as timer from types import ModuleType from typing import Optional, Dict -import homeassistant.config as conf_util -import homeassistant.core as core -import homeassistant.loader as loader -import homeassistant.util.package as pkg_util +from homeassistant import requirements, core, loader, config as conf_util from homeassistant.config import async_notify_setup_error -from homeassistant.const import ( - EVENT_COMPONENT_LOADED, PLATFORM_FORMAT, CONSTRAINT_FILE) +from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT +from homeassistant.exceptions import HomeAssistantError from homeassistant.util.async import run_coroutine_threadsafe + _LOGGER = logging.getLogger(__name__) ATTR_COMPONENT = 'component' DATA_SETUP = 'setup_tasks' -DATA_PIP_LOCK = 'pip_lock' +DATA_DEPS_REQS = 'deps_reqs_processed' SLOW_SETUP_WARNING = 10 @@ -60,43 +57,6 @@ def async_setup_component(hass: core.HomeAssistant, domain: str, return (yield from task) -@asyncio.coroutine -def _async_process_requirements(hass: core.HomeAssistant, name: str, - requirements) -> bool: - """Install the requirements for a component. - - This method is a coroutine. - """ - if hass.config.skip_pip: - return True - - pip_lock = hass.data.get(DATA_PIP_LOCK) - if pip_lock is None: - pip_lock = hass.data[DATA_PIP_LOCK] = asyncio.Lock(loop=hass.loop) - - def pip_install(mod): - """Install packages.""" - if pkg_util.running_under_virtualenv(): - return pkg_util.install_package( - mod, constraints=os.path.join( - os.path.dirname(__file__), CONSTRAINT_FILE)) - return pkg_util.install_package( - mod, target=hass.config.path('deps'), - constraints=os.path.join( - os.path.dirname(__file__), CONSTRAINT_FILE)) - - with (yield from pip_lock): - for req in requirements: - ret = yield from hass.async_add_job(pip_install, req) - if not ret: - _LOGGER.error("Not initializing %s because could not install " - "dependency %s", name, req) - async_notify_setup_error(hass, name) - return False - - return True - - @asyncio.coroutine def _async_process_dependencies(hass, config, name, dependencies): """Ensure all dependencies are set up.""" @@ -162,22 +122,11 @@ def _async_setup_component(hass: core.HomeAssistant, log_error("Invalid config.") return False - if not hass.config.skip_pip and hasattr(component, 'REQUIREMENTS'): - req_success = yield from _async_process_requirements( - hass, domain, component.REQUIREMENTS) - if not req_success: - log_error("Could not install all requirements.") - return False - - if hasattr(component, 'DEPENDENCIES'): - dep_success = yield from _async_process_dependencies( - hass, config, domain, component.DEPENDENCIES) - - if not dep_success: - log_error("Could not setup all dependencies.") - return False - - async_comp = hasattr(component, 'async_setup') + try: + yield from _process_deps_reqs(hass, config, domain, component) + except HomeAssistantError as err: + log_error(str(err)) + return False start = timer() _LOGGER.info("Setting up %s", domain) @@ -192,7 +141,7 @@ def _async_setup_component(hass: core.HomeAssistant, domain, SLOW_SETUP_WARNING) try: - if async_comp: + if hasattr(component, 'async_setup'): result = yield from component.async_setup(hass, processed_config) else: result = yield from hass.async_add_job( @@ -256,21 +205,40 @@ def async_prepare_setup_platform(hass: core.HomeAssistant, config, domain: str, elif platform_path in hass.config.components: return platform - # Load dependencies - if hasattr(platform, 'DEPENDENCIES'): - dep_success = yield from _async_process_dependencies( - hass, config, platform_path, platform.DEPENDENCIES) - - if not dep_success: - log_error("Could not setup all dependencies.") - return None - - if not hass.config.skip_pip and hasattr(platform, 'REQUIREMENTS'): - req_success = yield from _async_process_requirements( - hass, platform_path, platform.REQUIREMENTS) - - if not req_success: - log_error("Could not install all requirements.") - return None + try: + yield from _process_deps_reqs(hass, config, platform_name, platform) + except HomeAssistantError as err: + log_error(str(err)) + return None return platform + + +@asyncio.coroutine +def _process_deps_reqs(hass, config, name, module): + """Process all dependencies and requirements for a module. + + Module is a Python module of either a component or platform. + """ + processed = hass.data.get(DATA_DEPS_REQS) + + if processed is None: + processed = hass.data[DATA_DEPS_REQS] = set() + elif name in processed: + return + + if hasattr(module, 'DEPENDENCIES'): + dep_success = yield from _async_process_dependencies( + hass, config, name, module.DEPENDENCIES) + + if not dep_success: + raise HomeAssistantError("Could not setup all dependencies.") + + if not hass.config.skip_pip and hasattr(module, 'REQUIREMENTS'): + req_success = yield from requirements.async_process_requirements( + hass, name, module.REQUIREMENTS) + + if not req_success: + raise HomeAssistantError("Could not install all requirements.") + + processed.add(name) diff --git a/tests/test_requirements.py b/tests/test_requirements.py new file mode 100644 index 00000000000..946e64af847 --- /dev/null +++ b/tests/test_requirements.py @@ -0,0 +1,61 @@ +"""Test requirements module.""" +import os +from unittest import mock + +from homeassistant import loader, setup +from homeassistant.requirements import CONSTRAINT_FILE + +from tests.common import get_test_home_assistant, MockModule + + +class TestRequirements: + """Test the requirements module.""" + + hass = None + backup_cache = None + + # pylint: disable=invalid-name, no-self-use + def setup_method(self, method): + """Setup the test.""" + self.hass = get_test_home_assistant() + + def teardown_method(self, method): + """Clean up.""" + self.hass.stop() + + @mock.patch('os.path.dirname') + @mock.patch('homeassistant.util.package.running_under_virtualenv', + return_value=True) + @mock.patch('homeassistant.util.package.install_package', + return_value=True) + def test_requirement_installed_in_venv( + self, mock_install, mock_venv, mock_dirname): + """Test requirement installed in virtual environment.""" + mock_venv.return_value = True + mock_dirname.return_value = 'ha_package_path' + self.hass.config.skip_pip = False + loader.set_component( + 'comp', MockModule('comp', requirements=['package==0.0.1'])) + assert setup.setup_component(self.hass, 'comp') + assert 'comp' in self.hass.config.components + assert mock_install.call_args == mock.call( + 'package==0.0.1', + constraints=os.path.join('ha_package_path', CONSTRAINT_FILE)) + + @mock.patch('os.path.dirname') + @mock.patch('homeassistant.util.package.running_under_virtualenv', + return_value=False) + @mock.patch('homeassistant.util.package.install_package', + return_value=True) + def test_requirement_installed_in_deps( + self, mock_install, mock_venv, mock_dirname): + """Test requirement installed in deps directory.""" + mock_dirname.return_value = 'ha_package_path' + self.hass.config.skip_pip = False + loader.set_component( + 'comp', MockModule('comp', requirements=['package==0.0.1'])) + assert setup.setup_component(self.hass, 'comp') + assert 'comp' in self.hass.config.components + assert mock_install.call_args == mock.call( + 'package==0.0.1', target=self.hass.config.path('deps'), + constraints=os.path.join('ha_package_path', CONSTRAINT_FILE)) diff --git a/tests/test_setup.py b/tests/test_setup.py index afea30ddcd1..6a94310793c 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -9,7 +9,7 @@ import logging import voluptuous as vol from homeassistant.core import callback -from homeassistant.const import EVENT_HOMEASSISTANT_START, CONSTRAINT_FILE +from homeassistant.const import EVENT_HOMEASSISTANT_START import homeassistant.config as config_util from homeassistant import setup, loader import homeassistant.util.dt as dt_util @@ -41,9 +41,6 @@ class TestSetup: """Clean up.""" self.hass.stop() - # if os.path.isfile(VERSION_PATH): - # os.remove(VERSION_PATH) - def test_validate_component_config(self): """Test validating component configuration.""" config_schema = vol.Schema({ @@ -203,43 +200,6 @@ class TestSetup: assert not setup.setup_component(self.hass, 'comp') assert 'comp' not in self.hass.config.components - @mock.patch('homeassistant.setup.os.path.dirname') - @mock.patch('homeassistant.util.package.running_under_virtualenv', - return_value=True) - @mock.patch('homeassistant.util.package.install_package', - return_value=True) - def test_requirement_installed_in_venv( - self, mock_install, mock_venv, mock_dirname): - """Test requirement installed in virtual environment.""" - mock_venv.return_value = True - mock_dirname.return_value = 'ha_package_path' - self.hass.config.skip_pip = False - loader.set_component( - 'comp', MockModule('comp', requirements=['package==0.0.1'])) - assert setup.setup_component(self.hass, 'comp') - assert 'comp' in self.hass.config.components - assert mock_install.call_args == mock.call( - 'package==0.0.1', - constraints=os.path.join('ha_package_path', CONSTRAINT_FILE)) - - @mock.patch('homeassistant.setup.os.path.dirname') - @mock.patch('homeassistant.util.package.running_under_virtualenv', - return_value=False) - @mock.patch('homeassistant.util.package.install_package', - return_value=True) - def test_requirement_installed_in_deps( - self, mock_install, mock_venv, mock_dirname): - """Test requirement installed in deps directory.""" - mock_dirname.return_value = 'ha_package_path' - self.hass.config.skip_pip = False - loader.set_component( - 'comp', MockModule('comp', requirements=['package==0.0.1'])) - assert setup.setup_component(self.hass, 'comp') - assert 'comp' in self.hass.config.components - assert mock_install.call_args == mock.call( - 'package==0.0.1', target=self.hass.config.path('deps'), - constraints=os.path.join('ha_package_path', CONSTRAINT_FILE)) - def test_component_not_setup_twice_if_loaded_during_other_setup(self): """Test component setup while waiting for lock is not setup twice.""" result = []