Add base Entity class to enforce-class-module pylint plugin (#126026)

* Add base Entity class to enforcé-class-module pylint plugin

* Ignore bluetooth

* Ignore hue

* Ignore dominos

* Ignore ffmpeg

* Ignore mqtt

* Ignore microsoft_face

* Ignore plant

* Ignore point

* Ignore rfxtrx

* Ignore template

* Ignore tag

* Ignore deconz
This commit is contained in:
epenet 2024-09-18 20:38:45 +02:00 committed by GitHub
parent 5fcdcbf9b9
commit 6bc2d11c5e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 102 additions and 15 deletions

View File

@ -597,6 +597,7 @@ class PassiveBluetoothDataProcessor[_T, _DataT]:
self.async_update_listeners(new_data, was_available, changed_entity_keys) self.async_update_listeners(new_data, was_available, changed_entity_keys)
# pylint: disable-next=hass-enforce-class-module
class PassiveBluetoothProcessorEntity[ class PassiveBluetoothProcessorEntity[
_PassiveBluetoothDataProcessorT: PassiveBluetoothDataProcessor[Any, Any] _PassiveBluetoothDataProcessorT: PassiveBluetoothDataProcessor[Any, Any]
](Entity): ](Entity):

View File

@ -68,6 +68,7 @@ class DeconzBase[_DeviceT: _DeviceType]:
) )
# pylint: disable-next=hass-enforce-class-module
class DeconzDevice[_DeviceT: _DeviceType](DeconzBase[_DeviceT], Entity): class DeconzDevice[_DeviceT: _DeviceType](DeconzBase[_DeviceT], Entity):
"""Representation of a deCONZ device.""" """Representation of a deCONZ device."""

View File

@ -182,7 +182,7 @@ class DominosProductListView(http.HomeAssistantView):
return self.json(self.dominos.get_menu()) return self.json(self.dominos.get_menu())
class DominosOrder(Entity): class DominosOrder(Entity): # pylint: disable=hass-enforce-class-module
"""Represents a Dominos order entity.""" """Represents a Dominos order entity."""
def __init__(self, order_info, dominos): def __init__(self, order_info, dominos):

View File

@ -176,7 +176,7 @@ class FFmpegManager:
return CONTENT_TYPE_MULTIPART.format("ffserver") return CONTENT_TYPE_MULTIPART.format("ffserver")
class FFmpegBase[_HAFFmpegT: HAFFmpeg](Entity): class FFmpegBase[_HAFFmpegT: HAFFmpeg](Entity): # pylint: disable=hass-enforce-class-module
"""Interface object for FFmpeg.""" """Interface object for FFmpeg."""
_attr_should_poll = False _attr_should_poll = False

View File

@ -165,7 +165,7 @@ class SensorManager:
self._component_add_entities[platform](value) self._component_add_entities[platform](value)
class GenericHueSensor(GenericHueDevice, entity.Entity): class GenericHueSensor(GenericHueDevice, entity.Entity): # pylint: disable=hass-enforce-class-module
"""Representation of a Hue sensor.""" """Representation of a Hue sensor."""
should_poll = False should_poll = False

View File

@ -10,7 +10,7 @@ from ..const import (
) )
class GenericHueDevice(entity.Entity): class GenericHueDevice(entity.Entity): # pylint: disable=hass-enforce-class-module
"""Representation of a Hue device.""" """Representation of a Hue device."""
def __init__(self, sensor, name, bridge, primary_sensor=None): def __init__(self, sensor, name, bridge, primary_sensor=None):

View File

@ -34,7 +34,7 @@ RESOURCE_TYPE_NAMES = {
} }
class HueBaseEntity(Entity): class HueBaseEntity(Entity): # pylint: disable=hass-enforce-class-module
"""Generic Entity Class for a Hue resource.""" """Generic Entity Class for a Hue resource."""
_attr_should_poll = False _attr_should_poll = False

View File

@ -214,7 +214,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True return True
class MicrosoftFaceGroupEntity(Entity): class MicrosoftFaceGroupEntity(Entity): # pylint: disable=hass-enforce-class-module
"""Person-Group state/data Entity.""" """Person-Group state/data Entity."""
_attr_should_poll = False _attr_should_poll = False

View File

