Add platform Entity classes to pylint plugin (#125737)

* Add platform Entity classes to pylint plugin

* Fix violations

* Fix violations

* More

* Allow component package with same name as a platform

* One more
This commit is contained in:
epenet 2024-09-16 10:10:53 +02:00 committed by GitHub
parent 3dd6418160
commit 457f63cce0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 109 additions and 43 deletions

View File

@ -25,6 +25,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
)
# pylint: disable-next=hass-enforce-class-module
class HuePresence(GenericZLLSensor, BinarySensorEntity):
"""The presence sensor entity for a Hue motion sensor device."""

View File

@ -305,6 +305,7 @@ def hass_to_hue_brightness(value):
return max(1, round((value / 255) * 254))
# pylint: disable-next=hass-enforce-class-module
class HueLight(CoordinatorEntity, LightEntity):
"""Representation of a Hue light."""

View File

@ -32,10 +32,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
await bridge.sensor_manager.async_register_component("sensor", async_add_entities)
# pylint: disable-next=hass-enforce-class-module
class GenericHueGaugeSensorEntity(GenericZLLSensor, SensorEntity):
"""Parent class for all 'gauge' Hue device sensors."""
# pylint: disable-next=hass-enforce-class-module
class HueLightLevel(GenericHueGaugeSensorEntity):
"""The light level sensor entity for a Hue motion sensor device."""
@ -71,6 +73,7 @@ class HueLightLevel(GenericHueGaugeSensorEntity):
return attributes
# pylint: disable-next=hass-enforce-class-module
class HueTemperature(GenericHueGaugeSensorEntity):
"""The temperature sensor entity for a Hue motion sensor device."""
@ -87,6 +90,7 @@ class HueTemperature(GenericHueGaugeSensorEntity):
return self.sensor.temperature / 100
# pylint: disable-next=hass-enforce-class-module
class HueBattery(GenericHueSensor, SensorEntity):
"""Battery class for when a batt-powered device is only represented as an event."""

View File

