mirror of
https://github.com/home-assistant/core.git
synced 2026-05-19 23:35:20 +02:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d8e425e002 | |||
| 6e03333da6 | |||
| 00b8e67639 | |||
| 03a7590d20 | |||
| 5ffcd04ccb | |||
| 65f073ca15 |
+12
-13
@@ -9,10 +9,21 @@ import threading
|
||||
|
||||
from .backup_restore import restore_backup
|
||||
from .const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__
|
||||
from .util.package import is_docker_env, is_virtual_env
|
||||
|
||||
FAULT_LOG_FILENAME = "home-assistant.log.fault"
|
||||
|
||||
|
||||
def validate_environment() -> None:
|
||||
"""Validate that Home Assistant is started from a container or a venv."""
|
||||
if not is_virtual_env() and not is_docker_env():
|
||||
print(
|
||||
"Home Assistant must be run in a Python virtual environment or a container.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def validate_os() -> None:
|
||||
"""Validate that Home Assistant is running in a supported operating system."""
|
||||
if not sys.platform.startswith(("darwin", "linux")):
|
||||
@@ -38,8 +49,6 @@ def ensure_config_path(config_dir: str) -> None:
|
||||
"""Validate the configuration directory."""
|
||||
from . import config as config_util # noqa: PLC0415
|
||||
|
||||
lib_dir = os.path.join(config_dir, "deps")
|
||||
|
||||
# Test if configuration directory exists
|
||||
if not os.path.isdir(config_dir):
|
||||
if config_dir != config_util.get_default_config_dir():
|
||||
@@ -63,17 +72,6 @@ def ensure_config_path(config_dir: str) -> None:
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# Test if library directory exists
|
||||
if not os.path.isdir(lib_dir):
|
||||
try:
|
||||
os.mkdir(lib_dir)
|
||||
except OSError as ex:
|
||||
print(
|
||||
f"Fatal Error: Unable to create library directory {lib_dir}: {ex}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_arguments() -> argparse.Namespace:
|
||||
"""Get parsed passed in arguments."""
|
||||
@@ -166,6 +164,7 @@ def check_threads() -> None:
|
||||
def main() -> int:
|
||||
"""Start Home Assistant."""
|
||||
validate_python()
|
||||
validate_environment()
|
||||
|
||||
args = get_arguments()
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ from .setup import (
|
||||
from .util.async_ import create_eager_task
|
||||
from .util.hass_dict import HassKey
|
||||
from .util.logging import async_activate_log_queue_handler
|
||||
from .util.package import async_get_user_site, is_docker_env, is_virtual_env
|
||||
from .util.package import is_docker_env
|
||||
from .util.system_info import is_official_image
|
||||
|
||||
with contextlib.suppress(ImportError):
|
||||
@@ -351,9 +351,6 @@ async def async_setup_hass(
|
||||
err,
|
||||
)
|
||||
else:
|
||||
if not is_virtual_env():
|
||||
await async_mount_local_lib_path(runtime_config.config_dir)
|
||||
|
||||
if hass.config.safe_mode:
|
||||
_LOGGER.info("Starting in safe mode")
|
||||
|
||||
@@ -702,17 +699,6 @@ class _RotatingFileHandlerWithoutShouldRollOver(RotatingFileHandler):
|
||||
return False
|
||||
|
||||
|
||||
async def async_mount_local_lib_path(config_dir: str) -> str:
|
||||
"""Add local library to Python Path.
|
||||
|
||||
This function is a coroutine.
|
||||
"""
|
||||
deps_dir = os.path.join(config_dir, "deps")
|
||||
if (lib_dir := await async_get_user_site(deps_dir)) not in sys.path:
|
||||
sys.path.insert(0, lib_dir)
|
||||
return deps_dir
|
||||
|
||||
|
||||
def _get_domains(hass: core.HomeAssistant, config: dict[str, Any]) -> set[str]:
|
||||
"""Get domains of components to set up."""
|
||||
# The common config section [homeassistant] could be filtered here,
|
||||
|
||||
@@ -11,7 +11,6 @@ import logging
|
||||
import operator
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
from types import ModuleType
|
||||
from typing import TYPE_CHECKING, Any, Literal, overload
|
||||
|
||||
@@ -30,7 +29,6 @@ from .helpers.typing import ConfigType
|
||||
from .loader import ComponentProtocol, Integration, IntegrationNotFound
|
||||
from .requirements import RequirementsNotFound, async_get_integration_with_requirements
|
||||
from .util.async_ import create_eager_task
|
||||
from .util.package import is_docker_env
|
||||
from .util.yaml import SECRET_YAML, Secrets, YamlTypeError, load_yaml_dict
|
||||
from .util.yaml.objects import NodeStrClass
|
||||
|
||||
@@ -306,12 +304,6 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None:
|
||||
|
||||
version_obj = AwesomeVersion(conf_version)
|
||||
|
||||
if version_obj < AwesomeVersion("0.50"):
|
||||
# 0.50 introduced persistent deps dir.
|
||||
lib_path = hass.config.path("deps")
|
||||
if os.path.isdir(lib_path):
|
||||
shutil.rmtree(lib_path)
|
||||
|
||||
if version_obj < AwesomeVersion("0.92"):
|
||||
# 0.92 moved google/tts.py to google_translate/tts.py
|
||||
config_path = hass.config.path(YAML_CONFIG_FILE)
|
||||
@@ -328,13 +320,6 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None:
|
||||
except OSError:
|
||||
_LOGGER.exception("Migrating to google_translate tts failed")
|
||||
|
||||
if version_obj < AwesomeVersion("0.94") and is_docker_env():
|
||||
# In 0.94 we no longer install packages inside the deps folder when
|
||||
# running inside a Docker container.
|
||||
lib_path = hass.config.path("deps")
|
||||
if os.path.isdir(lib_path):
|
||||
shutil.rmtree(lib_path)
|
||||
|
||||
with open(version_path, "w", encoding="utf8") as outp:
|
||||
outp.write(__version__)
|
||||
|
||||
|
||||
@@ -92,16 +92,12 @@ def async_clear_install_history(hass: HomeAssistant) -> None:
|
||||
_async_get_manager(hass).install_failure_history.clear()
|
||||
|
||||
|
||||
def pip_kwargs(config_dir: str | None) -> dict[str, Any]:
|
||||
def pip_kwargs() -> dict[str, Any]:
|
||||
"""Return keyword arguments for PIP install."""
|
||||
is_docker = pkg_util.is_docker_env()
|
||||
kwargs = {
|
||||
return {
|
||||
"constraints": os.path.join(os.path.dirname(__file__), CONSTRAINT_FILE),
|
||||
"timeout": PIP_TIMEOUT,
|
||||
}
|
||||
if not (config_dir is None or pkg_util.is_virtual_env()) and not is_docker:
|
||||
kwargs["target"] = os.path.join(config_dir, "deps")
|
||||
return kwargs
|
||||
|
||||
|
||||
def _install_with_retry(requirement: str, kwargs: dict[str, Any]) -> bool:
|
||||
@@ -334,7 +330,7 @@ class RequirementsManager:
|
||||
requirements: list[str],
|
||||
) -> None:
|
||||
"""Install a requirement and save failures."""
|
||||
kwargs = pip_kwargs(self.hass.config.config_dir)
|
||||
kwargs = pip_kwargs()
|
||||
installed, failures = await self.hass.async_add_executor_job(
|
||||
_install_requirements_if_missing, requirements, kwargs
|
||||
)
|
||||
|
||||
@@ -9,10 +9,9 @@ import os
|
||||
import sys
|
||||
|
||||
from homeassistant import runner
|
||||
from homeassistant.bootstrap import async_mount_local_lib_path
|
||||
from homeassistant.config import get_default_config_dir
|
||||
from homeassistant.requirements import pip_kwargs
|
||||
from homeassistant.util.package import install_package, is_installed, is_virtual_env
|
||||
from homeassistant.util.package import install_package, is_installed
|
||||
|
||||
# mypy: allow-untyped-defs, disallow-any-generics, no-warn-return-any
|
||||
|
||||
@@ -42,12 +41,7 @@ def run(args: list[str]) -> int:
|
||||
|
||||
script = importlib.import_module(f"homeassistant.scripts.{args[0]}")
|
||||
|
||||
config_dir = extract_config_dir()
|
||||
|
||||
if not is_virtual_env():
|
||||
asyncio.run(async_mount_local_lib_path(config_dir))
|
||||
|
||||
_pip_kwargs = pip_kwargs(config_dir)
|
||||
_pip_kwargs = pip_kwargs()
|
||||
|
||||
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ def run(script_args: list) -> int:
|
||||
yaml_files = [
|
||||
f
|
||||
for f in glob(os.path.join(config_dir, "**/*.yaml"), recursive=True)
|
||||
if not f.startswith(deps)
|
||||
if not f.startswith(deps) # Avoid scanning legacy deps folder
|
||||
]
|
||||
|
||||
for yfn in sorted(yaml_files):
|
||||
|
||||
@@ -220,8 +220,6 @@ async def test_config_does_not_turn_off_debug(hass: HomeAssistant) -> None:
|
||||
@pytest.mark.usefixtures("mock_hass_config")
|
||||
async def test_asyncio_debug_on_turns_hass_debug_on(
|
||||
mock_enable_logging: AsyncMock,
|
||||
mock_is_virtual_env: Mock,
|
||||
mock_mount_local_lib_path: AsyncMock,
|
||||
mock_ensure_config_exists: AsyncMock,
|
||||
mock_process_ha_config_upgrade: Mock,
|
||||
) -> None:
|
||||
@@ -684,15 +682,6 @@ async def test_setup_after_deps_not_present(hass: HomeAssistant) -> None:
|
||||
assert order == ["root", "second_dep"]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_is_virtual_env() -> Generator[Mock]:
|
||||
"""Mock is_virtual_env."""
|
||||
with patch(
|
||||
"homeassistant.bootstrap.is_virtual_env", return_value=False
|
||||
) as is_virtual_env:
|
||||
yield is_virtual_env
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_enable_logging() -> Generator[AsyncMock]:
|
||||
"""Mock enable logging."""
|
||||
@@ -700,15 +689,6 @@ def mock_enable_logging() -> Generator[AsyncMock]:
|
||||
yield enable_logging
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_mount_local_lib_path() -> Generator[AsyncMock]:
|
||||
"""Mock enable logging."""
|
||||
with patch(
|
||||
"homeassistant.bootstrap.async_mount_local_lib_path"
|
||||
) as mount_local_lib_path:
|
||||
yield mount_local_lib_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_process_ha_config_upgrade() -> Generator[Mock]:
|
||||
"""Mock enable logging."""
|
||||
@@ -731,8 +711,6 @@ def mock_ensure_config_exists() -> Generator[AsyncMock]:
|
||||
@pytest.mark.usefixtures("mock_hass_config")
|
||||
async def test_setup_hass(
|
||||
mock_enable_logging: AsyncMock,
|
||||
mock_is_virtual_env: Mock,
|
||||
mock_mount_local_lib_path: AsyncMock,
|
||||
mock_ensure_config_exists: AsyncMock,
|
||||
mock_process_ha_config_upgrade: Mock,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
@@ -770,7 +748,6 @@ async def test_setup_hass(
|
||||
log_file,
|
||||
log_no_color,
|
||||
)
|
||||
assert len(mock_mount_local_lib_path.mock_calls) == 1
|
||||
assert len(mock_ensure_config_exists.mock_calls) == 1
|
||||
assert len(mock_process_ha_config_upgrade.mock_calls) == 1
|
||||
|
||||
@@ -784,8 +761,6 @@ async def test_setup_hass(
|
||||
@pytest.mark.usefixtures("mock_hass_config")
|
||||
async def test_setup_hass_takes_longer_than_log_slow_startup(
|
||||
mock_enable_logging: AsyncMock,
|
||||
mock_is_virtual_env: Mock,
|
||||
mock_mount_local_lib_path: AsyncMock,
|
||||
mock_ensure_config_exists: AsyncMock,
|
||||
mock_process_ha_config_upgrade: Mock,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
@@ -825,8 +800,6 @@ async def test_setup_hass_takes_longer_than_log_slow_startup(
|
||||
|
||||
async def test_setup_hass_invalid_yaml(
|
||||
mock_enable_logging: AsyncMock,
|
||||
mock_is_virtual_env: Mock,
|
||||
mock_mount_local_lib_path: AsyncMock,
|
||||
mock_ensure_config_exists: AsyncMock,
|
||||
mock_process_ha_config_upgrade: Mock,
|
||||
) -> None:
|
||||
@@ -847,13 +820,10 @@ async def test_setup_hass_invalid_yaml(
|
||||
)
|
||||
|
||||
assert "recovery_mode" in hass.config.components
|
||||
assert len(mock_mount_local_lib_path.mock_calls) == 0
|
||||
|
||||
|
||||
async def test_setup_hass_config_dir_nonexistent(
|
||||
mock_enable_logging: AsyncMock,
|
||||
mock_is_virtual_env: Mock,
|
||||
mock_mount_local_lib_path: AsyncMock,
|
||||
mock_ensure_config_exists: AsyncMock,
|
||||
mock_process_ha_config_upgrade: Mock,
|
||||
) -> None:
|
||||
@@ -878,8 +848,6 @@ async def test_setup_hass_config_dir_nonexistent(
|
||||
|
||||
async def test_setup_hass_recovery_mode(
|
||||
mock_enable_logging: AsyncMock,
|
||||
mock_is_virtual_env: Mock,
|
||||
mock_mount_local_lib_path: AsyncMock,
|
||||
mock_ensure_config_exists: AsyncMock,
|
||||
mock_process_ha_config_upgrade: Mock,
|
||||
) -> None:
|
||||
@@ -909,7 +877,6 @@ async def test_setup_hass_recovery_mode(
|
||||
mock_hass.assert_called_once()
|
||||
|
||||
assert "recovery_mode" in hass.config.components
|
||||
assert len(mock_mount_local_lib_path.mock_calls) == 0
|
||||
|
||||
# Validate we didn't try to set up config entry.
|
||||
assert "browser" not in hass.config.components
|
||||
@@ -919,8 +886,6 @@ async def test_setup_hass_recovery_mode(
|
||||
@pytest.mark.parametrize("domain", ["cloud", "backup"])
|
||||
async def test_setup_hass_recovery_mode_with_failing_integration(
|
||||
mock_enable_logging: AsyncMock,
|
||||
mock_is_virtual_env: Mock,
|
||||
mock_mount_local_lib_path: AsyncMock,
|
||||
mock_ensure_config_exists: AsyncMock,
|
||||
mock_process_ha_config_upgrade: Mock,
|
||||
domain: str,
|
||||
@@ -949,8 +914,6 @@ async def test_setup_hass_recovery_mode_with_failing_integration(
|
||||
@pytest.mark.usefixtures("mock_hass_config")
|
||||
async def test_setup_hass_safe_mode(
|
||||
mock_enable_logging: AsyncMock,
|
||||
mock_is_virtual_env: Mock,
|
||||
mock_mount_local_lib_path: AsyncMock,
|
||||
mock_ensure_config_exists: AsyncMock,
|
||||
mock_process_ha_config_upgrade: Mock,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
@@ -984,8 +947,6 @@ async def test_setup_hass_safe_mode(
|
||||
@pytest.mark.usefixtures("mock_hass_config")
|
||||
async def test_setup_hass_recovery_mode_and_safe_mode(
|
||||
mock_enable_logging: AsyncMock,
|
||||
mock_is_virtual_env: Mock,
|
||||
mock_mount_local_lib_path: AsyncMock,
|
||||
mock_ensure_config_exists: AsyncMock,
|
||||
mock_process_ha_config_upgrade: Mock,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
@@ -1021,8 +982,6 @@ async def test_setup_hass_recovery_mode_and_safe_mode(
|
||||
async def test_storage_version_too_new_triggers_recovery_mode(
|
||||
hass_storage: dict[str, Any],
|
||||
mock_enable_logging: AsyncMock,
|
||||
mock_is_virtual_env: Mock,
|
||||
mock_mount_local_lib_path: AsyncMock,
|
||||
mock_ensure_config_exists: AsyncMock,
|
||||
mock_process_ha_config_upgrade: Mock,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
@@ -1060,8 +1019,6 @@ async def test_storage_version_too_new_triggers_recovery_mode(
|
||||
@pytest.mark.usefixtures("mock_hass_config")
|
||||
async def test_setup_hass_invalid_core_config(
|
||||
mock_enable_logging: AsyncMock,
|
||||
mock_is_virtual_env: Mock,
|
||||
mock_mount_local_lib_path: AsyncMock,
|
||||
mock_ensure_config_exists: AsyncMock,
|
||||
mock_process_ha_config_upgrade: Mock,
|
||||
) -> None:
|
||||
@@ -1099,8 +1056,6 @@ async def test_setup_hass_invalid_core_config(
|
||||
@pytest.mark.usefixtures("mock_hass_config")
|
||||
async def test_setup_recovery_mode_if_no_frontend(
|
||||
mock_enable_logging: AsyncMock,
|
||||
mock_is_virtual_env: Mock,
|
||||
mock_mount_local_lib_path: AsyncMock,
|
||||
mock_ensure_config_exists: AsyncMock,
|
||||
mock_process_ha_config_upgrade: Mock,
|
||||
) -> None:
|
||||
|
||||
@@ -477,52 +477,6 @@ async def test_create_default_config_returns_none_if_write_error(
|
||||
assert mock_print.called
|
||||
|
||||
|
||||
@patch("homeassistant.config.shutil")
|
||||
@patch("homeassistant.config.os")
|
||||
@patch("homeassistant.config.is_docker_env", return_value=False)
|
||||
def test_remove_lib_on_upgrade(
|
||||
mock_docker, mock_os, mock_shutil, hass: HomeAssistant
|
||||
) -> None:
|
||||
"""Test removal of library on upgrade from before 0.50."""
|
||||
ha_version = "0.49.0"
|
||||
mock_os.path.isdir = mock.Mock(return_value=True)
|
||||
mock_open = mock.mock_open()
|
||||
with patch("homeassistant.config.open", mock_open, create=True):
|
||||
opened_file = mock_open.return_value
|
||||
opened_file.readline.return_value = ha_version
|
||||
hass.config.path = mock.Mock()
|
||||
config_util.process_ha_config_upgrade(hass)
|
||||
hass_path = hass.config.path.return_value
|
||||
|
||||
assert mock_os.path.isdir.call_count == 1
|
||||
assert mock_os.path.isdir.call_args == mock.call(hass_path)
|
||||
assert mock_shutil.rmtree.call_count == 1
|
||||
assert mock_shutil.rmtree.call_args == mock.call(hass_path)
|
||||
|
||||
|
||||
@patch("homeassistant.config.shutil")
|
||||
@patch("homeassistant.config.os")
|
||||
@patch("homeassistant.config.is_docker_env", return_value=True)
|
||||
def test_remove_lib_on_upgrade_94(
|
||||
mock_docker, mock_os, mock_shutil, hass: HomeAssistant
|
||||
) -> None:
|
||||
"""Test removal of library on upgrade from before 0.94 and in Docker."""
|
||||
ha_version = "0.93.0.dev0"
|
||||
mock_os.path.isdir = mock.Mock(return_value=True)
|
||||
mock_open = mock.mock_open()
|
||||
with patch("homeassistant.config.open", mock_open, create=True):
|
||||
opened_file = mock_open.return_value
|
||||
opened_file.readline.return_value = ha_version
|
||||
hass.config.path = mock.Mock()
|
||||
config_util.process_ha_config_upgrade(hass)
|
||||
hass_path = hass.config.path.return_value
|
||||
|
||||
assert mock_os.path.isdir.call_count == 1
|
||||
assert mock_os.path.isdir.call_args == mock.call(hass_path)
|
||||
assert mock_shutil.rmtree.call_count == 1
|
||||
assert mock_shutil.rmtree.call_args == mock.call(hass_path)
|
||||
|
||||
|
||||
def test_process_config_upgrade(hass: HomeAssistant) -> None:
|
||||
"""Test update of version on upgrade."""
|
||||
ha_version = "0.92.0"
|
||||
|
||||
+36
-1
@@ -1,11 +1,46 @@
|
||||
"""Test methods in __main__."""
|
||||
|
||||
from unittest.mock import PropertyMock, patch
|
||||
from unittest.mock import Mock, PropertyMock, call, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant import __main__ as main
|
||||
from homeassistant.const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE
|
||||
|
||||
|
||||
@patch("sys.exit")
|
||||
@pytest.mark.parametrize(
|
||||
("is_venv", "is_docker", "expected_exit_calls", "expected_stderr"),
|
||||
[
|
||||
(
|
||||
False,
|
||||
False,
|
||||
[call(1)],
|
||||
"Home Assistant must be run in a Python virtual environment or a container.\n",
|
||||
),
|
||||
(True, False, [], ""),
|
||||
(False, True, [], ""),
|
||||
(True, True, [], ""),
|
||||
],
|
||||
)
|
||||
def test_validate_environment(
|
||||
mock_exit: Mock,
|
||||
is_venv: bool,
|
||||
is_docker: bool,
|
||||
expected_exit_calls: list[call],
|
||||
expected_stderr: str,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
"""Test validate Python environment."""
|
||||
with (
|
||||
patch("homeassistant.__main__.is_virtual_env", return_value=is_venv),
|
||||
patch("homeassistant.__main__.is_docker_env", return_value=is_docker),
|
||||
):
|
||||
main.validate_environment()
|
||||
assert mock_exit.call_args_list == expected_exit_calls
|
||||
assert capsys.readouterr().err == expected_stderr
|
||||
|
||||
|
||||
@patch("sys.exit")
|
||||
def test_validate_python(mock_exit) -> None:
|
||||
"""Test validate Python version method."""
|
||||
|
||||
@@ -52,29 +52,6 @@ async def test_requirement_installed_in_venv(hass: HomeAssistant) -> None:
|
||||
)
|
||||
|
||||
|
||||
async def test_requirement_installed_in_deps(hass: HomeAssistant) -> None:
|
||||
"""Test requirement installed in deps directory."""
|
||||
with (
|
||||
patch("os.path.dirname", return_value="ha_package_path"),
|
||||
patch("homeassistant.util.package.is_virtual_env", return_value=False),
|
||||
patch("homeassistant.util.package.is_docker_env", return_value=False),
|
||||
patch(
|
||||
"homeassistant.util.package.install_package", return_value=True
|
||||
) as mock_install,
|
||||
patch.dict(os.environ, env_without_wheel_links(), clear=True),
|
||||
):
|
||||
hass.config.skip_pip = False
|
||||
mock_integration(hass, MockModule("comp", requirements=["package==0.0.1"]))
|
||||
assert await setup.async_setup_component(hass, "comp", {})
|
||||
assert "comp" in hass.config.components
|
||||
assert mock_install.call_args == call(
|
||||
"package==0.0.1",
|
||||
target=hass.config.path("deps"),
|
||||
constraints=os.path.join("ha_package_path", CONSTRAINT_FILE),
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
|
||||
async def test_install_existing_package(hass: HomeAssistant) -> None:
|
||||
"""Test an install attempt on an existing package."""
|
||||
with patch(
|
||||
|
||||
Reference in New Issue
Block a user