2016-03-07 22:20:48 +00:00
|
|
|
"""Helpers to install PyPi packages."""
|
2017-07-14 02:26:21 +00:00
|
|
|
import asyncio
|
2015-09-05 08:50:35 +00:00
|
|
|
import logging
|
2015-09-17 06:12:38 +00:00
|
|
|
import os
|
2017-07-15 14:25:02 +00:00
|
|
|
from subprocess import PIPE, Popen
|
2015-07-16 01:37:24 +00:00
|
|
|
import sys
|
2015-09-05 08:50:35 +00:00
|
|
|
import threading
|
2015-09-09 02:49:27 +00:00
|
|
|
from urllib.parse import urlparse
|
2015-07-07 07:00:21 +00:00
|
|
|
|
2016-07-28 03:33:49 +00:00
|
|
|
from typing import Optional
|
|
|
|
|
2015-11-29 21:49:05 +00:00
|
|
|
import pkg_resources
|
|
|
|
|
2015-09-05 08:50:35 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
2017-06-08 13:53:12 +00:00
|
|
|
|
2015-09-05 08:50:35 +00:00
|
|
|
INSTALL_LOCK = threading.Lock()
|
2015-07-07 07:00:21 +00:00
|
|
|
|
2015-09-05 08:50:35 +00:00
|
|
|
|
2018-07-23 08:24:39 +00:00
|
|
|
def is_virtual_env() -> bool:
|
2018-03-05 23:51:37 +00:00
|
|
|
"""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'))
|
|
|
|
|
|
|
|
|
2018-02-11 17:20:28 +00:00
|
|
|
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.
|
|
|
|
"""
|
2015-07-07 07:00:21 +00:00
|
|
|
# Not using 'import pip; pip.main([])' because it breaks the logger
|
2015-09-05 08:50:35 +00:00
|
|
|
with INSTALL_LOCK:
|
2018-08-22 10:17:14 +00:00
|
|
|
if package_loadable(package):
|
2015-09-05 08:50:35 +00:00
|
|
|
return True
|
|
|
|
|
2017-07-14 02:26:21 +00:00
|
|
|
_LOGGER.info('Attempting install of %s', package)
|
|
|
|
env = os.environ.copy()
|
2015-09-17 06:12:38 +00:00
|
|
|
args = [sys.executable, '-m', 'pip', 'install', '--quiet', package]
|
|
|
|
if upgrade:
|
|
|
|
args.append('--upgrade')
|
2017-03-22 15:50:54 +00:00
|
|
|
if constraints is not None:
|
|
|
|
args += ['--constraint', constraints]
|
2017-07-14 02:26:21 +00:00
|
|
|
if target:
|
2018-03-05 23:51:37 +00:00
|
|
|
assert not is_virtual_env()
|
2017-07-14 02:26:21 +00:00
|
|
|
# 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)
|
2017-04-21 12:15:05 +00:00
|
|
|
_, stderr = process.communicate()
|
|
|
|
if process.returncode != 0:
|
2017-06-08 13:53:12 +00:00
|
|
|
_LOGGER.error("Unable to install package %s: %s",
|
2017-04-21 12:15:05 +00:00
|
|
|
package, stderr.decode('utf-8').lstrip().strip())
|
2015-09-05 08:50:35 +00:00
|
|
|
return False
|
|
|
|
|
2017-04-21 12:15:05 +00:00
|
|
|
return True
|
|
|
|
|
2015-09-05 08:50:35 +00:00
|
|
|
|
2018-08-22 10:17:14 +00:00
|
|
|
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
|
|
|
|
2015-09-05 08:50:35 +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.
|
|
|
|
"""
|
2015-09-09 02:49:27 +00:00
|
|
|
try:
|
|
|
|
req = pkg_resources.Requirement.parse(package)
|
|
|
|
except ValueError:
|
|
|
|
# This is a zip file
|
|
|
|
req = pkg_resources.Requirement.parse(urlparse(package).fragment)
|
2015-09-05 08:50:35 +00:00
|
|
|
|
2018-08-22 10:17:14 +00:00
|
|
|
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 == req.project_name:
|
|
|
|
return dist in req
|
|
|
|
|
|
|
|
return False
|
2017-07-14 02:26:21 +00:00
|
|
|
|
|
|
|
|
2018-06-16 14:48:41 +00:00
|
|
|
async def async_get_user_site(deps_dir: str) -> str:
|
2017-07-14 02:26:21 +00:00
|
|
|
"""Return user local library path.
|
|
|
|
|
|
|
|
This function is a coroutine.
|
|
|
|
"""
|
2018-06-16 14:48:41 +00:00
|
|
|
env = os.environ.copy()
|
|
|
|
env['PYTHONUSERBASE'] = os.path.abspath(deps_dir)
|
|
|
|
args = [sys.executable, '-m', 'site', '--user-site']
|
2018-02-25 11:38:46 +00:00
|
|
|
process = await asyncio.create_subprocess_exec(
|
2018-06-16 14:48:41 +00:00
|
|
|
*args, stdin=asyncio.subprocess.PIPE,
|
2017-07-14 02:26:21 +00:00
|
|
|
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL,
|
|
|
|
env=env)
|
2018-02-25 11:38:46 +00:00
|
|
|
stdout, _ = await process.communicate()
|
2017-07-14 02:26:21 +00:00
|
|
|
lib_dir = stdout.decode().strip()
|
|
|
|
return lib_dir
|