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.
This commit is contained in:
Martin Hjelmare 2017-07-14 04:26:21 +02:00 committed by Paulus Schoutsen
parent 5581c6295e
commit ba019c799a
11 changed files with 364 additions and 167 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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