mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Add pylint plugin to check for calls to base implementation (#100432)
This commit is contained in:
parent
ddd62a8f63
commit
37288d7788
@ -421,6 +421,7 @@ class AirVisualEntity(CoordinatorEntity):
|
||||
self._entry = entry
|
||||
self.entity_description = description
|
||||
|
||||
# pylint: disable-next=hass-missing-super-call
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
|
||||
|
@ -100,6 +100,7 @@ class FloSwitch(FloEntity, SwitchEntity):
|
||||
self._attr_is_on = self._device.last_known_valve_state == "open"
|
||||
self.async_write_ha_state()
|
||||
|
||||
# pylint: disable-next=hass-missing-super-call
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
self.async_on_remove(self._device.async_add_listener(self.async_update_state))
|
||||
|
@ -311,6 +311,7 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity):
|
||||
await self.async_update()
|
||||
self.async_write_ha_state()
|
||||
|
||||
# pylint: disable-next=hass-missing-super-call
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
self.async_on_remove(
|
||||
|
@ -136,6 +136,7 @@ class PowerViewSensor(ShadeEntity, SensorEntity):
|
||||
"""Get the current value in percentage."""
|
||||
return self.entity_description.native_value_fn(self._shade)
|
||||
|
||||
# pylint: disable-next=hass-missing-super-call
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
self.async_on_remove(
|
||||
|
@ -192,6 +192,7 @@ class HvvDepartureBinarySensor(CoordinatorEntity, BinarySensorEntity):
|
||||
if v is not None
|
||||
}
|
||||
|
||||
# pylint: disable-next=hass-missing-super-call
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
self.async_on_remove(
|
||||
|
@ -262,6 +262,7 @@ class ISYAuxSensorEntity(ISYSensorEntity):
|
||||
"""Return the target value."""
|
||||
return None if self.target is None else self.target.value
|
||||
|
||||
# pylint: disable-next=hass-missing-super-call
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to the node control change events.
|
||||
|
||||
|
@ -156,6 +156,7 @@ class ISYEnableSwitchEntity(ISYAuxControlEntity, SwitchEntity):
|
||||
self._attr_name = description.name # Override super
|
||||
self._change_handler: EventListener = None
|
||||
|
||||
# pylint: disable-next=hass-missing-super-call
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to the node control change events."""
|
||||
self._change_handler = self._node.isy.nodes.status_events.subscribe(
|
||||
|
@ -64,6 +64,7 @@ class LivisiEntity(CoordinatorEntity[LivisiDataUpdateCoordinator]):
|
||||
)
|
||||
super().__init__(coordinator)
|
||||
|
||||
# pylint: disable-next=hass-missing-super-call
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callback for reachability."""
|
||||
self.async_on_remove(
|
||||
|
@ -63,6 +63,7 @@ class LutronOccupancySensor(LutronCasetaDevice, BinarySensorEntity):
|
||||
"""Return the brightness of the light."""
|
||||
return self._device["status"] == OCCUPANCY_GROUP_OCCUPIED
|
||||
|
||||
# pylint: disable-next=hass-missing-super-call
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
self._smartbridge.add_occupancy_subscriber(
|
||||
|
@ -352,6 +352,7 @@ class RflinkSensor(RflinkDevice, SensorEntity):
|
||||
"""Domain specific event handler."""
|
||||
self._state = event["value"]
|
||||
|
||||
# pylint: disable-next=hass-missing-super-call
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register update callback."""
|
||||
# Remove temporary bogus entity_id if added
|
||||
|
@ -35,6 +35,7 @@ class RiscoCloudEntity(CoordinatorEntity[RiscoDataUpdateCoordinator]):
|
||||
self._get_data_from_coordinator()
|
||||
self.async_write_ha_state()
|
||||
|
||||
# pylint: disable-next=hass-missing-super-call
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
self.async_on_remove(
|
||||
|
@ -86,6 +86,7 @@ class RiscoSensor(CoordinatorEntity[RiscoEventsDataUpdateCoordinator], SensorEnt
|
||||
self._attr_name = f"Risco {self.coordinator.risco.site_name} {name} Events"
|
||||
self._attr_device_class = SensorDeviceClass.TIMESTAMP
|
||||
|
||||
# pylint: disable-next=hass-missing-super-call
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
self._entity_registry = er.async_get(self.hass)
|
||||
|
@ -332,6 +332,7 @@ class ShellyBlockEntity(CoordinatorEntity[ShellyBlockCoordinator]):
|
||||
)
|
||||
self._attr_unique_id = f"{coordinator.mac}-{block.description}"
|
||||
|
||||
# pylint: disable-next=hass-missing-super-call
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to HASS."""
|
||||
self.async_on_remove(self.coordinator.async_add_listener(self._update_callback))
|
||||
@ -375,6 +376,7 @@ class ShellyRpcEntity(CoordinatorEntity[ShellyRpcCoordinator]):
|
||||
"""Device status by entity key."""
|
||||
return cast(dict, self.coordinator.device.status[self.key])
|
||||
|
||||
# pylint: disable-next=hass-missing-super-call
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to HASS."""
|
||||
self.async_on_remove(self.coordinator.async_add_listener(self._update_callback))
|
||||
|
@ -73,6 +73,7 @@ class SmartMeterTexasSensor(CoordinatorEntity, RestoreEntity, SensorEntity):
|
||||
self._attr_native_value = self.meter.reading
|
||||
self.async_write_ha_state()
|
||||
|
||||
# pylint: disable-next=hass-missing-super-call
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe to updates."""
|
||||
self.async_on_remove(self.coordinator.async_add_listener(self._state_update))
|
||||
|
@ -99,6 +99,7 @@ class TractiveDeviceTracker(TractiveEntity, TrackerEntity):
|
||||
self._attr_available = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
# pylint: disable-next=hass-missing-super-call
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle entity which will be added."""
|
||||
if not self._client.subscribed:
|
||||
|
79
pylint/plugins/hass_enforce_super_call.py
Normal file
79
pylint/plugins/hass_enforce_super_call.py
Normal file
@ -0,0 +1,79 @@
|
||||
"""Plugin for checking super calls."""
|
||||
from __future__ import annotations
|
||||
|
||||
from astroid import nodes
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.interfaces import INFERENCE
|
||||
from pylint.lint import PyLinter
|
||||
|
||||
METHODS = {
|
||||
"async_added_to_hass",
|
||||
}
|
||||
|
||||
|
||||
class HassEnforceSuperCallChecker(BaseChecker): # type: ignore[misc]
|
||||
"""Checker for super calls."""
|
||||
|
||||
name = "hass_enforce_super_call"
|
||||
priority = -1
|
||||
msgs = {
|
||||
"W7441": (
|
||||
"Missing call to: super().%s",
|
||||
"hass-missing-super-call",
|
||||
"Used when method should call its parent implementation.",
|
||||
),
|
||||
}
|
||||
options = ()
|
||||
|
||||
def visit_functiondef(
|
||||
self, node: nodes.FunctionDef | nodes.AsyncFunctionDef
|
||||
) -> None:
|
||||
"""Check for super calls in method body."""
|
||||
if node.name not in METHODS:
|
||||
return
|
||||
|
||||
assert node.parent
|
||||
parent = node.parent.frame()
|
||||
if not isinstance(parent, nodes.ClassDef):
|
||||
return
|
||||
|
||||
# Check function body for super call
|
||||
for child_node in node.body:
|
||||
while isinstance(child_node, (nodes.Expr, nodes.Await, nodes.Return)):
|
||||
child_node = child_node.value
|
||||
match child_node:
|
||||
case nodes.Call(
|
||||
func=nodes.Attribute(
|
||||
expr=nodes.Call(func=nodes.Name(name="super")),
|
||||
attrname=node.name,
|
||||
),
|
||||
):
|
||||
return
|
||||
|
||||
# Check for non-empty base implementation
|
||||
found_base_implementation = False
|
||||
for base in parent.ancestors():
|
||||
for method in base.mymethods():
|
||||
if method.name != node.name:
|
||||
continue
|
||||
if method.body and not (
|
||||
len(method.body) == 1 and isinstance(method.body[0], nodes.Pass)
|
||||
):
|
||||
found_base_implementation = True
|
||||
break
|
||||
|
||||
if found_base_implementation:
|
||||
self.add_message(
|
||||
"hass-missing-super-call",
|
||||
node=node,
|
||||
args=(node.name,),
|
||||
confidence=INFERENCE,
|
||||
)
|
||||
break
|
||||
|
||||
visit_asyncfunctiondef = visit_functiondef
|
||||
|
||||
|
||||
def register(linter: PyLinter) -> None:
|
||||
"""Register the checker."""
|
||||
linter.register_checker(HassEnforceSuperCallChecker(linter))
|
@ -100,6 +100,7 @@ init-hook = """\
|
||||
load-plugins = [
|
||||
"pylint.extensions.code_style",
|
||||
"pylint.extensions.typing",
|
||||
"hass_enforce_super_call",
|
||||
"hass_enforce_type_hints",
|
||||
"hass_inheritance",
|
||||
"hass_imports",
|
||||
|
@ -11,13 +11,11 @@ import pytest
|
||||
BASE_PATH = Path(__file__).parents[2]
|
||||
|
||||
|
||||
@pytest.fixture(name="hass_enforce_type_hints", scope="session")
|
||||
def hass_enforce_type_hints_fixture() -> ModuleType:
|
||||
"""Fixture to provide a requests mocker."""
|
||||
module_name = "hass_enforce_type_hints"
|
||||
def _load_plugin_from_file(module_name: str, file: str) -> ModuleType:
|
||||
"""Load plugin from file path."""
|
||||
spec = spec_from_file_location(
|
||||
module_name,
|
||||
str(BASE_PATH.joinpath("pylint/plugins/hass_enforce_type_hints.py")),
|
||||
str(BASE_PATH.joinpath(file)),
|
||||
)
|
||||
assert spec and spec.loader
|
||||
|
||||
@ -27,6 +25,15 @@ def hass_enforce_type_hints_fixture() -> ModuleType:
|
||||
return module
|
||||
|
||||
|
||||
@pytest.fixture(name="hass_enforce_type_hints", scope="session")
|
||||
def hass_enforce_type_hints_fixture() -> ModuleType:
|
||||
"""Fixture to provide a requests mocker."""
|
||||
return _load_plugin_from_file(
|
||||
"hass_enforce_type_hints",
|
||||
"pylint/plugins/hass_enforce_type_hints.py",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="linter")
|
||||
def linter_fixture() -> UnittestLinter:
|
||||
"""Fixture to provide a requests mocker."""
|
||||
@ -44,16 +51,10 @@ def type_hint_checker_fixture(hass_enforce_type_hints, linter) -> BaseChecker:
|
||||
@pytest.fixture(name="hass_imports", scope="session")
|
||||
def hass_imports_fixture() -> ModuleType:
|
||||
"""Fixture to provide a requests mocker."""
|
||||
module_name = "hass_imports"
|
||||
spec = spec_from_file_location(
|
||||
module_name, str(BASE_PATH.joinpath("pylint/plugins/hass_imports.py"))
|
||||
return _load_plugin_from_file(
|
||||
"hass_imports",
|
||||
"pylint/plugins/hass_imports.py",
|
||||
)
|
||||
assert spec and spec.loader
|
||||
|
||||
module = module_from_spec(spec)
|
||||
sys.modules[module_name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
@pytest.fixture(name="imports_checker")
|
||||
@ -62,3 +63,20 @@ def imports_checker_fixture(hass_imports, linter) -> BaseChecker:
|
||||
type_hint_checker = hass_imports.HassImportsFormatChecker(linter)
|
||||
type_hint_checker.module = "homeassistant.components.pylint_test"
|
||||
return type_hint_checker
|
||||
|
||||
|
||||
@pytest.fixture(name="hass_enforce_super_call", scope="session")
|
||||
def hass_enforce_super_call_fixture() -> ModuleType:
|
||||
"""Fixture to provide a requests mocker."""
|
||||
return _load_plugin_from_file(
|
||||
"hass_enforce_super_call",
|
||||
"pylint/plugins/hass_enforce_super_call.py",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="super_call_checker")
|
||||
def super_call_checker_fixture(hass_enforce_super_call, linter) -> BaseChecker:
|
||||
"""Fixture to provide a requests mocker."""
|
||||
super_call_checker = hass_enforce_super_call.HassEnforceSuperCallChecker(linter)
|
||||
super_call_checker.module = "homeassistant.components.pylint_test"
|
||||
return super_call_checker
|
||||
|
221
tests/pylint/test_enforce_super_call.py
Normal file
221
tests/pylint/test_enforce_super_call.py
Normal file
@ -0,0 +1,221 @@
|
||||
"""Tests for pylint hass_enforce_super_call plugin."""
|
||||
from __future__ import annotations
|
||||
|
||||
from types import ModuleType
|
||||
from unittest.mock import patch
|
||||
|
||||
import astroid
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.interfaces import INFERENCE
|
||||
from pylint.testutils import MessageTest
|
||||
from pylint.testutils.unittest_linter import UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
import pytest
|
||||
|
||||
from . import assert_adds_messages, assert_no_messages
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"code",
|
||||
[
|
||||
pytest.param(
|
||||
"""
|
||||
class Entity:
|
||||
async def async_added_to_hass(self) -> None:
|
||||
pass
|
||||
""",
|
||||
id="no_parent",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
class Entity:
|
||||
async def async_added_to_hass(self) -> None:
|
||||
\"\"\"Some docstring.\"\"\"
|
||||
|
||||
class Child(Entity):
|
||||
async def async_added_to_hass(self) -> None:
|
||||
x = 2
|
||||
""",
|
||||
id="empty_parent_implementation",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
class Entity:
|
||||
async def async_added_to_hass(self) -> None:
|
||||
\"\"\"Some docstring.\"\"\"
|
||||
pass
|
||||
|
||||
class Child(Entity):
|
||||
async def async_added_to_hass(self) -> None:
|
||||
x = 2
|
||||
""",
|
||||
id="empty_parent_implementation2",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
class Entity:
|
||||
async def async_added_to_hass(self) -> None:
|
||||
x = 2
|
||||
|
||||
class Child(Entity):
|
||||
async def async_added_to_hass(self) -> None:
|
||||
await super().async_added_to_hass()
|
||||
""",
|
||||
id="correct_super_call",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
class Entity:
|
||||
async def async_added_to_hass(self) -> None:
|
||||
x = 2
|
||||
|
||||
class Child(Entity):
|
||||
async def async_added_to_hass(self) -> None:
|
||||
return await super().async_added_to_hass()
|
||||
""",
|
||||
id="super_call_in_return",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
class Entity:
|
||||
def added_to_hass(self) -> None:
|
||||
x = 2
|
||||
|
||||
class Child(Entity):
|
||||
def added_to_hass(self) -> None:
|
||||
super().added_to_hass()
|
||||
""",
|
||||
id="super_call_not_async",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
class Entity:
|
||||
async def async_added_to_hass(self) -> None:
|
||||
\"\"\"\"\"\"
|
||||
|
||||
class Coordinator:
|
||||
async def async_added_to_hass(self) -> None:
|
||||
x = 2
|
||||
|
||||
class Child(Entity, Coordinator):
|
||||
async def async_added_to_hass(self) -> None:
|
||||
await super().async_added_to_hass()
|
||||
""",
|
||||
id="multiple_inheritance",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
async def async_added_to_hass() -> None:
|
||||
x = 2
|
||||
""",
|
||||
id="not_a_method",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_enforce_super_call(
|
||||
linter: UnittestLinter,
|
||||
hass_enforce_super_call: ModuleType,
|
||||
super_call_checker: BaseChecker,
|
||||
code: str,
|
||||
) -> None:
|
||||
"""Good test cases."""
|
||||
root_node = astroid.parse(code, "homeassistant.components.pylint_test")
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(super_call_checker)
|
||||
|
||||
with patch.object(
|
||||
hass_enforce_super_call, "METHODS", new={"added_to_hass", "async_added_to_hass"}
|
||||
), assert_no_messages(linter):
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("code", "node_idx"),
|
||||
[
|
||||
pytest.param(
|
||||
"""
|
||||
class Entity:
|
||||
def added_to_hass(self) -> None:
|
||||
x = 2
|
||||
|
||||
class Child(Entity):
|
||||
def added_to_hass(self) -> None:
|
||||
x = 3
|
||||
""",
|
||||
1,
|
||||
id="no_super_call",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
class Entity:
|
||||
async def async_added_to_hass(self) -> None:
|
||||
x = 2
|
||||
|
||||
class Child(Entity):
|
||||
async def async_added_to_hass(self) -> None:
|
||||
x = 3
|
||||
""",
|
||||
1,
|
||||
id="no_super_call_async",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
class Entity:
|
||||
async def async_added_to_hass(self) -> None:
|
||||
x = 2
|
||||
|
||||
class Child(Entity):
|
||||
async def async_added_to_hass(self) -> None:
|
||||
await Entity.async_added_to_hass()
|
||||
""",
|
||||
1,
|
||||
id="explicit_call_to_base_implementation",
|
||||
),
|
||||
pytest.param(
|
||||
"""
|
||||
class Entity:
|
||||
async def async_added_to_hass(self) -> None:
|
||||
\"\"\"\"\"\"
|
||||
|
||||
class Coordinator:
|
||||
async def async_added_to_hass(self) -> None:
|
||||
x = 2
|
||||
|
||||
class Child(Entity, Coordinator):
|
||||
async def async_added_to_hass(self) -> None:
|
||||
x = 3
|
||||
""",
|
||||
2,
|
||||
id="multiple_inheritance",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_enforce_super_call_bad(
|
||||
linter: UnittestLinter,
|
||||
hass_enforce_super_call: ModuleType,
|
||||
super_call_checker: BaseChecker,
|
||||
code: str,
|
||||
node_idx: int,
|
||||
) -> None:
|
||||
"""Bad test cases."""
|
||||
root_node = astroid.parse(code, "homeassistant.components.pylint_test")
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(super_call_checker)
|
||||
node = root_node.body[node_idx].body[0]
|
||||
|
||||
with patch.object(
|
||||
hass_enforce_super_call, "METHODS", new={"added_to_hass", "async_added_to_hass"}
|
||||
), assert_adds_messages(
|
||||
linter,
|
||||
MessageTest(
|
||||
msg_id="hass-missing-super-call",
|
||||
node=node,
|
||||
line=node.lineno,
|
||||
args=(node.name,),
|
||||
col_offset=node.col_offset,
|
||||
end_line=node.position.end_lineno,
|
||||
end_col_offset=node.position.end_col_offset,
|
||||
confidence=INFERENCE,
|
||||
),
|
||||
):
|
||||
walker.walk(root_node)
|
Loading…
x
Reference in New Issue
Block a user