@ -82,6 +82,7 @@ async def async_setup_entry(
register_items(api.sensors.tamper, HueTamperSensor)
# pylint: disable-next=hass-enforce-class-module
class HueMotionSensor(HueBaseEntity, BinarySensorEntity):
"""Representation of a Hue Motion sensor."""
@ -103,6 +104,7 @@ class HueMotionSensor(HueBaseEntity, BinarySensorEntity):
return self.resource.motion.value
# pylint: disable-next=hass-enforce-class-module
class HueEntertainmentActiveSensor(HueBaseEntity, BinarySensorEntity):
"""Representation of a Hue Entertainment Configuration as binary sensor."""
@ -126,6 +128,7 @@ class HueEntertainmentActiveSensor(HueBaseEntity, BinarySensorEntity):
return self.resource.metadata.name
# pylint: disable-next=hass-enforce-class-module
class HueContactSensor(HueBaseEntity, BinarySensorEntity):
"""Representation of a Hue Contact sensor."""
@ -147,6 +150,7 @@ class HueContactSensor(HueBaseEntity, BinarySensorEntity):
return self.resource.contact_report.state != ContactState.CONTACT
# pylint: disable-next=hass-enforce-class-module
class HueTamperSensor(HueBaseEntity, BinarySensorEntity):
"""Representation of a Hue Tamper sensor."""

View File

@ -76,6 +76,7 @@ async def async_setup_entry(
)
# pylint: disable-next=hass-enforce-class-module
class GroupedHueLight(HueBaseEntity, LightEntity):
"""Representation of a Grouped Hue light."""

View File

@ -68,6 +68,7 @@ async def async_setup_entry(
)
# pylint: disable-next=hass-enforce-class-module
class HueLight(HueBaseEntity, LightEntity):
"""Representation of a Hue light."""

View File

@ -79,6 +79,7 @@ async def async_setup_entry(
register_items(ctrl_base.zigbee_connectivity, HueZigbeeConnectivitySensor)
# pylint: disable-next=hass-enforce-class-module
class HueSensorBase(HueBaseEntity, SensorEntity):
"""Representation of a Hue sensor."""
@ -94,6 +95,7 @@ class HueSensorBase(HueBaseEntity, SensorEntity):
self.controller = controller
# pylint: disable-next=hass-enforce-class-module
class HueTemperatureSensor(HueSensorBase):
"""Representation of a Hue Temperature sensor."""
@ -111,6 +113,7 @@ class HueTemperatureSensor(HueSensorBase):
return round(self.resource.temperature.value, 1)
# pylint: disable-next=hass-enforce-class-module
class HueLightLevelSensor(HueSensorBase):
"""Representation of a Hue LightLevel (illuminance) sensor."""
@ -139,6 +142,7 @@ class HueLightLevelSensor(HueSensorBase):
}
# pylint: disable-next=hass-enforce-class-module
class HueBatterySensor(HueSensorBase):
"""Representation of a Hue Battery sensor."""
@ -164,6 +168,7 @@ class HueBatterySensor(HueSensorBase):
return {"battery_state": self.resource.power_state.battery_state.value}
# pylint: disable-next=hass-enforce-class-module
class HueZigbeeConnectivitySensor(HueSensorBase):
"""Representation of a Hue ZigbeeConnectivity sensor."""

View File

@ -128,6 +128,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True
# pylint: disable-next=hass-enforce-class-module
class InputButton(collection.CollectionEntity, ButtonEntity, RestoreEntity):
"""Representation of a button."""

View File

@ -246,6 +246,7 @@ class InputSelectStorageCollection(collection.DictStorageCollection):
return {CONF_ID: item[CONF_ID]} | update_data
# pylint: disable-next=hass-enforce-class-module
class InputSelect(collection.CollectionEntity, SelectEntity, RestoreEntity):
"""Representation of a select input."""

View File

@ -3,49 +3,66 @@
from __future__ import annotations
from ast import ClassDef
from dataclasses import dataclass
from astroid import nodes
from pylint.checkers import BaseChecker
from pylint.lint import PyLinter
@dataclass
class ClassModuleMatch:
"""Class for pattern matching."""
expected_module: str
base_class: str
_MODULES = [
ClassModuleMatch("alarm_control_panel", "AlarmControlPanelEntityDescription"),
ClassModuleMatch("assist_satellite", "AssistSatelliteEntityDescription"),
ClassModuleMatch("binary_sensor", "BinarySensorEntityDescription"),
ClassModuleMatch("button", "ButtonEntityDescription"),
ClassModuleMatch("camera", "CameraEntityDescription"),
ClassModuleMatch("climate", "ClimateEntityDescription"),
ClassModuleMatch("coordinator", "DataUpdateCoordinator"),
ClassModuleMatch("cover", "CoverEntityDescription"),
ClassModuleMatch("date", "DateEntityDescription"),
ClassModuleMatch("datetime", "DateTimeEntityDescription"),
ClassModuleMatch("event", "EventEntityDescription"),
ClassModuleMatch("image", "ImageEntityDescription"),
ClassModuleMatch("image_processing", "ImageProcessingEntityDescription"),
ClassModuleMatch("lawn_mower", "LawnMowerEntityDescription"),
ClassModuleMatch("lock", "LockEntityDescription"),
ClassModuleMatch("media_player", "MediaPlayerEntityDescription"),
ClassModuleMatch("notify", "NotifyEntityDescription"),
ClassModuleMatch("number", "NumberEntityDescription"),
ClassModuleMatch("select", "SelectEntityDescription"),
ClassModuleMatch("sensor", "SensorEntityDescription"),
ClassModuleMatch("text", "TextEntityDescription"),
ClassModuleMatch("time", "TimeEntityDescription"),
ClassModuleMatch("update", "UpdateEntityDescription"),
ClassModuleMatch("vacuum", "VacuumEntityDescription"),
ClassModuleMatch("water_heater", "WaterHeaterEntityDescription"),
ClassModuleMatch("weather", "WeatherEntityDescription"),
]
_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"},
"camera": {"CameraEntity", "CameraEntityDescription"},
"climate": {"ClimateEntity", "ClimateEntityDescription"},
"coordinator": {"DataUpdateCoordinator"},
"conversation": {"ConversationEntity"},
"cover": {"CoverEntity", "CoverEntityDescription"},
"date": {"DateEntity", "DateEntityDescription"},
"datetime": {"DateTimeEntity", "DateTimeEntityDescription"},
"device_tracker": {"DeviceTrackerEntity"},
"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": {"UpdateEntityDescription"},
"vacuum": {"VacuumEntity", "VacuumEntityDescription"},
"wake_word": {"WakeWordDetectionEntity"},
"water_heater": {"WaterHeaterEntity"},
"weather": {
"CoordinatorWeatherEntity",
"SingleCoordinatorWeatherEntity",
"WeatherEntity",
"WeatherEntityDescription",
},
}
class HassEnforceClassModule(BaseChecker):
@ -69,24 +86,24 @@ class HassEnforceClassModule(BaseChecker):
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[ClassDef] | None = None
for match in _MODULES:
# Allow module.py and module/sub_module.py
if current_module == match.expected_module:
for expected_module, classes in _MODULES.items():
if expected_module in (current_module, current_integration):
continue
if ancestors is None:
ancestors = list(node.ancestors()) # cache result for other modules
for ancestor in ancestors:
if ancestor.name == match.base_class:
if ancestor.name in classes:
self.add_message(
"hass-enforce-class-module",
node=node,
args=(match.base_class, match.expected_module),
args=(ancestor.name, expected_module),
)
return

View File

@ -63,6 +63,36 @@ def test_enforce_class_module_good(
walker.walk(root_node)
@pytest.mark.parametrize(
"path",
[
"homeassistant.components.sensor",
"homeassistant.components.sensor.entity",
"homeassistant.components.pylint_test.sensor",
"homeassistant.components.pylint_test.sensor.entity",
],
)
def test_enforce_class_platform_good(
linter: UnittestLinter,
enforce_class_module_checker: BaseChecker,
path: str,
) -> None:
"""Good test cases."""
code = """
class SensorEntity:
pass
class CustomSensorEntity(SensorEntity):
pass
"""
root_node = astroid.parse(code, path)
walker = ASTWalker(linter)
walker.add_checker(enforce_class_module_checker)
with assert_no_messages(linter):
walker.walk(root_node)
@pytest.mark.parametrize(
"path",
[