From b7b82b1e3f45f180b33c1cf5493521893e679cfa Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 11 Feb 2023 13:48:53 +0100 Subject: [PATCH] Add more type hints to conftest.py (#87842) * Add more type hints in conftest.py * Adjust stop_hass * Adjust mock_integration_frame * Adjust pylint plugin --- pylint/plugins/hass_enforce_type_hints.py | 21 +++++++++ tests/conftest.py | 56 +++++++++++++---------- tests/helpers/test_frame.py | 4 +- tests/helpers/test_state.py | 10 ++-- tests/scripts/test_check_config.py | 2 +- tests/test_bootstrap.py | 2 +- 6 files changed, 64 insertions(+), 31 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index e794418e17c..e1637d51cfe 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -96,16 +96,36 @@ _TEST_FIXTURES: dict[str, list[str] | str] = { "area_registry": "AreaRegistry", "async_setup_recorder_instance": "RecorderInstanceGenerator", "caplog": "pytest.LogCaptureFixture", + "current_request_with_host": "None", "device_registry": "DeviceRegistry", + "enable_bluetooth": "None", + "enable_custom_integrations": "None", "enable_nightly_purge": "bool", "enable_statistics": "bool", "enable_statistics_table_validation": "bool", "entity_registry": "EntityRegistry", + "hass_access_token": "str", + "hass_admin_credential": "Credentials", + "hass_admin_user": "MockUser", "hass_client": "ClientSessionGenerator", "hass_client_no_auth": "ClientSessionGenerator", + "hass_owner_user": "MockUser", + "hass_read_only_access_token": "str", + "hass_read_only_user": "MockUser", "hass_recorder": "Callable[..., HomeAssistant]", + "hass_supervisor_access_token": "str", + "hass_supervisor_user": "MockUser", "hass_ws_client": "WebSocketGenerator", "issue_registry": "IssueRegistry", + "legacy_auth": "LegacyApiPasswordAuthProvider", + "local_auth": "HassAuthProvider", + "mock_async_zeroconf": "None", + "mock_bleak_scanner_start": "MagicMock", + "mock_bluetooth": "None", + "mock_bluetooth_adapters": "None", + "mock_device_tracker_conf": "list[Device]", + "mock_get_source_ip": "None", + "mock_zeroconf": "None", "mqtt_client_mock": "MqttMockPahoClient", "mqtt_mock": "MqttMockHAClient", "mqtt_mock_entry_no_yaml_config": "MqttMockHAClientGenerator", @@ -113,6 +133,7 @@ _TEST_FIXTURES: dict[str, list[str] | str] = { "recorder_db_url": "str", "recorder_mock": "Recorder", "requests_mock": "requests_mock.Mocker", + "tmp_path": "Path", } _TEST_FUNCTION_MATCH = TypeHintMatch( function_name="test_*", diff --git a/tests/conftest.py b/tests/conftest.py index 7628d50a412..510be346540 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,12 +8,11 @@ import datetime import functools import gc import itertools -from json import JSONDecoder import logging import sqlite3 import ssl import threading -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar, cast from unittest.mock import AsyncMock, MagicMock, Mock, patch from aiohttp import client @@ -23,6 +22,7 @@ from aiohttp.test_utils import ( TestServer, make_mocked_request, ) +from aiohttp.typedefs import JSONDecoder from aiohttp.web import Application import freezegun import multidict @@ -101,13 +101,13 @@ asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(False)) asyncio.set_event_loop_policy = lambda policy: None -def _utcnow(): +def _utcnow() -> datetime.datetime: """Make utcnow patchable by freezegun.""" return datetime.datetime.now(datetime.timezone.utc) -dt_util.utcnow = _utcnow -event.time_tracker_utcnow = _utcnow +dt_util.utcnow = _utcnow # type: ignore[assignment] +event.time_tracker_utcnow = _utcnow # type: ignore[assignment] def pytest_addoption(parser: pytest.Parser) -> None: @@ -143,8 +143,8 @@ def pytest_runtest_setup() -> None: pytest_socket.socket_allow_hosts(["127.0.0.1"]) pytest_socket.disable_socket(allow_unix_socket=True) - freezegun.api.datetime_to_fakedatetime = ha_datetime_to_fakedatetime - freezegun.api.FakeDatetime = HAFakeDatetime + freezegun.api.datetime_to_fakedatetime = ha_datetime_to_fakedatetime # type: ignore[attr-defined] + freezegun.api.FakeDatetime = HAFakeDatetime # type: ignore[attr-defined] def adapt_datetime(val): return val.isoformat(" ") @@ -154,6 +154,7 @@ def pytest_runtest_setup() -> None: # Setup HAFakeDatetime converter for pymysql try: + # pylint: disable-next=import-outside-toplevel import MySQLdb.converters as MySQLdb_converters except ImportError: pass @@ -163,12 +164,12 @@ def pytest_runtest_setup() -> None: ] = MySQLdb_converters.DateTime2literal -def ha_datetime_to_fakedatetime(datetime): +def ha_datetime_to_fakedatetime(datetime) -> freezegun.api.FakeDatetime: # type: ignore[name-defined] """Convert datetime to FakeDatetime. Modified to include https://github.com/spulec/freezegun/pull/424. """ - return freezegun.api.FakeDatetime( + return freezegun.api.FakeDatetime( # type: ignore[attr-defined] datetime.year, datetime.month, datetime.day, @@ -181,7 +182,7 @@ def ha_datetime_to_fakedatetime(datetime): ) -class HAFakeDatetime(freezegun.api.FakeDatetime): +class HAFakeDatetime(freezegun.api.FakeDatetime): # type: ignore[name-defined] """Modified to include https://github.com/spulec/freezegun/pull/424.""" @classmethod @@ -200,16 +201,20 @@ class HAFakeDatetime(freezegun.api.FakeDatetime): return ha_datetime_to_fakedatetime(result) -def check_real(func): +_R = TypeVar("_R") +_P = ParamSpec("_P") + + +def check_real(func: Callable[_P, Coroutine[Any, Any, _R]]): """Force a function to require a keyword _test_real to be passed in.""" @functools.wraps(func) - async def guard_func(*args, **kwargs): + async def guard_func(*args: _P.args, **kwargs: _P.kwargs) -> _R: real = kwargs.pop("_test_real", None) if not real: - raise Exception( - 'Forgot to mock or pass "_test_real=True" to %s', func.__name__ + raise RuntimeError( + f'Forgot to mock or pass "_test_real=True" to {func.__name__}' ) return await func(*args, **kwargs) @@ -268,7 +273,7 @@ def verify_cleanup( if tasks: event_loop.run_until_complete(asyncio.wait(tasks)) - for handle in event_loop._scheduled: + for handle in event_loop._scheduled: # type: ignore[attr-defined] if not handle.cancelled(): _LOGGER.warning("Lingering timer after test %r", handle) handle.cancel() @@ -382,6 +387,7 @@ def aiohttp_client( else: assert not args, "args should be empty" + client: TestClient if isinstance(__param, Application): server_kwargs = server_kwargs or {} server = TestServer(__param, loop=loop, **server_kwargs) @@ -441,7 +447,7 @@ def hass( ) orig_exception_handler(loop, context) - exceptions = [] + exceptions: list[Exception] = [] hass = loop.run_until_complete(async_test_home_assistant(loop, load_registries)) ha._cv_hass.set(hass) @@ -692,7 +698,7 @@ def hass_client_no_auth( @pytest.fixture -def current_request(): +def current_request() -> Generator[MagicMock, None, None]: """Mock current request.""" with patch("homeassistant.components.http.current_request") as mock_request_context: mocked_request = make_mocked_request( @@ -706,7 +712,7 @@ def current_request(): @pytest.fixture -def current_request_with_host(current_request): +def current_request_with_host(current_request: MagicMock) -> None: """Mock current request with a host header.""" new_headers = multidict.CIMultiDict(current_request.get.return_value.headers) new_headers[config_entry_oauth2_flow.HEADER_FRONTEND_BASE] = "https://example.com" @@ -954,7 +960,7 @@ async def mqtt_mock_entry_with_yaml_config( @pytest.fixture(autouse=True) -def mock_network(): +def mock_network() -> Generator[None, None, None]: """Mock network.""" mock_adapter = Adapter( name="eth0", @@ -973,7 +979,7 @@ def mock_network(): @pytest.fixture(autouse=True) -def mock_get_source_ip(): +def mock_get_source_ip() -> Generator[None, None, None]: """Mock network util's async_get_source_ip.""" with patch( "homeassistant.components.network.util.async_get_source_ip", @@ -983,7 +989,7 @@ def mock_get_source_ip(): @pytest.fixture -def mock_zeroconf(): +def mock_zeroconf() -> Generator[None, None, None]: """Mock zeroconf.""" with patch("homeassistant.components.zeroconf.HaZeroconf", autospec=True), patch( "homeassistant.components.zeroconf.HaAsyncServiceBrowser", autospec=True @@ -992,7 +998,7 @@ def mock_zeroconf(): @pytest.fixture -def mock_async_zeroconf(mock_zeroconf): +def mock_async_zeroconf(mock_zeroconf: None) -> Generator[None, None, None]: """Mock AsyncZeroconf.""" with patch("homeassistant.components.zeroconf.HaAsyncZeroconf") as mock_aiozc: zc = mock_aiozc.return_value @@ -1007,7 +1013,7 @@ def mock_async_zeroconf(mock_zeroconf): @pytest.fixture -def enable_custom_integrations(hass): +def enable_custom_integrations(hass: HomeAssistant) -> None: """Enable custom integrations defined in the test dir.""" hass.data.pop(loader.DATA_CUSTOM_COMPONENTS) @@ -1334,7 +1340,7 @@ def mock_bleak_scanner_start() -> Generator[MagicMock, None, None]: # We need to drop the stop method from the object since we patched # out start and this fixture will expire before the stop method is called # when EVENT_HOMEASSISTANT_STOP is fired. - bluetooth_scanner.OriginalBleakScanner.stop = AsyncMock() + bluetooth_scanner.OriginalBleakScanner.stop = AsyncMock() # type: ignore[assignment] with patch( "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", ) as mock_bleak_scanner_start: @@ -1343,7 +1349,7 @@ def mock_bleak_scanner_start() -> Generator[MagicMock, None, None]: @pytest.fixture def mock_bluetooth( - mock_bleak_scanner_start: MagicMock, mock_bluetooth_adapters + mock_bleak_scanner_start: MagicMock, mock_bluetooth_adapters: None ) -> None: """Mock out bluetooth from starting.""" diff --git a/tests/helpers/test_frame.py b/tests/helpers/test_frame.py index 78319acb2da..320b2c51c66 100644 --- a/tests/helpers/test_frame.py +++ b/tests/helpers/test_frame.py @@ -7,7 +7,9 @@ import pytest from homeassistant.helpers import frame -async def test_extract_frame_integration(caplog, mock_integration_frame): +async def test_extract_frame_integration( + caplog: pytest.LogCaptureFixture, mock_integration_frame: Mock +) -> None: """Test extracting the current frame from integration context.""" found_frame, integration, path = frame.get_integration_frame() diff --git a/tests/helpers/test_state.py b/tests/helpers/test_state.py index 9744a94265b..995305e48d1 100644 --- a/tests/helpers/test_state.py +++ b/tests/helpers/test_state.py @@ -1,7 +1,7 @@ """Test state helpers.""" import asyncio from datetime import timedelta -from unittest.mock import patch +from unittest.mock import Mock, patch import pytest @@ -25,7 +25,9 @@ from homeassistant.util import dt as dt_util from tests.common import async_mock_service -async def test_async_track_states(hass, mock_integration_frame): +async def test_async_track_states( + hass: HomeAssistant, mock_integration_frame: Mock +) -> None: """Test AsyncTrackStates context manager.""" point1 = dt_util.utcnow() point2 = point1 + timedelta(seconds=5) @@ -82,7 +84,9 @@ async def test_call_to_component(hass: HomeAssistant) -> None: ) -async def test_get_changed_since(hass, mock_integration_frame): +async def test_get_changed_since( + hass: HomeAssistant, mock_integration_frame: Mock +) -> None: """Test get_changed_since.""" point1 = dt_util.utcnow() point2 = point1 + timedelta(seconds=5) diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 1fe9f8a2247..8720ae1a1f1 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -23,7 +23,7 @@ BAD_CORE_CONFIG = "homeassistant:\n unit_system: bad\n\n\n" @pytest.fixture(autouse=True) -async def apply_stop_hass(stop_hass): +async def apply_stop_hass(stop_hass: None) -> None: """Make sure all hass are stopped.""" diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 99304b5b336..cbe33bab6c4 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -32,7 +32,7 @@ def apply_mock_storage(hass_storage): @pytest.fixture(autouse=True) -async def apply_stop_hass(stop_hass): +async def apply_stop_hass(stop_hass: None) -> None: """Make sure all hass are stopped."""