@ -369,7 +369,7 @@ def init_entity_id_from_config(
) )
class MqttAttributesMixin(Entity): class MqttAttributesMixin(Entity): # pylint: disable=hass-enforce-class-module
"""Mixin used for platforms that support JSON attributes.""" """Mixin used for platforms that support JSON attributes."""
_attributes_extra_blocked: frozenset[str] = frozenset() _attributes_extra_blocked: frozenset[str] = frozenset()
@ -454,7 +454,7 @@ class MqttAttributesMixin(Entity):
_LOGGER.warning("JSON result was not a dictionary") _LOGGER.warning("JSON result was not a dictionary")
class MqttAvailabilityMixin(Entity): class MqttAvailabilityMixin(Entity): # pylint: disable=hass-enforce-class-module
"""Mixin used for platforms that report availability.""" """Mixin used for platforms that report availability."""
def __init__(self, config: ConfigType) -> None: def __init__(self, config: ConfigType) -> None:
@ -799,7 +799,7 @@ class MqttDiscoveryDeviceUpdateMixin(ABC):
"""Handle the cleanup of platform specific parts, extend to the platform.""" """Handle the cleanup of platform specific parts, extend to the platform."""
class MqttDiscoveryUpdateMixin(Entity): class MqttDiscoveryUpdateMixin(Entity): # pylint: disable=hass-enforce-class-module
"""Mixin used to handle updated discovery message for entity based platforms.""" """Mixin used to handle updated discovery message for entity based platforms."""
def __init__( def __init__(
@ -1021,7 +1021,7 @@ def device_info_from_specifications(
return info return info
class MqttEntityDeviceInfo(Entity): class MqttEntityDeviceInfo(Entity): # pylint: disable=hass-enforce-class-module
"""Mixin used for mqtt platforms that support the device registry.""" """Mixin used for mqtt platforms that support the device registry."""
def __init__( def __init__(

View File

@ -127,7 +127,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True return True
class Plant(Entity): class Plant(Entity): # pylint: disable=hass-enforce-class-module
"""Plant monitors the well-being of a plant. """Plant monitors the well-being of a plant.
It also checks the measurements against It also checks the measurements against

View File

@ -257,7 +257,7 @@ class MinutPointClient:
return await self._client.alarm_arm(home_id) return await self._client.alarm_arm(home_id)
class MinutPointEntity(Entity): class MinutPointEntity(Entity): # pylint: disable=hass-enforce-class-module # see PR 118243
"""Base Entity used by the sensors.""" """Base Entity used by the sensors."""
_attr_should_poll = False _attr_should_poll = False

View File

@ -93,7 +93,7 @@ async def async_setup_entry(
) )
class RfxtrxOffDelayMixin(Entity): class RfxtrxOffDelayMixin(Entity): # pylint: disable=hass-enforce-class-module
"""Mixin to support timeouts on data. """Mixin to support timeouts on data.
Many 433 devices only send data when active. They will Many 433 devices only send data when active. They will

View File

@ -360,7 +360,7 @@ async def async_scan_tag(
_LOGGER.debug("Tag: %s scanned by device: %s", tag_id, device_id) _LOGGER.debug("Tag: %s scanned by device: %s", tag_id, device_id)
class TagEntity(Entity): class TagEntity(Entity): # pylint: disable=hass-enforce-class-module
"""Representation of a Tag entity.""" """Representation of a Tag entity."""
_unrecorded_attributes = frozenset({TAG_ID}) _unrecorded_attributes = frozenset({TAG_ID})

View File

@ -244,7 +244,7 @@ class _TemplateAttribute:
return return
class TemplateEntity(Entity): class TemplateEntity(Entity): # pylint: disable=hass-enforce-class-module
"""Entity that uses templates to calculate attributes.""" """Entity that uses templates to calculate attributes."""
_attr_available = True _attr_available = True

View File

@ -8,6 +8,8 @@ from astroid import nodes
from pylint.checkers import BaseChecker from pylint.checkers import BaseChecker
from pylint.lint import PyLinter from pylint.lint import PyLinter
from homeassistant.const import Platform
_MODULES: dict[str, set[str]] = { _MODULES: dict[str, set[str]] = {
"air_quality": {"AirQualityEntity"}, "air_quality": {"AirQualityEntity"},
"alarm_control_panel": { "alarm_control_panel": {
@ -63,6 +65,7 @@ _MODULES: dict[str, set[str]] = {
"WeatherEntityDescription", "WeatherEntityDescription",
}, },
} }
_PLATFORMS: set[str] = {platform.value for platform in Platform}
class HassEnforceClassModule(BaseChecker): class HassEnforceClassModule(BaseChecker):
@ -89,6 +92,18 @@ class HassEnforceClassModule(BaseChecker):
current_integration = parts[2] current_integration = parts[2]
current_module = parts[3] if len(parts) > 3 else "" current_module = parts[3] if len(parts) > 3 else ""
if current_module != "entity" and current_integration not in _PLATFORMS:
top_level_ancestors = list(node.ancestors(recurs=False))
for ancestor in top_level_ancestors:
if ancestor.name == "Entity":
self.add_message(
"hass-enforce-class-module",
node=node,
args=(ancestor.name, "entity"),
)
return
ancestors: list[ClassDef] | None = None ancestors: list[ClassDef] | None = None
for expected_module, classes in _MODULES.items(): for expected_module, classes in _MODULES.items():

View File

@ -192,3 +192,73 @@ def test_enforce_class_module_bad_nested(
), ),
): ):
walker.walk(root_node) walker.walk(root_node)
@pytest.mark.parametrize(
"path",
[
"homeassistant.components.sensor",
"homeassistant.components.sensor.entity",
"homeassistant.components.pylint_test.entity",
],
)
def test_enforce_entity_good(
linter: UnittestLinter,
enforce_class_module_checker: BaseChecker,
path: str,
) -> None:
"""Good test cases."""
code = """
class Entity:
pass
class CustomEntity(Entity):
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",
[
"homeassistant.components.pylint_test",
"homeassistant.components.pylint_test.select",
"homeassistant.components.pylint_test.select.entity",
],
)
def test_enforce_entity_bad(
linter: UnittestLinter,
enforce_class_module_checker: BaseChecker,
path: str,
) -> None:
"""Good test cases."""
code = """
class Entity:
pass
class CustomEntity(Entity):
pass
"""
root_node = astroid.parse(code, path)
walker = ASTWalker(linter)
walker.add_checker(enforce_class_module_checker)
with assert_adds_messages(
linter,
MessageTest(
msg_id="hass-enforce-class-module",
line=5,
node=root_node.body[1],
args=("Entity", "entity"),
confidence=UNDEFINED,
col_offset=0,
end_line=5,
end_col_offset=18,
),
):
walker.walk(root_node)