ha-core/tests/pylint/test_enforce_type_hints.py

1199 lines
32 KiB
Python

"""Tests for pylint hass_enforce_type_hints plugin."""
from __future__ import annotations
import re
from types import ModuleType
from unittest.mock import patch
import astroid
from pylint.checkers import BaseChecker
import pylint.testutils
from pylint.testutils.unittest_linter import UnittestLinter
import pytest
from . import assert_adds_messages, assert_no_messages
@pytest.mark.parametrize(
("module_name", "expected_platform", "in_platforms"),
[
("homeassistant", None, False),
("homeassistant.components", None, False),
("homeassistant.components.pylint_test", "__init__", False),
("homeassistant.components.pylint_test.config_flow", "config_flow", False),
("homeassistant.components.pylint_test.light", "light", True),
("homeassistant.components.pylint_test.light.v1", None, False),
],
)
def test_regex_get_module_platform(
hass_enforce_type_hints: ModuleType,
module_name: str,
expected_platform: str | None,
in_platforms: bool,
) -> None:
"""Test _get_module_platform regex."""
platform = hass_enforce_type_hints._get_module_platform(module_name)
assert platform == expected_platform
assert (platform in hass_enforce_type_hints._PLATFORMS) == in_platforms
@pytest.mark.parametrize(
("string", "expected_count", "expected_items"),
[
("Callable[..., None]", 2, ("Callable", "...", "None")),
("Callable[..., Awaitable[None]]", 2, ("Callable", "...", "Awaitable[None]")),
("tuple[int, int, int, int]", 4, ("tuple", "int", "int", "int", "int")),
(
"tuple[int, int, int, int, int]",
5,
("tuple", "int", "int", "int", "int", "int"),
),
("Awaitable[None]", 1, ("Awaitable", "None")),
("list[dict[str, str]]", 1, ("list", "dict[str, str]")),
("list[dict[str, Any]]", 1, ("list", "dict[str, Any]")),
("tuple[bytes | None, str | None]", 2, ("tuple", "bytes | None", "str | None")),
],
)
def test_regex_x_of_y_i(
hass_enforce_type_hints: ModuleType,
string: str,
expected_count: int,
expected_items: tuple[str, ...],
) -> None:
"""Test x_of_y_i regexes."""
matchers: dict[str, re.Pattern] = hass_enforce_type_hints._TYPE_HINT_MATCHERS
assert (match := matchers[f"x_of_y_{expected_count}"].match(string))
assert match.group(0) == string
for index in range(expected_count):
assert match.group(index + 1) == expected_items[index]
@pytest.mark.parametrize(
("string", "expected_a", "expected_b"),
[
("DiscoveryInfoType | None", "DiscoveryInfoType", "None"),
("dict | list | None", "dict | list", "None"),
("dict[str, Any] | list[Any] | None", "dict[str, Any] | list[Any]", "None"),
("dict[str, Any] | list[Any]", "dict[str, Any]", "list[Any]"),
],
)
def test_regex_a_or_b(
hass_enforce_type_hints: ModuleType, string: str, expected_a: str, expected_b: str
) -> None:
"""Test a_or_b regexes."""
matchers: dict[str, re.Pattern] = hass_enforce_type_hints._TYPE_HINT_MATCHERS
assert (match := matchers["a_or_b"].match(string))
assert match.group(0) == string
assert match.group(1) == expected_a
assert match.group(2) == expected_b
@pytest.mark.parametrize(
"code",
[
"""
async def setup( #@
arg1, arg2
):
pass
"""
],
)
def test_ignore_no_annotations(
hass_enforce_type_hints: ModuleType, type_hint_checker: BaseChecker, code: str
) -> None:
"""Ensure that _is_valid_type is not run if there are no annotations."""
# Set ignore option
type_hint_checker.linter.config.ignore_missing_annotations = True
func_node = astroid.extract_node(
code,
"homeassistant.components.pylint_test",
)
type_hint_checker.visit_module(func_node.parent)
with patch.object(
hass_enforce_type_hints, "_is_valid_type", return_value=True
) as is_valid_type:
type_hint_checker.visit_asyncfunctiondef(func_node)
is_valid_type.assert_not_called()
@pytest.mark.parametrize(
"code",
[
"""
async def setup( #@
arg1, arg2
):
pass
"""
],
)
def test_bypass_ignore_no_annotations(
hass_enforce_type_hints: ModuleType, type_hint_checker: BaseChecker, code: str
) -> None:
"""Test `ignore-missing-annotations` option.
Ensure that `_is_valid_type` is run if there are no annotations
but `ignore-missing-annotations` option is forced to False.
"""
# Set bypass option
type_hint_checker.linter.config.ignore_missing_annotations = False
func_node = astroid.extract_node(
code,
"homeassistant.components.pylint_test",
)
type_hint_checker.visit_module(func_node.parent)
with patch.object(
hass_enforce_type_hints, "_is_valid_type", return_value=True
) as is_valid_type:
type_hint_checker.visit_asyncfunctiondef(func_node)
is_valid_type.assert_called()
@pytest.mark.parametrize(
"code",
[
"""
async def setup( #@
arg1: ArgHint, arg2
):
pass
""",
"""
async def setup( #@
arg1, arg2
) -> ReturnHint:
pass
""",
"""
async def setup( #@
arg1: ArgHint, arg2: ArgHint
) -> ReturnHint:
pass
""",
],
)
def test_dont_ignore_partial_annotations(
hass_enforce_type_hints: ModuleType, type_hint_checker: BaseChecker, code: str
) -> None:
"""Ensure that _is_valid_type is run if there is at least one annotation."""
func_node = astroid.extract_node(
code,
"homeassistant.components.pylint_test",
)
type_hint_checker.visit_module(func_node.parent)
with patch.object(
hass_enforce_type_hints, "_is_valid_type", return_value=True
) as is_valid_type:
type_hint_checker.visit_asyncfunctiondef(func_node)
is_valid_type.assert_called()
def test_invalid_discovery_info(
linter: UnittestLinter, type_hint_checker: BaseChecker
) -> None:
"""Ensure invalid hints are rejected for discovery_info."""
func_node, discovery_info_node = astroid.extract_node(
"""
async def async_setup_scanner( #@
hass: HomeAssistant,
config: ConfigType,
async_see: AsyncSeeCallback,
discovery_info: dict[str, Any] | None = None, #@
) -> bool:
pass
""",
"homeassistant.components.pylint_test.device_tracker",
)
type_hint_checker.visit_module(func_node.parent)
with assert_adds_messages(
linter,
pylint.testutils.MessageTest(
msg_id="hass-argument-type",
node=discovery_info_node,
args=(4, "DiscoveryInfoType | None", "async_setup_scanner"),
line=6,
col_offset=4,
end_line=6,
end_col_offset=41,
),
):
type_hint_checker.visit_asyncfunctiondef(func_node)
def test_valid_discovery_info(
linter: UnittestLinter, type_hint_checker: BaseChecker
) -> None:
"""Ensure valid hints are accepted for discovery_info."""
func_node = astroid.extract_node(
"""
async def async_setup_scanner( #@
hass: HomeAssistant,
config: ConfigType,
async_see: AsyncSeeCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> bool:
pass
""",
"homeassistant.components.pylint_test.device_tracker",
)
type_hint_checker.visit_module(func_node.parent)
with assert_no_messages(linter):
type_hint_checker.visit_asyncfunctiondef(func_node)
def test_invalid_list_dict_str_any(
linter: UnittestLinter, type_hint_checker: BaseChecker
) -> None:
"""Ensure invalid hints are rejected for discovery_info."""
func_node = astroid.extract_node(
"""
async def async_get_triggers( #@
hass: HomeAssistant,
device_id: str
) -> list:
pass
""",
"homeassistant.components.pylint_test.device_trigger",
)
type_hint_checker.visit_module(func_node.parent)
with assert_adds_messages(
linter,
pylint.testutils.MessageTest(
msg_id="hass-return-type",
node=func_node,
args=(
["list[dict[str, str]]", "list[dict[str, Any]]"],
"async_get_triggers",
),
line=2,
col_offset=0,
end_line=2,
end_col_offset=28,
),
):
type_hint_checker.visit_asyncfunctiondef(func_node)
def test_valid_list_dict_str_any(
linter: UnittestLinter, type_hint_checker: BaseChecker
) -> None:
"""Ensure valid hints are accepted for discovery_info."""
func_node = astroid.extract_node(
"""
async def async_get_triggers( #@
hass: HomeAssistant,
device_id: str
) -> list[dict[str, Any]]:
pass
""",
"homeassistant.components.pylint_test.device_trigger",
)
type_hint_checker.visit_module(func_node.parent)
with assert_no_messages(linter):
type_hint_checker.visit_asyncfunctiondef(func_node)
def test_invalid_config_flow_step(
linter: UnittestLinter, type_hint_checker: BaseChecker
) -> None:
"""Ensure invalid hints are rejected for ConfigFlow step."""
class_node, func_node, arg_node = astroid.extract_node(
"""
class FlowHandler():
pass
class ConfigFlow(FlowHandler):
pass
class AxisFlowHandler( #@
ConfigFlow, domain=AXIS_DOMAIN
):
async def async_step_zeroconf( #@
self,
device_config: dict #@
):
pass
""",
"homeassistant.components.pylint_test.config_flow",
)
type_hint_checker.visit_module(class_node.parent)
with assert_adds_messages(
linter,
pylint.testutils.MessageTest(
msg_id="hass-argument-type",
node=arg_node,
args=(2, "ZeroconfServiceInfo", "async_step_zeroconf"),
line=13,
col_offset=8,
end_line=13,
end_col_offset=27,
),
pylint.testutils.MessageTest(
msg_id="hass-return-type",
node=func_node,
args=("ConfigFlowResult", "async_step_zeroconf"),
line=11,
col_offset=4,
end_line=11,
end_col_offset=33,
),
):
type_hint_checker.visit_classdef(class_node)
def test_invalid_custom_config_flow_step(
linter: UnittestLinter, type_hint_checker: BaseChecker
) -> None:
"""Ensure invalid hints are rejected for ConfigFlow step."""
class_node, func_node, arg_node = astroid.extract_node(
"""
class FlowHandler():
pass
class ConfigFlow(FlowHandler):
pass
class AxisFlowHandler( #@
ConfigFlow, domain=AXIS_DOMAIN
):
async def async_step_axis_specific( #@
self,
device_config: dict #@
):
pass
""",
"homeassistant.components.pylint_test.config_flow",
)
type_hint_checker.visit_module(class_node.parent)
with assert_adds_messages(
linter,
pylint.testutils.MessageTest(
msg_id="hass-return-type",
node=func_node,
args=("ConfigFlowResult", "async_step_axis_specific"),
line=11,
col_offset=4,
end_line=11,
end_col_offset=38,
),
):
type_hint_checker.visit_classdef(class_node)
def test_valid_config_flow_step(
linter: UnittestLinter, type_hint_checker: BaseChecker
) -> None:
"""Ensure valid hints are accepted for ConfigFlow step."""
class_node = astroid.extract_node(
"""
class FlowHandler():
pass
class ConfigFlow(FlowHandler):
pass
class AxisFlowHandler( #@
ConfigFlow, domain=AXIS_DOMAIN
):
async def async_step_zeroconf(
self,
device_config: ZeroconfServiceInfo
) -> ConfigFlowResult:
pass
""",
"homeassistant.components.pylint_test.config_flow",
)
type_hint_checker.visit_module(class_node.parent)
with assert_no_messages(linter):
type_hint_checker.visit_classdef(class_node)
def test_invalid_config_flow_async_get_options_flow(
linter: UnittestLinter, type_hint_checker: BaseChecker
) -> None:
"""Ensure invalid hints are rejected for ConfigFlow async_get_options_flow."""
# AxisOptionsFlow doesn't inherit OptionsFlow, and therefore should fail
class_node, func_node, arg_node = astroid.extract_node(
"""
class FlowHandler():
pass
class ConfigFlow(FlowHandler):
pass
class OptionsFlow(FlowHandler):
pass
class AxisOptionsFlow():
pass
class AxisFlowHandler( #@
ConfigFlow, domain=AXIS_DOMAIN
):
def async_get_options_flow( #@
config_entry #@
) -> AxisOptionsFlow:
return AxisOptionsFlow(config_entry)
""",
"homeassistant.components.pylint_test.config_flow",
)
type_hint_checker.visit_module(class_node.parent)
with assert_adds_messages(
linter,
pylint.testutils.MessageTest(
msg_id="hass-argument-type",
node=arg_node,
args=(1, "ConfigEntry", "async_get_options_flow"),
line=18,
col_offset=8,
end_line=18,
end_col_offset=20,
),
pylint.testutils.MessageTest(
msg_id="hass-return-type",
node=func_node,
args=("OptionsFlow", "async_get_options_flow"),
line=17,
col_offset=4,
end_line=17,
end_col_offset=30,
),
):
type_hint_checker.visit_classdef(class_node)
def test_valid_config_flow_async_get_options_flow(
linter: UnittestLinter, type_hint_checker: BaseChecker
) -> None:
"""Ensure valid hints are accepted for ConfigFlow async_get_options_flow."""
class_node = astroid.extract_node(
"""
class FlowHandler():
pass
class ConfigFlow(FlowHandler):
pass
class OptionsFlow(FlowHandler):
pass
class AxisOptionsFlow(OptionsFlow):
pass
class OtherOptionsFlow(OptionsFlow):
pass
class AxisFlowHandler( #@
ConfigFlow, domain=AXIS_DOMAIN
):
def async_get_options_flow(
config_entry: ConfigEntry
) -> AxisOptionsFlow | OtherOptionsFlow | OptionsFlow:
if self.use_other:
return OtherOptionsFlow(config_entry)
return AxisOptionsFlow(config_entry)
""",
"homeassistant.components.pylint_test.config_flow",
)
type_hint_checker.visit_module(class_node.parent)
with assert_no_messages(linter):
type_hint_checker.visit_classdef(class_node)
def test_invalid_entity_properties(
linter: UnittestLinter, type_hint_checker: BaseChecker
) -> None:
"""Check missing entity properties when ignore_missing_annotations is False."""
# Set bypass option
type_hint_checker.linter.config.ignore_missing_annotations = False
class_node, prop_node, func_node = astroid.extract_node(
"""
class Entity():
pass
class LockEntity(Entity):
pass
class DoorLock( #@
LockEntity
):
@property
def changed_by( #@
self
):
pass
async def async_lock( #@
self,
**kwargs
) -> bool:
pass
""",
"homeassistant.components.pylint_test.lock",
)
type_hint_checker.visit_module(class_node.parent)
with assert_adds_messages(
linter,
pylint.testutils.MessageTest(
msg_id="hass-return-type",
node=prop_node,
args=(["str", None], "changed_by"),
line=12,
col_offset=4,
end_line=12,
end_col_offset=18,
),
pylint.testutils.MessageTest(
msg_id="hass-argument-type",
node=func_node,
args=("kwargs", "Any", "async_lock"),
line=17,
col_offset=4,
end_line=17,
end_col_offset=24,
),
pylint.testutils.MessageTest(
msg_id="hass-return-type",
node=func_node,
args=("None", "async_lock"),
line=17,
col_offset=4,
end_line=17,
end_col_offset=24,
),
):
type_hint_checker.visit_classdef(class_node)
def test_ignore_invalid_entity_properties(
linter: UnittestLinter, type_hint_checker: BaseChecker
) -> None:
"""Check invalid entity properties are ignored by default."""
# Set ignore option
type_hint_checker.linter.config.ignore_missing_annotations = True
class_node = astroid.extract_node(
"""
class Entity():
pass
class LockEntity(Entity):
pass
class DoorLock( #@
LockEntity
):
@property
def changed_by(
self
):
pass
async def async_lock(
self,
**kwargs
):
pass
""",
"homeassistant.components.pylint_test.lock",
)
type_hint_checker.visit_module(class_node.parent)
with assert_no_messages(linter):
type_hint_checker.visit_classdef(class_node)
def test_named_arguments(
linter: UnittestLinter, type_hint_checker: BaseChecker
) -> None:
"""Check missing entity properties when ignore_missing_annotations is False."""
# Set bypass option
type_hint_checker.linter.config.ignore_missing_annotations = False
class_node, func_node, percentage_node, preset_mode_node = astroid.extract_node(
"""
class Entity():
pass
class ToggleEntity(Entity):
pass
class FanEntity(ToggleEntity):
pass
class MyFan( #@
FanEntity
):
async def async_turn_on( #@
self,
percentage, #@
*,
preset_mode: str, #@
**kwargs
) -> bool:
pass
""",
"homeassistant.components.pylint_test.fan",
)
type_hint_checker.visit_module(class_node.parent)
with assert_adds_messages(
linter,
pylint.testutils.MessageTest(
msg_id="hass-argument-type",
node=percentage_node,
args=("percentage", "int | None", "async_turn_on"),
line=16,
col_offset=8,
end_line=16,
end_col_offset=18,
),
pylint.testutils.MessageTest(
msg_id="hass-argument-type",
node=preset_mode_node,
args=("preset_mode", "str | None", "async_turn_on"),
line=18,
col_offset=8,
end_line=18,
end_col_offset=24,
),
pylint.testutils.MessageTest(
msg_id="hass-argument-type",
node=func_node,
args=("kwargs", "Any", "async_turn_on"),
line=14,
col_offset=4,
end_line=14,
end_col_offset=27,
),
pylint.testutils.MessageTest(
msg_id="hass-return-type",
node=func_node,
args=("None", "async_turn_on"),
line=14,
col_offset=4,
end_line=14,
end_col_offset=27,
),
):
type_hint_checker.visit_classdef(class_node)
@pytest.mark.parametrize(
"return_hint",
[
"",
"-> Mapping[int, int]",
"-> dict[int, Any]",
],
)
def test_invalid_mapping_return_type(
linter: UnittestLinter,
type_hint_checker: BaseChecker,
return_hint: str,
) -> None:
"""Check that Mapping[xxx, Any] doesn't accept invalid Mapping or dict."""
# Set bypass option
type_hint_checker.linter.config.ignore_missing_annotations = False
class_node, property_node = astroid.extract_node(
f"""
class Entity():
pass
class ToggleEntity(Entity):
pass
class FanEntity(ToggleEntity):
pass
class MyFanA( #@
FanEntity
):
@property
def capability_attributes( #@
self
){return_hint}:
pass
""",
"homeassistant.components.pylint_test.fan",
)
type_hint_checker.visit_module(class_node.parent)
with assert_adds_messages(
linter,
pylint.testutils.MessageTest(
msg_id="hass-return-type",
node=property_node,
args=(["Mapping[str, Any]", None], "capability_attributes"),
line=15,
col_offset=4,
end_line=15,
end_col_offset=29,
),
):
type_hint_checker.visit_classdef(class_node)
@pytest.mark.parametrize(
"return_hint",
[
"-> Mapping[str, Any]",
"-> Mapping[str, bool | int]",
"-> dict[str, Any]",
"-> dict[str, str]",
"-> CustomTypedDict",
],
)
def test_valid_mapping_return_type(
linter: UnittestLinter,
type_hint_checker: BaseChecker,
return_hint: str,
) -> None:
"""Check that Mapping[xxx, Any] accepts both Mapping and dict."""
# Set bypass option
type_hint_checker.linter.config.ignore_missing_annotations = False
class_node = astroid.extract_node(
f"""
from typing import TypedDict
class CustomTypedDict(TypedDict):
pass
class Entity():
pass
class ToggleEntity(Entity):
pass
class FanEntity(ToggleEntity):
pass
class MyFanA( #@
FanEntity
):
@property
def capability_attributes(
self
){return_hint}:
pass
""",
"homeassistant.components.pylint_test.fan",
)
type_hint_checker.visit_module(class_node.parent)
with assert_no_messages(linter):
type_hint_checker.visit_classdef(class_node)
def test_valid_long_tuple(
linter: UnittestLinter, type_hint_checker: BaseChecker
) -> None:
"""Check invalid entity properties are ignored by default."""
# Set ignore option
type_hint_checker.linter.config.ignore_missing_annotations = False
class_node, _, _, _ = astroid.extract_node(
"""
class Entity():
pass
class ToggleEntity(Entity):
pass
class LightEntity(ToggleEntity):
pass
class TestLight( #@
LightEntity
):
@property
def hs_color( #@
self
) -> tuple[int, int]:
pass
@property
def rgbw_color( #@
self
) -> tuple[int, int, int, int]:
pass
@property
def rgbww_color( #@
self
) -> tuple[int, int, int, int, int]:
pass
""",
"homeassistant.components.pylint_test.light",
)
type_hint_checker.visit_module(class_node.parent)
with assert_no_messages(linter):
type_hint_checker.visit_classdef(class_node)
def test_invalid_long_tuple(
linter: UnittestLinter, type_hint_checker: BaseChecker
) -> None:
"""Check invalid entity properties are ignored by default."""
# Set ignore option
type_hint_checker.linter.config.ignore_missing_annotations = False
class_node, rgbw_node, rgbww_node = astroid.extract_node(
"""
class Entity():
pass
class ToggleEntity(Entity):
pass
class LightEntity(ToggleEntity):
pass
class TestLight( #@
LightEntity
):
@property
def rgbw_color( #@
self
) -> tuple[int, int, int, int, int]:
pass
@property
def rgbww_color( #@
self
) -> tuple[int, int, int, int, float]:
pass
""",
"homeassistant.components.pylint_test.light",
)
type_hint_checker.visit_module(class_node.parent)
with assert_adds_messages(
linter,
pylint.testutils.MessageTest(
msg_id="hass-return-type",
node=rgbw_node,
args=(["tuple[int, int, int, int]", None], "rgbw_color"),
line=15,
col_offset=4,
end_line=15,
end_col_offset=18,
),
pylint.testutils.MessageTest(
msg_id="hass-return-type",
node=rgbww_node,
args=(["tuple[int, int, int, int, int]", None], "rgbww_color"),
line=21,
col_offset=4,
end_line=21,
end_col_offset=19,
),
):
type_hint_checker.visit_classdef(class_node)
def test_invalid_device_class(
linter: UnittestLinter, type_hint_checker: BaseChecker
) -> None:
"""Ensure invalid hints are rejected for entity device_class."""
# Set bypass option
type_hint_checker.linter.config.ignore_missing_annotations = False
class_node, prop_node = astroid.extract_node(
"""
class Entity():
pass
class CoverEntity(Entity):
pass
class MyCover( #@
CoverEntity
):
@property
def device_class( #@
self
):
pass
""",
"homeassistant.components.pylint_test.cover",
)
type_hint_checker.visit_module(class_node.parent)
with assert_adds_messages(
linter,
pylint.testutils.MessageTest(
msg_id="hass-return-type",
node=prop_node,
args=(["CoverDeviceClass", None], "device_class"),
line=12,
col_offset=4,
end_line=12,
end_col_offset=20,
),
):
type_hint_checker.visit_classdef(class_node)
def test_media_player_entity(
linter: UnittestLinter, type_hint_checker: BaseChecker
) -> None:
"""Ensure valid hints are accepted for media_player entity."""
# Set bypass option
type_hint_checker.linter.config.ignore_missing_annotations = False
class_node = astroid.extract_node(
"""
class Entity():
pass
class MediaPlayerEntity(Entity):
pass
class MyMediaPlayer( #@
MediaPlayerEntity
):
async def async_get_media_image(self) -> tuple[bytes | None, str | None]:
pass
""",
"homeassistant.components.pylint_test.media_player",
)
type_hint_checker.visit_module(class_node.parent)
with assert_no_messages(linter):
type_hint_checker.visit_classdef(class_node)
def test_humidifier_entity(
linter: UnittestLinter, type_hint_checker: BaseChecker
) -> None:
"""Ensure valid hints are accepted for humidifier entity."""
# Set bypass option
type_hint_checker.linter.config.ignore_missing_annotations = False
# Ensure that `float` and `int` are valid for `int` argument type
class_node = astroid.extract_node(
"""
class Entity():
pass
class HumidifierEntity(Entity):
pass
class MyHumidifier( #@
HumidifierEntity
):
def set_humidity(self, humidity: int) -> None:
pass
def async_set_humidity(self, humidity: float) -> None:
pass
""",
"homeassistant.components.pylint_test.humidifier",
)
type_hint_checker.visit_module(class_node.parent)
with assert_no_messages(linter):
type_hint_checker.visit_classdef(class_node)
def test_number_entity(linter: UnittestLinter, type_hint_checker: BaseChecker) -> None:
"""Ensure valid hints are accepted for number entity."""
# Set bypass option
type_hint_checker.linter.config.ignore_missing_annotations = False
# Ensure that device class is valid despite Entity inheritance
# Ensure that `int` is valid for `float` return type
class_node = astroid.extract_node(
"""
class Entity():
pass
class RestoreEntity(Entity):
pass
class NumberEntity(Entity):
pass
class MyNumber( #@
RestoreEntity, NumberEntity
):
@property
def device_class(self) -> NumberDeviceClass:
pass
@property
def native_value(self) -> int:
pass
""",
"homeassistant.components.pylint_test.number",
)
type_hint_checker.visit_module(class_node.parent)
with assert_no_messages(linter):
type_hint_checker.visit_classdef(class_node)
def test_vacuum_entity(linter: UnittestLinter, type_hint_checker: BaseChecker) -> None:
"""Ensure valid hints are accepted for vacuum entity."""
# Set bypass option
type_hint_checker.linter.config.ignore_missing_annotations = False
# Ensure that `dict | list | None` is valid for params
class_node = astroid.extract_node(
"""
class Entity():
pass
class ToggleEntity(Entity):
pass
class _BaseVacuum(Entity):
pass
class VacuumEntity(_BaseVacuum, ToggleEntity):
pass
class MyVacuum( #@
VacuumEntity
):
def send_command(
self,
command: str,
params: dict[str, Any] | list[Any] | None = None,
**kwargs: Any,
) -> None:
pass
""",
"homeassistant.components.pylint_test.vacuum",
)
type_hint_checker.visit_module(class_node.parent)
with assert_no_messages(linter):
type_hint_checker.visit_classdef(class_node)
def test_notify_get_service(
linter: UnittestLinter, type_hint_checker: BaseChecker
) -> None:
"""Ensure valid hints are accepted for async_get_service."""
func_node = astroid.extract_node(
"""
class BaseNotificationService():
pass
async def async_get_service( #@
hass: HomeAssistant,
config: ConfigType,
discovery_info: DiscoveryInfoType | None = None,
) -> CustomNotificationService:
pass
class CustomNotificationService(BaseNotificationService):
pass
""",
"homeassistant.components.pylint_test.notify",
)
type_hint_checker.visit_module(func_node.parent)
with assert_no_messages(
linter,
):
type_hint_checker.visit_asyncfunctiondef(func_node)
def test_pytest_function(
linter: UnittestLinter, type_hint_checker: BaseChecker
) -> None:
"""Ensure valid hints are accepted for async_get_service."""
func_node = astroid.extract_node(
"""
async def test_sample( #@
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
) -> None:
pass
""",
"tests.components.pylint_test.notify",
)
type_hint_checker.visit_module(func_node.parent)
with assert_no_messages(
linter,
):
type_hint_checker.visit_asyncfunctiondef(func_node)
def test_pytest_invalid_function(
linter: UnittestLinter, type_hint_checker: BaseChecker
) -> None:
"""Ensure invalid hints are rejected for async_get_service."""
func_node, hass_node, caplog_node = astroid.extract_node(
"""
async def test_sample( #@
hass: Something, #@
caplog: SomethingElse, #@
) -> Anything:
pass
""",
"tests.components.pylint_test.notify",
)
type_hint_checker.visit_module(func_node.parent)
with assert_adds_messages(
linter,
pylint.testutils.MessageTest(
msg_id="hass-argument-type",
node=hass_node,
args=("hass", ["HomeAssistant", "HomeAssistant | None"], "test_sample"),
line=3,
col_offset=4,
end_line=3,
end_col_offset=19,
),
pylint.testutils.MessageTest(
msg_id="hass-return-type",
node=func_node,
args=("None", "test_sample"),
line=2,
col_offset=0,
end_line=2,
end_col_offset=21,
),
pylint.testutils.MessageTest(
msg_id="hass-argument-type",
node=caplog_node,
args=("caplog", "pytest.LogCaptureFixture", "test_sample"),
line=4,
col_offset=4,
end_line=4,
end_col_offset=25,
),
):
type_hint_checker.visit_asyncfunctiondef(func_node)