From c5ed721de0596cb992ef0762e0d669c592cb0fa8 Mon Sep 17 00:00:00 2001 From: Matthew Flamm Date: Wed, 3 Apr 2024 14:33:43 +0000 Subject: [PATCH] Bump version --- README.md | 2 +- ha_version | 2 +- requirements_dev.txt | 25 +- requirements_test.txt | 23 +- .../common.py | 213 ++++++++++---- .../components/recorder/common.py | 41 ++- .../const.py | 4 +- .../ignore_uncaught_exceptions.py | 1 + .../patch_time.py | 1 + .../plugins.py | 273 ++++++++++-------- .../syrupy.py | 6 +- .../test_util/__init__.py | 1 + .../test_util/aiohttp.py | 3 +- .../typing.py | 1 + version | 2 +- 15 files changed, 374 insertions(+), 224 deletions(-) diff --git a/README.md b/README.md index c086a4b..e3130ad 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # pytest-homeassistant-custom-component -![HA core version](https://img.shields.io/static/v1?label=HA+core+version&message=2024.3.3&labelColor=blue) +![HA core version](https://img.shields.io/static/v1?label=HA+core+version&message=2024.4.0b8&labelColor=blue) [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/MatthewFlamm/pytest-homeassistant-custom-component) diff --git a/ha_version b/ha_version index 6424ee2..e50e086 100644 --- a/ha_version +++ b/ha_version @@ -1 +1 @@ -2024.3.3 \ No newline at end of file +2024.4.0b8 \ No newline at end of file diff --git a/requirements_dev.txt b/requirements_dev.txt index 1eddb05..6e42f8b 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,22 +1,21 @@ # This file is originally from homeassistant/core and modified by pytest-homeassistant-custom-component. astroid==3.1.0 -mypy==1.8.0 -pre-commit==3.6.2 +pre-commit==3.7.0 pylint==3.1.0 -types-aiofiles==23.2.0.20240106 +types-aiofiles==23.2.0.20240311 types-atomicwrites==1.4.5.1 -types-croniter==1.0.6 -types-beautifulsoup4==4.12.0.20240106 +types-croniter==2.0.0.20240321 +types-beautifulsoup4==4.12.0.20240229 types-caldav==1.3.0.20240106 types-chardet==0.1.5 -types-decorator==5.1.8.20240106 -types-paho-mqtt==1.6.0.20240106 -types-pillow==10.2.0.20240111 +types-decorator==5.1.8.20240310 +types-paho-mqtt==1.6.0.20240321 +types-pillow==10.2.0.20240324 types-protobuf==4.24.0.20240106 -types-psutil==5.9.5.20240106 -types-python-dateutil==2.8.19.20240106 -types-python-slugify==8.0.2.20240127 -types-pytz==2023.3.1.1 -types-PyYAML==6.0.12.12 +types-psutil==5.9.5.20240316 +types-python-dateutil==2.9.0.20240316 +types-python-slugify==8.0.2.20240310 +types-pytz==2024.1.0.20240203 +types-PyYAML==6.0.12.20240311 types-requests==2.31.0.3 types-xmltodict==0.13.0.3 diff --git a/requirements_test.txt b/requirements_test.txt index 21fa73f..db179e6 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,30 +8,33 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt -coverage==7.4.3 +coverage==7.4.4 freezegun==1.4.0 mock-open==1.4.0 +mypy-dev==1.10.0a3 pydantic==1.10.12 pylint-per-file-ignores==1.3.2 -pipdeptree==2.15.1 -pytest-asyncio==0.23.5 +pipdeptree==2.16.1 +pytest-asyncio==0.23.6 pytest-aiohttp==1.0.5 -pytest-cov==4.1.0 +pytest-cov==5.0.0 pytest-freezer==0.4.8 +pytest-github-actions-annotate-failures==0.2.0 pytest-socket==0.7.0 pytest-test-groups==1.0.3 pytest-sugar==1.0.0 -pytest-timeout==2.2.0 -pytest-unordered==0.5.2 +pytest-timeout==2.3.1 +pytest-unordered==0.6.0 pytest-picked==0.5.0 pytest-xdist==3.3.1 -pytest==8.0.2 +pytest==8.1.1 requests-mock==1.11.0 -respx==0.20.2 +respx==0.21.0 syrupy==4.6.1 tqdm==4.66.2 -homeassistant==2024.3.3 -SQLAlchemy==2.0.27 +uv==0.1.24 +homeassistant==2024.4.0b8 +SQLAlchemy==2.0.29 paho-mqtt==1.6.1 diff --git a/src/pytest_homeassistant_custom_component/common.py b/src/pytest_homeassistant_custom_component/common.py index 0c410d1..89d34b1 100644 --- a/src/pytest_homeassistant_custom_component/common.py +++ b/src/pytest_homeassistant_custom_component/common.py @@ -3,6 +3,7 @@ This file is originally from homeassistant/core and modified by pytest-homeassistant-custom-component. """ + from __future__ import annotations import asyncio @@ -21,7 +22,7 @@ import threading import time import traceback -from types import ModuleType +from types import FrameType, ModuleType from typing import Any, NoReturn, TypeVar from unittest.mock import AsyncMock, Mock, patch @@ -42,7 +43,7 @@ _async_get_device_automation_capabilities as async_get_device_automation_capabilities, ) from homeassistant.config import async_process_component_config -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import ( DEVICE_DEFAULT_NAME, EVENT_HOMEASSISTANT_CLOSE, @@ -63,6 +64,7 @@ ) from homeassistant.helpers import ( area_registry as ar, + category_registry as cr, device_registry as dr, entity, entity_platform, @@ -79,11 +81,14 @@ translation, ) from homeassistant.helpers.dispatcher import ( + SignalType, async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.json import JSONEncoder, _orjson_default_encoder, json_dumps -from homeassistant.helpers.typing import ConfigType, StateType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.setup import setup_component from homeassistant.util.async_ import run_callback_threadsafe import homeassistant.util.dt as dt_util @@ -223,12 +228,10 @@ def async_save_delay(self, *args: Any, **kwargs: Any) -> None: async def async_test_home_assistant( event_loop: asyncio.AbstractEventLoop | None = None, load_registries: bool = True, - storage_dir: str | None = None, + config_dir: str | None = None, ) -> AsyncGenerator[HomeAssistant, None]: """Return a Home Assistant object pointing at test config dir.""" - hass = HomeAssistant(get_test_config_dir()) - if storage_dir: - hass.config.config_dir = storage_dir + hass = HomeAssistant(config_dir or get_test_config_dir()) store = auth_store.AuthStore(hass) hass.auth = auth.AuthManager(hass, store, {}, {}) ensure_auth_manager_loaded(hass.auth) @@ -239,7 +242,7 @@ async def async_test_home_assistant( orig_async_create_task = hass.async_create_task orig_tz = dt_util.DEFAULT_TIME_ZONE - def async_add_job(target, *args): + def async_add_job(target, *args, eager_start: bool = False): """Add job.""" check_target = target while isinstance(check_target, ft.partial): @@ -250,7 +253,7 @@ def async_add_job(target, *args): fut.set_result(target(*args)) return fut - return orig_async_add_job(target, *args) + return orig_async_add_job(target, *args, eager_start=eager_start) def async_add_executor_job(target, *args): """Add executor job.""" @@ -300,7 +303,9 @@ def async_create_task(coroutine, name=None, eager_start=False): }, ) hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, hass.config_entries._async_shutdown + EVENT_HOMEASSISTANT_STOP, + hass.config_entries._async_shutdown, + run_immediately=True, ) # Load the registries @@ -312,30 +317,38 @@ def async_create_task(coroutine, name=None, eager_start=False): hass ) if load_registries: - with patch.object( - StoreWithoutWriteLoad, "async_load", return_value=None - ), patch( - "homeassistant.helpers.area_registry.AreaRegistryStore", - StoreWithoutWriteLoad, - ), patch( - "homeassistant.helpers.device_registry.DeviceRegistryStore", - StoreWithoutWriteLoad, - ), patch( - "homeassistant.helpers.entity_registry.EntityRegistryStore", - StoreWithoutWriteLoad, - ), patch( - "homeassistant.helpers.storage.Store", # Floor & label registry are different - StoreWithoutWriteLoad, - ), patch( - "homeassistant.helpers.issue_registry.IssueRegistryStore", - StoreWithoutWriteLoad, - ), patch( - "homeassistant.helpers.restore_state.RestoreStateData.async_setup_dump", - return_value=None, - ), patch( - "homeassistant.helpers.restore_state.start.async_at_start", + with ( + patch.object(StoreWithoutWriteLoad, "async_load", return_value=None), + patch( + "homeassistant.helpers.area_registry.AreaRegistryStore", + StoreWithoutWriteLoad, + ), + patch( + "homeassistant.helpers.device_registry.DeviceRegistryStore", + StoreWithoutWriteLoad, + ), + patch( + "homeassistant.helpers.entity_registry.EntityRegistryStore", + StoreWithoutWriteLoad, + ), + patch( + "homeassistant.helpers.storage.Store", # Floor & label registry are different + StoreWithoutWriteLoad, + ), + patch( + "homeassistant.helpers.issue_registry.IssueRegistryStore", + StoreWithoutWriteLoad, + ), + patch( + "homeassistant.helpers.restore_state.RestoreStateData.async_setup_dump", + return_value=None, + ), + patch( + "homeassistant.helpers.restore_state.start.async_at_start", + ), ): await ar.async_load(hass) + await cr.async_load(hass) await dr.async_load(hass) await er.async_load(hass) await fr.async_load(hass) @@ -515,12 +528,15 @@ def _async_fire_time_changed( future_seconds = task.when() - (hass.loop.time() + _MONOTONIC_RESOLUTION) if fire_all or mock_seconds_into_future >= future_seconds: - with patch( - "homeassistant.helpers.event.time_tracker_utcnow", - return_value=utc_datetime, - ), patch( - "homeassistant.helpers.event.time_tracker_timestamp", - return_value=timestamp, + with ( + patch( + "homeassistant.helpers.event.time_tracker_utcnow", + return_value=utc_datetime, + ), + patch( + "homeassistant.helpers.event.time_tracker_timestamp", + return_value=timestamp, + ), ): task._run() task.cancel() @@ -905,7 +921,9 @@ def __init__( def _async_on_stop(_: Event) -> None: self.async_shutdown() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_on_stop) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _async_on_stop, run_immediately=True + ) class MockToggleEntity(entity.ToggleEntity): @@ -1081,11 +1099,11 @@ def assert_setup_component(count, domain=None): """ config = {} - async def mock_psc(hass, config_input, integration): + async def mock_psc(hass, config_input, integration, component=None): """Mock the prepare_setup_component to capture config.""" domain_input = integration.domain integration_config_info = await async_process_component_config( - hass, config_input, integration + hass, config_input, integration, component ) res = integration_config_info.config config[domain_input] = None if res is None else res.get(domain_input) @@ -1102,9 +1120,9 @@ async def mock_psc(hass, config_input, integration): yield config if domain is None: - assert len(config) == 1, "assert_setup_component requires DOMAIN: {}".format( - list(config.keys()) - ) + assert ( + len(config) == 1 + ), f"assert_setup_component requires DOMAIN: {list(config.keys())}" domain = list(config.keys())[0] res = config.get(domain) @@ -1279,11 +1297,6 @@ def should_poll(self) -> bool: """Return the ste of the polling.""" return self._handle("should_poll") - @property - def state(self) -> StateType: - """Return the state of the entity.""" - return self._handle("state") - @property def supported_features(self) -> int | None: """Info about supported features.""" @@ -1357,6 +1370,10 @@ async def mock_write_data( # To ensure that the data can be serialized _LOGGER.debug("Writing data to %s: %s", store.key, data_to_write) raise_contains_mocks(data_to_write) + + if "data_func" in data_to_write: + data_to_write["data"] = data_to_write.pop("data_func")() + encoder = store._encoder if encoder and encoder is not JSONEncoder: # If they pass a custom encoder that is not the @@ -1364,24 +1381,28 @@ async def mock_write_data( dump = ft.partial(json.dumps, cls=store._encoder) else: dump = _orjson_default_encoder - data[store.key] = json.loads(dump(data_to_write)) + data[store.key] = json_loads(dump(data_to_write)) async def mock_remove(store: storage.Store) -> None: """Remove data.""" data.pop(store.key, None) - with patch( - "homeassistant.helpers.storage.Store._async_load", - side_effect=mock_async_load, - autospec=True, - ), patch( - "homeassistant.helpers.storage.Store._async_write_data", - side_effect=mock_write_data, - autospec=True, - ), patch( - "homeassistant.helpers.storage.Store.async_remove", - side_effect=mock_remove, - autospec=True, + with ( + patch( + "homeassistant.helpers.storage.Store._async_load", + side_effect=mock_async_load, + autospec=True, + ), + patch( + "homeassistant.helpers.storage.Store._async_write_data", + side_effect=mock_write_data, + autospec=True, + ), + patch( + "homeassistant.helpers.storage.Store.async_remove", + side_effect=mock_remove, + autospec=True, + ), ): yield data @@ -1424,6 +1445,7 @@ def mock_integration( else f"{loader.PACKAGE_CUSTOM_COMPONENTS}.{module.DOMAIN}", pathlib.Path(""), module.mock_manifest(), + set(), ) def mock_import_platform(platform_name: str) -> NoReturn: @@ -1451,13 +1473,14 @@ def mock_platform( platform_path is in form hue.config_flow. """ - domain = platform_path.split(".")[0] + domain, _, platform_name = platform_path.partition(".") integration_cache = hass.data[loader.DATA_INTEGRATIONS] module_cache = hass.data[loader.DATA_COMPONENTS] if domain not in integration_cache: mock_integration(hass, MockModule(domain)) + integration_cache[domain]._top_level_files.add(f"{platform_name}.py") _LOGGER.info("Adding mock integration platform: %s", platform_path) module_cache[platform_path] = module or Mock() @@ -1476,7 +1499,9 @@ def capture_events(event: Event) -> None: @callback -def async_mock_signal(hass: HomeAssistant, signal: str) -> list[tuple[Any]]: +def async_mock_signal( + hass: HomeAssistant, signal: SignalType[Any] | str +) -> list[tuple[Any]]: """Catch all dispatches to a signal.""" calls = [] @@ -1622,3 +1647,63 @@ def help_test_all(module: ModuleType) -> None: assert set(module.__all__) == { itm for itm in module.__dir__() if not itm.startswith("_") } + + +def extract_stack_to_frame(extract_stack: list[Mock]) -> FrameType: + """Convert an extract stack to a frame list.""" + stack = list(extract_stack) + for frame in stack: + frame.f_back = None + frame.f_code.co_filename = frame.filename + frame.f_lineno = int(frame.lineno) + + top_frame = stack.pop() + current_frame = top_frame + while stack and (next_frame := stack.pop()): + current_frame.f_back = next_frame + current_frame = next_frame + + return top_frame + + +def setup_test_component_platform( + hass: HomeAssistant, + domain: str, + entities: Sequence[Entity], + from_config_entry: bool = False, +) -> MockPlatform: + """Mock a test component platform for .""" + + async def _async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, + ) -> None: + """Set up a test component platform.""" + async_add_entities(entities) + + platform = MockPlatform( + async_setup_platform=_async_setup_platform, + ) + + # avoid creating config entry setup if not needed + if from_config_entry: + + async def _async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up a test component platform.""" + async_add_entities(entities) + + platform.async_setup_entry = _async_setup_entry + platform.async_setup_platform = None + + mock_platform( + hass, + f"test.{domain}", + platform, + ) + return platform diff --git a/src/pytest_homeassistant_custom_component/components/recorder/common.py b/src/pytest_homeassistant_custom_component/components/recorder/common.py index a6dfc4d..55c7a29 100644 --- a/src/pytest_homeassistant_custom_component/components/recorder/common.py +++ b/src/pytest_homeassistant_custom_component/components/recorder/common.py @@ -3,6 +3,7 @@ This file is originally from homeassistant/core and modified by pytest-homeassistant-custom-component. """ + from __future__ import annotations import asyncio @@ -23,7 +24,13 @@ from homeassistant import core as ha from homeassistant.components import recorder -from homeassistant.components.recorder import Recorder, core, get_instance, statistics +from homeassistant.components.recorder import ( + Recorder, + core, + get_instance, + migration, + statistics, +) from homeassistant.components.recorder.db_schema import ( Events, EventTypes, @@ -190,6 +197,7 @@ def assert_states_equal_without_context(state: State, other: State) -> None: """Assert that two states are equal, ignoring context.""" assert_states_equal_without_context_and_last_changed(state, other) assert state.last_changed == other.last_changed + assert state.last_reported == other.last_reported def assert_states_equal_without_context_and_last_changed( @@ -412,19 +420,24 @@ def old_db_schema(schema_version_postfix: str) -> Iterator[None]: importlib.import_module(schema_module) old_db_schema = sys.modules[schema_module] - with patch.object(recorder, "db_schema", old_db_schema), patch.object( - recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION - ), patch.object(core, "StatesMeta", old_db_schema.StatesMeta), patch.object( - core, "EventTypes", old_db_schema.EventTypes - ), patch.object(core, "EventData", old_db_schema.EventData), patch.object( - core, "States", old_db_schema.States - ), patch.object(core, "Events", old_db_schema.Events), patch.object( - core, "StateAttributes", old_db_schema.StateAttributes - ), patch.object(core, "EntityIDMigrationTask", core.RecorderTask), patch( - CREATE_ENGINE_TARGET, - new=partial( - create_engine_test_for_schema_version_postfix, - schema_version_postfix=schema_version_postfix, + with ( + patch.object(recorder, "db_schema", old_db_schema), + patch.object( + recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION + ), + patch.object(core, "StatesMeta", old_db_schema.StatesMeta), + patch.object(core, "EventTypes", old_db_schema.EventTypes), + patch.object(core, "EventData", old_db_schema.EventData), + patch.object(core, "States", old_db_schema.States), + patch.object(core, "Events", old_db_schema.Events), + patch.object(core, "StateAttributes", old_db_schema.StateAttributes), + patch.object(migration.EntityIDMigration, "task", core.RecorderTask), + patch( + CREATE_ENGINE_TARGET, + new=partial( + create_engine_test_for_schema_version_postfix, + schema_version_postfix=schema_version_postfix, + ), ), ): yield diff --git a/src/pytest_homeassistant_custom_component/const.py b/src/pytest_homeassistant_custom_component/const.py index b1f2743..418fb7b 100644 --- a/src/pytest_homeassistant_custom_component/const.py +++ b/src/pytest_homeassistant_custom_component/const.py @@ -5,7 +5,7 @@ """ from typing import Final MAJOR_VERSION: Final = 2024 -MINOR_VERSION: Final = 3 -PATCH_VERSION: Final = "3" +MINOR_VERSION: Final = 4 +PATCH_VERSION: Final = "0b8" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" diff --git a/src/pytest_homeassistant_custom_component/ignore_uncaught_exceptions.py b/src/pytest_homeassistant_custom_component/ignore_uncaught_exceptions.py index 871c560..cbcb271 100644 --- a/src/pytest_homeassistant_custom_component/ignore_uncaught_exceptions.py +++ b/src/pytest_homeassistant_custom_component/ignore_uncaught_exceptions.py @@ -3,6 +3,7 @@ This file is originally from homeassistant/core and modified by pytest-homeassistant-custom-component. """ + IGNORE_UNCAUGHT_EXCEPTIONS = [ ( # This test explicitly throws an uncaught exception diff --git a/src/pytest_homeassistant_custom_component/patch_time.py b/src/pytest_homeassistant_custom_component/patch_time.py index 0c3ed56..1c277b7 100644 --- a/src/pytest_homeassistant_custom_component/patch_time.py +++ b/src/pytest_homeassistant_custom_component/patch_time.py @@ -3,6 +3,7 @@ This file is originally from homeassistant/core and modified by pytest-homeassistant-custom-component. """ + from __future__ import annotations import datetime diff --git a/src/pytest_homeassistant_custom_component/plugins.py b/src/pytest_homeassistant_custom_component/plugins.py index f0baf49..4d5177c 100644 --- a/src/pytest_homeassistant_custom_component/plugins.py +++ b/src/pytest_homeassistant_custom_component/plugins.py @@ -3,6 +3,7 @@ This file is originally from homeassistant/core and modified by pytest-homeassistant-custom-component. """ + from __future__ import annotations import asyncio @@ -56,6 +57,7 @@ from homeassistant.core import CoreState, HassJob, HomeAssistant from homeassistant.helpers import ( area_registry as ar, + category_registry as cr, config_entry_oauth2_flow, device_registry as dr, entity_registry as er, @@ -100,13 +102,13 @@ init_recorder_component, mock_storage, patch_yaml_files, + extract_stack_to_frame, ) from .test_util.aiohttp import ( # noqa: E402, isort:skip AiohttpClientMocker, mock_aiohttp_client, ) - _LOGGER = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) @@ -163,9 +165,9 @@ def adapt_datetime(val): except ImportError: pass else: - MySQLdb_converters.conversions[ - HAFakeDatetime - ] = MySQLdb_converters.DateTime2literal + MySQLdb_converters.conversions[HAFakeDatetime] = ( + MySQLdb_converters.DateTime2literal + ) def ha_datetime_to_fakedatetime(datetime) -> freezegun.api.FakeDatetime: # type: ignore[name-defined] @@ -624,15 +626,18 @@ def mock_device_tracker_conf() -> Generator[list[Device], None, None]: async def mock_update_config(path: str, dev_id: str, entity: Device) -> None: devices.append(entity) - with patch( - ( - "homeassistant.components.device_tracker.legacy" - ".DeviceTracker.async_update_config" + with ( + patch( + ( + "homeassistant.components.device_tracker.legacy" + ".DeviceTracker.async_update_config" + ), + side_effect=mock_update_config, + ), + patch( + "homeassistant.components.device_tracker.legacy.async_load_config", + side_effect=lambda *args: devices, ), - side_effect=mock_update_config, - ), patch( - "homeassistant.components.device_tracker.legacy.async_load_config", - side_effect=lambda *args: devices, ): yield devices @@ -937,8 +942,7 @@ async def mqtt_mock( mqtt_mock_entry: MqttMockHAClientGenerator, ) -> AsyncGenerator[MqttMockHAClient, None]: """Fixture to mock MQTT component.""" - with patch("homeassistant.components.mqtt.PLATFORMS", []): - return await mqtt_mock_entry() + return await mqtt_mock_entry() @asynccontextmanager @@ -995,7 +999,7 @@ def create_mock_mqtt(*args, **kwargs) -> MqttMockHAClient: nonlocal mock_mqtt_instance nonlocal real_mqtt_instance real_mqtt_instance = real_mqtt(*args, **kwargs) - spec = dir(real_mqtt_instance) + ["_mqttc"] + spec = [*dir(real_mqtt_instance), "_mqttc"] mock_mqtt_instance = MqttMockHAClient( return_value=real_mqtt_instance, spec_set=spec, @@ -1127,10 +1131,9 @@ def mock_zeroconf() -> Generator[None, None, None]: """Mock zeroconf.""" from zeroconf import DNSCache # pylint: disable=import-outside-toplevel - with patch( - "homeassistant.components.zeroconf.HaZeroconf", autospec=True - ) as mock_zc, patch( - "homeassistant.components.zeroconf.AsyncServiceBrowser", autospec=True + with ( + patch("homeassistant.components.zeroconf.HaZeroconf", autospec=True) as mock_zc, + patch("homeassistant.components.zeroconf.AsyncServiceBrowser", autospec=True), ): zc = mock_zc.return_value # DNSCache has strong Cython type checks, and MagicMock does not work @@ -1341,38 +1344,47 @@ def hass_recorder( migrate_entity_ids = ( recorder.Recorder._migrate_entity_ids if enable_migrate_entity_ids else None ) - with patch( - "homeassistant.components.recorder.Recorder.async_nightly_tasks", - side_effect=nightly, - autospec=True, - ), patch( - "homeassistant.components.recorder.Recorder.async_periodic_statistics", - side_effect=stats, - autospec=True, - ), patch( - "homeassistant.components.recorder.migration._find_schema_errors", - side_effect=schema_validate, - autospec=True, - ), patch( - "homeassistant.components.recorder.Recorder._migrate_events_context_ids", - side_effect=migrate_events_context_ids, - autospec=True, - ), patch( - "homeassistant.components.recorder.Recorder._migrate_states_context_ids", - side_effect=migrate_states_context_ids, - autospec=True, - ), patch( - "homeassistant.components.recorder.Recorder._migrate_event_type_ids", - side_effect=migrate_event_type_ids, - autospec=True, - ), patch( - "homeassistant.components.recorder.Recorder._migrate_entity_ids", - side_effect=migrate_entity_ids, - autospec=True, - ), patch( - "homeassistant.components.recorder.Recorder._schedule_compile_missing_statistics", - side_effect=compile_missing, - autospec=True, + with ( + patch( + "homeassistant.components.recorder.Recorder.async_nightly_tasks", + side_effect=nightly, + autospec=True, + ), + patch( + "homeassistant.components.recorder.Recorder.async_periodic_statistics", + side_effect=stats, + autospec=True, + ), + patch( + "homeassistant.components.recorder.migration._find_schema_errors", + side_effect=schema_validate, + autospec=True, + ), + patch( + "homeassistant.components.recorder.Recorder._migrate_events_context_ids", + side_effect=migrate_events_context_ids, + autospec=True, + ), + patch( + "homeassistant.components.recorder.Recorder._migrate_states_context_ids", + side_effect=migrate_states_context_ids, + autospec=True, + ), + patch( + "homeassistant.components.recorder.Recorder._migrate_event_type_ids", + side_effect=migrate_event_type_ids, + autospec=True, + ), + patch( + "homeassistant.components.recorder.Recorder._migrate_entity_ids", + side_effect=migrate_entity_ids, + autospec=True, + ), + patch( + "homeassistant.components.recorder.Recorder._schedule_compile_missing_statistics", + side_effect=compile_missing, + autospec=True, + ), ): def setup_recorder(config: dict[str, Any] | None = None) -> HomeAssistant: @@ -1465,38 +1477,47 @@ async def async_setup_recorder_instance( migrate_entity_ids = ( recorder.Recorder._migrate_entity_ids if enable_migrate_entity_ids else None ) - with patch( - "homeassistant.components.recorder.Recorder.async_nightly_tasks", - side_effect=nightly, - autospec=True, - ), patch( - "homeassistant.components.recorder.Recorder.async_periodic_statistics", - side_effect=stats, - autospec=True, - ), patch( - "homeassistant.components.recorder.migration._find_schema_errors", - side_effect=schema_validate, - autospec=True, - ), patch( - "homeassistant.components.recorder.Recorder._migrate_events_context_ids", - side_effect=migrate_events_context_ids, - autospec=True, - ), patch( - "homeassistant.components.recorder.Recorder._migrate_states_context_ids", - side_effect=migrate_states_context_ids, - autospec=True, - ), patch( - "homeassistant.components.recorder.Recorder._migrate_event_type_ids", - side_effect=migrate_event_type_ids, - autospec=True, - ), patch( - "homeassistant.components.recorder.Recorder._migrate_entity_ids", - side_effect=migrate_entity_ids, - autospec=True, - ), patch( - "homeassistant.components.recorder.Recorder._schedule_compile_missing_statistics", - side_effect=compile_missing, - autospec=True, + with ( + patch( + "homeassistant.components.recorder.Recorder.async_nightly_tasks", + side_effect=nightly, + autospec=True, + ), + patch( + "homeassistant.components.recorder.Recorder.async_periodic_statistics", + side_effect=stats, + autospec=True, + ), + patch( + "homeassistant.components.recorder.migration._find_schema_errors", + side_effect=schema_validate, + autospec=True, + ), + patch( + "homeassistant.components.recorder.Recorder._migrate_events_context_ids", + side_effect=migrate_events_context_ids, + autospec=True, + ), + patch( + "homeassistant.components.recorder.Recorder._migrate_states_context_ids", + side_effect=migrate_states_context_ids, + autospec=True, + ), + patch( + "homeassistant.components.recorder.Recorder._migrate_event_type_ids", + side_effect=migrate_event_type_ids, + autospec=True, + ), + patch( + "homeassistant.components.recorder.Recorder._migrate_entity_ids", + side_effect=migrate_entity_ids, + autospec=True, + ), + patch( + "homeassistant.components.recorder.Recorder._schedule_compile_missing_statistics", + side_effect=compile_missing, + autospec=True, + ), ): async def async_setup_recorder( @@ -1543,22 +1564,25 @@ async def mock_enable_bluetooth( @pytest.fixture(scope="session") def mock_bluetooth_adapters() -> Generator[None, None, None]: """Fixture to mock bluetooth adapters.""" - with patch("bluetooth_auto_recovery.recover_adapter"), patch( - "bluetooth_adapters.systems.platform.system", return_value="Linux" - ), patch("bluetooth_adapters.systems.linux.LinuxAdapters.refresh"), patch( - "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", - { - "hci0": { - "address": "00:00:00:00:00:01", - "hw_version": "usb:v1D6Bp0246d053F", - "passive_scan": False, - "sw_version": "homeassistant", - "manufacturer": "ACME", - "product": "Bluetooth Adapter 5.0", - "product_id": "aa01", - "vendor_id": "cc01", + with ( + patch("bluetooth_auto_recovery.recover_adapter"), + patch("bluetooth_adapters.systems.platform.system", return_value="Linux"), + patch("bluetooth_adapters.systems.linux.LinuxAdapters.refresh"), + patch( + "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", + { + "hci0": { + "address": "00:00:00:00:00:01", + "hw_version": "usb:v1D6Bp0246d053F", + "passive_scan": False, + "sw_version": "homeassistant", + "manufacturer": "ACME", + "product": "Bluetooth Adapter 5.0", + "product_id": "aa01", + "vendor_id": "cc01", + }, }, - }, + ), ): yield @@ -1576,10 +1600,13 @@ def mock_bleak_scanner_start() -> Generator[MagicMock, None, None]: # out start and this fixture will expire before the stop method is called # when EVENT_HOMEASSISTANT_STOP is fired. bluetooth_scanner.OriginalBleakScanner.stop = AsyncMock() # type: ignore[assignment] - with patch.object( - bluetooth_scanner.OriginalBleakScanner, - "start", - ) as mock_bleak_scanner_start, patch.object(bluetooth_scanner, "HaScanner"): + with ( + patch.object( + bluetooth_scanner.OriginalBleakScanner, + "start", + ) as mock_bleak_scanner_start, + patch.object(bluetooth_scanner, "HaScanner"), + ): yield mock_bleak_scanner_start @@ -1591,21 +1618,29 @@ def mock_integration_frame() -> Generator[Mock, None, None]: lineno="23", line="self.light.is_on", ) - with patch( - "homeassistant.helpers.frame.extract_stack", - return_value=[ - Mock( - filename="/home/paulus/homeassistant/core.py", - lineno="23", - line="do_something()", - ), - correct_frame, - Mock( - filename="/home/paulus/aiohue/lights.py", - lineno="2", - line="something()", + with ( + patch( + "homeassistant.helpers.frame.linecache.getline", + return_value=correct_frame.line, + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=extract_stack_to_frame( + [ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="23", + line="do_something()", + ), + correct_frame, + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="2", + line="something()", + ), + ] ), - ], + ), ): yield correct_frame @@ -1617,6 +1652,12 @@ def mock_bluetooth( """Mock out bluetooth from starting.""" +@pytest.fixture +def category_registry(hass: HomeAssistant) -> cr.CategoryRegistry: + """Return the category registry from the current hass instance.""" + return cr.async_get(hass) + + @pytest.fixture def area_registry(hass: HomeAssistant) -> ar.AreaRegistry: """Return the area registry from the current hass instance.""" diff --git a/src/pytest_homeassistant_custom_component/syrupy.py b/src/pytest_homeassistant_custom_component/syrupy.py index 3f3daaf..e9f8cba 100644 --- a/src/pytest_homeassistant_custom_component/syrupy.py +++ b/src/pytest_homeassistant_custom_component/syrupy.py @@ -3,6 +3,7 @@ This file is originally from homeassistant/core and modified by pytest-homeassistant-custom-component. """ + from __future__ import annotations from contextlib import suppress @@ -169,7 +170,7 @@ def _serializable_entity_registry_entry( cls, data: er.RegistryEntry ) -> SerializableData: """Prepare a Home Assistant entity registry entry for serialization.""" - return EntityRegistryEntrySnapshot( + serialized = EntityRegistryEntrySnapshot( attrs.asdict(data) | { "config_entry_id": ANY, @@ -178,6 +179,8 @@ def _serializable_entity_registry_entry( "options": {k: dict(v) for k, v in data.options.items()}, } ) + serialized.pop("categories") + return serialized @classmethod def _serializable_flow_result(cls, data: FlowResult) -> SerializableData: @@ -199,6 +202,7 @@ def _serializable_state(cls, data: State) -> SerializableData: | { "context": ANY, "last_changed": ANY, + "last_reported": ANY, "last_updated": ANY, } ) diff --git a/src/pytest_homeassistant_custom_component/test_util/__init__.py b/src/pytest_homeassistant_custom_component/test_util/__init__.py index 12dab12..da8a1ac 100644 --- a/src/pytest_homeassistant_custom_component/test_util/__init__.py +++ b/src/pytest_homeassistant_custom_component/test_util/__init__.py @@ -3,6 +3,7 @@ This file is originally from homeassistant/core and modified by pytest-homeassistant-custom-component. """ + from collections.abc import Awaitable, Callable from aiohttp.web import Application, Request, StreamResponse, middleware diff --git a/src/pytest_homeassistant_custom_component/test_util/aiohttp.py b/src/pytest_homeassistant_custom_component/test_util/aiohttp.py index 6efac3f..5c661e3 100644 --- a/src/pytest_homeassistant_custom_component/test_util/aiohttp.py +++ b/src/pytest_homeassistant_custom_component/test_util/aiohttp.py @@ -3,6 +3,7 @@ This file is originally from homeassistant/core and modified by pytest-homeassistant-custom-component. """ + import asyncio from contextlib import contextmanager from http import HTTPStatus @@ -338,7 +339,7 @@ def __init__(self): async def __call__(self, method, url, data): """Fetch the next response from the queue or wait until the queue has items.""" if self.stopping: - raise ClientError() + raise ClientError await self.semaphore.acquire() kwargs = self.response_list.pop(0) return AiohttpClientMockResponse(method=method, url=url, **kwargs) diff --git a/src/pytest_homeassistant_custom_component/typing.py b/src/pytest_homeassistant_custom_component/typing.py index bc51130..faea45f 100644 --- a/src/pytest_homeassistant_custom_component/typing.py +++ b/src/pytest_homeassistant_custom_component/typing.py @@ -3,6 +3,7 @@ This file is originally from homeassistant/core and modified by pytest-homeassistant-custom-component. """ + from __future__ import annotations from collections.abc import Callable, Coroutine diff --git a/version b/version index 57f2795..421fd5d 100644 --- a/version +++ b/version @@ -1 +1 @@ -0.13.109 +0.13.110