Make deps directory persistent over upgrades (#7801)
* Use pip install --user if venv not active * Set PYTHONUSERBASE to deps directory, when installing with --user option. * Reset --prefix option to workaround incompatability when installing with --user option. This requires pip version 8.0.0 or greater. * Require pip version 8.0.3. * Do not delete deps directory on home assistant upgrade. * Fix local lib mount and check package exist. * Update and add tests * Fix upgrade from before version 0.46 * Extract function to get user site * Add function(s) to package util to get user site. * Use async subprocess for one of the functions to get user site. * Add function to package util to check if virtual environment is active. * Add and update tests. * Update version for last removal of deps dir * Address comments * Rewrite package util tests with pytest * Rewrite all existing unittest class based tests for package util as test functions, and capitalize pytest fixtures. * Add test for installing with target inside venv.pull/8473/head
parent
5581c6295e
commit
ba019c799a
|
@ -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
|
||||
|
|
|
@ -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__)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
2
setup.py
2
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',
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Reference in New Issue