-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Updated plugins
- Loading branch information
Showing
7 changed files
with
683 additions
and
237 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
168
container_content/pylint/plugins/hass_enforce_class_module.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) |
55 changes: 0 additions & 55 deletions
55
container_content/pylint/plugins/hass_enforce_coordinator_module.py
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.