diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index cc4ce32d808..59321a1032e 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -65,6 +65,7 @@ async def async_get_integration_with_requirements( if isinstance(int_or_evt, asyncio.Event): await int_or_evt.wait() + int_or_evt = cache.get(domain, UNDEFINED) # When we have waited and it's UNDEFINED, it doesn't exist @@ -78,6 +79,22 @@ async def async_get_integration_with_requirements( event = cache[domain] = asyncio.Event() + try: + await _async_process_integration(hass, integration, done) + except Exception: # pylint: disable=broad-except + del cache[domain] + event.set() + raise + + cache[domain] = integration + event.set() + return integration + + +async def _async_process_integration( + hass: HomeAssistant, integration: Integration, done: set[str] +) -> None: + """Process an integration and requirements.""" if integration.requirements: await async_process_requirements( hass, integration.domain, integration.requirements @@ -97,26 +114,24 @@ async def async_get_integration_with_requirements( ): deps_to_check.append(check_domain) - if deps_to_check: - results = await asyncio.gather( - *[ - async_get_integration_with_requirements(hass, dep, done) - for dep in deps_to_check - ], - return_exceptions=True, - ) - for result in results: - if not isinstance(result, BaseException): - continue - if not isinstance(result, IntegrationNotFound) or not ( - not integration.is_built_in - and result.domain in integration.after_dependencies - ): - raise result + if not deps_to_check: + return - cache[domain] = integration - event.set() - return integration + results = await asyncio.gather( + *[ + async_get_integration_with_requirements(hass, dep, done) + for dep in deps_to_check + ], + return_exceptions=True, + ) + for result in results: + if not isinstance(result, BaseException): + continue + if not isinstance(result, IntegrationNotFound) or not ( + not integration.is_built_in + and result.domain in integration.after_dependencies + ): + raise result async def async_process_requirements( diff --git a/tests/test_requirements.py b/tests/test_requirements.py index acc83afeec2..ff3f5bcab87 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -139,6 +139,88 @@ async def test_get_integration_with_requirements(hass): ] +async def test_get_integration_with_requirements_pip_install_fails_two_passes(hass): + """Check getting an integration with loaded requirements and the pip install fails two passes.""" + hass.config.skip_pip = False + mock_integration( + hass, MockModule("test_component_dep", requirements=["test-comp-dep==1.0.0"]) + ) + mock_integration( + hass, + MockModule( + "test_component_after_dep", requirements=["test-comp-after-dep==1.0.0"] + ), + ) + mock_integration( + hass, + MockModule( + "test_component", + requirements=["test-comp==1.0.0"], + dependencies=["test_component_dep"], + partial_manifest={"after_dependencies": ["test_component_after_dep"]}, + ), + ) + + def _mock_install_package(package, **kwargs): + if package == "test-comp==1.0.0": + return True + return False + + # 1st pass + with pytest.raises(RequirementsNotFound), patch( + "homeassistant.util.package.is_installed", return_value=False + ) as mock_is_installed, patch( + "homeassistant.util.package.install_package", side_effect=_mock_install_package + ) as mock_inst: + + integration = await async_get_integration_with_requirements( + hass, "test_component" + ) + assert integration + assert integration.domain == "test_component" + + assert len(mock_is_installed.mock_calls) == 3 + assert sorted(mock_call[1][0] for mock_call in mock_is_installed.mock_calls) == [ + "test-comp-after-dep==1.0.0", + "test-comp-dep==1.0.0", + "test-comp==1.0.0", + ] + + assert len(mock_inst.mock_calls) == 3 + assert sorted(mock_call[1][0] for mock_call in mock_inst.mock_calls) == [ + "test-comp-after-dep==1.0.0", + "test-comp-dep==1.0.0", + "test-comp==1.0.0", + ] + + # 2nd pass + with pytest.raises(RequirementsNotFound), patch( + "homeassistant.util.package.is_installed", return_value=False + ) as mock_is_installed, patch( + "homeassistant.util.package.install_package", side_effect=_mock_install_package + ) as mock_inst: + + integration = await async_get_integration_with_requirements( + hass, "test_component" + ) + assert integration + assert integration.domain == "test_component" + + assert len(mock_is_installed.mock_calls) == 3 + assert sorted(mock_call[1][0] for mock_call in mock_is_installed.mock_calls) == [ + "test-comp-after-dep==1.0.0", + "test-comp-dep==1.0.0", + "test-comp==1.0.0", + ] + + assert len(mock_inst.mock_calls) == 3 + assert sorted(mock_call[1][0] for mock_call in mock_inst.mock_calls) == [ + "test-comp-after-dep==1.0.0", + "test-comp-dep==1.0.0", + "test-comp==1.0.0", + ] + + async def test_get_integration_with_missing_dependencies(hass): """Check getting an integration with missing dependencies.""" hass.config.skip_pip = False