mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +00:00
Allow only specific packages to be skipped during startup dependency installation (#82758)
This commit is contained in:
parent
fcf60a3b53
commit
8c8994352d
@ -89,11 +89,21 @@ def get_arguments() -> argparse.Namespace:
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--open-ui", action="store_true", help="Open the webinterface in a browser"
|
"--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",
|
"--skip-pip",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Skips pip install of required packages on startup",
|
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(
|
parser.add_argument(
|
||||||
"-v", "--verbose", action="store_true", help="Enable verbose logging to file."
|
"-v", "--verbose", action="store_true", help="Enable verbose logging to file."
|
||||||
)
|
)
|
||||||
@ -180,6 +190,7 @@ def main() -> int:
|
|||||||
log_file=args.log_file,
|
log_file=args.log_file,
|
||||||
log_no_color=args.log_no_color,
|
log_no_color=args.log_no_color,
|
||||||
skip_pip=args.skip_pip,
|
skip_pip=args.skip_pip,
|
||||||
|
skip_pip_packages=args.skip_pip_packages,
|
||||||
safe_mode=args.safe_mode,
|
safe_mode=args.safe_mode,
|
||||||
debug=args.debug,
|
debug=args.debug,
|
||||||
open_ui=args.open_ui,
|
open_ui=args.open_ui,
|
||||||
|
@ -118,7 +118,8 @@ async def async_setup_hass(
|
|||||||
)
|
)
|
||||||
|
|
||||||
hass.config.skip_pip = runtime_config.skip_pip
|
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(
|
_LOGGER.warning(
|
||||||
"Skipping pip installation of required modules. This may cause issues"
|
"Skipping pip installation of required modules. This may cause issues"
|
||||||
)
|
)
|
||||||
@ -176,6 +177,7 @@ async def async_setup_hass(
|
|||||||
if old_logging:
|
if old_logging:
|
||||||
hass.data[DATA_LOGGING] = old_logging
|
hass.data[DATA_LOGGING] = old_logging
|
||||||
hass.config.skip_pip = old_config.skip_pip
|
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.internal_url = old_config.internal_url
|
||||||
hass.config.external_url = old_config.external_url
|
hass.config.external_url = old_config.external_url
|
||||||
hass.config.config_dir = old_config.config_dir
|
hass.config.config_dir = old_config.config_dir
|
||||||
|
@ -1816,6 +1816,9 @@ class Config:
|
|||||||
# If True, pip install is skipped for requirements on startup
|
# If True, pip install is skipped for requirements on startup
|
||||||
self.skip_pip: bool = False
|
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
|
# List of loaded components
|
||||||
self.components: set[str] = set()
|
self.components: set[str] = set()
|
||||||
|
|
||||||
|
@ -7,6 +7,8 @@ import logging
|
|||||||
import os
|
import os
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
|
import pkg_resources
|
||||||
|
|
||||||
from .core import HomeAssistant, callback
|
from .core import HomeAssistant, callback
|
||||||
from .exceptions import HomeAssistantError
|
from .exceptions import HomeAssistantError
|
||||||
from .helpers.typing import UNDEFINED, UndefinedType
|
from .helpers.typing import UNDEFINED, UndefinedType
|
||||||
@ -225,6 +227,19 @@ class RequirementsManager:
|
|||||||
This method is a coroutine. It will raise RequirementsNotFound
|
This method is a coroutine. It will raise RequirementsNotFound
|
||||||
if an requirement can't be satisfied.
|
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)):
|
if not (missing := self._find_missing_requirements(requirements)):
|
||||||
return
|
return
|
||||||
self._raise_for_failed_requirements(name, missing)
|
self._raise_for_failed_requirements(name, missing)
|
||||||
|
@ -36,6 +36,7 @@ class RuntimeConfig:
|
|||||||
|
|
||||||
config_dir: str
|
config_dir: str
|
||||||
skip_pip: bool = False
|
skip_pip: bool = False
|
||||||
|
skip_pip_packages: list[str] = dataclasses.field(default_factory=list)
|
||||||
safe_mode: bool = False
|
safe_mode: bool = False
|
||||||
|
|
||||||
verbose: bool = False
|
verbose: bool = False
|
||||||
|
@ -289,6 +289,7 @@ async def async_test_home_assistant(event_loop, load_registries=True):
|
|||||||
hass.config.units = METRIC_SYSTEM
|
hass.config.units = METRIC_SYSTEM
|
||||||
hass.config.media_dirs = {"local": get_test_config_dir("media")}
|
hass.config.media_dirs = {"local": get_test_config_dir("media")}
|
||||||
hass.config.skip_pip = True
|
hass.config.skip_pip = True
|
||||||
|
hass.config.skip_pip_packages = []
|
||||||
|
|
||||||
hass.config_entries = config_entries.ConfigEntries(
|
hass.config_entries = config_entries.ConfigEntries(
|
||||||
hass,
|
hass,
|
||||||
|
@ -939,6 +939,7 @@ async def test_config_defaults():
|
|||||||
assert config.external_url is None
|
assert config.external_url is None
|
||||||
assert config.config_source is ha.ConfigSource.DEFAULT
|
assert config.config_source is ha.ConfigSource.DEFAULT
|
||||||
assert config.skip_pip is False
|
assert config.skip_pip is False
|
||||||
|
assert config.skip_pip_packages == []
|
||||||
assert config.components == set()
|
assert config.components == set()
|
||||||
assert config.api is None
|
assert config.api is None
|
||||||
assert config.config_dir is None
|
assert config.config_dir is None
|
||||||
|
@ -61,3 +61,27 @@ def test_validate_python(mock_exit):
|
|||||||
assert mock_exit.called is False
|
assert mock_exit.called is False
|
||||||
|
|
||||||
mock_exit.reset_mock()
|
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
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
"""Test requirements module."""
|
"""Test requirements module."""
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
from unittest.mock import call, patch
|
from unittest.mock import call, patch
|
||||||
|
|
||||||
@ -93,6 +94,23 @@ async def test_install_missing_package(hass):
|
|||||||
assert len(mock_inst.mock_calls) == 3
|
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):
|
async def test_get_integration_with_requirements(hass):
|
||||||
"""Check getting an integration with loaded requirements."""
|
"""Check getting an integration with loaded requirements."""
|
||||||
hass.config.skip_pip = False
|
hass.config.skip_pip = False
|
||||||
|
Loading…
x
Reference in New Issue
Block a user