1
0
mirror of https://github.com/home-assistant/core.git synced 2026-02-15 07:36:16 +00:00

Drop hass argument from verify_domain_control (#147946)

This commit is contained in:
epenet
2025-09-22 08:15:41 +02:00
committed by GitHub
parent 181741cab6
commit 8a70a1badb
11 changed files with 216 additions and 31 deletions

View File

@@ -162,12 +162,12 @@ def setup_service_functions(
It appears that all TCC-compatible systems support the same three zones modes.
"""
@verify_domain_control(hass, DOMAIN)
@verify_domain_control(DOMAIN)
async def force_refresh(call: ServiceCall) -> None:
"""Obtain the latest state data via the vendor's RESTful API."""
await coordinator.async_refresh()
@verify_domain_control(hass, DOMAIN)
@verify_domain_control(DOMAIN)
async def set_system_mode(call: ServiceCall) -> None:
"""Set the system mode."""
assert coordinator.tcs is not None # mypy
@@ -179,7 +179,7 @@ def setup_service_functions(
}
async_dispatcher_send(hass, DOMAIN, payload)
@verify_domain_control(hass, DOMAIN)
@verify_domain_control(DOMAIN)
async def set_zone_override(call: ServiceCall) -> None:
"""Set the zone override (setpoint)."""
entity_id = call.data[ATTR_ENTITY_ID]

View File

@@ -124,7 +124,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: GeniusHubConfigEntry) ->
def setup_service_functions(hass: HomeAssistant, broker):
"""Set up the service functions."""
@verify_domain_control(hass, DOMAIN)
@verify_domain_control(DOMAIN)
async def set_zone_mode(call: ServiceCall) -> None:
"""Set the system mode."""
entity_id = call.data[ATTR_ENTITY_ID]

View File

@@ -124,7 +124,7 @@ SCHEMA_SET_HOME_COOLING_MODE = vol.Schema(
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up the HomematicIP Cloud services."""
@verify_domain_control(hass, DOMAIN)
@verify_domain_control(DOMAIN)
async def async_call_hmipc_service(service: ServiceCall) -> None:
"""Call correct HomematicIP Cloud service."""
service_name = service.service

View File

@@ -64,7 +64,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
hass.services.async_register(
DOMAIN,
SERVICE_HUE_ACTIVATE_SCENE,
verify_domain_control(hass, DOMAIN)(hue_activate_scene),
verify_domain_control(DOMAIN)(hue_activate_scene),
schema=vol.Schema(
{
vol.Required(ATTR_GROUP_NAME): cv.string,

View File

@@ -89,7 +89,7 @@ async def async_setup_entry(
elif service_call.service == SERVICE_RESTORE:
entity.restore()
@service.verify_domain_control(hass, DOMAIN)
@service.verify_domain_control(DOMAIN)
async def async_service_handle(service_call: core.ServiceCall) -> None:
"""Handle for services."""
entities = await platform.async_extract_from_service(service_call)

View File

@@ -290,7 +290,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up SimpliSafe as config entry."""
_async_standardize_config_entry(hass, entry)
_verify_domain_control = verify_domain_control(hass, DOMAIN)
_verify_domain_control = verify_domain_control(DOMAIN)
websession = aiohttp_client.async_get_clientsession(hass)
try:

View File

@@ -35,7 +35,7 @@ ATTR_WITH_GROUP = "with_group"
def async_setup_services(hass: HomeAssistant) -> None:
"""Register Sonos services."""
@service.verify_domain_control(hass, DOMAIN)
@service.verify_domain_control(DOMAIN)
async def async_service_handle(service_call: ServiceCall) -> None:
"""Handle dispatched services."""
platform_entities = hass.data.get(DATA_DOMAIN_PLATFORM_ENTITIES, {}).get(

View File

@@ -138,6 +138,41 @@ def deprecated_function[**_P, _R](
return deprecated_decorator
def deprecated_hass_argument[**_P, _T](
breaks_in_ha_version: str | None = None,
) -> Callable[[Callable[_P, _T]], Callable[_P, _T]]:
"""Decorate function to indicate that first argument hass will be ignored."""
def _decorator(func: Callable[_P, _T]) -> Callable[_P, _T]:
@functools.wraps(func)
def _inner(*args: _P.args, **kwargs: _P.kwargs) -> _T:
from homeassistant.core import HomeAssistant # noqa: PLC0415
in_arg = len(args) > 0 and isinstance(args[0], HomeAssistant)
in_kwarg = "hass" in kwargs and isinstance(kwargs["hass"], HomeAssistant)
if in_arg or in_kwarg:
_print_deprecation_warning_internal(
"hass",
func.__module__,
f"{func.__name__} without hass argument",
"argument",
f"passed to {func.__name__}",
breaks_in_ha_version,
log_when_no_integration_is_found=True,
)
if in_arg:
args = args[1:] # type: ignore[assignment]
if in_kwarg:
kwargs.pop("hass")
return func(*args, **kwargs)
return _inner
return _decorator
def _print_deprecation_warning(
obj: Any,
replacement: str,

View File

@@ -60,7 +60,7 @@ from . import (
template,
translation,
)
from .deprecation import deprecated_class, deprecated_function
from .deprecation import deprecated_class, deprecated_function, deprecated_hass_argument
from .selector import TargetSelector
from .typing import ConfigType, TemplateVarsType, VolDictType, VolSchemaType
@@ -995,10 +995,10 @@ def async_register_admin_service(
)
@bind_hass
@deprecated_hass_argument(breaks_in_ha_version="2026.10")
@callback
def verify_domain_control(
hass: HomeAssistant, domain: str
domain: str,
) -> Callable[[Callable[[ServiceCall], Any]], Callable[[ServiceCall], Any]]:
"""Ensure permission to access any entity under domain in service call."""
@@ -1014,6 +1014,7 @@ def verify_domain_control(
if not call.context.user_id:
return await service_handler(call)
hass = call.hass
user = await hass.auth.async_get_user(call.context.user_id)
if user is None:

View File

@@ -17,6 +17,7 @@ from homeassistant.helpers.deprecation import (
check_if_deprecated_constant,
deprecated_class,
deprecated_function,
deprecated_hass_argument,
deprecated_substitute,
dir_with_deprecated_constants,
get_deprecated,
@@ -638,3 +639,77 @@ def test_enum_with_deprecated_members_integration_not_found(
TestEnum.DOGS # noqa: B018
assert len(caplog.record_tuples) == 0
@pytest.mark.parametrize(
("positional_arguments", "keyword_arguments"),
[
# without kwargs
([], {}),
(["first_arg"], {}),
(["first_arg", "second_arg"], {}),
# with single kwargs
([], {"first_kwarg": "first_value"}),
(["first_arg"], {"first_kwarg": "first_value"}),
(["first_arg", "second_arg"], {"first_kwarg": "first_value"}),
# with double kwargs
([], {"first_kwarg": "first_value", "second_kwarg": "second_value"}),
(["first_arg"], {"first_kwarg": "first_value", "second_kwarg": "second_value"}),
(
["first_arg", "second_arg"],
{"first_kwarg": "first_value", "second_kwarg": "second_value"},
),
],
)
@pytest.mark.parametrize(
("breaks_in_ha_version", "extra_msg"),
[
(None, ""),
("2099.1", " It will be removed in HA Core 2099.1."),
],
)
def test_deprecated_hass_argument(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
positional_arguments: list[str],
keyword_arguments: dict[str, str],
breaks_in_ha_version: str | None,
extra_msg: str,
) -> None:
"""Test deprecated_hass_argument decorator."""
calls = []
@deprecated_hass_argument(breaks_in_ha_version=breaks_in_ha_version)
def mock_deprecated_function(*args: str, **kwargs: str) -> None:
calls.append((args, kwargs))
mock_deprecated_function(*positional_arguments, **keyword_arguments)
assert (
"The deprecated argument hass was passed to mock_deprecated_function."
f"{extra_msg}"
" Use mock_deprecated_function without hass argument instead"
) not in caplog.text
assert len(calls) == 1
mock_deprecated_function(hass, *positional_arguments, **keyword_arguments)
assert (
"The deprecated argument hass was passed to mock_deprecated_function."
f"{extra_msg}"
" Use mock_deprecated_function without hass argument instead"
) in caplog.text
assert len(calls) == 2
caplog.clear()
mock_deprecated_function(*positional_arguments, hass=hass, **keyword_arguments)
assert (
"The deprecated argument hass was passed to mock_deprecated_function."
f"{extra_msg}"
" Use mock_deprecated_function without hass argument instead"
) in caplog.text
assert len(calls) == 3
# Ensure that the two calls are the same, as the second call should have been
# modified to remove the hass argument.
assert calls[0] == calls[1]
assert calls[0] == calls[2]

View File

@@ -1,7 +1,7 @@
"""Test service helpers."""
import asyncio
from collections.abc import Iterable
from collections.abc import Callable, Iterable
from copy import deepcopy
import dataclasses
import io
@@ -1785,7 +1785,28 @@ async def test_register_admin_service_return_response(
assert result == {"test-reply": "test-value1"}
async def test_domain_control_not_async(hass: HomeAssistant, mock_entities) -> None:
_DEPRECATED_VERIFY_DOMAIN_CONTROL_MESSAGE = (
"The deprecated argument hass was passed to verify_domain_control. It will be"
" removed in HA Core 2026.10. Use verify_domain_control without hass argument"
" instead"
)
@pytest.mark.parametrize(
# Check that with or without hass behaves the same
("decorator", "in_caplog"),
[
(service.verify_domain_control, True), # old pass-through
(lambda _, domain: service.verify_domain_control(domain), False), # new
],
)
async def test_domain_control_not_async(
hass: HomeAssistant,
mock_entities,
decorator: Callable[[HomeAssistant, str], Any],
in_caplog: bool,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test domain verification in a service call with an unknown user."""
calls = []
@@ -1794,10 +1815,26 @@ async def test_domain_control_not_async(hass: HomeAssistant, mock_entities) -> N
calls.append(call)
with pytest.raises(exceptions.HomeAssistantError):
service.verify_domain_control(hass, "test_domain")(mock_service_log)
decorator(hass, "test_domain")(mock_service_log)
assert (_DEPRECATED_VERIFY_DOMAIN_CONTROL_MESSAGE in caplog.text) == in_caplog
async def test_domain_control_unknown(hass: HomeAssistant, mock_entities) -> None:
@pytest.mark.parametrize(
# Check that with or without hass behaves the same
("decorator", "in_caplog"),
[
(service.verify_domain_control, True), # old pass-through
(lambda _, domain: service.verify_domain_control(domain), False), # new
],
)
async def test_domain_control_unknown(
hass: HomeAssistant,
mock_entities,
decorator: Callable[[HomeAssistant, str], Any],
in_caplog: bool,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test domain verification in a service call with an unknown user."""
calls = []
@@ -1809,9 +1846,7 @@ async def test_domain_control_unknown(hass: HomeAssistant, mock_entities) -> Non
"homeassistant.helpers.entity_registry.async_get",
return_value=Mock(entities=mock_entities),
):
protected_mock_service = service.verify_domain_control(hass, "test_domain")(
mock_service_log
)
protected_mock_service = decorator(hass, "test_domain")(mock_service_log)
hass.services.async_register(
"test_domain", "test_service", protected_mock_service, schema=None
@@ -1827,9 +1862,23 @@ async def test_domain_control_unknown(hass: HomeAssistant, mock_entities) -> Non
)
assert len(calls) == 0
assert (_DEPRECATED_VERIFY_DOMAIN_CONTROL_MESSAGE in caplog.text) == in_caplog
@pytest.mark.parametrize(
# Check that with or without hass behaves the same
("decorator", "in_caplog"),
[
(service.verify_domain_control, True), # old pass-through
(lambda _, domain: service.verify_domain_control(domain), False), # new
],
)
async def test_domain_control_unauthorized(
hass: HomeAssistant, hass_read_only_user: MockUser
hass: HomeAssistant,
hass_read_only_user: MockUser,
decorator: Callable[[HomeAssistant, str], Any],
in_caplog: bool,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test domain verification in a service call with an unauthorized user."""
mock_registry(
@@ -1849,9 +1898,7 @@ async def test_domain_control_unauthorized(
"""Define a protected service."""
calls.append(call)
protected_mock_service = service.verify_domain_control(hass, "test_domain")(
mock_service_log
)
protected_mock_service = decorator(hass, "test_domain")(mock_service_log)
hass.services.async_register(
"test_domain", "test_service", protected_mock_service, schema=None
@@ -1868,9 +1915,23 @@ async def test_domain_control_unauthorized(
assert len(calls) == 0
assert (_DEPRECATED_VERIFY_DOMAIN_CONTROL_MESSAGE in caplog.text) == in_caplog
@pytest.mark.parametrize(
# Check that with or without hass behaves the same
("decorator", "in_caplog"),
[
(service.verify_domain_control, True), # old pass-through
(lambda _, domain: service.verify_domain_control(domain), False), # new
],
)
async def test_domain_control_admin(
hass: HomeAssistant, hass_admin_user: MockUser
hass: HomeAssistant,
hass_admin_user: MockUser,
decorator: Callable[[HomeAssistant, str], Any],
in_caplog: bool,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test domain verification in a service call with an admin user."""
mock_registry(
@@ -1890,9 +1951,7 @@ async def test_domain_control_admin(
"""Define a protected service."""
calls.append(call)
protected_mock_service = service.verify_domain_control(hass, "test_domain")(
mock_service_log
)
protected_mock_service = decorator(hass, "test_domain")(mock_service_log)
hass.services.async_register(
"test_domain", "test_service", protected_mock_service, schema=None
@@ -1908,8 +1967,23 @@ async def test_domain_control_admin(
assert len(calls) == 1
assert (_DEPRECATED_VERIFY_DOMAIN_CONTROL_MESSAGE in caplog.text) == in_caplog
async def test_domain_control_no_user(hass: HomeAssistant) -> None:
@pytest.mark.parametrize(
# Check that with or without hass behaves the same
("decorator", "in_caplog"),
[
(service.verify_domain_control, True), # old pass-through
(lambda _, domain: service.verify_domain_control(domain), False), # new
],
)
async def test_domain_control_no_user(
hass: HomeAssistant,
decorator: Callable[[HomeAssistant, str], Any],
in_caplog: bool,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test domain verification in a service call with no user."""
mock_registry(
hass,
@@ -1928,9 +2002,7 @@ async def test_domain_control_no_user(hass: HomeAssistant) -> None:
"""Define a protected service."""
calls.append(call)
protected_mock_service = service.verify_domain_control(hass, "test_domain")(
mock_service_log
)
protected_mock_service = decorator(hass, "test_domain")(mock_service_log)
hass.services.async_register(
"test_domain", "test_service", protected_mock_service, schema=None
@@ -1946,6 +2018,8 @@ async def test_domain_control_no_user(hass: HomeAssistant) -> None:
assert len(calls) == 1
assert (_DEPRECATED_VERIFY_DOMAIN_CONTROL_MESSAGE in caplog.text) == in_caplog
async def test_extract_from_service_available_device(hass: HomeAssistant) -> None:
"""Test the extraction of entity from service and device is available."""