1 """Module to handle installing requirements."""
3 from __future__
import annotations
6 from collections.abc
import Iterable
10 from typing
import Any
12 from packaging.requirements
import Requirement
14 from .core
import HomeAssistant, callback
15 from .exceptions
import HomeAssistantError
16 from .helpers
import singleton
17 from .loader
import Integration, IntegrationNotFound, async_get_integration
18 from .util
import package
as pkg_util
22 MAX_INSTALL_FAILURES = 3
23 DATA_REQUIREMENTS_MANAGER =
"requirements_manager"
24 CONSTRAINT_FILE =
"package_constraints.txt"
25 DISCOVERY_INTEGRATIONS: dict[str, Iterable[str]] = {
29 "zeroconf": (
"zeroconf",
"homekit"),
31 _LOGGER = logging.getLogger(__name__)
35 """Raised when a component is not found."""
37 def __init__(self, domain: str, requirements: list[str]) ->
None:
38 """Initialize a component not found error."""
39 super().
__init__(f
"Requirements for {domain} not found: {requirements}.")
45 hass: HomeAssistant, domain: str
47 """Get an integration with all requirements installed, including the dependencies.
49 This can raise IntegrationNotFound if manifest or integration
50 is invalid, RequirementNotFound if there was some type of
51 failure to install requirements.
54 return await manager.async_get_integration_with_requirements(domain)
58 hass: HomeAssistant, name: str, requirements: list[str]
60 """Install the requirements for a component or platform.
62 This method is a coroutine. It will raise RequirementsNotFound
63 if an requirement can't be satisfied.
69 hass: HomeAssistant, requirements: set[str]
71 """Load the installed version of requirements."""
76 @singleton.singleton(DATA_REQUIREMENTS_MANAGER)
78 """Get the requirements manager."""
84 """Forget the install history."""
88 def pip_kwargs(config_dir: str |
None) -> dict[str, Any]:
89 """Return keyword arguments for PIP install."""
90 is_docker = pkg_util.is_docker_env()
92 "constraints": os.path.join(os.path.dirname(__file__), CONSTRAINT_FILE),
93 "timeout": PIP_TIMEOUT,
95 if not (config_dir
is None or pkg_util.is_virtual_env())
and not is_docker:
96 kwargs[
"target"] = os.path.join(config_dir,
"deps")
101 """Try to install a package up to MAX_INSTALL_FAILURES times."""
102 for _
in range(MAX_INSTALL_FAILURES):
103 if pkg_util.install_package(requirement, **kwargs):
109 requirements: list[str], kwargs: dict[str, Any]
110 ) -> tuple[set[str], set[str]]:
111 """Install requirements if missing."""
112 installed: set[str] = set()
113 failures: set[str] = set()
114 for req
in requirements:
119 return installed, failures
123 """Manage requirements."""
126 """Init the requirements manager."""
129 self.integrations_with_reqs: dict[
130 str, Integration | asyncio.Future[Integration]
132 self.install_failure_history: set[str] = set()
133 self.is_installed_cache: set[str] = set()
136 self, domain: str, done: set[str] |
None =
None
138 """Get an integration with all requirements installed, including dependencies.
140 This can raise IntegrationNotFound if manifest or integration
141 is invalid, RequirementNotFound if there was some type of
142 failure to install requirements.
149 cache = self.integrations_with_reqs
150 if int_or_fut := cache.get(domain):
151 if isinstance(int_or_fut, Integration):
153 return await int_or_fut
155 future = cache[domain] = self.
hasshass.loop.create_future()
158 if not self.
hasshass.config.skip_pip:
160 except BaseException
as ex:
165 future.set_exception(ex)
166 with contextlib.suppress(BaseException):
173 cache[domain] = integration
174 future.set_result(integration)
178 self, integration: Integration, done: set[str]
180 """Process an integration and requirements."""
181 if integration.requirements:
183 integration.domain, integration.requirements
186 cache = self.integrations_with_reqs
190 for dep
in integration.dependencies + integration.after_dependencies
196 not (cached_integration := cache.get(dep))
197 or type(cached_integration)
is not Integration
201 for check_domain, to_check
in DISCOVERY_INTEGRATIONS.items():
203 check_domain
not in done
204 and check_domain
not in deps_to_check
209 not (cached_integration := cache.get(check_domain))
210 or type(cached_integration)
is not Integration
212 and any(check
in integration.manifest
for check
in to_check)
214 deps_to_check.add(check_domain)
216 if not deps_to_check:
219 exceptions: list[Exception] = []
223 for dep
in deps_to_check:
230 except IntegrationNotFound
as ex:
232 integration.is_built_in
233 or ex.domain
not in integration.after_dependencies
235 exceptions.append(ex)
236 except Exception
as ex:
237 exceptions.insert(0, ex)
243 self, name: str, requirements: list[str]
245 """Install the requirements for a component or platform.
247 This method is a coroutine. It will raise RequirementsNotFound
248 if an requirement can't be satisfied.
250 if self.
hasshass.config.skip_pip_packages:
251 skipped_requirements = {
253 for req
in requirements
254 if Requirement(req).name
in self.
hasshass.config.skip_pip_packages
257 for req
in skipped_requirements:
258 _LOGGER.warning(
"Skipping requirement %s. This may cause issues", req)
260 requirements = [r
for r
in requirements
if r
not in skipped_requirements]
272 """Find requirements that are missing in the cache."""
273 return [req
for req
in requirements
if req
not in self.is_installed_cache]
276 self, integration: str, missing: list[str]
278 """Raise for failed installing integration requirements.
280 Raise RequirementsNotFound so we do not keep trying requirements
281 that have already failed.
284 if req
in self.install_failure_history:
287 "Multiple attempts to install %s failed, install will be"
288 " retried after next configuration check or restart"
297 requirements: list[str],
299 """Install a requirement and save failures."""
301 installed, failures = await self.
hasshass.async_add_executor_job(
302 _install_requirements_if_missing, requirements, kwargs
304 self.is_installed_cache |= installed
305 self.install_failure_history |= failures
311 requirements: set[str],
313 """Load the installed version of requirements."""
314 if not (requirements_to_check := requirements - self.is_installed_cache):
317 self.is_installed_cache |= await self.
hasshass.async_add_executor_job(
318 pkg_util.get_installed_versions, requirements_to_check
None _raise_for_failed_requirements(self, str integration, list[str] missing)
list[str] _find_missing_requirements(self, list[str] requirements)
None _async_process_integration(self, Integration integration, set[str] done)
Integration async_get_integration_with_requirements(self, str domain, set[str]|None done=None)
None __init__(self, HomeAssistant hass)
None async_load_installed_versions(self, set[str] requirements)
None _async_process_requirements(self, str name, list[str] requirements)
None async_process_requirements(self, str name, list[str] requirements)
None __init__(self, str domain, list[str] requirements)
Integration async_get_integration(HomeAssistant hass, str domain)
None async_clear_install_history(HomeAssistant hass)
None async_load_installed_versions(HomeAssistant hass, set[str] requirements)
RequirementsManager _async_get_manager(HomeAssistant hass)
None async_process_requirements(HomeAssistant hass, str name, list[str] requirements)
tuple[set[str], set[str]] _install_requirements_if_missing(list[str] requirements, dict[str, Any] kwargs)
dict[str, Any] pip_kwargs(str|None config_dir)
Integration async_get_integration_with_requirements(HomeAssistant hass, str domain)
bool _install_with_retry(str requirement, dict[str, Any] kwargs)