diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 55a32670288..706e3136a8a 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -13,6 +13,7 @@ import logging import math import sys from timeit import default_timer as timer +from types import FunctionType from typing import ( TYPE_CHECKING, Any, @@ -381,6 +382,9 @@ class CachedProperties(type): # Check if an _attr_ class attribute exits and move it to __attr_. We check # __dict__ here because we don't care about _attr_ class attributes in parents. if attr_name in cls.__dict__: + attr = getattr(cls, attr_name) + if isinstance(attr, (FunctionType, property)): + raise TypeError(f"Can't override {attr_name} in subclass") setattr(cls, private_attr_name, getattr(cls, attr_name)) annotations = cls.__annotations__ if attr_name in annotations: diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index dd26b947f67..1dc878a8eba 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -2406,6 +2406,47 @@ async def test_cached_entity_property_class_attribute(hass: HomeAssistant) -> No assert getattr(ent[1], property) == values[0] +async def test_cached_entity_property_override(hass: HomeAssistant) -> None: + """Test overriding cached _attr_ raises.""" + + class EntityWithClassAttribute1(entity.Entity): + """A derived class which overrides an _attr_ from a parent.""" + + _attr_attribution: str + + class EntityWithClassAttribute2(entity.Entity): + """A derived class which overrides an _attr_ from a parent.""" + + _attr_attribution = "blabla" + + class EntityWithClassAttribute3(entity.Entity): + """A derived class which overrides an _attr_ from a parent.""" + + _attr_attribution: str = "blabla" + + class EntityWithClassAttribute4(entity.Entity): + @property + def _attr_not_cached(self): + return "blabla" + + class EntityWithClassAttribute5(entity.Entity): + def _attr_not_cached(self): + return "blabla" + + with pytest.raises(TypeError): + + class EntityWithClassAttribute6(entity.Entity): + @property + def _attr_attribution(self): + return "🤡" + + with pytest.raises(TypeError): + + class EntityWithClassAttribute7(entity.Entity): + def _attr_attribution(self): + return "🤡" + + async def test_entity_report_deprecated_supported_features_values( caplog: pytest.LogCaptureFixture, ) -> None: