Add pylint plugin to check for calls to base implementation (#100432)

This commit is contained in:
Marc Mueller 2023-09-18 20:39:36 +02:00 committed by GitHub
parent ddd62a8f63
commit 37288d7788
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 349 additions and 14 deletions

View File

@ -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."""

View File

@ -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))

View File

@ -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(

View File

@ -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(

View File

@ -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(

View File

@ -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.

View File

@ -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(

View File

@ -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(

View File

@ -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(

View File

@ -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

View File

@ -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(

View File

@ -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)

View File

@ -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))

View File

@ -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))

View File

@ -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:

View 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))

View File

@ -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",

View File

@ -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

View 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)