From 8c8994352dd72107ae551bc980dad56921020ca7 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 30 Nov 2022 02:38:52 -0500 Subject: [PATCH] Allow only specific packages to be skipped during startup dependency installation (#82758) --- homeassistant/__main__.py | 13 ++++++++++++- homeassistant/bootstrap.py | 4 +++- homeassistant/core.py | 3 +++ homeassistant/requirements.py | 15 +++++++++++++++ homeassistant/runner.py | 1 + tests/common.py | 1 + tests/test_core.py | 1 + tests/test_main.py | 24 ++++++++++++++++++++++++ tests/test_requirements.py | 18 ++++++++++++++++++ 9 files changed, 78 insertions(+), 2 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index cfe93a8fa1a..9dfe4f4f9ed 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -89,11 +89,21 @@ def get_arguments() -> argparse.Namespace: parser.add_argument( "--open-ui", action="store_true", help="Open the webinterface in a browser" ) - parser.add_argument( + + skip_pip_group = parser.add_mutually_exclusive_group() + skip_pip_group.add_argument( "--skip-pip", action="store_true", help="Skips pip install of required packages on startup", ) + skip_pip_group.add_argument( + "--skip-pip-packages", + metavar="package_names", + type=lambda arg: arg.split(","), + default=[], + help="Skip pip install of specific packages on startup", + ) + parser.add_argument( "-v", "--verbose", action="store_true", help="Enable verbose logging to file." ) @@ -180,6 +190,7 @@ def main() -> int: log_file=args.log_file, log_no_color=args.log_no_color, skip_pip=args.skip_pip, + skip_pip_packages=args.skip_pip_packages, safe_mode=args.safe_mode, debug=args.debug, open_ui=args.open_ui, diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 7d1ea91b9aa..6b91557a476 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -118,7 +118,8 @@ async def async_setup_hass( ) hass.config.skip_pip = runtime_config.skip_pip - if runtime_config.skip_pip: + hass.config.skip_pip_packages = runtime_config.skip_pip_packages + if runtime_config.skip_pip or runtime_config.skip_pip_packages: _LOGGER.warning( "Skipping pip installation of required modules. This may cause issues" ) @@ -176,6 +177,7 @@ async def async_setup_hass( if old_logging: hass.data[DATA_LOGGING] = old_logging hass.config.skip_pip = old_config.skip_pip + hass.config.skip_pip_packages = old_config.skip_pip_packages hass.config.internal_url = old_config.internal_url hass.config.external_url = old_config.external_url hass.config.config_dir = old_config.config_dir diff --git a/homeassistant/core.py b/homeassistant/core.py index c5a7599c369..b5ea7fdf194 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1816,6 +1816,9 @@ class Config: # If True, pip install is skipped for requirements on startup self.skip_pip: bool = False + # List of packages to skip when installing requirements on startup + self.skip_pip_packages: list[str] = [] + # List of loaded components self.components: set[str] = set() diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index bd06cf61e4b..27472a2bbd8 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -7,6 +7,8 @@ import logging import os from typing import Any, cast +import pkg_resources + from .core import HomeAssistant, callback from .exceptions import HomeAssistantError from .helpers.typing import UNDEFINED, UndefinedType @@ -225,6 +227,19 @@ class RequirementsManager: This method is a coroutine. It will raise RequirementsNotFound if an requirement can't be satisfied. """ + if self.hass.config.skip_pip_packages: + skipped_requirements = [ + req + for req in requirements + if pkg_resources.Requirement.parse(req).project_name + in self.hass.config.skip_pip_packages + ] + + for req in skipped_requirements: + _LOGGER.warning("Skipping requirement %s. This may cause issues", req) + + requirements = [r for r in requirements if r not in skipped_requirements] + if not (missing := self._find_missing_requirements(requirements)): return self._raise_for_failed_requirements(name, missing) diff --git a/homeassistant/runner.py b/homeassistant/runner.py index 5788a6b155a..51b47e0fe2d 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -36,6 +36,7 @@ class RuntimeConfig: config_dir: str skip_pip: bool = False + skip_pip_packages: list[str] = dataclasses.field(default_factory=list) safe_mode: bool = False verbose: bool = False diff --git a/tests/common.py b/tests/common.py index f32dbd11081..46022022df6 100644 --- a/tests/common.py +++ b/tests/common.py @@ -289,6 +289,7 @@ async def async_test_home_assistant(event_loop, load_registries=True): hass.config.units = METRIC_SYSTEM hass.config.media_dirs = {"local": get_test_config_dir("media")} hass.config.skip_pip = True + hass.config.skip_pip_packages = [] hass.config_entries = config_entries.ConfigEntries( hass, diff --git a/tests/test_core.py b/tests/test_core.py index c5f153ad3bd..2f8db7fc0d6 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -939,6 +939,7 @@ async def test_config_defaults(): assert config.external_url is None assert config.config_source is ha.ConfigSource.DEFAULT assert config.skip_pip is False + assert config.skip_pip_packages == [] assert config.components == set() assert config.api is None assert config.config_dir is None diff --git a/tests/test_main.py b/tests/test_main.py index 5ec6460301f..522515b0d31 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -61,3 +61,27 @@ def test_validate_python(mock_exit): assert mock_exit.called is False mock_exit.reset_mock() + + +@patch("sys.exit") +def test_skip_pip_mutually_exclusive(mock_exit): + """Test --skip-pip and --skip-pip-package are mutually exclusive.""" + + def parse_args(*args): + with patch("sys.argv", ["python"] + list(args)): + return main.get_arguments() + + args = parse_args("--skip-pip") + assert args.skip_pip is True + + args = parse_args("--skip-pip-packages", "foo") + assert args.skip_pip is False + assert args.skip_pip_packages == ["foo"] + + args = parse_args("--skip-pip-packages", "foo-asd,bar-xyz") + assert args.skip_pip is False + assert args.skip_pip_packages == ["foo-asd", "bar-xyz"] + + assert mock_exit.called is False + args = parse_args("--skip-pip", "--skip-pip-packages", "foo") + assert mock_exit.called is True diff --git a/tests/test_requirements.py b/tests/test_requirements.py index c10b49aa110..6dadeba3797 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -1,4 +1,5 @@ """Test requirements module.""" +import logging import os from unittest.mock import call, patch @@ -93,6 +94,23 @@ async def test_install_missing_package(hass): assert len(mock_inst.mock_calls) == 3 +async def test_install_skipped_package(hass, caplog): + """Test an install attempt on a dependency that should be skipped.""" + with patch( + "homeassistant.util.package.install_package", return_value=True + ) as mock_inst: + hass.config.skip_pip_packages = ["hello"] + with caplog.at_level(logging.WARNING): + await async_process_requirements( + hass, "test_component", ["hello==1.0.0", "not_skipped==1.2.3"] + ) + + assert "Skipping requirement hello==1.0.0" in caplog.text + + assert len(mock_inst.mock_calls) == 1 + assert mock_inst.mock_calls[0].args[0] == "not_skipped==1.2.3" + + async def test_get_integration_with_requirements(hass): """Check getting an integration with loaded requirements.""" hass.config.skip_pip = False