Skip to content

Commit

Permalink
Updated homeassistant to 2024.10.0
Browse files Browse the repository at this point in the history
* Updated plugins
  • Loading branch information
iprak committed Oct 4, 2024
1 parent da45fab commit 8923fae
Show file tree
Hide file tree
Showing 7 changed files with 683 additions and 237 deletions.
119 changes: 119 additions & 0 deletions container_content/pylint/plugins/hass_decorator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""Plugin to check decorators."""

from __future__ import annotations

from astroid import nodes
from pylint.checkers import BaseChecker
from pylint.lint import PyLinter


class HassDecoratorChecker(BaseChecker):
"""Checker for decorators."""

name = "hass_decorator"
priority = -1
msgs = {
"W7471": (
"A coroutine function should not be decorated with @callback",
"hass-async-callback-decorator",
"Used when a coroutine function has an invalid @callback decorator",
),
"W7472": (
"Fixture %s is invalid here, please %s",
"hass-pytest-fixture-decorator",
"Used when a pytest fixture is invalid",
),
}

def _get_pytest_fixture_node(self, node: nodes.FunctionDef) -> nodes.Call | None:
for decorator in node.decorators.nodes:
if (
isinstance(decorator, nodes.Call)
and decorator.func.as_string() == "pytest.fixture"
):
return decorator

return None

def _get_pytest_fixture_node_keyword(
self, decorator: nodes.Call, search_arg: str
) -> nodes.Keyword | None:
for keyword in decorator.keywords:
if keyword.arg == search_arg:
return keyword

return None

def _check_pytest_fixture(
self, node: nodes.FunctionDef, decoratornames: set[str]
) -> None:
if (
"_pytest.fixtures.FixtureFunctionMarker" not in decoratornames
or not (root_name := node.root().name).startswith("tests.")
or (decorator := self._get_pytest_fixture_node(node)) is None
or not (
scope_keyword := self._get_pytest_fixture_node_keyword(
decorator, "scope"
)
)
or not isinstance(scope_keyword.value, nodes.Const)
or not (scope := scope_keyword.value.value)
):
return

parts = root_name.split(".")
test_component: str | None = None
if root_name.startswith("tests.components.") and parts[2] != "conftest":
test_component = parts[2]

if scope == "session":
if test_component:
self.add_message(
"hass-pytest-fixture-decorator",
node=decorator,
args=("scope `session`", "use `package` or lower"),
)
return
if not (
autouse_keyword := self._get_pytest_fixture_node_keyword(
decorator, "autouse"
)
) or (
isinstance(autouse_keyword.value, nodes.Const)
and not autouse_keyword.value.value
):
self.add_message(
"hass-pytest-fixture-decorator",
node=decorator,
args=(
"scope/autouse combination",
"set `autouse=True` or reduce scope",
),
)
return

test_module = parts[3] if len(parts) > 3 else ""

if test_component and scope == "package" and test_module != "conftest":
self.add_message(
"hass-pytest-fixture-decorator",
node=decorator,
args=("scope `package`", "use `module` or lower"),
)

def visit_asyncfunctiondef(self, node: nodes.AsyncFunctionDef) -> None:
"""Apply checks on an AsyncFunctionDef node."""
if decoratornames := node.decoratornames():
if "homeassistant.core.callback" in decoratornames:
self.add_message("hass-async-callback-decorator", node=node)
self._check_pytest_fixture(node, decoratornames)

def visit_functiondef(self, node: nodes.FunctionDef) -> None:
"""Apply checks on an AsyncFunctionDef node."""
if decoratornames := node.decoratornames():
self._check_pytest_fixture(node, decoratornames)


def register(linter: PyLinter) -> None:
"""Register the checker."""
linter.register_checker(HassDecoratorChecker(linter))
168 changes: 168 additions & 0 deletions container_content/pylint/plugins/hass_enforce_class_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
"""Plugin for checking if class is in correct module."""

from __future__ import annotations

from astroid import nodes
from pylint.checkers import BaseChecker
from pylint.lint import PyLinter

from homeassistant.const import Platform

_BASE_ENTITY_MODULES: set[str] = {
"BaseCoordinatorEntity",
"CoordinatorEntity",
"Entity",
"EntityDescription",
"ManualTriggerEntity",
"RestoreEntity",
"ToggleEntity",
"ToggleEntityDescription",
"TriggerBaseEntity",
}
_MODULES: dict[str, set[str]] = {
"air_quality": {"AirQualityEntity"},
"alarm_control_panel": {
"AlarmControlPanelEntity",
"AlarmControlPanelEntityDescription",
},
"assist_satellite": {"AssistSatelliteEntity", "AssistSatelliteEntityDescription"},
"binary_sensor": {"BinarySensorEntity", "BinarySensorEntityDescription"},
"button": {"ButtonEntity", "ButtonEntityDescription"},
"calendar": {"CalendarEntity", "CalendarEntityDescription"},
"camera": {"Camera", "CameraEntityDescription"},
"climate": {"ClimateEntity", "ClimateEntityDescription"},
"coordinator": {"DataUpdateCoordinator"},
"conversation": {"ConversationEntity"},
"cover": {"CoverEntity", "CoverEntityDescription"},
"date": {"DateEntity", "DateEntityDescription"},
"datetime": {"DateTimeEntity", "DateTimeEntityDescription"},
"device_tracker": {
"DeviceTrackerEntity",
"ScannerEntity",
"ScannerEntityDescription",
"TrackerEntity",
"TrackerEntityDescription",
},
"event": {"EventEntity", "EventEntityDescription"},
"fan": {"FanEntity", "FanEntityDescription"},
"geo_location": {"GeolocationEvent"},
"humidifier": {"HumidifierEntity", "HumidifierEntityDescription"},
"image": {"ImageEntity", "ImageEntityDescription"},
"image_processing": {
"ImageProcessingEntity",
"ImageProcessingFaceEntity",
"ImageProcessingEntityDescription",
},
"lawn_mower": {"LawnMowerEntity", "LawnMowerEntityDescription"},
"light": {"LightEntity", "LightEntityDescription"},
"lock": {"LockEntity", "LockEntityDescription"},
"media_player": {"MediaPlayerEntity", "MediaPlayerEntityDescription"},
"notify": {"NotifyEntity", "NotifyEntityDescription"},
"number": {"NumberEntity", "NumberEntityDescription", "RestoreNumber"},
"remote": {"RemoteEntity", "RemoteEntityDescription"},
"select": {"SelectEntity", "SelectEntityDescription"},
"sensor": {"RestoreSensor", "SensorEntity", "SensorEntityDescription"},
"siren": {"SirenEntity", "SirenEntityDescription"},
"stt": {"SpeechToTextEntity"},
"switch": {"SwitchEntity", "SwitchEntityDescription"},
"text": {"TextEntity", "TextEntityDescription"},
"time": {"TimeEntity", "TimeEntityDescription"},
"todo": {"TodoListEntity"},
"tts": {"TextToSpeechEntity"},
"update": {"UpdateEntity", "UpdateEntityDescription"},
"vacuum": {"StateVacuumEntity", "VacuumEntity", "VacuumEntityDescription"},
"wake_word": {"WakeWordDetectionEntity"},
"water_heater": {"WaterHeaterEntity"},
"weather": {
"CoordinatorWeatherEntity",
"SingleCoordinatorWeatherEntity",
"WeatherEntity",
"WeatherEntityDescription",
},
}
_ENTITY_COMPONENTS: set[str] = {platform.value for platform in Platform}.union(
{
"alert",
"automation",
"counter",
"dominos",
"input_boolean",
"input_button",
"input_datetime",
"input_number",
"input_select",
"input_text",
"microsoft_face",
"person",
"plant",
"remember_the_milk",
"schedule",
"script",
"tag",
"timer",
}
)


_MODULE_CLASSES = {
class_name for classes in _MODULES.values() for class_name in classes
}


class HassEnforceClassModule(BaseChecker):
"""Checker for class in correct module."""

name = "hass_enforce_class_module"
priority = -1
msgs = {
"C7461": (
"Derived %s is recommended to be placed in the '%s' module",
"hass-enforce-class-module",
"Used when derived class should be placed in its own module.",
),
}

def visit_classdef(self, node: nodes.ClassDef) -> None:
"""Check if derived class is placed in its own module."""
root_name = node.root().name

# we only want to check components
if not root_name.startswith("homeassistant.components."):
return
parts = root_name.split(".")
current_integration = parts[2]
current_module = parts[3] if len(parts) > 3 else ""

ancestors = list(node.ancestors())

if current_module != "entity" and current_integration not in _ENTITY_COMPONENTS:
top_level_ancestors = list(node.ancestors(recurs=False))

for ancestor in top_level_ancestors:
if ancestor.name in _BASE_ENTITY_MODULES and not any(
anc.name in _MODULE_CLASSES for anc in ancestors
):
self.add_message(
"hass-enforce-class-module",
node=node,
args=(ancestor.name, "entity"),
)
return

for expected_module, classes in _MODULES.items():
if expected_module in (current_module, current_integration):
continue

for ancestor in ancestors:
if ancestor.name in classes:
self.add_message(
"hass-enforce-class-module",
node=node,
args=(ancestor.name, expected_module),
)
return


def register(linter: PyLinter) -> None:
"""Register the checker."""
linter.register_checker(HassEnforceClassModule(linter))

This file was deleted.

Loading

0 comments on commit 8923fae

Please sign in to comment.