From 6243f24b05e2c766a107ffaa44072b6cef036255 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 15 Aug 2022 09:48:03 +0200 Subject: [PATCH] Add media-player checks to pylint plugin (#76675) * Add media-player checks to pylint plugin * Fix invalid hints * Add tests * Adjust tests * Add extra test * Adjust regex * Cleanup comment * Move media player tests up --- pylint/plugins/hass_enforce_type_hints.py | 318 +++++++++++++++++++++- tests/pylint/test_enforce_type_hints.py | 30 ++ 2 files changed, 347 insertions(+), 1 deletion(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 852c5b544c4..b6eecdadfee 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -59,7 +59,7 @@ _TYPE_HINT_MATCHERS: dict[str, re.Pattern[str]] = { # a_or_b matches items such as "DiscoveryInfoType | None" "a_or_b": re.compile(r"^(\w+) \| (\w+)$"), } -_INNER_MATCH = r"((?:\w+)|(?:\.{3})|(?:\w+\[.+\]))" +_INNER_MATCH = r"((?:[\w\| ]+)|(?:\.{3})|(?:\w+\[.+\]))" _INNER_MATCH_POSSIBILITIES = [i + 1 for i in range(5)] _TYPE_HINT_MATCHERS.update( { @@ -1465,6 +1465,322 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "media_player": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="MediaPlayerEntity", + matches=[ + TypeHintMatch( + function_name="device_class", + return_type=["MediaPlayerDeviceClass", "str", None], + ), + TypeHintMatch( + function_name="state", + return_type=["str", None], + ), + TypeHintMatch( + function_name="access_token", + return_type="str", + ), + TypeHintMatch( + function_name="volume_level", + return_type=["float", None], + ), + TypeHintMatch( + function_name="is_volume_muted", + return_type=["bool", None], + ), + TypeHintMatch( + function_name="media_content_id", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_content_type", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_duration", + return_type=["int", None], + ), + TypeHintMatch( + function_name="media_position", + return_type=["int", None], + ), + TypeHintMatch( + function_name="media_position_updated_at", + return_type=["datetime", None], + ), + TypeHintMatch( + function_name="media_image_url", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_image_remotely_accessible", + return_type="bool", + ), + TypeHintMatch( + function_name="media_image_hash", + return_type=["str", None], + ), + TypeHintMatch( + function_name="async_get_media_image", + return_type="tuple[bytes | None, str | None]", + ), + TypeHintMatch( + function_name="async_get_browse_image", + arg_types={ + 1: "str", + 2: "str", + 3: "str | None", + }, + return_type="tuple[bytes | None, str | None]", + ), + TypeHintMatch( + function_name="media_title", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_artist", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_album_name", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_album_artist", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_track", + return_type=["int", None], + ), + TypeHintMatch( + function_name="media_series_title", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_season", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_episode", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_channel", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_playlist", + return_type=["str", None], + ), + TypeHintMatch( + function_name="app_id", + return_type=["str", None], + ), + TypeHintMatch( + function_name="app_name", + return_type=["str", None], + ), + TypeHintMatch( + function_name="source", + return_type=["str", None], + ), + TypeHintMatch( + function_name="source_list", + return_type=["list[str]", None], + ), + TypeHintMatch( + function_name="sound_mode", + return_type=["str", None], + ), + TypeHintMatch( + function_name="sound_mode_list", + return_type=["list[str]", None], + ), + TypeHintMatch( + function_name="shuffle", + return_type=["bool", None], + ), + TypeHintMatch( + function_name="repeat", + return_type=["str", None], + ), + TypeHintMatch( + function_name="group_members", + return_type=["list[str]", None], + ), + TypeHintMatch( + function_name="turn_on", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="turn_off", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="mute_volume", + arg_types={ + 1: "bool", + }, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="set_volume_level", + arg_types={ + 1: "float", + }, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="media_play", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="media_pause", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="media_stop", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="media_previous_track", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="media_next_track", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="media_seek", + arg_types={ + 1: "float", + }, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="play_media", + arg_types={ + 1: "str", + 2: "str", + }, + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="select_source", + arg_types={ + 1: "str", + }, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="select_sound_mode", + arg_types={ + 1: "str", + }, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="clear_playlist", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="set_shuffle", + arg_types={ + 1: "bool", + }, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="set_repeat", + arg_types={ + 1: "str", + }, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="toggle", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="volume_up", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="volume_down", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="media_play_pause", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="media_image_local", + return_type=["str", None], + ), + TypeHintMatch( + function_name="capability_attributes", + return_type="dict[str, Any]", + ), + TypeHintMatch( + function_name="async_browse_media", + arg_types={ + 1: "str | None", + 2: "str | None", + }, + return_type="BrowseMedia", + ), + TypeHintMatch( + function_name="join_players", + arg_types={ + 1: "list[str]", + }, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="unjoin_player", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="get_browse_image_url", + arg_types={ + 1: "str", + 2: "str", + 3: "str | None", + }, + return_type="str", + ), + ], + ), + ], "number": [ ClassTypeHintMatch( base_class="Entity", diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index b3c233d1c3b..1381ed34a7b 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -53,6 +53,7 @@ def test_regex_get_module_platform( ("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( @@ -902,6 +903,35 @@ def test_invalid_device_class( 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.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_number_entity(linter: UnittestLinter, type_hint_checker: BaseChecker) -> None: """Ensure valid hints are accepted for number entity.""" # Set bypass option