Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion homeassistant/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
__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 = '{}.{}'
Expand Down
45 changes: 45 additions & 0 deletions homeassistant/requirements.py
Original file line number Diff line number Diff line change
@@ -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')
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is changing the target path. Target should be the library folder.

Copy link
Copy Markdown
Member

@MartinHjelmare MartinHjelmare Jan 30, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nevermind, I'm wrong and remembered incorrectly.

return kwargs
18 changes: 7 additions & 11 deletions homeassistant/scripts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@

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:
Expand Down Expand Up @@ -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
Expand Down
114 changes: 41 additions & 73 deletions homeassistant/setup.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -162,22 +122,11 @@ def log_error(msg, link=True):
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)
Expand All @@ -192,7 +141,7 @@ def log_error(msg, link=True):
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(
Expand Down Expand Up @@ -256,21 +205,40 @@ def log_error(msg):
elif platform_path in hass.config.components:
return platform

# Load dependencies
if hasattr(platform, 'DEPENDENCIES'):
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, platform_path, platform.DEPENDENCIES)
hass, config, name, module.DEPENDENCIES)

if not dep_success:
log_error("Could not setup all dependencies.")
return None
raise HomeAssistantError("Could not setup all dependencies.")

if not hass.config.skip_pip and hasattr(platform, 'REQUIREMENTS'):
req_success = yield from _async_process_requirements(
hass, platform_path, platform.REQUIREMENTS)
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:
log_error("Could not install all requirements.")
return None
raise HomeAssistantError("Could not install all requirements.")

return platform
processed.add(name)
61 changes: 61 additions & 0 deletions tests/test_requirements.py
Original file line number Diff line number Diff line change
@@ -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))
42 changes: 1 addition & 41 deletions tests/test_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
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
Expand Down Expand Up @@ -41,9 +41,6 @@ def teardown_method(self, method):
"""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({
Expand Down Expand Up @@ -203,43 +200,6 @@ def test_component_not_installed_if_requirement_fails(self, mock_install):
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 = []
Expand Down