Skip to content

Commit

Permalink
Allow only specific packages to be skipped during startup dependency …
Browse files Browse the repository at this point in the history
…installation (#82758)
  • Loading branch information
puddly authored Nov 30, 2022
1 parent fcf60a3 commit 8c89943
Show file tree
Hide file tree
Showing 9 changed files with 78 additions and 2 deletions.
13 changes: 12 additions & 1 deletion homeassistant/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."
)
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion homeassistant/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions homeassistant/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1816,6 +1816,9 @@ def __init__(self, hass: HomeAssistant) -> None:
# 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()

Expand Down
15 changes: 15 additions & 0 deletions homeassistant/requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
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
Expand Down Expand Up @@ -225,6 +227,19 @@ async def async_process_requirements(
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)
Expand Down
1 change: 1 addition & 0 deletions homeassistant/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ async def _await_count_and_log_pending(
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,
Expand Down
1 change: 1 addition & 0 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
18 changes: 18 additions & 0 deletions tests/test_requirements.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Test requirements module."""
import logging
import os
from unittest.mock import call, patch

Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 8c89943

Please sign in to comment.