mirror of https://github.com/home-assistant/core
120 lines
4.1 KiB
Python
120 lines
4.1 KiB
Python
"""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))
|