Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow only specific packages to be skipped during startup dependency installation #82758

Merged
merged 2 commits into from
Nov 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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