Extract requirements (#12051)

This commit is contained in:
Paulus Schoutsen 2018-01-30 03:30:47 -08:00 committed by Pascal Vizeli
parent 71cb4df817
commit ec1c395f09
6 changed files with 160 additions and 131 deletions

View file

@ -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 = '{}.{}'

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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))

View file

@ -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 = []