From 02cb6a6af7cd790a78f967e7569c74a90cf0af34 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Mon, 16 Sep 2024 11:28:42 +0200
Subject: [PATCH] Force root import of references from other components
 (#125816)

* Force root import of references from other components

* Improve

* Adjust

* Tweak exceptions

* Another

* Another

* Another

* Another

* Another

* Another

* Another

* Another

* Another

* Another

* Another

* Another

* Adjust

* More

* Ignore violations in test

* Improve
---
 pylint/plugins/hass_imports.py                | 44 ++++++++++++++++---
 tests/components/cloud/test_http_api.py       |  2 +
 .../components/deconz/test_device_trigger.py  |  2 +
 .../esphome/test_assist_satellite.py          |  2 +
 .../google_assistant/test_smart_home.py       | 10 +++++
 tests/components/logbook/test_init.py         |  2 +
 .../traccar_server/test_config_flow.py        |  2 +
 tests/components/voip/test_voip.py            |  2 +
 tests/conftest.py                             |  4 ++
 tests/pylint/test_imports.py                  |  8 ++++
 10 files changed, 72 insertions(+), 6 deletions(-)

diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py
index afe307dce42..f7713daabe8 100644
--- a/pylint/plugins/hass_imports.py
+++ b/pylint/plugins/hass_imports.py
@@ -394,6 +394,31 @@ _OBSOLETE_IMPORT: dict[str, list[ObsoleteImportMatch]] = {
     ],
 }
 
+_IGNORE_ROOT_IMPORT = (
+    "assist_pipeline",
+    "automation",
+    "bluetooth",
+    "camera",
+    "cast",
+    "device_automation",
+    "device_tracker",
+    "ffmpeg",
+    "ffmpeg_motion",
+    "google_assistant",
+    "hardware",
+    "homeassistant",
+    "homeassistant_hardware",
+    "http",
+    "manual",
+    "plex",
+    "recorder",
+    "rest",
+    "script",
+    "sensor",
+    "stream",
+    "zha",
+)
+
 
 # Blacklist of imports that should be using the namespace
 @dataclass
@@ -489,8 +514,9 @@ class HassImportsFormatChecker(BaseChecker):
             if module.startswith(f"{self.current_package}."):
                 self.add_message("hass-relative-import", node=node)
                 continue
