From 6bc2d11c5e988d000b8408327e6ab2679dd0577e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 Sep 2024 20:38:45 +0200 Subject: [PATCH] Add base Entity class to enforce-class-module pylint plugin (#126026) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- .../bluetooth/passive_update_processor.py | 1 + .../components/deconz/deconz_device.py | 1 + homeassistant/components/dominos/__init__.py | 2 +- homeassistant/components/ffmpeg/__init__.py | 2 +- .../components/hue/v1/sensor_base.py | 2 +- .../components/hue/v1/sensor_device.py | 2 +- homeassistant/components/hue/v2/entity.py | 2 +- .../components/microsoft_face/__init__.py | 2 +- homeassistant/components/mqtt/mixins.py | 8 +-- homeassistant/components/plant/__init__.py | 2 +- homeassistant/components/point/__init__.py | 2 +- homeassistant/components/rfxtrx/siren.py | 2 +- homeassistant/components/tag/__init__.py | 2 +- .../components/template/template_entity.py | 2 +- pylint/plugins/hass_enforce_class_module.py | 15 ++++ tests/pylint/test_enforce_class_module.py | 70 +++++++++++++++++++ 16 files changed, 102 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index 3e7e4e96659..8f66a3582ea 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -597,6 +597,7 @@ class PassiveBluetoothDataProcessor[_T, _DataT]: self.async_update_listeners(new_data, was_available, changed_entity_keys) +# pylint: disable-next=hass-enforce-class-module class PassiveBluetoothProcessorEntity[ _PassiveBluetoothDataProcessorT: PassiveBluetoothDataProcessor[Any, Any] ](Entity): diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index 8551ad33cf5..48cf94ea5aa 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -68,6 +68,7 @@ class DeconzBase[_DeviceT: _DeviceType]: ) +# pylint: disable-next=hass-enforce-class-module class DeconzDevice[_DeviceT: _DeviceType](DeconzBase[_DeviceT], Entity): """Representation of a deCONZ device.""" diff --git a/homeassistant/components/dominos/__init__.py b/homeassistant/components/dominos/__init__.py index 9b11b667e84..609cb93ba0d 100644 --- a/homeassistant/components/dominos/__init__.py +++ b/homeassistant/components/dominos/__init__.py @@ -182,7 +182,7 @@ class DominosProductListView(http.HomeAssistantView): return self.json(self.dominos.get_menu()) -class DominosOrder(Entity): +class DominosOrder(Entity): # pylint: disable=hass-enforce-class-module """Represents a Dominos order entity.""" def __init__(self, order_info, dominos): diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py index 5e1be36f398..94503108deb 100644 --- a/homeassistant/components/ffmpeg/__init__.py +++ b/homeassistant/components/ffmpeg/__init__.py @@ -176,7 +176,7 @@ class FFmpegManager: 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.""" _attr_should_poll = False diff --git a/homeassistant/components/hue/v1/sensor_base.py b/homeassistant/components/hue/v1/sensor_base.py index bac02c45209..393069b0c7c 100644 --- a/homeassistant/components/hue/v1/sensor_base.py +++ b/homeassistant/components/hue/v1/sensor_base.py @@ -165,7 +165,7 @@ class SensorManager: 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.""" should_poll = False diff --git a/homeassistant/components/hue/v1/sensor_device.py b/homeassistant/components/hue/v1/sensor_device.py index 1ff97af2e62..cb0a2721334 100644 --- a/homeassistant/components/hue/v1/sensor_device.py +++ b/homeassistant/components/hue/v1/sensor_device.py @@ -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.""" def __init__(self, sensor, name, bridge, primary_sensor=None): diff --git a/homeassistant/components/hue/v2/entity.py b/homeassistant/components/hue/v2/entity.py index 6575d7f4702..e472009286d 100644 --- a/homeassistant/components/hue/v2/entity.py +++ b/homeassistant/components/hue/v2/entity.py @@ -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.""" _attr_should_poll = False diff --git a/homeassistant/components/microsoft_face/__init__.py b/homeassistant/components/microsoft_face/__init__.py index fa4de7f9c99..6a7e2d42fd9 100644 --- a/homeassistant/components/microsoft_face/__init__.py +++ b/homeassistant/components/microsoft_face/__init__.py @@ -214,7 +214,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -class MicrosoftFaceGroupEntity(Entity): +class MicrosoftFaceGroupEntity(Entity): # pylint: disable=hass-enforce-class-module """Person-Group state/data Entity.""" _attr_should_poll = False diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index ce811e13a24..b1c7c6edadb 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -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.""" _attributes_extra_blocked: frozenset[str] = frozenset() @@ -454,7 +454,7 @@ class MqttAttributesMixin(Entity): _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.""" def __init__(self, config: ConfigType) -> None: @@ -799,7 +799,7 @@ class MqttDiscoveryDeviceUpdateMixin(ABC): """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.""" def __init__( @@ -1021,7 +1021,7 @@ def device_info_from_specifications( return info -class MqttEntityDeviceInfo(Entity): +class MqttEntityDeviceInfo(Entity): # pylint: disable=hass-enforce-class-module """Mixin used for mqtt platforms that support the device registry.""" def __init__( diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py index c6e527290df..b3e1084f501 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -127,7 +127,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -class Plant(Entity): +class Plant(Entity): # pylint: disable=hass-enforce-class-module """Plant monitors the well-being of a plant. It also checks the measurements against diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index acfa53ae215..dc461f7200e 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -257,7 +257,7 @@ class MinutPointClient: 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.""" _attr_should_poll = False diff --git a/homeassistant/components/rfxtrx/siren.py b/homeassistant/components/rfxtrx/siren.py index 67a0c6b7dce..17112619acb 100644 --- a/homeassistant/components/rfxtrx/siren.py +++ b/homeassistant/components/rfxtrx/siren.py @@ -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. Many 433 devices only send data when active. They will diff --git a/homeassistant/components/tag/__init__.py b/homeassistant/components/tag/__init__.py index 0462c5bec34..160408732c9 100644 --- a/homeassistant/components/tag/__init__.py +++ b/homeassistant/components/tag/__init__.py @@ -360,7 +360,7 @@ async def async_scan_tag( _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.""" _unrecorded_attributes = frozenset({TAG_ID}) diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index a074f828284..8930edc03e6 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -244,7 +244,7 @@ class _TemplateAttribute: return -class TemplateEntity(Entity): +class TemplateEntity(Entity): # pylint: disable=hass-enforce-class-module """Entity that uses templates to calculate attributes.""" _attr_available = True diff --git a/pylint/plugins/hass_enforce_class_module.py b/pylint/plugins/hass_enforce_class_module.py index fe233d4afe7..0fce0e13f63 100644 --- a/pylint/plugins/hass_enforce_class_module.py +++ b/pylint/plugins/hass_enforce_class_module.py @@ -8,6 +8,8 @@ from astroid import nodes from pylint.checkers import BaseChecker from pylint.lint import PyLinter +from homeassistant.const import Platform + _MODULES: dict[str, set[str]] = { "air_quality": {"AirQualityEntity"}, "alarm_control_panel": { @@ -63,6 +65,7 @@ _MODULES: dict[str, set[str]] = { "WeatherEntityDescription", }, } +_PLATFORMS: set[str] = {platform.value for platform in Platform} class HassEnforceClassModule(BaseChecker): @@ -89,6 +92,18 @@ class HassEnforceClassModule(BaseChecker): current_integration = parts[2] 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 for expected_module, classes in _MODULES.items(): diff --git a/tests/pylint/test_enforce_class_module.py b/tests/pylint/test_enforce_class_module.py index db7daf0a258..8927147e89a 100644 --- a/tests/pylint/test_enforce_class_module.py +++ b/tests/pylint/test_enforce_class_module.py @@ -192,3 +192,73 @@ def test_enforce_class_module_bad_nested( ), ): 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)