Add home_assistant_light_color_mode pylint checker (#180922)

This commit is contained in:
Erik Montnemery
2026-09-01 12:58:55 +02:00
committed by GitHub
parent 4b65ed7ef0
commit bbf0a5e235
4 changed files with 784 additions and 0 deletions
+39
View File
@@ -140,6 +140,8 @@ Every check has a code following the
| `W7433` | [`home-assistant-missing-test-before-configure`](#w7433-home-assistant-missing-test-before-configure) | Config flow should test the connection before creating an entry |
| `W7434` | [`home-assistant-config-flow-menu-missing-step`](#w7434-home-assistant-config-flow-menu-missing-step) | `async_show_menu` option has no matching `async_step_*` method |
| `W7435` | [`home-assistant-json-fixture`](#w7435-home-assistant-json-fixture) | Use a JSON fixture helper instead of parsing a loaded fixture |
| `W7436` | [`home-assistant-light-missing-color-mode`](#w7436-home-assistant-light-missing-color-mode) | Light entity sets supported color modes but does not report a `color_mode` |
| `W7437` | [`home-assistant-light-missing-supported-color-modes`](#w7437-home-assistant-light-missing-supported-color-modes) | Light entity reports a `color_mode` but does not set supported color modes |
## `home_assistant_logger` checker
@@ -1003,3 +1005,40 @@ Use the dedicated helper that loads and parses in one step instead:
data = load_json_object_fixture("data.json", DOMAIN)
data = await async_load_json_object_fixture(hass, "data.json", DOMAIN)
```
## `home_assistant_light_color_mode` checker
A `LightEntity` must report **both** `supported_color_modes` and a current
`color_mode`; setting one without the other raises `HomeAssistantError` at
runtime. These two checks flag each half of that inconsistency. A light
that sets *neither* is deliberately not flagged: the realistic both-missing
class is an abstract base, so flagging it would false-positive; the tradeoff
is that a concrete both-missing light, which also raises, is not caught.
A value is considered *provided* by a class when its effective declaration,
resolved in MRO order and excluding `LightEntity`'s own `None` defaults, is a
non-`None` class-body `_attr_...` assignment, a `self._attr_... = ...`
assignment in a method body, or a property/method override of the public
name. Subclass shadowing is respected: a subclass that assigns the
`_attr_...` to `None` nullifies a non-`None` value inherited from an
ancestor. Mixin/abstract bases that are subclassed by another class in the
same module are exempted, on the assumption that the concrete subclass is the
runtime entity (and may supply the missing half).
### `W7436`: `home-assistant-light-missing-color-mode`
The light provides `supported_color_modes` but no `color_mode`. At runtime
`LightEntity.state_attributes` raises `HomeAssistantError` ("does not report a
color mode") whenever the light is on and `color_mode` is `None` -- there is
no inference of the mode from a single supported mode, so this holds even for
lights that support only `ONOFF` or `BRIGHTNESS`. Set `_attr_color_mode` or
override the `color_mode` property.
### `W7437`: `home-assistant-light-missing-supported-color-modes`
The light provides `color_mode` but no `supported_color_modes`. At runtime
`LightEntity._light_internal_supported_color_modes` raises `HomeAssistantError`
("does not set supported color modes") from both `state_attributes` and
`capability_attributes` whenever `supported_color_modes` is `None`. Set
`_attr_supported_color_modes` or override the `supported_color_modes` property.
@@ -0,0 +1,288 @@
"""Checker for light entities that report only one of the color-mode attributes.
A ``LightEntity`` must report **both** ``supported_color_modes`` and
a current ``color_mode``; setting one without the other raises
``HomeAssistantError`` at runtime. These two checks flag each half of that
inconsistency. A light that sets *neither* is deliberately not flagged: the
realistic both-missing class is an abstract base (which the concrete
subclass completes), so flagging it would produce false positives — the
tradeoff is that a concrete both-missing light, which also raises at
runtime, is not caught.
A value is considered *provided* by a class when its effective declaration,
resolved in MRO order and excluding ``LightEntity``'s own ``None`` defaults,
is one of:
- a non-``None`` class-body ``_attr_...`` assignment,
- a ``self._attr_... = ...`` assignment in a method body, or
- a property/method override of the public name.
Subclass shadowing is respected: a subclass that assigns the ``_attr_...``
to ``None`` nullifies a non-``None`` value inherited from an ancestor, so
the pair is treated as unset from that subclass down.
Mixin/abstract bases that are subclassed by another class in the same
module are exempted, on the assumption that the concrete subclass is the
runtime entity (and may supply the missing half itself).
``W7436`` (``home-assistant-light-missing-color-mode``)
-------------------------------------------------------
Fires when ``supported_color_modes`` is provided but ``color_mode`` is not.
At runtime ``LightEntity.state_attributes`` raises ``HomeAssistantError``
("does not report a color mode") whenever the light is on and
``color_mode`` is ``None`` — there is no inference of the mode from a
single supported mode, so this holds even for lights that support only
``ONOFF`` or ``BRIGHTNESS``.
``W7437`` (``home-assistant-light-missing-supported-color-modes``)
------------------------------------------------------------------
Fires when ``color_mode`` is provided but ``supported_color_modes`` is not.
At runtime ``LightEntity._light_internal_supported_color_modes`` raises
``HomeAssistantError`` ("does not set supported color modes") from both
``state_attributes`` and ``capability_attributes`` whenever
``supported_color_modes`` is ``None``.
Known limitations:
- A base defined in one module whose missing half is only supplied by
subclasses in a *different* module is flagged, because the subclasses are
not visible when the base's module is linted. Suppress with a
``# pylint: disable=...`` on the base.
- A property override is treated as providing the value regardless of what
it returns; a property that returns ``None`` at runtime is the
integration's responsibility.
"""
import astroid
from astroid import nodes
from pylint.checkers import BaseChecker
from pylint.lint import PyLinter
from pylint_home_assistant.helpers.ast_utils import extended_ancestors
from pylint_home_assistant.helpers.entity_class import (
LIGHT_ENTITY_QNAME,
collect_same_module_ancestor_qnames,
inherits_from_light_entity,
)
from pylint_home_assistant.helpers.module_info import is_integration_module
_SUPPORTED_ATTR = "_attr_supported_color_modes"
_SUPPORTED_PROPERTY = "supported_color_modes"
_COLOR_MODE_ATTR = "_attr_color_mode"
_COLOR_MODE_PROPERTY = "color_mode"
def _is_non_none_value(value: nodes.NodeNG | None) -> bool:
"""Return True if the AST value is present and not a literal ``None``."""
if value is None:
return False
return not (isinstance(value, nodes.Const) and value.value is None)
def _is_self_attr_target(target: nodes.NodeNG, attr_name: str) -> bool:
"""Return True if *target* is ``self.<attr_name>``."""
match target:
case nodes.AssignAttr(attrname=name, expr=nodes.Name(name="self")) if (
name == attr_name
):
return True
return False
def _class_body_attr_state(class_node: nodes.ClassDef, attr_name: str) -> bool | None:
"""Return the effect of the class body's final assignment to *attr_name*.
``True`` if the last source-order assignment sets a non-``None`` value,
``False`` if it sets a literal ``None``, or ``None`` if the class body
does not assign *attr_name* at all. Later assignments win, matching
Python's class-body evaluation, so ``x = ColorMode.HS`` followed by
``x = None`` resolves to ``False``. Annotation-only statements (``x: T``
with no value) are not assignments and are ignored.
"""
state: bool | None = None
for item in class_node.body:
match item:
case nodes.AnnAssign(target=nodes.AssignName(name=name), value=value) if (
name == attr_name and value is not None
):
state = _is_non_none_value(value)
case nodes.Assign(targets=targets, value=value) if any(
isinstance(t, nodes.AssignName) and t.name == attr_name for t in targets
):
state = _is_non_none_value(value)
return state
def _method_sets_self_attr(class_node: nodes.ClassDef, attr_name: str) -> bool:
"""Return True if any method assigns ``self.<attr_name> = <non-None>``.
The assignment need not be unconditional: any assignment means the class
reports the attribute and must therefore also report the paired value.
Assignments in nested functions or classes (which have their own
``self``) are ignored — only the method's own scope counts.
"""
for method in class_node.body:
if not isinstance(method, nodes.FunctionDef | nodes.AsyncFunctionDef):
continue
for stmt in method.nodes_of_class((nodes.Assign, nodes.AnnAssign)):
if stmt.scope() is not method:
continue
match stmt:
case nodes.Assign(targets=targets, value=value):
target_list = list(targets)
case nodes.AnnAssign(target=target, value=value):
target_list = [target]
case _:
continue
if _is_non_none_value(value) and any(
_is_self_attr_target(t, attr_name) for t in target_list
):
return True
return False
def _class_defines_method(class_node: nodes.ClassDef, method_name: str) -> bool:
"""Return True if the class body overrides *method_name* (property/method)."""
return any(
isinstance(item, nodes.FunctionDef | nodes.AsyncFunctionDef)
and item.name == method_name
for item in class_node.body
)
def _class_declaration(
class_node: nodes.ClassDef, attr_name: str, property_name: str
) -> bool | None:
"""Return this class's *effective* declaration for the attr/property.
``True`` if the class provides a value, ``False`` if it nullifies an
inherited value (class-body ``_attr_... = None``), or ``None`` if the
class does not declare the pair at all. ``LightEntity`` declares the
``None`` defaults, so it resolves to ``False`` — reaching it means no
subclass provided a value.
Precedence within the class follows runtime resolution: a
``property``/method override or a non-``None`` ``self._attr_...``
assignment wins over a class-body ``_attr_... = None``.
"""
if class_node.qname() == LIGHT_ENTITY_QNAME:
return False
if _class_defines_method(class_node, property_name):
return True
if _method_sets_self_attr(class_node, attr_name):
return True
return _class_body_attr_state(class_node, attr_name)
def _mro(class_node: nodes.ClassDef) -> list[nodes.ClassDef]:
"""Return the class's MRO, falling back to a DFS ancestor walk."""
try:
return class_node.mro() # type: ignore[no-any-return]
except astroid.exceptions.MroError:
return [class_node, *extended_ancestors(class_node)]
def _provides_effective(
class_node: nodes.ClassDef, attr_name: str, property_name: str
) -> bool:
"""Return True if the effective value for *class_node* is provided.
Walks the MRO most-derived first and returns the first class that
declares the pair, so a subclass ``_attr_... = None`` shadows a
non-``None`` value set by an ancestor.
"""
for klass in _mro(class_node):
decl = _class_declaration(klass, attr_name, property_name)
if decl is not None:
return decl
return False
class HassLightColorModeChecker(BaseChecker):
"""Flag light entities that report only one of the color-mode attributes."""
name = "home_assistant_light_color_mode"
priority = -1
msgs = {
"W7436": (
(
"Light entity class `%s` reports supported color modes but "
"does not report a color mode; set `_attr_color_mode` or "
"override the `color_mode` property"
),
"home-assistant-light-missing-color-mode",
(
"Used when a LightEntity subclass provides "
"supported_color_modes (via _attr_supported_color_modes or a "
"supported_color_modes override) but neither sets "
"_attr_color_mode nor overrides the color_mode property. Such "
"a light raises HomeAssistantError at runtime because it does "
"not report a color mode when turned on."
),
),
"W7437": (
(
"Light entity class `%s` reports a color mode but does not "
"report supported color modes; set "
"`_attr_supported_color_modes` or override the "
"`supported_color_modes` property"
),
"home-assistant-light-missing-supported-color-modes",
(
"Used when a LightEntity subclass provides color_mode (via "
"_attr_color_mode or a color_mode override) but neither sets "
"_attr_supported_color_modes nor overrides the "
"supported_color_modes property. Such a light raises "
"HomeAssistantError at runtime because it does not set "
"supported color modes."
),
),
}
options = ()
_check_module: bool
_subclassed_qnames: set[str]
def visit_module(self, node: nodes.Module) -> None:
"""Cache per-module state."""
self._check_module = is_integration_module(node.name)
self._subclassed_qnames = (
collect_same_module_ancestor_qnames(node) if self._check_module else set()
)
def visit_classdef(self, node: nodes.ClassDef) -> None:
"""Flag light entities reporting only one of the color-mode attributes."""
if not self._check_module:
return
# Skip mixin / abstract bases: another class in the same module
# inherits from this one, so this class is not the runtime entity.
if node.qname() in self._subclassed_qnames:
return
if not inherits_from_light_entity(node):
return
provides_supported = _provides_effective(
node, _SUPPORTED_ATTR, _SUPPORTED_PROPERTY
)
provides_color_mode = _provides_effective(
node, _COLOR_MODE_ATTR, _COLOR_MODE_PROPERTY
)
# Only the XOR is flagged: reporting both is correct, and reporting
# neither is skipped to avoid false positives on abstract bases (a
# concrete both-missing light also raises but is not caught).
if provides_supported and not provides_color_mode:
self.add_message(
"home-assistant-light-missing-color-mode",
node=node,
args=(node.name,),
)
elif provides_color_mode and not provides_supported:
self.add_message(
"home-assistant-light-missing-supported-color-modes",
node=node,
args=(node.name,),
)
def register(linter: PyLinter) -> None:
"""Register the checker."""
linter.register_checker(HassLightColorModeChecker(linter))
@@ -5,6 +5,7 @@ from astroid import nodes
from .ast_utils import extended_ancestors
ENTITY_QNAME = "homeassistant.helpers.entity.Entity"
LIGHT_ENTITY_QNAME = "homeassistant.components.light.LightEntity"
def inherits_from_entity(class_node: nodes.ClassDef) -> bool:
@@ -12,6 +13,11 @@ def inherits_from_entity(class_node: nodes.ClassDef) -> bool:
return any(a.qname() == ENTITY_QNAME for a in extended_ancestors(class_node))
def inherits_from_light_entity(class_node: nodes.ClassDef) -> bool:
"""Return True if class inherits from ``LightEntity``."""
return any(a.qname() == LIGHT_ENTITY_QNAME for a in extended_ancestors(class_node))
def collect_same_module_ancestor_qnames(module: nodes.Module) -> set[str]:
"""Return qnames of every class used as an ancestor in *module*.
+451
View File
@@ -0,0 +1,451 @@
"""Tests for the light_color_mode pylint checker."""
import json
from pathlib import Path
import astroid
from astroid import nodes
from pylint.testutils import MessageTest, UnittestLinter
from pylint_home_assistant.checkers.light_color_mode import HassLightColorModeChecker
import pytest
from . import assert_adds_messages, assert_no_messages, walk_checker
_MISSING_COLOR_MODE = "home-assistant-light-missing-color-mode"
_MISSING_SUPPORTED = "home-assistant-light-missing-supported-color-modes"
@pytest.fixture(name="checker")
def checker_fixture(linter: UnittestLinter) -> HassLightColorModeChecker:
"""Fixture to provide the W7436 + W7437 checker."""
return HassLightColorModeChecker(linter)
def _make_integration(tmp_path: Path, *, domain: str = "test_integration") -> Path:
"""Create a fake integration directory under components/."""
integration_dir = tmp_path / "homeassistant" / "components" / "test_integration"
integration_dir.mkdir(parents=True)
(integration_dir / "manifest.json").write_text(json.dumps({"domain": domain}))
return integration_dir
def _parse(
code: str,
integration_dir: Path,
module_name: str = "homeassistant.components.test_integration.light",
file_name: str = "light.py",
) -> nodes.Module:
"""Parse code as a module of the integration with .file set."""
root_node = astroid.parse(code, module_name)
root_node.file = str(integration_dir / file_name)
return root_node
def _find_class(root_node: nodes.Module, name: str) -> nodes.ClassDef:
"""Return the ClassDef named *name*."""
for class_node in root_node.nodes_of_class(nodes.ClassDef):
if class_node.name == name:
return class_node
raise AssertionError(f"no class named {name} found")
def _expect(class_node: nodes.ClassDef, msg_id: str) -> MessageTest:
"""Build the expected MessageTest for a flagged class."""
pos = class_node.position
return MessageTest(
msg_id=msg_id,
node=class_node,
line=pos.lineno,
col_offset=pos.col_offset,
end_line=pos.end_lineno,
end_col_offset=pos.end_col_offset,
args=(class_node.name,),
)
@pytest.mark.parametrize(
("code", "class_name"),
[
pytest.param(
"""
from homeassistant.components.light import ColorMode, LightEntity
class MyLight(LightEntity):
_attr_supported_color_modes = {ColorMode.ONOFF}
""",
"MyLight",
id="class_body_supported_no_color_mode",
),
pytest.param(
"""
from homeassistant.components.light import ColorMode, LightEntity
class MyLight(LightEntity):
def __init__(self, status) -> None:
modes = {ColorMode.HS}
self._attr_supported_color_modes = modes
""",
"MyLight",
id="self_assign_supported_no_color_mode",
),
pytest.param(
"""
from homeassistant.components.light import ColorMode, LightEntity
class MyLight(LightEntity):
@property
def supported_color_modes(self):
return {ColorMode.HS}
""",
"MyLight",
id="supported_property_override_no_color_mode",
),
pytest.param(
"""
from homeassistant.components.light import ColorMode, LightEntity
class MyLight(LightEntity):
_attr_supported_color_modes = {ColorMode.HS}
_attr_color_mode = None
""",
"MyLight",
id="color_mode_explicitly_none",
),
pytest.param(
"""
from homeassistant.components.light import ColorMode, LightEntity
class MyLight(LightEntity):
_attr_supported_color_modes = {ColorMode.HS}
_attr_color_mode = ColorMode.HS
_attr_color_mode = None
""",
"MyLight",
id="color_mode_reassigned_none_wins",
),
pytest.param(
"""
from homeassistant.components.light import ColorMode, LightEntity
class MyLight(LightEntity):
_attr_supported_color_modes = {ColorMode.HS}
def _factory(self):
class _Inner:
def run(self):
self._attr_color_mode = ColorMode.HS
return _Inner
""",
"MyLight",
id="color_mode_only_in_nested_class_scope",
),
pytest.param(
"""
from homeassistant.components.light import ColorMode, LightEntity
class MyBaseLight(LightEntity):
_attr_supported_color_modes = {ColorMode.HS}
class MyLight(MyBaseLight):
pass
""",
"MyLight",
id="supported_inherited_color_mode_missing",
),
pytest.param(
"""
from homeassistant.components.light import ColorMode, LightEntity
class MyBaseLight(LightEntity):
_attr_supported_color_modes = {ColorMode.HS}
_attr_color_mode = ColorMode.HS
class MyLight(MyBaseLight):
_attr_color_mode = None
""",
"MyLight",
id="subclass_nullifies_inherited_color_mode",
),
pytest.param(
"""
from homeassistant.components.light import ColorMode, LightEntity
class MyBaseLight(LightEntity):
pass
class MyColorLight(MyBaseLight):
def __init__(self, status) -> None:
self._attr_supported_color_modes = {ColorMode.HS}
@property
def color_mode(self):
return ColorMode.HS
class MySwitchLight(MyBaseLight):
def __init__(self, status) -> None:
self._attr_supported_color_modes = {ColorMode.ONOFF}
""",
"MySwitchLight",
id="two_subclasses_only_offender",
),
],
)
def test_fires_w7436(
linter: UnittestLinter,
checker: HassLightColorModeChecker,
tmp_path: Path,
code: str,
class_name: str,
) -> None:
"""W7436 fires when supported color modes are set but no color mode is reported."""
integration_dir = _make_integration(tmp_path)
root_node = _parse(code, integration_dir)
class_node = _find_class(root_node, class_name)
with assert_adds_messages(linter, _expect(class_node, _MISSING_COLOR_MODE)):
walk_checker(linter, checker, root_node)
@pytest.mark.parametrize(
("code", "class_name"),
[
pytest.param(
"""
from homeassistant.components.light import ColorMode, LightEntity
class MyLight(LightEntity):
_attr_color_mode = ColorMode.ONOFF
""",
"MyLight",
id="class_body_color_mode_no_supported",
),
pytest.param(
"""
from homeassistant.components.light import ColorMode, LightEntity
class MyLight(LightEntity):
def __init__(self, status) -> None:
self._attr_color_mode = ColorMode.HS
""",
"MyLight",
id="self_assign_color_mode_no_supported",
),
pytest.param(
"""
from homeassistant.components.light import ColorMode, LightEntity
class MyLight(LightEntity):
@property
def color_mode(self):
return ColorMode.HS
""",
"MyLight",
id="color_mode_property_override_no_supported",
),
pytest.param(
"""
from homeassistant.components.light import ColorMode, LightEntity
class MyBaseLight(LightEntity):
_attr_color_mode = ColorMode.HS
class MyLight(MyBaseLight):
pass
""",
"MyLight",
id="color_mode_inherited_supported_missing",
),
pytest.param(
"""
from homeassistant.components.light import ColorMode, LightEntity
class MyBaseLight(LightEntity):
_attr_supported_color_modes = {ColorMode.HS}
_attr_color_mode = ColorMode.HS
class MyLight(MyBaseLight):
_attr_supported_color_modes = None
""",
"MyLight",
id="subclass_nullifies_inherited_supported",
),
],
)
def test_fires_w7437(
linter: UnittestLinter,
checker: HassLightColorModeChecker,
tmp_path: Path,
code: str,
class_name: str,
) -> None:
"""W7437 fires when a color mode is reported but no supported modes are set."""
integration_dir = _make_integration(tmp_path)
root_node = _parse(code, integration_dir)
class_node = _find_class(root_node, class_name)
with assert_adds_messages(linter, _expect(class_node, _MISSING_SUPPORTED)):
walk_checker(linter, checker, root_node)
@pytest.mark.parametrize(
"code",
[
pytest.param(
"""
from homeassistant.components.light import ColorMode, LightEntity
class MyLight(LightEntity):
_attr_supported_color_modes = {ColorMode.ONOFF}
_attr_color_mode = ColorMode.ONOFF
""",
id="both_class_attrs",
),
pytest.param(
"""
from homeassistant.components.light import ColorMode, LightEntity
class MyLight(LightEntity):
_attr_supported_color_modes = {ColorMode.HS}
@property
def color_mode(self):
return ColorMode.HS
""",
id="supported_attr_and_color_mode_property",
),
pytest.param(
"""
from homeassistant.components.light import ColorMode, LightEntity
class MyLight(LightEntity):
def __init__(self, status) -> None:
self._attr_supported_color_modes = {ColorMode.HS}
self._attr_color_mode = ColorMode.HS
""",
id="both_self_assigned",
),
pytest.param(
"""
from homeassistant.components.light import LightEntity
class MyLight(LightEntity):
@property
def is_on(self) -> bool:
return True
""",
id="neither_set",
),
pytest.param(
"""
from homeassistant.components.light import ColorMode, LightEntity
class MyLight(LightEntity):
_attr_supported_color_modes: set[ColorMode] | None = None
""",
id="supported_explicitly_none",
),
pytest.param(
"""
from homeassistant.components.light import ColorMode, LightEntity
class MyBaseLight(LightEntity):
_attr_color_mode = ColorMode.HS
class MyLight(MyBaseLight):
_attr_supported_color_modes = {ColorMode.HS}
""",
id="color_mode_from_base_supported_from_subclass",
),
pytest.param(
"""
from homeassistant.components.light import ColorMode, LightEntity
class MyBaseLight(LightEntity):
_attr_supported_color_modes = {ColorMode.HS}
class MyLight(MyBaseLight):
_attr_color_mode = ColorMode.HS
""",
id="supported_from_base_color_mode_from_subclass",
),
pytest.param(
"""
from homeassistant.components.light import ColorMode, LightEntity
class MyBaseLight(LightEntity):
_attr_supported_color_modes = {ColorMode.HS}
_attr_color_mode = ColorMode.HS
class MyLight(MyBaseLight):
_attr_color_mode = None
def __init__(self) -> None:
self._attr_color_mode = ColorMode.HS
""",
id="subclass_reassigns_inherited_color_mode_via_init",
),
],
)
def test_good(
linter: UnittestLinter,
checker: HassLightColorModeChecker,
tmp_path: Path,
code: str,
) -> None:
"""No message when a light reports both halves, or neither.
Covers the both-reported cases (directly or via inheritance, including a
runtime ``self._attr_...`` assignment that wins over a class-body
``None``) and the both-missing case (legacy/abstract, deliberately not
flagged).
"""
integration_dir = _make_integration(tmp_path)
root_node = _parse(code, integration_dir)
with assert_no_messages(linter):
walk_checker(linter, checker, root_node)
@pytest.mark.parametrize(
("code", "module_name", "file_name"),
[
pytest.param(
"""
from homeassistant.components.light import ColorMode
class NotALight:
_attr_supported_color_modes = {ColorMode.HS}
""",
"homeassistant.components.test_integration.light",
"light.py",
id="non_light_entity_class",
),
pytest.param(
"""
from homeassistant.components.light import ColorMode, LightEntity
class MyLight(LightEntity):
_attr_supported_color_modes = {ColorMode.HS}
""",
"not_homeassistant.something.light",
"light.py",
id="module_outside_integration",
),
],
)
def test_out_of_scope_ignored(
linter: UnittestLinter,
checker: HassLightColorModeChecker,
tmp_path: Path,
code: str,
module_name: str,
file_name: str,
) -> None:
"""W7436 doesn't fire for classes/modules outside the rule's scope."""
integration_dir = _make_integration(tmp_path)
root_node = _parse(
code, integration_dir, module_name=module_name, file_name=file_name
)
with assert_no_messages(linter):
walk_checker(linter, checker, root_node)