-            if module.startswith("homeassistant.components.") and module.endswith(
-                "const"
+            if (
+                module.startswith("homeassistant.components.")
+                and len(module.split(".")) > 3
             ):
                 if (
                     self.current_package.startswith("tests.components.")
@@ -546,11 +572,17 @@ class HassImportsFormatChecker(BaseChecker):
                     self.add_message("hass-relative-import", node=node)
                     return
 
-        if node.modname.startswith("homeassistant.components.") and not (
-            self.current_package.startswith("tests.components.")
-            and self.current_package.split(".")[2] == node.modname.split(".")[2]
+        if (
+            node.modname.startswith("homeassistant.components.")
+            and (module_parts := node.modname.split("."))
+            and (module_integration := module_parts[2])
+            and module_integration not in _IGNORE_ROOT_IMPORT
+            and not (
+                self.current_package.startswith("tests.components.")
+                and self.current_package.split(".")[2] == module_integration
+            )
         ):
-            if node.modname.endswith(".const"):
+            if len(module_parts) > 3:
                 self.add_message("hass-component-root-import", node=node)
                 return
             for name, alias in node.names:
diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py
index 5ee9af88681..15339f43dae 100644
--- a/tests/components/cloud/test_http_api.py
+++ b/tests/components/cloud/test_http_api.py
@@ -14,6 +14,8 @@ from hass_nabucasa.voice import TTS_VOICES
 import pytest
 
 from homeassistant.components.alexa import errors as alexa_errors
+
+# pylint: disable-next=hass-component-root-import
 from homeassistant.components.alexa.entities import LightCapabilities
 from homeassistant.components.assist_pipeline.pipeline import STORAGE_KEY
 from homeassistant.components.cloud.const import DEFAULT_EXPOSED_DOMAINS, DOMAIN
diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py
index 6f74db0b82c..1502cc4081d 100644
--- a/tests/components/deconz/test_device_trigger.py
+++ b/tests/components/deconz/test_device_trigger.py
@@ -7,6 +7,8 @@ from pytest_unordered import unordered
 
 from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN
 from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
+
+# pylint: disable-next=hass-component-root-import
 from homeassistant.components.binary_sensor.device_trigger import (
     CONF_BAT_LOW,
     CONF_NOT_BAT_LOW,
diff --git a/tests/components/esphome/test_assist_satellite.py b/tests/components/esphome/test_assist_satellite.py
index f9a431e19d8..5136e160e89 100644
--- a/tests/components/esphome/test_assist_satellite.py
+++ b/tests/components/esphome/test_assist_satellite.py
@@ -30,6 +30,8 @@ from homeassistant.components.assist_satellite import (
     AssistSatelliteEntity,
     AssistSatelliteEntityFeature,
 )
+
+# pylint: disable-next=hass-component-root-import
 from homeassistant.components.assist_satellite.entity import AssistSatelliteState
 from homeassistant.components.esphome import DOMAIN
 from homeassistant.components.esphome.assist_satellite import (
diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py
index ea8f6957e38..214fc4a38de 100644
--- a/tests/components/google_assistant/test_smart_home.py
+++ b/tests/components/google_assistant/test_smart_home.py
@@ -9,10 +9,20 @@ from pytest_unordered import unordered
 
 from homeassistant.components.camera import CameraEntityFeature
 from homeassistant.components.climate import ATTR_MAX_TEMP, ATTR_MIN_TEMP, HVACMode
+
+# pylint: disable-next=hass-component-root-import
 from homeassistant.components.demo.binary_sensor import DemoBinarySensor
+
+# pylint: disable-next=hass-component-root-import
 from homeassistant.components.demo.cover import DemoCover
+
+# pylint: disable-next=hass-component-root-import
 from homeassistant.components.demo.light import LIGHT_EFFECT_LIST, DemoLight
+
+# pylint: disable-next=hass-component-root-import
 from homeassistant.components.demo.media_player import AbstractDemoPlayer
+
+# pylint: disable-next=hass-component-root-import
 from homeassistant.components.demo.switch import DemoSwitch
 from homeassistant.components.google_assistant import (
     EVENT_COMMAND_RECEIVED,
diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py
index 606c398c31f..8ac7dde67ab 100644
--- a/tests/components/logbook/test_init.py
+++ b/tests/components/logbook/test_init.py
@@ -11,6 +11,8 @@ import pytest
 import voluptuous as vol
 
 from homeassistant.components import logbook, recorder
+
+# pylint: disable-next=hass-component-root-import
 from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME
 from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED
 from homeassistant.components.logbook.models import EventAsRow, LazyEventPartialState
diff --git a/tests/components/traccar_server/test_config_flow.py b/tests/components/traccar_server/test_config_flow.py
index 62f39f00dc1..d9500441519 100644
--- a/tests/components/traccar_server/test_config_flow.py
+++ b/tests/components/traccar_server/test_config_flow.py
@@ -8,6 +8,8 @@ import pytest
 from pytraccar import TraccarException
 
 from homeassistant import config_entries
+
+# pylint: disable-next=hass-component-root-import
 from homeassistant.components.traccar.device_tracker import PLATFORM_SCHEMA
 from homeassistant.components.traccar_server.const import (
     CONF_CUSTOM_ATTRIBUTES,
diff --git a/tests/components/voip/test_voip.py b/tests/components/voip/test_voip.py
index f856da8b1e9..cf5148e8ba0 100644
--- a/tests/components/voip/test_voip.py
+++ b/tests/components/voip/test_voip.py
@@ -13,6 +13,8 @@ from voip_utils import CallInfo
 
 from homeassistant.components import assist_pipeline, assist_satellite, tts, voip
 from homeassistant.components.assist_satellite import AssistSatelliteEntity
+
+# pylint: disable-next=hass-component-root-import
 from homeassistant.components.assist_satellite.entity import AssistSatelliteState
 from homeassistant.components.voip import HassVoipDatagramProtocol
 from homeassistant.components.voip.assist_satellite import Tones, VoipAssistSatellite
diff --git a/tests/conftest.py b/tests/conftest.py
index 178fdd74a69..cfcfaf8526c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -51,11 +51,15 @@ from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY
 from homeassistant.auth.models import Credentials
 from homeassistant.auth.providers import homeassistant
 from homeassistant.components.device_tracker.legacy import Device
+
+# pylint: disable-next=hass-component-root-import
 from homeassistant.components.websocket_api.auth import (
     TYPE_AUTH,
     TYPE_AUTH_OK,
     TYPE_AUTH_REQUIRED,
 )
+
+# pylint: disable-next=hass-component-root-import
 from homeassistant.components.websocket_api.http import URL
 from homeassistant.config import YAML_CONFIG_FILE
 from homeassistant.config_entries import ConfigEntries, ConfigEntry, ConfigEntryState
diff --git a/tests/pylint/test_imports.py b/tests/pylint/test_imports.py
index 980b9ead74c..5044e73d253 100644
--- a/tests/pylint/test_imports.py
+++ b/tests/pylint/test_imports.py
@@ -208,6 +208,10 @@ def test_good_root_import(
             "from homeassistant.components.climate.const import ClimateEntityFeature",
             "homeassistant.components.pylint_test.climate",
         ),
+        (
+            "from homeassistant.components.climate.entity import ClimateEntityFeature",
+            "homeassistant.components.pylint_test.climate",
+        ),
         (
             "from homeassistant.components.climate import const",
             "tests.components.pylint_test.climate",
@@ -220,6 +224,10 @@ def test_good_root_import(
             "import homeassistant.components.climate.const as climate",
             "tests.components.pylint_test.climate",
         ),
+        (
+            "import homeassistant.components.climate.entity as climate",
+            "tests.components.pylint_test.climate",
+        ),
     ],
 )
 def test_bad_root_import(
-- 
GitLab