Skip to content

Commit

Permalink
Add pylint plugin to check for calls to base implementation (#100432)
Browse files Browse the repository at this point in the history
  • Loading branch information
cdce8p authored Sep 18, 2023
1 parent ddd62a8 commit 37288d7
Show file tree
Hide file tree
Showing 19 changed files with 349 additions and 14 deletions.
1 change: 1 addition & 0 deletions homeassistant/components/airvisual/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,7 @@ def __init__(
self._entry = entry
self.entity_description = description

# pylint: disable-next=hass-missing-super-call
async def async_added_to_hass(self) -> None:
"""Register callbacks."""

Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/flo/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ def async_update_state(self) -> None:
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))
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/hunterdouglas_powerview/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ async def _async_force_refresh_state(self) -> None:
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(
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/hunterdouglas_powerview/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ def native_value(self) -> int:
"""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(
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/hvv_departures/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ def extra_state_attributes(self) -> dict[str, Any] | None:
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(
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/isy994/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ def target_value(self) -> Any:
"""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.
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/isy994/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ def __init__(
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(
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/livisi/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def __init__(
)
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(
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/lutron_caseta/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ def is_on(self):
"""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(
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/rflink/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ def _handle_event(self, event):
"""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
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/risco/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def _refresh_from_coordinator(self) -> None:
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(
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/risco/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ def __init__(
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)
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/shelly/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ def __init__(self, coordinator: ShellyBlockCoordinator, block: Block) -> None:
)
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))
Expand Down Expand Up @@ -375,6 +376,7 @@ def status(self) -> dict:
"""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))
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/smart_meter_texas/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ def _state_update(self):
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))
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/tractive/device_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ def _handle_position_update(self, event: dict[str, Any]) -> None:
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:
Expand Down
79 changes: 79 additions & 0 deletions pylint/plugins/hass_enforce_super_call.py
Original file line number Diff line number Diff line change
@@ -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))
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
46 changes: 32 additions & 14 deletions tests/pylint/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,11 @@
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

Expand All @@ -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."""
Expand All @@ -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")
Expand All @@ -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
Loading

0 comments on commit 37288d7

Please sign in to comment.