Adjust pylint plugin for absolute/relative imports (#77219)
* Adjust pylint plugin for absolute/relative imports * Adjust components * One more * Adjust mqtt * Adjust mqtt.DOMAIN import * Adjust internal import * Add tests for valid local component imports * Adjust relative path check * Fixes * Fixes
This commit is contained in:
parent
1fb8fbf5de
commit
38ca74b547
7 changed files with 176 additions and 15 deletions
|
@ -6,7 +6,7 @@ from pyplaato.plaato import PlaatoKeg
|
||||||
|
|
||||||
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
|
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.dispatcher import (
|
from homeassistant.helpers.dispatcher import (
|
||||||
async_dispatcher_connect,
|
async_dispatcher_connect,
|
||||||
async_dispatcher_send,
|
async_dispatcher_send,
|
||||||
|
@ -15,7 +15,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
from . import ATTR_TEMP, SENSOR_UPDATE
|
from . import ATTR_TEMP, SENSOR_UPDATE
|
||||||
from ...core import callback
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_USE_WEBHOOK,
|
CONF_USE_WEBHOOK,
|
||||||
COORDINATOR,
|
COORDINATOR,
|
||||||
|
|
|
@ -18,11 +18,11 @@ from homeassistant.components.notify import (
|
||||||
from homeassistant.config_entries import SOURCE_IMPORT
|
from homeassistant.config_entries import SOURCE_IMPORT
|
||||||
from homeassistant.const import CONF_API_KEY
|
from homeassistant.const import CONF_API_KEY
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
from ...exceptions import HomeAssistantError
|
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_ATTACHMENT,
|
ATTR_ATTACHMENT,
|
||||||
ATTR_CALLBACK_URL,
|
ATTR_CALLBACK_URL,
|
||||||
|
|
|
@ -8,15 +8,19 @@ from homeassistant.components.sensor import (
|
||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import (
|
||||||
|
DATA_GIGABYTES,
|
||||||
|
DATA_MEGABYTES,
|
||||||
|
DATA_RATE_MEGABYTES_PER_SECOND,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
from homeassistant.helpers.entity import DeviceInfo
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import DOMAIN, SIGNAL_SABNZBD_UPDATED
|
from . import DOMAIN, SIGNAL_SABNZBD_UPDATED
|
||||||
from ...config_entries import ConfigEntry
|
|
||||||
from ...const import DATA_GIGABYTES, DATA_MEGABYTES, DATA_RATE_MEGABYTES_PER_SECOND
|
|
||||||
from ...core import HomeAssistant
|
|
||||||
from ...helpers.device_registry import DeviceEntryType
|
|
||||||
from ...helpers.entity import DeviceInfo
|
|
||||||
from ...helpers.entity_platform import AddEntitiesCallback
|
|
||||||
from .const import DEFAULT_NAME, KEY_API_DATA, KEY_NAME
|
from .const import DEFAULT_NAME, KEY_API_DATA, KEY_NAME
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -13,12 +13,12 @@ from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
|
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
|
||||||
|
from homeassistant.helpers.entity import DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import (
|
from homeassistant.helpers.update_coordinator import (
|
||||||
CoordinatorEntity,
|
CoordinatorEntity,
|
||||||
DataUpdateCoordinator,
|
DataUpdateCoordinator,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ...helpers.entity import DeviceInfo
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CHARGER_CURRENT_VERSION_KEY,
|
CHARGER_CURRENT_VERSION_KEY,
|
||||||
CHARGER_DATA_KEY,
|
CHARGER_DATA_KEY,
|
||||||
|
|
|
@ -41,15 +41,15 @@ _OBSOLETE_IMPORT: dict[str, list[ObsoleteImportMatch]] = {
|
||||||
"homeassistant.components.automation": [
|
"homeassistant.components.automation": [
|
||||||
ObsoleteImportMatch(
|
ObsoleteImportMatch(
|
||||||
reason="replaced by TriggerActionType from helpers.trigger",
|
reason="replaced by TriggerActionType from helpers.trigger",
|
||||||
constant=re.compile(r"^AutomationActionType$")
|
constant=re.compile(r"^AutomationActionType$"),
|
||||||
),
|
),
|
||||||
ObsoleteImportMatch(
|
ObsoleteImportMatch(
|
||||||
reason="replaced by TriggerData from helpers.trigger",
|
reason="replaced by TriggerData from helpers.trigger",
|
||||||
constant=re.compile(r"^AutomationTriggerData$")
|
constant=re.compile(r"^AutomationTriggerData$"),
|
||||||
),
|
),
|
||||||
ObsoleteImportMatch(
|
ObsoleteImportMatch(
|
||||||
reason="replaced by TriggerInfo from helpers.trigger",
|
reason="replaced by TriggerInfo from helpers.trigger",
|
||||||
constant=re.compile(r"^AutomationTriggerInfo$")
|
constant=re.compile(r"^AutomationTriggerInfo$"),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
"homeassistant.components.binary_sensor": [
|
"homeassistant.components.binary_sensor": [
|
||||||
|
@ -111,13 +111,13 @@ _OBSOLETE_IMPORT: dict[str, list[ObsoleteImportMatch]] = {
|
||||||
"homeassistant.components.device_tracker": [
|
"homeassistant.components.device_tracker": [
|
||||||
ObsoleteImportMatch(
|
ObsoleteImportMatch(
|
||||||
reason="replaced by SourceType enum",
|
reason="replaced by SourceType enum",
|
||||||
constant=re.compile(r"^SOURCE_TYPE_\w+$")
|
constant=re.compile(r"^SOURCE_TYPE_\w+$"),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
"homeassistant.components.device_tracker.const": [
|
"homeassistant.components.device_tracker.const": [
|
||||||
ObsoleteImportMatch(
|
ObsoleteImportMatch(
|
||||||
reason="replaced by SourceType enum",
|
reason="replaced by SourceType enum",
|
||||||
constant=re.compile(r"^SOURCE_TYPE_\w+$")
|
constant=re.compile(r"^SOURCE_TYPE_\w+$"),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
"homeassistant.components.fan": [
|
"homeassistant.components.fan": [
|
||||||
|
@ -277,6 +277,11 @@ class HassImportsFormatChecker(BaseChecker): # type: ignore[misc]
|
||||||
"hass-deprecated-import",
|
"hass-deprecated-import",
|
||||||
"Used when import is deprecated",
|
"Used when import is deprecated",
|
||||||
),
|
),
|
||||||
|
"W7423": (
|
||||||
|
"Absolute import should be used",
|
||||||
|
"hass-absolute-import",
|
||||||
|
"Used when relative import should be replaced with absolute import",
|
||||||
|
),
|
||||||
}
|
}
|
||||||
options = ()
|
options = ()
|
||||||
|
|
||||||
|
@ -298,9 +303,27 @@ class HassImportsFormatChecker(BaseChecker): # type: ignore[misc]
|
||||||
if module.startswith(f"{self.current_package}."):
|
if module.startswith(f"{self.current_package}."):
|
||||||
self.add_message("hass-relative-import", node=node)
|
self.add_message("hass-relative-import", node=node)
|
||||||
|
|
||||||
|
def _visit_importfrom_relative(self, current_package: str, node: nodes.ImportFrom) -> None:
|
||||||
|
"""Called when a ImportFrom node is visited."""
|
||||||
|
if node.level <= 1 or not current_package.startswith("homeassistant.components"):
|
||||||
|
return
|
||||||
|
split_package = current_package.split(".")
|
||||||
|
if not node.modname and len(split_package) == node.level + 1:
|
||||||
|
for name in node.names:
|
||||||
|
# Allow relative import to component root
|
||||||
|
if name[0] != split_package[2]:
|
||||||
|
self.add_message("hass-absolute-import", node=node)
|
||||||
|
return
|
||||||
|
return
|
||||||
|
if len(split_package) < node.level + 2:
|
||||||
|
self.add_message("hass-absolute-import", node=node)
|
||||||
|
|
||||||
def visit_importfrom(self, node: nodes.ImportFrom) -> None:
|
def visit_importfrom(self, node: nodes.ImportFrom) -> None:
|
||||||
"""Called when a ImportFrom node is visited."""
|
"""Called when a ImportFrom node is visited."""
|
||||||
|
if not self.current_package:
|
||||||
|
return
|
||||||
if node.level is not None:
|
if node.level is not None:
|
||||||
|
self._visit_importfrom_relative(self.current_package, node)
|
||||||
return
|
return
|
||||||
if node.modname == self.current_package or node.modname.startswith(
|
if node.modname == self.current_package or node.modname.startswith(
|
||||||
f"{self.current_package}."
|
f"{self.current_package}."
|
||||||
|
|
|
@ -32,3 +32,21 @@ def type_hint_checker_fixture(hass_enforce_type_hints, linter) -> BaseChecker:
|
||||||
type_hint_checker = hass_enforce_type_hints.HassTypeHintChecker(linter)
|
type_hint_checker = hass_enforce_type_hints.HassTypeHintChecker(linter)
|
||||||
type_hint_checker.module = "homeassistant.components.pylint_test"
|
type_hint_checker.module = "homeassistant.components.pylint_test"
|
||||||
return type_hint_checker
|
return type_hint_checker
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="hass_imports", scope="session")
|
||||||
|
def hass_imports_fixture() -> ModuleType:
|
||||||
|
"""Fixture to provide a requests mocker."""
|
||||||
|
loader = SourceFileLoader(
|
||||||
|
"hass_imports",
|
||||||
|
str(BASE_PATH.joinpath("pylint/plugins/hass_imports.py")),
|
||||||
|
)
|
||||||
|
return loader.load_module(None)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="imports_checker")
|
||||||
|
def imports_checker_fixture(hass_imports, linter) -> BaseChecker:
|
||||||
|
"""Fixture to provide a requests mocker."""
|
||||||
|
type_hint_checker = hass_imports.HassImportsFormatChecker(linter)
|
||||||
|
type_hint_checker.module = "homeassistant.components.pylint_test"
|
||||||
|
return type_hint_checker
|
||||||
|
|
117
tests/pylint/test_imports.py
Normal file
117
tests/pylint/test_imports.py
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
"""Tests for pylint hass_imports plugin."""
|
||||||
|
# pylint:disable=protected-access
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import astroid
|
||||||
|
from pylint.checkers import BaseChecker
|
||||||
|
import pylint.testutils
|
||||||
|
from pylint.testutils.unittest_linter import UnittestLinter
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from . import assert_adds_messages, assert_no_messages
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("module_name", "import_from", "import_what"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"homeassistant.components.pylint_test.sensor",
|
||||||
|
"homeassistant.const",
|
||||||
|
"CONSTANT",
|
||||||
|
),
|
||||||
|
("homeassistant.components.pylint_test.sensor", ".const", "CONSTANT"),
|
||||||
|
("homeassistant.components.pylint_test.sensor", ".", "CONSTANT"),
|
||||||
|
("homeassistant.components.pylint_test.sensor", "..", "pylint_test"),
|
||||||
|
(
|
||||||
|
"homeassistant.components.pylint_test.api.hub",
|
||||||
|
"homeassistant.const",
|
||||||
|
"CONSTANT",
|
||||||
|
),
|
||||||
|
("homeassistant.components.pylint_test.api.hub", "..const", "CONSTANT"),
|
||||||
|
("homeassistant.components.pylint_test.api.hub", "..", "CONSTANT"),
|
||||||
|
("homeassistant.components.pylint_test.api.hub", "...", "pylint_test"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_good_import(
|
||||||
|
linter: UnittestLinter,
|
||||||
|
imports_checker: BaseChecker,
|
||||||
|
module_name: str,
|
||||||
|
import_from: str,
|
||||||
|
import_what: str,
|
||||||
|
) -> None:
|
||||||
|
"""Ensure good imports pass through ok."""
|
||||||
|
|
||||||
|
import_node = astroid.extract_node(
|
||||||
|
f"from {import_from} import {import_what} #@",
|
||||||
|
module_name,
|
||||||
|
)
|
||||||
|
imports_checker.visit_module(import_node.parent)
|
||||||
|
|
||||||
|
with assert_no_messages(linter):
|
||||||
|
imports_checker.visit_importfrom(import_node)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("module_name", "import_from", "import_what", "error_code"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"homeassistant.components.pylint_test.sensor",
|
||||||
|
"homeassistant.components.pylint_test.const",
|
||||||
|
"CONSTANT",
|
||||||
|
"hass-relative-import",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"homeassistant.components.pylint_test.sensor",
|
||||||
|
"..const",
|
||||||
|
"CONSTANT",
|
||||||
|
"hass-absolute-import",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"homeassistant.components.pylint_test.sensor",
|
||||||
|
"...const",
|
||||||
|
"CONSTANT",
|
||||||
|
"hass-absolute-import",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"homeassistant.components.pylint_test.api.hub",
|
||||||
|
"homeassistant.components.pylint_test.api.const",
|
||||||
|
"CONSTANT",
|
||||||
|
"hass-relative-import",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"homeassistant.components.pylint_test.api.hub",
|
||||||
|
"...const",
|
||||||
|
"CONSTANT",
|
||||||
|
"hass-absolute-import",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_bad_import(
|
||||||
|
linter: UnittestLinter,
|
||||||
|
imports_checker: BaseChecker,
|
||||||
|
module_name: str,
|
||||||
|
import_from: str,
|
||||||
|
import_what: str,
|
||||||
|
error_code: str,
|
||||||
|
) -> None:
|
||||||
|
"""Ensure bad imports are rejected."""
|
||||||
|
|
||||||
|
import_node = astroid.extract_node(
|
||||||
|
f"from {import_from} import {import_what} #@",
|
||||||
|
module_name,
|
||||||
|
)
|
||||||
|
imports_checker.visit_module(import_node.parent)
|
||||||
|
|
||||||
|
with assert_adds_messages(
|
||||||
|
linter,
|
||||||
|
pylint.testutils.MessageTest(
|
||||||
|
msg_id=error_code,
|
||||||
|
node=import_node,
|
||||||
|
args=None,
|
||||||
|
line=1,
|
||||||
|
col_offset=0,
|
||||||
|
end_line=1,
|
||||||
|
end_col_offset=len(import_from) + 21,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
imports_checker.visit_importfrom(import_node)
|
Loading…
Add table
Add a link
Reference in a new issue