core/homeassistant/util/package.py

103 lines
3.3 KiB
Python
Raw Normal View History

2016-03-07 22:20:48 +00:00
"""Helpers to install PyPi packages."""
import asyncio
import logging
import os
from subprocess import PIPE, Popen
import sys
import threading
from urllib.parse import urlparse
from typing import Optional
2015-11-29 21:49:05 +00:00
import pkg_resources
_LOGGER = logging.getLogger(__name__)
INSTALL_LOCK = threading.Lock()
def is_virtual_env() -> bool:
"""Return if we run in a virtual environtment."""
# Check supports venv && virtualenv
return (getattr(sys, 'base_prefix', sys.prefix) != sys.prefix or
hasattr(sys, 'real_prefix'))
def install_package(package: str, upgrade: bool = True,
target: Optional[str] = None,
constraints: Optional[str] = None) -> bool:
2016-03-07 22:20:48 +00:00
"""Install a package on PyPi. Accepts pip compatible package strings.
2016-01-26 23:08:06 +00:00
Return boolean if install successful.
"""
# Not using 'import pip; pip.main([])' because it breaks the logger
with INSTALL_LOCK:
if package_loadable(package):
return True
_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 constraints is not None:
args += ['--constraint', constraints]
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",
package, stderr.decode('utf-8').lstrip().strip())
return False
return True
def package_loadable(package: str) -> bool:
"""Check if a package is what will be loaded when we import it.
2016-03-07 22:20:48 +00:00
Returns True when the requirement is met.
2016-01-26 23:08:06 +00:00
Returns False when the package is not installed or doesn't meet req.
"""
try:
req = pkg_resources.Requirement.parse(package)
except ValueError:
# This is a zip file
req = pkg_resources.Requirement.parse(urlparse(package).fragment)
req_proj_name = req.project_name.lower()
for path in sys.path:
for dist in pkg_resources.find_distributions(path):
# If the project name is the same, it will be the one that is
# loaded when we import it.
if dist.project_name.lower() == req_proj_name:
return dist in req
return False
async def async_get_user_site(deps_dir: str) -> str:
"""Return user local library path.
This function is a coroutine.
"""
env = os.environ.copy()
env['PYTHONUSERBASE'] = os.path.abspath(deps_dir)
args = [sys.executable, '-m', 'site', '--user-site']
process = await asyncio.create_subprocess_exec(
*args, stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL,
env=env)
stdout, _ = await process.communicate()
lib_dir = stdout.decode().strip()
return lib_dir