diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index de26cfef982..d3e6412d301 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -310,6 +310,12 @@ class HassImportsFormatChecker(BaseChecker): # type: ignore[misc] "hass-absolute-import", "Used when relative import should be replaced with absolute import", ), + "W7424": ( + "Import should be using the component root", + "hass-component-root-import", + "Used when an import from another component should be " + "from the component root", + ), } options = () @@ -330,6 +336,10 @@ class HassImportsFormatChecker(BaseChecker): # type: ignore[misc] for module, _alias in node.names: if module.startswith(f"{self.current_package}."): self.add_message("hass-relative-import", node=node) + if module.startswith("homeassistant.components.") and module.endswith( + "const" + ): + self.add_message("hass-component-root-import", node=node) def _visit_importfrom_relative( self, current_package: str, node: nodes.ImportFrom @@ -374,6 +384,12 @@ class HassImportsFormatChecker(BaseChecker): # type: ignore[misc] ): self.add_message("hass-relative-import", node=node) return + if node.modname.startswith("homeassistant.components.") and ( + node.modname.endswith(".const") + or "const" in {names[0] for names in node.names} + ): + self.add_message("hass-component-root-import", node=node) + return if obsolete_imports := _OBSOLETE_IMPORT.get(node.modname): for name_tuple in node.names: for obsolete_import in obsolete_imports: diff --git a/tests/pylint/test_imports.py b/tests/pylint/test_imports.py index f535e34e8de..fadaaf159a3 100644 --- a/tests/pylint/test_imports.py +++ b/tests/pylint/test_imports.py @@ -132,3 +132,69 @@ def test_bad_import( ), ): imports_checker.visit_importfrom(import_node) + + +@pytest.mark.parametrize( + "import_node", + [ + "from homeassistant.components import climate", + "from homeassistant.components.climate import ClimateEntityFeature", + ], +) +def test_good_root_import( + linter: UnittestLinter, + imports_checker: BaseChecker, + import_node: str, +) -> None: + """Ensure bad root imports are rejected.""" + + node = astroid.extract_node( + f"{import_node} #@", + "homeassistant.components.pylint_test.climate", + ) + imports_checker.visit_module(node.parent) + + with assert_no_messages(linter): + if import_node.startswith("import"): + imports_checker.visit_import(node) + if import_node.startswith("from"): + imports_checker.visit_importfrom(node) + + +@pytest.mark.parametrize( + "import_node", + [ + "import homeassistant.components.climate.const as climate", + "from homeassistant.components.climate import const", + "from homeassistant.components.climate.const import ClimateEntityFeature", + ], +) +def test_bad_root_import( + linter: UnittestLinter, + imports_checker: BaseChecker, + import_node: str, +) -> None: + """Ensure bad root imports are rejected.""" + + node = astroid.extract_node( + f"{import_node} #@", + "homeassistant.components.pylint_test.climate", + ) + imports_checker.visit_module(node.parent) + + with assert_adds_messages( + linter, + pylint.testutils.MessageTest( + msg_id="hass-component-root-import", + node=node, + args=None, + line=1, + col_offset=0, + end_line=1, + end_col_offset=len(import_node), + ), + ): + if import_node.startswith("import"): + imports_checker.visit_import(node) + if import_node.startswith("from"): + imports_checker.visit_importfrom(node)