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.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_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.config.ignore_missing_annotations = False class_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_no_messages(linter): type_hint_checker.visit_classdef(class_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_number_entity(linter: UnittestLinter, type_hint_checker: BaseChecker) -> None: """Ensure valid hints are accepted for number entity.""" # Set bypass option type_hint_checker.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_bad_import( linter: UnittestLinter, imports_checker: BaseChecker, module_name: str, import_from: str, import_what: str, error_code: str, ) -> None: """Ensure bad imports are rejected.""" import_node = astroid.extract_node( f"from {import_from} import {import_what} #@", module_name, ) imports_checker.visit_module(import_node.parent) with assert_adds_messages( linter, pylint.testutils.MessageTest( msg_id=error_code, node=import_node, args=None, line=1, col_offset=0, end_line=1, end_col_offset=len(import_from) + len(import_what) + 13, ), ): imports_checker.visit_importfrom(import_node)
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_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 ) -> FlowResult: 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_long_tuple( linter: UnittestLinter, type_hint_checker: BaseChecker ) -> None: """Check invalid entity properties are ignored by default.""" # Set ignore option type_hint_checker.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_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_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_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=("FlowResult", "async_step_zeroconf"), line=11, col_offset=4, end_line=11, end_col_offset=33, ), ): type_hint_checker.visit_classdef(class_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_good_import( linter: UnittestLinter, imports_checker: BaseChecker, module_name: str, import_from: str, import_what: str, ) -> None: """Ensure good imports pass through ok.""" import_node = astroid.extract_node( f"from {import_from} import {import_what} #@", module_name, ) imports_checker.visit_module(import_node.parent) with assert_no_messages(linter): imports_checker.visit_importfrom(import_node)
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.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)
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.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()
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: Callable[..., Awaitable[None]], 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_device_class( linter: UnittestLinter, type_hint_checker: BaseChecker ) -> None: """Ensure invalid hints are rejected for entity device_class.""" # Set bypass option type_hint_checker.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", "str", None], "device_class"), line=12, col_offset=4, end_line=12, end_col_offset=20, ), ): 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.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 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_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_vacuum_entity(linter: UnittestLinter, type_hint_checker: BaseChecker) -> None: """Ensure valid hints are accepted for vacuum entity.""" # Set bypass option type_hint_checker.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_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.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 ) -> bool: 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_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.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_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.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)