mirror of
https://github.com/esphome/esphome.git
synced 2026-09-28 23:40:25 +00:00
[core] Lint duplicate UNIT_/CONF_ constants against both const homes (#19422)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.components.const import UNIT_COUNTS
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_CLEAR,
|
||||
@@ -33,8 +34,6 @@ CONF_F7 = "f7"
|
||||
CONF_F8 = "f8"
|
||||
CONF_NIR = "nir"
|
||||
|
||||
UNIT_COUNTS = "#"
|
||||
|
||||
AS7341_GAIN = as7341_ns.enum("AS7341Gain")
|
||||
GAIN_OPTIONS = {
|
||||
"X0.5": AS7341_GAIN.AS7341_GAIN_0_5X,
|
||||
|
||||
@@ -61,3 +61,4 @@ ICON_SOLAR_POWER = "mdi:solar-power"
|
||||
KEY_METADATA = "metadata"
|
||||
|
||||
UNIT_AMPERE_HOUR = "Ah"
|
||||
UNIT_COUNTS = "#"
|
||||
|
||||
@@ -3,7 +3,7 @@ import encodings
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32_ble
|
||||
from esphome.components.const import CONF_MANUFACTURER
|
||||
from esphome.components.const import CONF_DESCRIPTION, CONF_MANUFACTURER
|
||||
from esphome.components.esp32 import request_bluetooth
|
||||
from esphome.components.esp32_ble import BTLoggers, bt_uuid
|
||||
import esphome.config_validation as cv
|
||||
@@ -37,7 +37,6 @@ CONF_ADVERTISE = "advertise"
|
||||
CONF_APPEARANCE = "appearance"
|
||||
CONF_BROADCAST = "broadcast"
|
||||
CONF_CHARACTERISTICS = "characteristics"
|
||||
CONF_DESCRIPTION = "description"
|
||||
CONF_DESCRIPTORS = "descriptors"
|
||||
CONF_ENDIANNESS = "endianness"
|
||||
CONF_FIRMWARE_VERSION = "firmware_version"
|
||||
|
||||
@@ -22,6 +22,7 @@ from esphome.const import (
|
||||
UNIT_AMPERE,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_HERTZ,
|
||||
UNIT_KILOWATT_HOURS,
|
||||
UNIT_VOLT,
|
||||
UNIT_WATT,
|
||||
)
|
||||
@@ -33,7 +34,6 @@ CONF_TOTAL_GENERATION_TIME = "total_generation_time"
|
||||
CONF_TODAY_GENERATION_TIME = "today_generation_time"
|
||||
CONF_PV1 = "pv1"
|
||||
CONF_PV2 = "pv2"
|
||||
UNIT_KILOWATT_HOURS = "kWh"
|
||||
UNIT_HOURS = "h"
|
||||
UNIT_KOHM = "kΩ"
|
||||
UNIT_MILLIAMPERE = "mA"
|
||||
|
||||
@@ -23,6 +23,7 @@ from esphome.const import (
|
||||
UNIT_AMPERE,
|
||||
UNIT_DEGREES,
|
||||
UNIT_HERTZ,
|
||||
UNIT_KILOWATT_HOURS,
|
||||
UNIT_MINUTE,
|
||||
UNIT_VOLT,
|
||||
UNIT_VOLT_AMPS_REACTIVE,
|
||||
@@ -36,7 +37,6 @@ CONF_TOTAL_GENERATION_TIME = "total_generation_time"
|
||||
CONF_TODAY_GENERATION_TIME = "today_generation_time"
|
||||
CONF_PV1 = "pv1"
|
||||
CONF_PV2 = "pv2"
|
||||
UNIT_KILOWATT_HOURS = "kWh"
|
||||
UNIT_HOURS = "h"
|
||||
UNIT_KOHM = "kΩ"
|
||||
UNIT_MILLIAMPERE = "mA"
|
||||
|
||||
@@ -24,6 +24,7 @@ from esphome.const import (
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
UNIT_AMPERE,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_MILLIVOLT,
|
||||
UNIT_VOLT,
|
||||
UNIT_WATT,
|
||||
UNIT_WATT_HOURS,
|
||||
@@ -44,7 +45,6 @@ CONF_TEMPERATURE_COEFFICIENT = "temperature_coefficient"
|
||||
CONF_RESET_ON_BOOT = "reset_on_boot"
|
||||
UNIT_COULOMB = "C"
|
||||
UNIT_JOULE = "J"
|
||||
UNIT_MILLIVOLT = "mV"
|
||||
|
||||
ina2xx_base_ns = cg.esphome_ns.namespace("ina2xx_base")
|
||||
INA2XX = ina2xx_base_ns.class_("INA2XX", cg.PollingComponent)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.components.const import UNIT_COUNTS
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_AMBIENT_LIGHT,
|
||||
@@ -28,7 +29,6 @@ CONF_UV_INDEX = "uv_index"
|
||||
CONF_UV = "uv"
|
||||
CONF_WINDOW_CORRECTION_FACTOR = "window_correction_factor"
|
||||
|
||||
UNIT_COUNTS = "#"
|
||||
UNIT_UVI = "UVI"
|
||||
|
||||
LTR390GAIN = ltr390_ns.enum("LTR390GAIN")
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import Any
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.components.const import UNIT_COUNTS
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ACTUAL_GAIN,
|
||||
@@ -42,7 +43,6 @@ CONF_PS_LOW_THRESHOLD = "ps_low_threshold"
|
||||
ICON_BRIGHTNESS_7 = "mdi:brightness-7"
|
||||
ICON_GAIN = "mdi:multiplication"
|
||||
ICON_PROXIMITY = "mdi:hand-wave-outline"
|
||||
UNIT_COUNTS = "#"
|
||||
|
||||
ltr501_ns = cg.esphome_ns.namespace("ltr501")
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import Any
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.components.const import UNIT_COUNTS
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ACTUAL_GAIN,
|
||||
@@ -41,7 +42,6 @@ CONF_PS_LOW_THRESHOLD = "ps_low_threshold"
|
||||
ICON_BRIGHTNESS_7 = "mdi:brightness-7"
|
||||
ICON_GAIN = "mdi:multiplication"
|
||||
ICON_PROXIMITY = "mdi:hand-wave-outline"
|
||||
UNIT_COUNTS = "#"
|
||||
|
||||
ltr_als_ps_ns = cg.esphome_ns.namespace("ltr_als_ps")
|
||||
|
||||
|
||||
@@ -680,7 +680,6 @@ CONF_BODY = "body"
|
||||
CONF_BUTTONS = "buttons"
|
||||
CONF_CHANGE_RATE = "change_rate"
|
||||
CONF_CLOSE_BUTTON = "close_button"
|
||||
CONF_COLOR_DEPTH = "color_depth"
|
||||
CONF_COLOR_END = "color_end"
|
||||
CONF_COLOR_START = "color_start"
|
||||
CONF_CONTAINER = "container"
|
||||
@@ -767,7 +766,6 @@ CONF_RESUME_ON_INPUT = "resume_on_input"
|
||||
CONF_RIGHT_BUTTON = "right_button"
|
||||
CONF_ROLLOVER = "rollover"
|
||||
CONF_ROOT_BACK_BTN = "root_back_btn"
|
||||
CONF_ROWS = "rows"
|
||||
CONF_SCALE = "scale"
|
||||
CONF_SCALE_LINES = "scale_lines"
|
||||
CONF_SCROLLBAR_MODE = "scrollbar_mode"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_ON_STATE_CHANGE
|
||||
from esphome.config_helpers import (
|
||||
filter_source_files_from_defines,
|
||||
filter_source_files_from_platform,
|
||||
@@ -38,7 +39,6 @@ CONF_ON_ABORT = "on_abort"
|
||||
CONF_ON_BEGIN = "on_begin"
|
||||
CONF_ON_END = "on_end"
|
||||
CONF_ON_PROGRESS = "on_progress"
|
||||
CONF_ON_STATE_CHANGE = "on_state_change"
|
||||
|
||||
|
||||
ota_ns = cg.esphome_ns.namespace("ota")
|
||||
|
||||
@@ -27,6 +27,9 @@ from esphome.const import (
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
UNIT_AMPERE,
|
||||
UNIT_HERTZ,
|
||||
UNIT_KILOVOLT_AMPS_HOURS,
|
||||
UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
|
||||
UNIT_KILOWATT_HOURS,
|
||||
UNIT_VOLT,
|
||||
UNIT_VOLT_AMPS,
|
||||
UNIT_VOLT_AMPS_REACTIVE,
|
||||
@@ -44,9 +47,6 @@ CONF_MAXIMUM_DEMAND_ACTIVE_POWER = "maximum_demand_active_power"
|
||||
CONF_MAXIMUM_DEMAND_REACTIVE_POWER = "maximum_demand_reactive_power"
|
||||
CONF_MAXIMUM_DEMAND_APPARENT_POWER = "maximum_demand_apparent_power"
|
||||
|
||||
UNIT_KILOWATT_HOURS = "kWh"
|
||||
UNIT_KILOVOLT_AMPS_HOURS = "kVAh"
|
||||
UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kVARh"
|
||||
|
||||
selec_meter_ns = cg.esphome_ns.namespace("selec_meter")
|
||||
SelecMeter = selec_meter_ns.class_(
|
||||
|
||||
@@ -2,6 +2,7 @@ from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.components.const import UNIT_COUNTS
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ACTUAL_GAIN,
|
||||
@@ -31,7 +32,6 @@ from esphome.types import ConfigType
|
||||
CODEOWNERS = ["@latonita"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
UNIT_COUNTS = "#"
|
||||
ICON_MULTIPLICATION = "mdi:multiplication"
|
||||
ICON_BRIGHTNESS_7 = "mdi:brightness-7"
|
||||
|
||||
|
||||
+67
-7
@@ -529,21 +529,67 @@ def lint_conf_matches(fname, match):
|
||||
CONF_RE = r'^(CONF_[a-zA-Z0-9_]+)\s*=\s*[\'"].*?[\'"]\s*?$'
|
||||
with codecs.open("esphome/const.py", "r", encoding="utf-8") as const_f_handle:
|
||||
constants_content = const_f_handle.read()
|
||||
with codecs.open(
|
||||
"esphome/components/const/__init__.py", "r", encoding="utf-8"
|
||||
) as component_const_f_handle:
|
||||
component_constants_content = component_const_f_handle.read()
|
||||
|
||||
# The two canonical homes for shared constants: esphome/const.py (core, frozen) and
|
||||
# esphome/components/const/__init__.py (shared by components). A constant defined in
|
||||
# either must be imported from there rather than redefined in a component.
|
||||
CONST_HOMES = ["esphome/const.py", "esphome/components/const/__init__.py"]
|
||||
|
||||
CONSTANTS = [m.group(1) for m in re.finditer(CONF_RE, constants_content, re.MULTILINE)]
|
||||
COMPONENT_CONSTANTS = [
|
||||
m.group(1) for m in re.finditer(CONF_RE, component_constants_content, re.MULTILINE)
|
||||
]
|
||||
|
||||
CONSTANTS_USES = collections.defaultdict(list)
|
||||
|
||||
|
||||
@lint_re_check(CONF_RE, include=["*.py"], exclude=["esphome/const.py"])
|
||||
def _const_home_error(name, core_constants, component_constants):
|
||||
"""Return an error if the constant already lives in one of the canonical homes."""
|
||||
if name in core_constants:
|
||||
return (
|
||||
f"Constant {highlight(name)} has already been defined in const.py - "
|
||||
"please import the constant from const.py directly."
|
||||
)
|
||||
if name in component_constants:
|
||||
return (
|
||||
f"Constant {highlight(name)} has already been defined in "
|
||||
"esphome/components/const/__init__.py - please import the constant from "
|
||||
"esphome.components.const directly."
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@lint_re_check(CONF_RE, include=["*.py"], exclude=CONST_HOMES)
|
||||
def lint_conf_from_const_py(fname, match):
|
||||
name = match.group(1)
|
||||
if name not in CONSTANTS:
|
||||
err = _const_home_error(name, CONSTANTS, COMPONENT_CONSTANTS)
|
||||
if err is None:
|
||||
CONSTANTS_USES[name].append(fname)
|
||||
return None
|
||||
return (
|
||||
f"Constant {highlight(name)} has already been defined in const.py - "
|
||||
"please import the constant from const.py directly."
|
||||
)
|
||||
return err
|
||||
|
||||
|
||||
UNIT_RE = r'^(UNIT_[a-zA-Z0-9_]+)\s*=\s*[\'"].*?[\'"]\s*?$'
|
||||
UNIT_CONSTANTS = [
|
||||
m.group(1) for m in re.finditer(UNIT_RE, constants_content, re.MULTILINE)
|
||||
]
|
||||
COMPONENT_UNIT_CONSTANTS = [
|
||||
m.group(1) for m in re.finditer(UNIT_RE, component_constants_content, re.MULTILINE)
|
||||
]
|
||||
|
||||
UNIT_CONSTANTS_USES = collections.defaultdict(list)
|
||||
|
||||
|
||||
@lint_re_check(UNIT_RE, include=["*.py"], exclude=CONST_HOMES)
|
||||
def lint_unit_from_const_py(fname, match):
|
||||
name = match.group(1)
|
||||
err = _const_home_error(name, UNIT_CONSTANTS, COMPONENT_UNIT_CONSTANTS)
|
||||
if err is None:
|
||||
UNIT_CONSTANTS_USES[name].append(fname)
|
||||
return err
|
||||
|
||||
|
||||
RAW_PIN_ACCESS_RE = (
|
||||
@@ -711,6 +757,20 @@ def lint_constants_usage():
|
||||
return errs
|
||||
|
||||
|
||||
@lint_post_check
|
||||
def lint_unit_constants_usage():
|
||||
errs = []
|
||||
for constant, uses in UNIT_CONSTANTS_USES.items():
|
||||
if len(uses) < 3:
|
||||
continue
|
||||
errs.append(
|
||||
f"Constant {highlight(constant)} is defined in {len(uses)} files. Please move all definitions of the "
|
||||
f"constant to esphome/components/const/__init__.py (Uses: {', '.join(str(u) for u in uses)}) in a separate PR. "
|
||||
"See https://developers.esphome.io/contributing/code/#python"
|
||||
)
|
||||
return errs
|
||||
|
||||
|
||||
# Maximum allowed CONF_ constants in esphome/const.py.
|
||||
# This file is frozen — new constants go in esphome/components/const/__init__.py.
|
||||
# Decrease this number when constants are moved out of const.py.
|
||||
|
||||
@@ -151,6 +151,82 @@ def test_nolint_on_control_line_suppresses() -> None:
|
||||
assert not _lint("if (x) // NOLINT\n ESP_LOGD(t);\n")
|
||||
|
||||
|
||||
# --- rule: UNIT_ constants must not be redefined (mirror of the CONF_ check) ---
|
||||
|
||||
# Real UNIT_ constants that live in each canonical home.
|
||||
UNIT_IN_CONST_PY = ci_custom.UNIT_CONSTANTS[0]
|
||||
UNIT_IN_COMPONENT_CONST = ci_custom.COMPONENT_UNIT_CONSTANTS[0]
|
||||
|
||||
|
||||
def _unit_def(fname: str, content: str) -> list:
|
||||
return ci_custom.lint_unit_from_const_py(fname, content)
|
||||
|
||||
|
||||
def test_unit_already_in_const_py_is_flagged() -> None:
|
||||
errs = _unit_def("esphome/components/x/sensor.py", f'{UNIT_IN_CONST_PY} = "x"\n')
|
||||
assert errs
|
||||
assert "const.py" in errs[0][2]
|
||||
|
||||
|
||||
def test_unit_already_in_component_const_is_flagged() -> None:
|
||||
errs = _unit_def(
|
||||
"esphome/components/x/sensor.py", f'{UNIT_IN_COMPONENT_CONST} = "x"\n'
|
||||
)
|
||||
assert errs
|
||||
assert "esphome.components.const" in errs[0][2]
|
||||
|
||||
|
||||
def test_unit_not_in_const_py_is_tracked_not_flagged() -> None:
|
||||
ci_custom.UNIT_CONSTANTS_USES.clear()
|
||||
assert _unit_def("a.py", 'UNIT_FOO_BAR = "fb"\n') == []
|
||||
assert ci_custom.UNIT_CONSTANTS_USES["UNIT_FOO_BAR"] == ["a.py"]
|
||||
|
||||
|
||||
def test_unit_defined_in_three_files_is_flagged() -> None:
|
||||
ci_custom.UNIT_CONSTANTS_USES.clear()
|
||||
for fname in ("a.py", "b.py", "c.py"):
|
||||
_unit_def(fname, 'UNIT_FOO_BAR = "fb"\n')
|
||||
errs = ci_custom.lint_unit_constants_usage()
|
||||
assert any("UNIT_FOO_BAR" in e and "3 files" in e for e in errs)
|
||||
|
||||
|
||||
def test_unit_defined_in_two_files_is_not_flagged() -> None:
|
||||
ci_custom.UNIT_CONSTANTS_USES.clear()
|
||||
for fname in ("a.py", "b.py"):
|
||||
_unit_def(fname, 'UNIT_FOO_BAR = "fb"\n')
|
||||
assert ci_custom.lint_unit_constants_usage() == []
|
||||
|
||||
|
||||
# --- same rule for CONF_, now also recognising the components/const home ---
|
||||
|
||||
CONF_IN_CONST_PY = ci_custom.CONSTANTS[0]
|
||||
CONF_IN_COMPONENT_CONST = ci_custom.COMPONENT_CONSTANTS[0]
|
||||
|
||||
|
||||
def _conf_def(fname: str, content: str) -> list:
|
||||
return ci_custom.lint_conf_from_const_py(fname, content)
|
||||
|
||||
|
||||
def test_conf_already_in_const_py_is_flagged() -> None:
|
||||
errs = _conf_def("esphome/components/x/sensor.py", f'{CONF_IN_CONST_PY} = "x"\n')
|
||||
assert errs
|
||||
assert "const.py" in errs[0][2]
|
||||
|
||||
|
||||
def test_conf_already_in_component_const_is_flagged() -> None:
|
||||
errs = _conf_def(
|
||||
"esphome/components/x/sensor.py", f'{CONF_IN_COMPONENT_CONST} = "x"\n'
|
||||
)
|
||||
assert errs
|
||||
assert "esphome.components.const" in errs[0][2]
|
||||
|
||||
|
||||
def test_conf_not_in_a_const_home_is_tracked_not_flagged() -> None:
|
||||
ci_custom.CONSTANTS_USES.pop("CONF_FOO_BAR", None)
|
||||
assert _conf_def("a.py", 'CONF_FOO_BAR = "foo_bar"\n') == []
|
||||
assert ci_custom.CONSTANTS_USES["CONF_FOO_BAR"] == ["a.py"]
|
||||
|
||||
|
||||
# --- ESP_LOG call scanner and bare-literal-ternary lint ---
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user