2016-03-07 22:20:48 +00:00
|
|
|
"""Helpers to install PyPi packages."""
|
2021-03-17 20:46:07 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2017-07-14 02:26:21 +00:00
|
|
|
import asyncio
|
2022-08-02 20:38:01 +00:00
|
|
|
from functools import cache
|
2021-01-04 10:47:29 +00:00
|
|
|
from importlib.metadata import PackageNotFoundError, version
|
2015-09-05 08:50:35 +00:00
|
|
|
import logging
|
2015-09-17 06:12:38 +00:00
|
|
|
import os
|
2019-12-09 15:42:10 +00:00
|
|
|
from pathlib import Path
|
2017-07-15 14:25:02 +00:00
|
|
|
from subprocess import PIPE, Popen
|
2015-07-16 01:37:24 +00:00
|
|
|
import sys
|
2019-05-26 18:58:42 +00:00
|
|
|
from urllib.parse import urlparse
|
|
|
|
|
|
|
|
import pkg_resources
|
2016-07-28 03:33:49 +00:00
|
|
|
|
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
|
|
|
|
2018-07-23 08:24:39 +00:00
|
|
|
def is_virtual_env() -> bool:
|
2020-01-31 16:33:00 +00:00
|
|
|
"""Return if we run in a virtual environment."""
|
2018-03-05 23:51:37 +00:00
|
|
|
# Check supports venv && virtualenv
|
2019-07-31 19:25:30 +00:00
|
|
|
return getattr(sys, "base_prefix", sys.prefix) != sys.prefix or hasattr(
|
|
|
|
sys, "real_prefix"
|
|
|
|
)
|
2018-03-05 23:51:37 +00:00
|
|
|
|
|
|
|
|
2022-08-02 20:38:01 +00:00
|
|
|
@cache
|
2019-05-29 22:30:09 +00:00
|
|
|
def is_docker_env() -> bool:
|
|
|
|
"""Return True if we run in a docker env."""
|
|
|
|
return Path("/.dockerenv").exists()
|
|
|
|
|
|
|
|
|
2019-05-26 18:58:42 +00:00
|
|
|
def is_installed(package: str) -> bool:
|
|
|
|
"""Check if a package is installed and will be loaded when we import it.
|
|
|
|
|
|
|
|
Returns True when the requirement is met.
|
|
|
|
Returns False when the package is not installed or doesn't meet req.
|
|
|
|
"""
|
|
|
|
try:
|
2021-03-11 07:12:02 +00:00
|
|
|
pkg_resources.get_distribution(package)
|
|
|
|
return True
|
Fix checking if a package is installed on py3.11 (#88768)
pkg_resources is abandoned and we need to move away
from using it https://github.com/pypa/pkg_resources
In the mean time we need to keep it working. This fixes
a new exception in py3.11 when a module is not installed
which allows proper fallback to pkg_resources.Requirement.parse
when needed
```
2023-02-25 15:46:21.101 ERROR (MainThread) [aiohttp.server] Error handling request
Traceback (most recent call last):
File "/opt/homebrew/lib/python3.11/site-packages/aiohttp/web_protocol.py", line 433, in _handle_request
resp = await request_handler(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/lib/python3.11/site-packages/aiohttp/web_app.py", line 504, in _handle
resp = await handler(request)
^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/lib/python3.11/site-packages/aiohttp/web_middlewares.py", line 117, in impl
return await handler(request)
^^^^^^^^^^^^^^^^^^^^^^
File "/Users/bdraco/home-assistant/homeassistant/components/http/security_filter.py", line 60, in security_filter_middleware
return await handler(request)
^^^^^^^^^^^^^^^^^^^^^^
File "/Users/bdraco/home-assistant/homeassistant/components/http/forwarded.py", line 100, in forwarded_middleware
return await handler(request)
^^^^^^^^^^^^^^^^^^^^^^
File "/Users/bdraco/home-assistant/homeassistant/components/http/request_context.py", line 28, in request_context_middleware
return await handler(request)
^^^^^^^^^^^^^^^^^^^^^^
File "/Users/bdraco/home-assistant/homeassistant/components/http/ban.py", line 80, in ban_middleware
return await handler(request)
^^^^^^^^^^^^^^^^^^^^^^
File "/Users/bdraco/home-assistant/homeassistant/components/http/auth.py", line 235, in auth_middleware
return await handler(request)
^^^^^^^^^^^^^^^^^^^^^^
File "/Users/bdraco/home-assistant/homeassistant/components/http/view.py", line 146, in handle
result = await result
^^^^^^^^^^^^
File "/Users/bdraco/home-assistant/homeassistant/components/config/config_entries.py", line 148, in post
return await super().post(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/bdraco/home-assistant/homeassistant/components/http/data_validator.py", line 72, in wrapper
result = await method(view, request, data, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/bdraco/home-assistant/homeassistant/helpers/data_entry_flow.py", line 71, in post
result = await self._flow_mgr.async_init(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/bdraco/home-assistant/homeassistant/config_entries.py", line 826, in async_init
flow, result = await task
^^^^^^^^^^
File "/Users/bdraco/home-assistant/homeassistant/config_entries.py", line 844, in _async_init
flow = await self.async_create_flow(handler, context=context, data=data)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/bdraco/home-assistant/homeassistant/config_entries.py", line 950, in async_create_flow
await async_process_deps_reqs(self.hass, self._hass_config, integration)
File "/Users/bdraco/home-assistant/homeassistant/setup.py", line 384, in async_process_deps_reqs
await requirements.async_get_integration_with_requirements(
File "/Users/bdraco/home-assistant/homeassistant/requirements.py", line 52, in async_get_integration_with_requirements
return await manager.async_get_integration_with_requirements(domain)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/bdraco/home-assistant/homeassistant/requirements.py", line 171, in async_get_integration_with_requirements
await self._async_process_integration(integration, done)
File "/Users/bdraco/home-assistant/homeassistant/requirements.py", line 186, in _async_process_integration
await self.async_process_requirements(
File "/Users/bdraco/home-assistant/homeassistant/requirements.py", line 252, in async_process_requirements
await self._async_process_requirements(name, missing)
File "/Users/bdraco/home-assistant/homeassistant/requirements.py", line 284, in _async_process_requirements
installed, failures = await self.hass.async_add_executor_job(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/concurrent/futures/thread.py", line 58, in run
result = self.fn(*self.args, **self.kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/bdraco/home-assistant/homeassistant/requirements.py", line 113, in _install_requirements_if_missing
if pkg_util.is_installed(req) or _install_with_retry(req, kwargs):
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/bdraco/home-assistant/homeassistant/util/package.py", line 40, in is_installed
pkg_resources.get_distribution(package)
File "/opt/homebrew/lib/python3.11/site-packages/pkg_resources/__init__.py", line 478, in get_distribution
dist = get_provider(dist)
^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/lib/python3.11/site-packages/pkg_resources/__init__.py", line 354, in get_provider
return working_set.find(moduleOrReq) or require(str(moduleOrReq))[0]
~~~~~~~~~~~~~~~~~~~~~~~~~^^^
IndexError: list index out of range
``
2023-02-26 03:47:18 +00:00
|
|
|
except (IndexError, pkg_resources.ResolutionError, pkg_resources.ExtractionError):
|
2019-05-26 18:58:42 +00:00
|
|
|
req = pkg_resources.Requirement.parse(package)
|
|
|
|
except ValueError:
|
|
|
|
# This is a zip file. We no longer use this in Home Assistant,
|
|
|
|
# leaving it in for custom components.
|
|
|
|
req = pkg_resources.Requirement.parse(urlparse(package).fragment)
|
|
|
|
|
|
|
|
try:
|
2021-03-11 07:12:02 +00:00
|
|
|
installed_version = version(req.project_name)
|
|
|
|
# This will happen when an install failed or
|
|
|
|
# was aborted while in progress see
|
|
|
|
# https://github.com/home-assistant/core/issues/47699
|
|
|
|
if installed_version is None:
|
2023-01-09 06:01:55 +00:00
|
|
|
_LOGGER.error( # type: ignore[unreachable]
|
|
|
|
"Installed version for %s resolved to None", req.project_name
|
|
|
|
)
|
2021-03-11 07:12:02 +00:00
|
|
|
return False
|
|
|
|
return installed_version in req
|
2019-05-26 18:58:42 +00:00
|
|
|
except PackageNotFoundError:
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def install_package(
|
|
|
|
package: str,
|
|
|
|
upgrade: bool = True,
|
2021-03-17 20:46:07 +00:00
|
|
|
target: str | None = None,
|
|
|
|
constraints: str | None = None,
|
|
|
|
find_links: str | None = None,
|
2021-07-24 12:07:10 +00:00
|
|
|
timeout: int | None = None,
|
2021-03-17 20:46:07 +00:00
|
|
|
no_cache_dir: bool | None = False,
|
2019-07-31 19:25:30 +00:00
|
|
|
) -> 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
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.info("Attempting install of %s", package)
|
2018-08-28 10:52:18 +00:00
|
|
|
env = os.environ.copy()
|
2019-07-31 19:25:30 +00:00
|
|
|
args = [sys.executable, "-m", "pip", "install", "--quiet", package]
|
2021-07-24 12:07:10 +00:00
|
|
|
if timeout:
|
|
|
|
args += ["--timeout", str(timeout)]
|
2019-06-01 08:04:12 +00:00
|
|
|
if no_cache_dir:
|
2019-07-31 19:25:30 +00:00
|
|
|
args.append("--no-cache-dir")
|
2018-08-28 10:52:18 +00:00
|
|
|
if upgrade:
|
2019-07-31 19:25:30 +00:00
|
|
|
args.append("--upgrade")
|
2018-08-28 10:52:18 +00:00
|
|
|
if constraints is not None:
|
2019-07-31 19:25:30 +00:00
|
|
|
args += ["--constraint", constraints]
|
2019-05-29 22:30:09 +00:00
|
|
|
if find_links is not None:
|
2019-07-31 19:25:30 +00:00
|
|
|
args += ["--find-links", find_links, "--prefer-binary"]
|
2018-08-28 10:52:18 +00:00
|
|
|
if target:
|
|
|
|
assert not is_virtual_env()
|
|
|
|
# This only works if not running in venv
|
2019-07-31 19:25:30 +00:00
|
|
|
args += ["--user"]
|
|
|
|
env["PYTHONUSERBASE"] = os.path.abspath(target)
|
2021-12-05 21:30:02 +00:00
|
|
|
_LOGGER.debug("Running pip command: args=%s", args)
|
2023-02-13 14:02:51 +00:00
|
|
|
with Popen(
|
|
|
|
args,
|
|
|
|
stdin=PIPE,
|
|
|
|
stdout=PIPE,
|
|
|
|
stderr=PIPE,
|
|
|
|
env=env,
|
|
|
|
close_fds=False, # required for posix_spawn
|
|
|
|
) as process:
|
2021-04-25 00:39:24 +00:00
|
|
|
_, stderr = process.communicate()
|
|
|
|
if process.returncode != 0:
|
|
|
|
_LOGGER.error(
|
|
|
|
"Unable to install package %s: %s",
|
|
|
|
package,
|
|
|
|
stderr.decode("utf-8").lstrip().strip(),
|
|
|
|
)
|
|
|
|
return False
|
2018-08-28 10:52:18 +00:00
|
|
|
|
|
|
|
return True
|
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()
|
2019-07-31 19:25:30 +00:00
|
|
|
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(
|
2019-07-31 19:25:30 +00:00
|
|
|
*args,
|
|
|
|
stdin=asyncio.subprocess.PIPE,
|
|
|
|
stdout=asyncio.subprocess.PIPE,
|
|
|
|
stderr=asyncio.subprocess.DEVNULL,
|
|
|
|
env=env,
|
2023-02-13 14:02:51 +00:00
|
|
|
close_fds=False, # required for posix_spawn
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
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
|