From 89c73f56b109612d5766ffabc0bd01ad004413ea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Jan 2025 12:06:28 -1000 Subject: [PATCH] Migrate to using aiohttp-asyncmdnsresolver for aiohttp resolver (#134830) --- homeassistant/components/zeroconf/__init__.py | 22 +++++++++++----- homeassistant/helpers/aiohttp_client.py | 10 ++++++-- homeassistant/package_constraints.txt | 1 + pyproject.toml | 2 ++ requirements.txt | 2 ++ tests/conftest.py | 25 +++++++++++++++++++ tests/helpers/test_aiohttp_client.py | 13 ++++++++++ tests/test_bootstrap.py | 6 ++++- 8 files changed, 72 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 449c2ccef91..69c745c46a3 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -144,17 +144,27 @@ class ZeroconfServiceInfo(BaseServiceInfo): @bind_hass async def async_get_instance(hass: HomeAssistant) -> HaZeroconf: - """Zeroconf instance to be shared with other integrations that use it.""" - return cast(HaZeroconf, (await _async_get_instance(hass)).zeroconf) + """Get or create the shared HaZeroconf instance.""" + return cast(HaZeroconf, (_async_get_instance(hass)).zeroconf) @bind_hass async def async_get_async_instance(hass: HomeAssistant) -> HaAsyncZeroconf: - """Zeroconf instance to be shared with other integrations that use it.""" - return await _async_get_instance(hass) + """Get or create the shared HaAsyncZeroconf instance.""" + return _async_get_instance(hass) -async def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaAsyncZeroconf: +@callback +def async_get_async_zeroconf(hass: HomeAssistant) -> HaAsyncZeroconf: + """Get or create the shared HaAsyncZeroconf instance. + + This method must be run in the event loop, and is an alternative + to the async_get_async_instance method when a coroutine cannot be used. + """ + return _async_get_instance(hass) + + +def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaAsyncZeroconf: if DOMAIN in hass.data: return cast(HaAsyncZeroconf, hass.data[DOMAIN]) @@ -221,7 +231,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) ] - aio_zc = await _async_get_instance(hass, **zc_args) + aio_zc = _async_get_instance(hass, **zc_args) zeroconf = cast(HaZeroconf, aio_zc.zeroconf) zeroconf_types = await async_get_zeroconf(hass) homekit_models = await async_get_homekit(hass) diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 70b5df6b5e2..b5f5ee9a961 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -14,10 +14,11 @@ from typing import TYPE_CHECKING, Any, Self import aiohttp from aiohttp import web from aiohttp.hdrs import CONTENT_TYPE, USER_AGENT -from aiohttp.resolver import AsyncResolver from aiohttp.web_exceptions import HTTPBadGateway, HTTPGatewayTimeout +from aiohttp_asyncmdnsresolver.api import AsyncMDNSResolver from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.const import APPLICATION_NAME, EVENT_HOMEASSISTANT_CLOSE, __version__ from homeassistant.core import Event, HomeAssistant, callback from homeassistant.loader import bind_hass @@ -362,7 +363,7 @@ def _async_get_connector( ssl=ssl_context, limit=MAXIMUM_CONNECTIONS, limit_per_host=MAXIMUM_CONNECTIONS_PER_HOST, - resolver=AsyncResolver(), + resolver=_async_make_resolver(hass), ) connectors[connector_key] = connector @@ -373,3 +374,8 @@ def _async_get_connector( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_close_connector) return connector + + +@callback +def _async_make_resolver(hass: HomeAssistant) -> AsyncMDNSResolver: + return AsyncMDNSResolver(async_zeroconf=zeroconf.async_get_async_zeroconf(hass)) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a320e9d66da..8ed4638c50d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,6 +4,7 @@ aiodhcpwatcher==1.0.2 aiodiscover==2.1.0 aiodns==3.2.0 aiohasupervisor==0.2.2b5 +aiohttp-asyncmdnsresolver==0.0.1 aiohttp-fast-zlib==0.2.0 aiohttp==3.11.11 aiohttp_cors==0.7.0 diff --git a/pyproject.toml b/pyproject.toml index 2570b5163c7..89bd2ab3b50 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ dependencies = [ "aiohttp==3.11.11", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.2.0", + "aiohttp-asyncmdnsresolver==0.0.1", "aiozoneinfo==0.2.1", "astral==2.2", "async-interrupt==1.2.0", @@ -82,6 +83,7 @@ dependencies = [ "voluptuous-openapi==0.0.5", "yarl==1.18.3", "webrtc-models==0.3.0", + "zeroconf==0.136.2" ] [project.urls] diff --git a/requirements.txt b/requirements.txt index addea3da1d3..f31f9e06e81 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,6 +8,7 @@ aiohasupervisor==0.2.2b5 aiohttp==3.11.11 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.2.0 +aiohttp-asyncmdnsresolver==0.0.1 aiozoneinfo==0.2.1 astral==2.2 async-interrupt==1.2.0 @@ -50,3 +51,4 @@ voluptuous-serialize==2.6.0 voluptuous-openapi==0.0.5 yarl==1.18.3 webrtc-models==0.3.0 +zeroconf==0.136.2 diff --git a/tests/conftest.py b/tests/conftest.py index 2cefe72f414..987173a0b5e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,6 +20,7 @@ from typing import TYPE_CHECKING, Any, cast from unittest.mock import AsyncMock, MagicMock, Mock, _patch, patch from aiohttp import client +from aiohttp.resolver import AsyncResolver from aiohttp.test_utils import ( BaseTestServer, TestClient, @@ -1220,6 +1221,30 @@ def disable_translations_once( translations_once.start() +@pytest.fixture(autouse=True, scope="session") +def mock_zeroconf_resolver() -> Generator[_patch]: + """Mock out the zeroconf resolver.""" + patcher = patch( + "homeassistant.helpers.aiohttp_client._async_make_resolver", + return_value=AsyncResolver(), + ) + patcher.start() + try: + yield patcher + finally: + patcher.stop() + + +@pytest.fixture +def disable_mock_zeroconf_resolver( + mock_zeroconf_resolver: _patch, +) -> Generator[None]: + """Disable the zeroconf resolver.""" + mock_zeroconf_resolver.stop() + yield + mock_zeroconf_resolver.start() + + @pytest.fixture def mock_zeroconf() -> Generator[MagicMock]: """Mock zeroconf.""" diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index 1788da74c3b..3fb83ae5781 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -390,3 +390,16 @@ async def test_client_session_immutable_headers(hass: HomeAssistant) -> None: with pytest.raises(AttributeError): session.headers.update({"user-agent": "bla"}) + + +@pytest.mark.usefixtures("disable_mock_zeroconf_resolver") +@pytest.mark.usefixtures("mock_async_zeroconf") +async def test_async_mdnsresolver( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test async_mdnsresolver.""" + resp = aioclient_mock.post("http://localhost/xyz", json={"x": 1}) + session = client.async_create_clientsession(hass) + resp = await session.post("http://localhost/xyz", json={"x": 1}) + assert resp.status == 200 + assert await resp.json() == {"x": 1} diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index a32d7d1e50b..c1c532c94b5 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -1392,9 +1392,13 @@ async def test_bootstrap_does_not_preload_stage_1_integrations() -> None: assert process.returncode == 0 decoded_stdout = stdout.decode() + disallowed_integrations = bootstrap.STAGE_1_INTEGRATIONS.copy() + # zeroconf is a top level dep now + disallowed_integrations.remove("zeroconf") + # Ensure no stage1 integrations have been imported # as a side effect of importing the pre-imports - for integration in bootstrap.STAGE_1_INTEGRATIONS: + for integration in disallowed_integrations: assert f"homeassistant.components.{integration}" not in decoded_stdout