From 5b84e50dc0158474b4bd8f9dfc668e9e23852647 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 9 Jan 2024 19:16:45 +0100 Subject: [PATCH] Prevent overriding cached attribute as property (#107657) * Prevent overriding cached attribute as property * Remove debug --- homeassistant/helpers/entity.py | 4 ++++ tests/helpers/test_entity.py | 41 +++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 743d3675a3b..1f3f96f300c 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, @@ -374,6 +375,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 a18d8963947..2e2aac570ea 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -2039,6 +2039,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: