diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 5f64fd447a6..a90a96e4fa0 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -19,6 +19,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE from homeassistant.setup import async_setup_component import homeassistant.loader as loader from homeassistant.util.logging import AsyncHandler +from homeassistant.util.package import async_get_user_site, get_user_site from homeassistant.util.yaml import clear_secret_cache from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.signal import async_register_signal_handling @@ -48,7 +49,8 @@ def from_config_dict(config: Dict[str, Any], if config_dir is not None: config_dir = os.path.abspath(config_dir) hass.config.config_dir = config_dir - mount_local_lib_path(config_dir) + hass.loop.run_until_complete( + async_mount_local_lib_path(config_dir, hass.loop)) # run task hass = hass.loop.run_until_complete( @@ -183,7 +185,7 @@ def async_from_config_file(config_path: str, # Set config dir to directory holding config file config_dir = os.path.abspath(os.path.dirname(config_path)) hass.config.config_dir = config_dir - yield from hass.async_add_job(mount_local_lib_path, config_dir) + yield from async_mount_local_lib_path(config_dir, hass.loop) async_enable_logging(hass, verbose, log_rotate_days) @@ -276,11 +278,23 @@ def async_enable_logging(hass: core.HomeAssistant, verbose: bool=False, def mount_local_lib_path(config_dir: str) -> str: + """Add local library to Python Path.""" + deps_dir = os.path.join(config_dir, 'deps') + lib_dir = get_user_site(deps_dir) + if lib_dir not in sys.path: + sys.path.insert(0, lib_dir) + return deps_dir + + +@asyncio.coroutine +def async_mount_local_lib_path(config_dir: str, + loop: asyncio.AbstractEventLoop) -> str: """Add local library to Python Path. - Async friendly. + This function is a coroutine. """ deps_dir = os.path.join(config_dir, 'deps') - if deps_dir not in sys.path: - sys.path.insert(0, os.path.join(config_dir, 'deps')) + lib_dir = yield from async_get_user_site(deps_dir, loop=loop) + if lib_dir not in sys.path: + sys.path.insert(0, lib_dir) return deps_dir diff --git a/homeassistant/config.py b/homeassistant/config.py index d91854c5162..cb8d0b442c1 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -1,6 +1,8 @@ """Module to help with parsing and generating configuration files.""" import asyncio from collections import OrderedDict +# pylint: disable=no-name-in-module +from distutils.version import LooseVersion # pylint: disable=import-error import logging import os import re @@ -295,9 +297,11 @@ def process_ha_config_upgrade(hass): _LOGGER.info('Upgrading config directory from %s to %s', conf_version, __version__) - lib_path = hass.config.path('deps') - if os.path.isdir(lib_path): - shutil.rmtree(lib_path) + if LooseVersion(conf_version) < LooseVersion('0.49'): + # 0.49 introduced persistent deps dir. + lib_path = hass.config.path('deps') + if os.path.isdir(lib_path): + shutil.rmtree(lib_path) with open(version_path, 'wt') as outp: outp.write(__version__) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1de3671a296..9414bc98dc8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,7 +1,7 @@ requests==2.14.2 pyyaml>=3.11,<4 pytz>=2017.02 -pip>=7.1.0 +pip>=8.0.3 jinja2>=2.9.5 voluptuous==0.10.5 typing>=3,<4 diff --git a/homeassistant/scripts/__init__.py b/homeassistant/scripts/__init__.py index af9e00626dd..2324613acfb 100644 --- a/homeassistant/scripts/__init__.py +++ b/homeassistant/scripts/__init__.py @@ -7,7 +7,8 @@ import logging from typing import List from homeassistant.config import get_default_config_dir -from homeassistant.util.package import install_package +from homeassistant.const import CONSTRAINT_FILE +from homeassistant.util.package import install_package, is_virtual_env from homeassistant.bootstrap import mount_local_lib_path @@ -40,7 +41,14 @@ def run(args: List) -> int: logging.basicConfig(stream=sys.stdout, level=logging.INFO) for req in getattr(script, 'REQUIREMENTS', []): - if not install_package(req, target=deps_dir): + if is_virtual_env(): + 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)) + if not returncode: print('Aborting scipt, could not install dependency', req) return 1 diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 285a5755145..3594399281f 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -77,6 +77,10 @@ def _async_process_requirements(hass: core.HomeAssistant, name: str, def pip_install(mod): """Install packages.""" + if pkg_util.is_virtual_env(): + 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( diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index a5a863b0880..4736c1acc1a 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -1,4 +1,5 @@ """Helpers to install PyPi packages.""" +import asyncio import logging import os import sys @@ -24,20 +25,26 @@ def install_package(package: str, upgrade: bool=True, """ # Not using 'import pip; pip.main([])' because it breaks the logger with INSTALL_LOCK: - if check_package_exists(package, target): + if check_package_exists(package): return True - _LOGGER.info("Attempting install of %s", package) + _LOGGER.info('Attempting install of %s', package) + env = os.environ.copy() args = [sys.executable, '-m', 'pip', 'install', '--quiet', package] if upgrade: args.append('--upgrade') - if target: - args += ['--target', os.path.abspath(target)] - if constraints is not None: args += ['--constraint', constraints] - - process = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE) + if target: + assert not is_virtual_env() + # This only works if not running in venv + args += ['--user'] + env['PYTHONUSERBASE'] = os.path.abspath(target) + if sys.platform != 'win32': + # Workaround for incompatible prefix setting + # See http://stackoverflow.com/a/4495175 + args += ['--prefix='] + process = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) _, stderr = process.communicate() if process.returncode != 0: _LOGGER.error("Unable to install package %s: %s", @@ -47,7 +54,7 @@ def install_package(package: str, upgrade: bool=True, return True -def check_package_exists(package: str, lib_dir: str) -> bool: +def check_package_exists(package: str) -> bool: """Check if a package is installed globally or in lib_dir. Returns True when the requirement is met. @@ -59,12 +66,43 @@ def check_package_exists(package: str, lib_dir: str) -> bool: # This is a zip file req = pkg_resources.Requirement.parse(urlparse(package).fragment) - # Check packages from lib dir - if lib_dir is not None: - if any(dist in req for dist in - pkg_resources.find_distributions(lib_dir)): - return True + env = pkg_resources.Environment() + return any(dist in req for dist in env[req.project_name]) - # Check packages from global + virtual environment - # pylint: disable=not-an-iterable - return any(dist in req for dist in pkg_resources.working_set) + +def is_virtual_env() -> bool: + """Return true if environment is a virtual environment.""" + return hasattr(sys, 'real_prefix') + + +def _get_user_site(deps_dir: str) -> tuple: + """Get arguments and environment for subprocess used in get_user_site.""" + env = os.environ.copy() + env['PYTHONUSERBASE'] = os.path.abspath(deps_dir) + args = [sys.executable, '-m', 'site', '--user-site'] + return args, env + + +def get_user_site(deps_dir: str) -> str: + """Return user local library path.""" + args, env = _get_user_site(deps_dir) + process = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) + stdout, _ = process.communicate() + lib_dir = stdout.decode().strip() + return lib_dir + + +@asyncio.coroutine +def async_get_user_site(deps_dir: str, loop: asyncio.AbstractEventLoop) -> str: + """Return user local library path. + + This function is a coroutine. + """ + args, env = _get_user_site(deps_dir) + process = yield from asyncio.create_subprocess_exec( + *args, loop=loop, stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL, + env=env) + stdout, _ = yield from process.communicate() + lib_dir = stdout.decode().strip() + return lib_dir diff --git a/requirements_all.txt b/requirements_all.txt index d4e4a30add7..60d25a41049 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2,7 +2,7 @@ requests==2.14.2 pyyaml>=3.11,<4 pytz>=2017.02 -pip>=7.1.0 +pip>=8.0.3 jinja2>=2.9.5 voluptuous==0.10.5 typing>=3,<4 diff --git a/setup.py b/setup.py index 4476bc2f9f0..d19a074889a 100755 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ REQUIRES = [ 'requests==2.14.2', 'pyyaml>=3.11,<4', 'pytz>=2017.02', - 'pip>=7.1.0', + 'pip>=8.0.3', 'jinja2>=2.9.5', 'voluptuous==0.10.5', 'typing>=3,<4', diff --git a/tests/test_config.py b/tests/test_config.py index 00b631a2f78..eba98795fe9 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -260,40 +260,30 @@ class TestConfig(unittest.TestCase): @mock.patch('homeassistant.config.shutil') @mock.patch('homeassistant.config.os') def test_remove_lib_on_upgrade(self, mock_os, mock_shutil): - """Test removal of library on upgrade.""" - ha_version = '0.7.0' - + """Test removal of library on upgrade from before 0.49.""" + ha_version = '0.48.0' mock_os.path.isdir = mock.Mock(return_value=True) - mock_open = mock.mock_open() with mock.patch('homeassistant.config.open', mock_open, create=True): opened_file = mock_open.return_value # pylint: disable=no-member opened_file.readline.return_value = ha_version - self.hass.config.path = mock.Mock() - config_util.process_ha_config_upgrade(self.hass) - hass_path = self.hass.config.path.return_value self.assertEqual(mock_os.path.isdir.call_count, 1) self.assertEqual( mock_os.path.isdir.call_args, mock.call(hass_path) ) - self.assertEqual(mock_shutil.rmtree.call_count, 1) self.assertEqual( mock_shutil.rmtree.call_args, mock.call(hass_path) ) - @mock.patch('homeassistant.config.shutil') - @mock.patch('homeassistant.config.os') - def test_not_remove_lib_if_not_upgrade(self, mock_os, mock_shutil): - """Test removal of library with no upgrade.""" - ha_version = __version__ - - mock_os.path.isdir = mock.Mock(return_value=True) + def test_process_config_upgrade(self): + """Test update of version on upgrade.""" + ha_version = '0.8.0' mock_open = mock.mock_open() with mock.patch('homeassistant.config.open', mock_open, create=True): @@ -301,12 +291,38 @@ class TestConfig(unittest.TestCase): # pylint: disable=no-member opened_file.readline.return_value = ha_version - self.hass.config.path = mock.Mock() + config_util.process_ha_config_upgrade(self.hass) + + self.assertEqual(opened_file.write.call_count, 1) + self.assertEqual( + opened_file.write.call_args, mock.call(__version__) + ) + + def test_config_upgrade_same_version(self): + """Test no update of version on no upgrade.""" + ha_version = __version__ + + mock_open = mock.mock_open() + with mock.patch('homeassistant.config.open', mock_open, create=True): + opened_file = mock_open.return_value + # pylint: disable=no-member + opened_file.readline.return_value = ha_version config_util.process_ha_config_upgrade(self.hass) - assert mock_os.path.isdir.call_count == 0 - assert mock_shutil.rmtree.call_count == 0 + assert opened_file.write.call_count == 0 + + def test_config_upgrade_no_file(self): + """Test update of version on upgrade, with no version file.""" + mock_open = mock.mock_open() + mock_open.side_effect = [FileNotFoundError(), mock.DEFAULT] + with mock.patch('homeassistant.config.open', mock_open, create=True): + opened_file = mock_open.return_value + # pylint: disable=no-member + config_util.process_ha_config_upgrade(self.hass) + self.assertEqual(opened_file.write.call_count, 1) + self.assertEqual( + opened_file.write.call_args, mock.call(__version__)) @mock.patch('homeassistant.config.shutil') @mock.patch('homeassistant.config.os') diff --git a/tests/test_setup.py b/tests/test_setup.py index 291dfdd741f..742df86242f 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 +from homeassistant.const import EVENT_HOMEASSISTANT_START, CONSTRAINT_FILE import homeassistant.config as config_util from homeassistant import setup, loader import homeassistant.util.dt as dt_util @@ -203,6 +203,41 @@ 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.sys') + @mock.patch('homeassistant.util.package.install_package', + return_value=True) + def test_requirement_installed_in_venv( + self, mock_install, mock_sys, mock_dirname): + """Test requirement installed in virtual environment.""" + mock_sys.real_prefix = 'pythonpath' + 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.sys', spec=object()) + @mock.patch('homeassistant.util.package.install_package', + return_value=True) + def test_requirement_installed_in_deps( + self, mock_install, mock_sys, 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 = [] diff --git a/tests/util/test_package.py b/tests/util/test_package.py index e0682d79f57..7ca1315c2e1 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -1,11 +1,13 @@ """Test Home Assistant package util methods.""" +import asyncio +import logging import os -import pkg_resources -import unittest - +import sys from subprocess import PIPE -from distutils.sysconfig import get_python_lib -from unittest.mock import call, patch, Mock +from unittest.mock import MagicMock, call, patch + +import pkg_resources +import pytest import homeassistant.util.package as package @@ -18,124 +20,200 @@ TEST_ZIP_REQ = 'file://{}#{}' \ .format(os.path.join(RESOURCE_DIR, 'pyhelloworld3.zip'), TEST_NEW_REQ) -@patch('homeassistant.util.package.Popen') -@patch('homeassistant.util.package.check_package_exists') -class TestPackageUtilInstallPackage(unittest.TestCase): - """Test for homeassistant.util.package module.""" - - def setUp(self): - """Setup the tests.""" - self.mock_process = Mock() - self.mock_process.communicate.return_value = (b'message', b'error') - self.mock_process.returncode = 0 - - def test_install_existing_package(self, mock_exists, mock_popen): - """Test an install attempt on an existing package.""" - mock_popen.return_value = self.mock_process - mock_exists.return_value = True - - self.assertTrue(package.install_package(TEST_EXIST_REQ)) - - self.assertEqual(mock_exists.call_count, 1) - self.assertEqual(mock_exists.call_args, call(TEST_EXIST_REQ, None)) - - self.assertEqual(self.mock_process.communicate.call_count, 0) - - @patch('homeassistant.util.package.sys') - def test_install(self, mock_sys, mock_exists, mock_popen): - """Test an install attempt on a package that doesn't exist.""" - mock_exists.return_value = False - mock_popen.return_value = self.mock_process - - self.assertTrue(package.install_package(TEST_NEW_REQ, False)) - - self.assertEqual(mock_exists.call_count, 1) - - self.assertEqual(self.mock_process.communicate.call_count, 1) - self.assertEqual(mock_popen.call_count, 1) - self.assertEqual( - mock_popen.call_args, - call([ - mock_sys.executable, '-m', 'pip', 'install', '--quiet', - TEST_NEW_REQ - ], stdin=PIPE, stdout=PIPE, stderr=PIPE) - ) - - @patch('homeassistant.util.package.sys') - def test_install_upgrade(self, mock_sys, mock_exists, mock_popen): - """Test an upgrade attempt on a package.""" - mock_exists.return_value = False - mock_popen.return_value = self.mock_process - - self.assertTrue(package.install_package(TEST_NEW_REQ)) - - self.assertEqual(mock_exists.call_count, 1) - - self.assertEqual(self.mock_process.communicate.call_count, 1) - self.assertEqual(mock_popen.call_count, 1) - self.assertEqual( - mock_popen.call_args, - call([ - mock_sys.executable, '-m', 'pip', 'install', '--quiet', - TEST_NEW_REQ, '--upgrade' - ], stdin=PIPE, stdout=PIPE, stderr=PIPE) - ) - - @patch('homeassistant.util.package.sys') - def test_install_target(self, mock_sys, mock_exists, mock_popen): - """Test an install with a target.""" - target = 'target_folder' - mock_exists.return_value = False - mock_popen.return_value = self.mock_process - - self.assertTrue( - package.install_package(TEST_NEW_REQ, False, target=target) - ) - - self.assertEqual(mock_exists.call_count, 1) - - self.assertEqual(self.mock_process.communicate.call_count, 1) - self.assertEqual(mock_popen.call_count, 1) - self.assertEqual( - mock_popen.call_args, - call([ - mock_sys.executable, '-m', 'pip', 'install', '--quiet', - TEST_NEW_REQ, '--target', os.path.abspath(target) - ], stdin=PIPE, stdout=PIPE, stderr=PIPE) - ) - - @patch('homeassistant.util.package._LOGGER') - @patch('homeassistant.util.package.sys') - def test_install_error(self, mock_sys, mock_logger, mock_exists, - mock_popen): - """Test an install with a target.""" - mock_exists.return_value = False - mock_popen.return_value = self.mock_process - self.mock_process.returncode = 1 - - self.assertFalse(package.install_package(TEST_NEW_REQ)) - - self.assertEqual(mock_logger.error.call_count, 1) +@pytest.fixture +def mock_sys(): + """Mock sys.""" + with patch('homeassistant.util.package.sys', spec=object) as sys_mock: + sys_mock.executable = 'python3' + yield sys_mock -class TestPackageUtilCheckPackageExists(unittest.TestCase): - """Test for homeassistant.util.package module.""" +@pytest.fixture +def mock_exists(): + """Mock check_package_exists.""" + with patch('homeassistant.util.package.check_package_exists') as mock: + mock.return_value = False + yield mock - def test_check_package_global(self): - """Test for a globally-installed package.""" - installed_package = list(pkg_resources.working_set)[0].project_name - self.assertTrue(package.check_package_exists(installed_package, None)) +@pytest.fixture +def deps_dir(): + """Return path to deps directory.""" + return os.path.abspath('/deps_dir') - def test_check_package_local(self): - """Test for a locally-installed package.""" - lib_dir = get_python_lib() - installed_package = list(pkg_resources.working_set)[0].project_name - self.assertTrue( - package.check_package_exists(installed_package, lib_dir) - ) +@pytest.fixture +def lib_dir(deps_dir): + """Return path to lib directory.""" + return os.path.join(deps_dir, 'lib_dir') - def test_check_package_zip(self): - """Test for an installed zip package.""" - self.assertFalse(package.check_package_exists(TEST_ZIP_REQ, None)) + +@pytest.fixture +def mock_popen(lib_dir): + """Return a Popen mock.""" + with patch('homeassistant.util.package.Popen') as popen_mock: + popen_mock.return_value.communicate.return_value = ( + bytes(lib_dir, 'utf-8'), b'error') + popen_mock.return_value.returncode = 0 + yield popen_mock + + +@pytest.fixture +def mock_env_copy(): + """Mock os.environ.copy.""" + with patch('homeassistant.util.package.os.environ.copy') as env_copy: + env_copy.return_value = {} + yield env_copy + + +@asyncio.coroutine +def mock_async_subprocess(): + """Return an async Popen mock.""" + async_popen = MagicMock() + + @asyncio.coroutine + def communicate(input=None): + """Communicate mock.""" + stdout = bytes('/deps_dir/lib_dir', 'utf-8') + return (stdout, None) + + async_popen.communicate = communicate + return async_popen + + +def test_install_existing_package(mock_exists, mock_popen): + """Test an install attempt on an existing package.""" + mock_exists.return_value = True + assert package.install_package(TEST_EXIST_REQ) + assert mock_exists.call_count == 1 + assert mock_exists.call_args == call(TEST_EXIST_REQ) + assert mock_popen.return_value.communicate.call_count == 0 + + +def test_install(mock_sys, mock_exists, mock_popen, mock_env_copy): + """Test an install attempt on a package that doesn't exist.""" + env = mock_env_copy() + assert package.install_package(TEST_NEW_REQ, False) + assert mock_exists.call_count == 1 + assert mock_popen.call_count == 1 + assert ( + mock_popen.call_args == + call([ + mock_sys.executable, '-m', 'pip', 'install', '--quiet', + TEST_NEW_REQ + ], stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) + ) + assert mock_popen.return_value.communicate.call_count == 1 + + +def test_install_upgrade(mock_sys, mock_exists, mock_popen, mock_env_copy): + """Test an upgrade attempt on a package.""" + env = mock_env_copy() + assert package.install_package(TEST_NEW_REQ) + assert mock_exists.call_count == 1 + assert mock_popen.call_count == 1 + assert ( + mock_popen.call_args == + call([ + mock_sys.executable, '-m', 'pip', 'install', '--quiet', + TEST_NEW_REQ, '--upgrade' + ], stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) + ) + assert mock_popen.return_value.communicate.call_count == 1 + + +def test_install_target(mock_sys, mock_exists, mock_popen, mock_env_copy): + """Test an install with a target.""" + target = 'target_folder' + env = mock_env_copy() + env['PYTHONUSERBASE'] = os.path.abspath(target) + mock_sys.platform = 'linux' + args = [ + mock_sys.executable, '-m', 'pip', 'install', '--quiet', + TEST_NEW_REQ, '--user', '--prefix='] + + assert package.install_package(TEST_NEW_REQ, False, target=target) + assert mock_exists.call_count == 1 + assert mock_popen.call_count == 1 + assert ( + mock_popen.call_args == + call(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) + ) + assert mock_popen.return_value.communicate.call_count == 1 + + +def test_install_target_venv(mock_sys, mock_exists, mock_popen, mock_env_copy): + """Test an install with a target in a virtual environment.""" + target = 'target_folder' + mock_sys.real_prefix = '/usr' + with pytest.raises(AssertionError): + package.install_package(TEST_NEW_REQ, False, target=target) + + +def test_install_error(caplog, mock_sys, mock_exists, mock_popen): + """Test an install with a target.""" + caplog.set_level(logging.WARNING) + mock_popen.return_value.returncode = 1 + assert not package.install_package(TEST_NEW_REQ) + assert len(caplog.records) == 1 + for record in caplog.records: + assert record.levelname == 'ERROR' + + +def test_install_constraint(mock_sys, mock_exists, mock_popen, mock_env_copy): + """Test install with constraint file on not installed package.""" + env = mock_env_copy() + constraints = 'constraints_file.txt' + assert package.install_package( + TEST_NEW_REQ, False, constraints=constraints) + assert mock_exists.call_count == 1 + assert mock_popen.call_count == 1 + assert ( + mock_popen.call_args == + call([ + mock_sys.executable, '-m', 'pip', 'install', '--quiet', + TEST_NEW_REQ, '--constraint', constraints + ], stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) + ) + assert mock_popen.return_value.communicate.call_count == 1 + + +def test_check_package_global(): + """Test for an installed package.""" + installed_package = list(pkg_resources.working_set)[0].project_name + assert package.check_package_exists(installed_package) + + +def test_check_package_zip(): + """Test for an installed zip package.""" + assert not package.check_package_exists(TEST_ZIP_REQ) + + +def test_get_user_site(deps_dir, lib_dir, mock_popen, mock_env_copy): + """Test get user site directory.""" + env = mock_env_copy() + env['PYTHONUSERBASE'] = os.path.abspath(deps_dir) + args = [sys.executable, '-m', 'site', '--user-site'] + ret = package.get_user_site(deps_dir) + assert mock_popen.call_count == 1 + assert mock_popen.call_args == call( + args, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) + assert ret == lib_dir + + +@asyncio.coroutine +def test_async_get_user_site(hass, mock_env_copy): + """Test async get user site directory.""" + deps_dir = '/deps_dir' + env = mock_env_copy() + env['PYTHONUSERBASE'] = os.path.abspath(deps_dir) + args = [sys.executable, '-m', 'site', '--user-site'] + with patch('homeassistant.util.package.asyncio.create_subprocess_exec', + return_value=mock_async_subprocess()) as popen_mock: + ret = yield from package.async_get_user_site(deps_dir, hass.loop) + assert popen_mock.call_count == 1 + assert popen_mock.call_args == call( + *args, loop=hass.loop, stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL, + env=env) + assert ret == os.path.join(deps_dir, 'lib_dir')