mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 08:47:57 +00:00
Extract requirements (#12051)
This commit is contained in:
parent
71cb4df817
commit
ec1c395f09
@ -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 = '{}.{}'
|
||||
|
45
homeassistant/requirements.py
Normal file
45
homeassistant/requirements.py
Normal 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
|
@ -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
|
||||
|
@ -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)
|
||||
|
61
tests/test_requirements.py
Normal file
61
tests/test_requirements.py
Normal 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))
|
@ -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 = []
|
||||
|
Loading…
x
Reference in New Issue
Block a user