mirror of
https://github.com/esphome/esphome.git
synced 2026-06-29 20:16:08 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 99467c911d | |||
| d3294addf1 | |||
| d4b3cd342d | |||
| 71a1ba6f36 | |||
| d1d01e5718 |
@@ -820,7 +820,7 @@ jobs:
|
||||
run: echo ${{ matrix.components }}
|
||||
|
||||
- name: Cache apt packages
|
||||
uses: awalsh128/cache-apt-pkgs-action@5513791f75b039e2a79653b1a92238d3fb8d99b4 # v1.6.2
|
||||
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.5.3
|
||||
with:
|
||||
packages: libsdl2-dev ccache
|
||||
version: 1.1
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
"""ESPHome.
|
||||
|
||||
ESPHome validates configuration with probatio. External (third-party) components
|
||||
still ``import voluptuous`` directly, so probatio's compatibility shim is installed
|
||||
here, at package import, before any submodule or component imports voluptuous. This
|
||||
aliases ``voluptuous`` (and the submodules dependencies reach into) to probatio in
|
||||
``sys.modules`` for the lifetime of the process.
|
||||
"""
|
||||
|
||||
from probatio.compat import install_as_voluptuous
|
||||
|
||||
install_as_voluptuous()
|
||||
|
||||
@@ -198,7 +198,13 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False):
|
||||
try:
|
||||
return cv.Schema([schema])(value)
|
||||
except cv.Invalid as err2:
|
||||
if "extra keys not allowed" in str(err2) and len(err2.path) == 2:
|
||||
err2_errors = (
|
||||
err2.errors if isinstance(err2, cv.MultipleInvalid) else [err2]
|
||||
)
|
||||
if (
|
||||
any(isinstance(e, cv.ExtraKeysInvalid) for e in err2_errors)
|
||||
and len(err2.path) == 2
|
||||
):
|
||||
raise err from None
|
||||
if "Unable to find action" in str(err):
|
||||
raise err2 from None
|
||||
@@ -414,8 +420,8 @@ async def delay_action_to_code(
|
||||
cv.Optional(CONF_THEN): validate_action_list,
|
||||
cv.Optional(CONF_ELSE): validate_action_list,
|
||||
},
|
||||
cv.has_at_least_one_key(CONF_THEN, CONF_ELSE),
|
||||
cv.has_at_least_one_key(CONF_CONDITION, CONF_ANY, CONF_ALL),
|
||||
cv.AtLeastOne(CONF_THEN, CONF_ELSE),
|
||||
cv.AtLeastOne(CONF_CONDITION, CONF_ANY, CONF_ALL),
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
|
||||
@@ -34,7 +34,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_PIXEL_MAPPER): cv.returning_lambda,
|
||||
}
|
||||
),
|
||||
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
|
||||
cv.AtMostOne(CONF_PAGES, CONF_LAMBDA),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -221,7 +221,7 @@ ACTIONS_SCHEMA = automation.validate_automation(
|
||||
),
|
||||
},
|
||||
cv.All(
|
||||
cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION),
|
||||
cv.ExactlyOne(CONF_SERVICE, CONF_ACTION),
|
||||
cv.rename_key(CONF_SERVICE, CONF_ACTION),
|
||||
_auto_detect_supports_response,
|
||||
# Re-validate supports_response after auto-detection sets it
|
||||
@@ -534,7 +534,7 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True),
|
||||
}
|
||||
),
|
||||
cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION),
|
||||
cv.ExactlyOne(CONF_SERVICE, CONF_ACTION),
|
||||
cv.rename_key(CONF_SERVICE, CONF_ACTION),
|
||||
_validate_response_config,
|
||||
)
|
||||
|
||||
@@ -198,7 +198,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x36)),
|
||||
# ensure end_position and range are mutually exclusive
|
||||
cv.has_at_most_one_key(CONF_END_POSITION, CONF_RANGE),
|
||||
cv.AtMostOne(CONF_END_POSITION, CONF_RANGE),
|
||||
has_valid_range_config(),
|
||||
)
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ RADAR_SETTINGS_SCHEMA = cv.Schema(
|
||||
),
|
||||
}
|
||||
).add_extra(
|
||||
cv.has_at_least_one_key(
|
||||
cv.AtLeastOne(
|
||||
CONF_HW_FRONTEND_RESET,
|
||||
CONF_FREQUENCY,
|
||||
CONF_SENSING_DISTANCE,
|
||||
|
||||
@@ -37,7 +37,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
cv.has_at_least_one_key(CONF_COOL_ACTION, CONF_HEAT_ACTION),
|
||||
cv.AtLeastOne(CONF_COOL_ACTION, CONF_HEAT_ACTION),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -50,9 +50,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
.extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
cv.has_exactly_one_key(
|
||||
CONF_MAC_ADDRESS, CONF_IRK, CONF_SERVICE_UUID, CONF_IBEACON_UUID
|
||||
),
|
||||
cv.ExactlyOne(CONF_MAC_ADDRESS, CONF_IRK, CONF_SERVICE_UUID, CONF_IBEACON_UUID),
|
||||
_validate,
|
||||
)
|
||||
|
||||
|
||||
@@ -50,9 +50,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
.extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
cv.has_exactly_one_key(
|
||||
CONF_MAC_ADDRESS, CONF_IRK, CONF_SERVICE_UUID, CONF_IBEACON_UUID
|
||||
),
|
||||
cv.ExactlyOne(CONF_MAC_ADDRESS, CONF_IRK, CONF_SERVICE_UUID, CONF_IBEACON_UUID),
|
||||
_validate,
|
||||
)
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ CONFIG_SCHEMA = (
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key(CONF_TEMPERATURE, CONF_DURATION),
|
||||
cv.AtLeastOne(CONF_TEMPERATURE, CONF_DURATION),
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_CONSTANT_BRIGHTNESS, default=False): cv.boolean,
|
||||
}
|
||||
),
|
||||
cv.has_none_or_all_keys(
|
||||
cv.AllOrNone(
|
||||
[CONF_COLD_WHITE_COLOR_TEMPERATURE, CONF_WARM_WHITE_COLOR_TEMPERATURE]
|
||||
),
|
||||
light.validate_color_temperature_channels,
|
||||
|
||||
@@ -406,7 +406,7 @@ DEEP_SLEEP_ENTER_SCHEMA = cv.All(
|
||||
)
|
||||
)
|
||||
),
|
||||
cv.has_none_or_all_keys(CONF_UNTIL, CONF_TIME_ID),
|
||||
cv.AllOrNone(CONF_UNTIL, CONF_TIME_ID),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ MMWAVE_SETTINGS_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_SENSITIVITY): cv.templatable(cv.int_range(min=0, max=9)),
|
||||
}
|
||||
).add_extra(
|
||||
cv.has_at_least_one_key(
|
||||
cv.AtLeastOne(
|
||||
CONF_FACTORY_RESET,
|
||||
CONF_DETECTION_SEGMENTS,
|
||||
CONF_OUTPUT_LATENCY,
|
||||
|
||||
@@ -62,7 +62,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s")),
|
||||
cv.has_at_most_one_key(CONF_SENSOR, CONF_LAMBDA),
|
||||
cv.AtMostOne(CONF_SENSOR, CONF_LAMBDA),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x4C)),
|
||||
cv.has_exactly_one_key(CONF_PWM, CONF_DAC),
|
||||
cv.ExactlyOne(CONF_PWM, CONF_DAC),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1550,7 +1550,7 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
||||
): cv.one_of(*SIGNING_SCHEMES, lower=True),
|
||||
}
|
||||
),
|
||||
cv.has_exactly_one_key(CONF_SIGNING_KEY, CONF_VERIFICATION_KEY),
|
||||
cv.ExactlyOne(CONF_SIGNING_KEY, CONF_VERIFICATION_KEY),
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_USE_FULL_CERTIFICATE_BUNDLE, default=False
|
||||
@@ -1708,7 +1708,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
_set_default_framework,
|
||||
_check_versions,
|
||||
set_core_data,
|
||||
cv.has_at_least_one_key(CONF_BOARD, CONF_VARIANT),
|
||||
cv.AtLeastOne(CONF_BOARD, CONF_VARIANT),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -306,7 +306,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
validate_jpeg_quality,
|
||||
cv.has_exactly_one_key(CONF_I2C_PINS, CONF_I2C_ID),
|
||||
cv.ExactlyOne(CONF_I2C_PINS, CONF_I2C_ID),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
): cv.positive_time_period_nanoseconds,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.has_exactly_one_key(CONF_CHIPSET, CONF_BIT0_HIGH),
|
||||
cv.ExactlyOne(CONF_CHIPSET, CONF_BIT0_HIGH),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -279,15 +279,15 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_WATERPROOF_SHIELD_DRIVER): cv.int_range(min=0, max=7),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.has_none_or_all_keys(CONF_DENOISE_GRADE, CONF_DENOISE_CAP_LEVEL),
|
||||
cv.has_none_or_all_keys(
|
||||
cv.AllOrNone(CONF_DENOISE_GRADE, CONF_DENOISE_CAP_LEVEL),
|
||||
cv.AllOrNone(
|
||||
CONF_DEBOUNCE_COUNT,
|
||||
CONF_FILTER_MODE,
|
||||
CONF_NOISE_THRESHOLD,
|
||||
CONF_JITTER_STEP,
|
||||
CONF_SMOOTH_MODE,
|
||||
),
|
||||
cv.has_none_or_all_keys(CONF_WATERPROOF_GUARD_RING, CONF_WATERPROOF_SHIELD_DRIVER),
|
||||
cv.AllOrNone(CONF_WATERPROOF_GUARD_RING, CONF_WATERPROOF_SHIELD_DRIVER),
|
||||
esp32.only_on_variant(
|
||||
supported=[
|
||||
esp32.VARIANT_ESP32,
|
||||
|
||||
@@ -90,7 +90,7 @@ CONFIG_FEEDBACK_COVER_BASE_SCHEMA = (
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
CONFIG_FEEDBACK_COVER_BASE_SCHEMA,
|
||||
cv.has_none_or_all_keys(CONF_OPEN_SENSOR, CONF_CLOSE_SENSOR),
|
||||
cv.AllOrNone(CONF_OPEN_SENSOR, CONF_CLOSE_SENSOR),
|
||||
validate_infer_endstop,
|
||||
)
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x40))
|
||||
.add_extra(cv.has_at_least_one_key(CONF_TEMPERATURE, CONF_HUMIDITY))
|
||||
.add_extra(cv.AtLeastOne(CONF_TEMPERATURE, CONF_HUMIDITY))
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA = cv.All(
|
||||
cv.Required(CONF_URL): cv.templatable(cv.url),
|
||||
}
|
||||
),
|
||||
cv.has_exactly_one_key(CONF_MD5, CONF_MD5_URL),
|
||||
cv.ExactlyOne(CONF_MD5, CONF_MD5_URL),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -191,7 +191,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
.extend(cv.polling_component_schema("1s"))
|
||||
.extend(spi.spi_device_schema(False, "40MHz")),
|
||||
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
|
||||
cv.AtMostOne(CONF_PAGES, CONF_LAMBDA),
|
||||
_validate,
|
||||
)
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
.extend(cv.polling_component_schema("5s"))
|
||||
.extend(i2c.i2c_device_schema(0x48)),
|
||||
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
|
||||
cv.AtMostOne(CONF_PAGES, CONF_LAMBDA),
|
||||
_validate_custom_waveform,
|
||||
)
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.has_exactly_one_key(CONF_REMOTE_RECEIVER_ID, CONF_REMOTE_TRANSMITTER_ID),
|
||||
cv.ExactlyOne(CONF_REMOTE_RECEIVER_ID, CONF_REMOTE_TRANSMITTER_ID),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.has_exactly_one_key(CONF_REMOTE_RECEIVER_ID, CONF_REMOTE_TRANSMITTER_ID),
|
||||
cv.ExactlyOne(CONF_REMOTE_RECEIVER_ID, CONF_REMOTE_TRANSMITTER_ID),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean,
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key(CONF_END_KEYS, CONF_MAX_LENGTH),
|
||||
cv.AtLeastOne(CONF_END_KEYS, CONF_MAX_LENGTH),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -302,7 +302,7 @@ async def random_effect_to_code(config, effect_id):
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key(
|
||||
cv.AtLeastOne(
|
||||
CONF_STATE,
|
||||
CONF_BRIGHTNESS,
|
||||
CONF_COLOR_MODE,
|
||||
|
||||
@@ -106,7 +106,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x53)),
|
||||
cv.has_at_least_one_key(CONF_LIGHT, CONF_AMBIENT_LIGHT, CONF_UV_INDEX, CONF_UV),
|
||||
cv.AtLeastOne(CONF_LIGHT, CONF_AMBIENT_LIGHT, CONF_UV_INDEX, CONF_UV),
|
||||
)
|
||||
|
||||
TYPES = {
|
||||
|
||||
@@ -650,7 +650,7 @@ LVGL_TOP_LEVEL_SCHEMA = (
|
||||
|
||||
LVGL_SCHEMA = cv.All(
|
||||
container_schema(obj_spec, LVGL_TOP_LEVEL_SCHEMA),
|
||||
cv.has_at_most_one_key(CONF_PAGES, df.CONF_LAYOUT),
|
||||
cv.AtMostOne(CONF_PAGES, df.CONF_LAYOUT),
|
||||
add_hello_world,
|
||||
)
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ INDICATOR_LINE_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_VALUE, default=0.0): lv_float,
|
||||
cv.Optional(CONF_OPA, default=1.0): opacity,
|
||||
}
|
||||
).add_extra(cv.has_at_most_one_key(CONF_R_MOD, CONF_LENGTH))
|
||||
).add_extra(cv.AtMostOne(CONF_R_MOD, CONF_LENGTH))
|
||||
|
||||
|
||||
class ScaleType(WidgetType):
|
||||
@@ -186,7 +186,7 @@ INDICATOR_ARC_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_OPA, default=1.0): opacity,
|
||||
cv.Optional(CONF_ROUNDED, default=False): cv.boolean,
|
||||
}
|
||||
).add_extra(cv.has_at_most_one_key(CONF_VALUE, CONF_START_VALUE))
|
||||
).add_extra(cv.AtMostOne(CONF_VALUE, CONF_START_VALUE))
|
||||
|
||||
INDICATOR_TICKS_SCHEMA = cv.Schema(
|
||||
{
|
||||
@@ -198,7 +198,7 @@ INDICATOR_TICKS_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_END_VALUE): lv_float,
|
||||
cv.Optional(CONF_LOCAL, default=False): lv_bool,
|
||||
}
|
||||
).add_extra(cv.has_at_most_one_key(CONF_VALUE, CONF_START_VALUE))
|
||||
).add_extra(cv.AtMostOne(CONF_VALUE, CONF_START_VALUE))
|
||||
|
||||
INDICATOR_SCHEMA = cv.Schema(
|
||||
{
|
||||
@@ -355,7 +355,7 @@ class MeterType(WidgetType):
|
||||
return CONF_SCALE, CONF_LINE, CONF_IMAGE, CONF_LABEL
|
||||
|
||||
def validate(self, value):
|
||||
return cv.has_at_most_one_key(CONF_INDICATOR, CONF_PIVOT)(value)
|
||||
return cv.AtMostOne(CONF_INDICATOR, CONF_PIVOT)(value)
|
||||
|
||||
async def on_create(self, var: MockObj, config: dict):
|
||||
# Remove theme styling from outer container
|
||||
|
||||
@@ -122,7 +122,7 @@ tabview_spec = TabviewType()
|
||||
cv.Optional(CONF_ANIMATED, default=False): animated,
|
||||
cv.Required(CONF_INDEX): lv_int,
|
||||
},
|
||||
).add_extra(cv.has_at_least_one_key(CONF_INDEX, CONF_TAB_ID)),
|
||||
).add_extra(cv.AtLeastOne(CONF_INDEX, CONF_TAB_ID)),
|
||||
synchronous=True,
|
||||
)
|
||||
async def tabview_select(config, action_id, template_arg, args):
|
||||
|
||||
@@ -6,7 +6,7 @@ from collections.abc import Callable
|
||||
import functools
|
||||
from typing import Any, Self
|
||||
|
||||
import voluptuous as vol
|
||||
import probatio
|
||||
|
||||
from esphome.components.const import CONF_COLOR_DEPTH
|
||||
from esphome.components.display import CONF_SHOW_TEST_CARD, display_ns
|
||||
@@ -280,12 +280,12 @@ def model_schema_extractor(
|
||||
names = sorted(models)
|
||||
representative = next((n for n in names if n != _CUSTOM_MODEL), names[0])
|
||||
schema = model_schema({CONF_MODEL: representative, **(extra or {})})
|
||||
if isinstance(schema, vol.All):
|
||||
if isinstance(schema, probatio.All):
|
||||
schema = next(
|
||||
(v for v in schema.validators if isinstance(v, vol.Schema)),
|
||||
(v for v in schema.validators if isinstance(v, probatio.Schema)),
|
||||
schema,
|
||||
)
|
||||
if isinstance(schema, vol.Schema):
|
||||
if isinstance(schema, probatio.Schema):
|
||||
# The resolved schema pins ``model`` to the representative; expose
|
||||
# the full model list so the dumped enum offers every model.
|
||||
schema = schema.extend(
|
||||
|
||||
@@ -32,7 +32,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
.extend(CONFIG_BINARY_SENSOR_SCHEMA)
|
||||
.extend(cv.polling_component_schema("never")),
|
||||
cv.has_at_least_one_key(
|
||||
cv.AtLeastOne(
|
||||
CONF_PAGE_ID,
|
||||
CONF_COMPONENT_ID,
|
||||
CONF_COMPONENT_NAME,
|
||||
|
||||
@@ -63,7 +63,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
.extend(CONFIG_SENSOR_COMPONENT_SCHEMA)
|
||||
.extend(cv.polling_component_schema("never")),
|
||||
cv.has_exactly_one_key(CONF_COMPONENT_ID, CONF_COMPONENT_NAME, CONF_VARIABLE_NAME),
|
||||
cv.ExactlyOne(CONF_COMPONENT_ID, CONF_COMPONENT_NAME, CONF_VARIABLE_NAME),
|
||||
_validate,
|
||||
)
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
switch.switch_schema(NextionSwitch)
|
||||
.extend(CONFIG_SWITCH_COMPONENT_SCHEMA)
|
||||
.extend(cv.polling_component_schema("never")),
|
||||
cv.has_exactly_one_key(CONF_COMPONENT_NAME, CONF_VARIABLE_NAME),
|
||||
cv.ExactlyOne(CONF_COMPONENT_NAME, CONF_VARIABLE_NAME),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
cv.has_exactly_one_key(CONF_NDEF_CONTAINS, CONF_TAG_ID, CONF_UID),
|
||||
cv.ExactlyOne(CONF_NDEF_CONTAINS, CONF_TAG_ID, CONF_UID),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -208,7 +208,7 @@ _NUMBER_SCHEMA = (
|
||||
cv.Optional(CONF_ABOVE): cv.templatable(cv.float_),
|
||||
cv.Optional(CONF_BELOW): cv.templatable(cv.float_),
|
||||
},
|
||||
cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW),
|
||||
cv.AtLeastOne(CONF_ABOVE, CONF_BELOW),
|
||||
),
|
||||
cv.Optional(CONF_UNIT_OF_MEASUREMENT): validate_unit_of_measurement,
|
||||
cv.Optional(CONF_MODE, default="AUTO"): cv.enum(NUMBER_MODES, upper=True),
|
||||
@@ -323,7 +323,7 @@ NUMBER_IN_RANGE_CONDITION_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_ABOVE): cv.float_,
|
||||
cv.Optional(CONF_BELOW): cv.float_,
|
||||
},
|
||||
cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW),
|
||||
cv.AtLeastOne(CONF_ABOVE, CONF_BELOW),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from collections.abc import Callable
|
||||
|
||||
from voluptuous import Schema
|
||||
|
||||
import esphome.config_validation as cv
|
||||
|
||||
from . import const, generate, schema
|
||||
@@ -11,7 +9,7 @@ from .schema import TSchema
|
||||
def create_entities_schema(
|
||||
entities: dict[str, TSchema],
|
||||
get_entity_validation_schema: Callable[[TSchema], cv.Schema],
|
||||
) -> Schema:
|
||||
) -> cv.Schema:
|
||||
entity_schema = {}
|
||||
for key, entity in entities.items():
|
||||
schema_key = (
|
||||
@@ -26,7 +24,7 @@ def create_entities_schema(
|
||||
def create_component_schema(
|
||||
entities: dict[str, schema.EntitySchema],
|
||||
get_entity_validation_schema: Callable[[TSchema], cv.Schema],
|
||||
) -> Schema:
|
||||
) -> cv.Schema:
|
||||
return (
|
||||
cv.Schema(
|
||||
{cv.GenerateID(const.CONF_OPENTHERM_ID): cv.use_id(generate.OpenthermHub)}
|
||||
|
||||
@@ -234,7 +234,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_POLL_PERIOD): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
).extend(_CONNECTION_SCHEMA),
|
||||
cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV),
|
||||
cv.ExactlyOne(CONF_NETWORK_KEY, CONF_TLV),
|
||||
_validate_platform,
|
||||
_validate,
|
||||
_require_vfs_select,
|
||||
|
||||
@@ -149,7 +149,7 @@ REMOTE_PACKAGE_SCHEMA = cv.All(
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key(CONF_FILE, CONF_FILES),
|
||||
cv.AtLeastOne(CONF_FILE, CONF_FILES),
|
||||
expand_file_to_files,
|
||||
)
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ def packet_transport_sensor_schema(base_schema):
|
||||
cv.Required(CONF_PROVIDER): provider_name_validate,
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key(CONF_ID, CONF_REMOTE_ID),
|
||||
cv.AtLeastOne(CONF_ID, CONF_REMOTE_ID),
|
||||
validate_packet_transport_sensor,
|
||||
)
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
.extend(cv.polling_component_schema("1s"))
|
||||
.extend(spi.spi_device_schema()),
|
||||
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
|
||||
cv.AtMostOne(CONF_PAGES, CONF_LAMBDA),
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
|
||||
@@ -78,7 +78,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key(CONF_COOL_OUTPUT, CONF_HEAT_OUTPUT),
|
||||
cv.AtLeastOne(CONF_COOL_OUTPUT, CONF_HEAT_OUTPUT),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ CUSTOMIZED_ENTITY = cv.All(
|
||||
cv.Optional(CONF_NAME): cv.string_strict,
|
||||
},
|
||||
),
|
||||
cv.has_at_least_one_key(CONF_ID, CONF_NAME),
|
||||
cv.AtLeastOne(CONF_ID, CONF_NAME),
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
|
||||
@@ -33,7 +33,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_COLOR_INTERLOCK, default=False): cv.boolean,
|
||||
}
|
||||
),
|
||||
cv.has_none_or_all_keys(
|
||||
cv.AllOrNone(
|
||||
[CONF_COLD_WHITE_COLOR_TEMPERATURE, CONF_WARM_WHITE_COLOR_TEMPERATURE]
|
||||
),
|
||||
light.validate_color_temperature_channels,
|
||||
|
||||
@@ -251,7 +251,7 @@ def _detect_variant(value: ConfigType) -> ConfigType:
|
||||
variant: str | None = value.get(CONF_VARIANT)
|
||||
|
||||
if board is None:
|
||||
# `cv.has_at_least_one_key` guarantees variant is set here.
|
||||
# `cv.AtLeastOne` guarantees variant is set here.
|
||||
board = STANDARD_BOARDS[variant]
|
||||
value[CONF_BOARD] = board
|
||||
|
||||
@@ -298,7 +298,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_ENABLE_FULL_PRINTF, default=False): cv.boolean,
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key(CONF_BOARD, CONF_VARIANT),
|
||||
cv.AtLeastOne(CONF_BOARD, CONF_VARIANT),
|
||||
_detect_variant,
|
||||
set_core_data,
|
||||
)
|
||||
|
||||
@@ -221,7 +221,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
): _validate_timing,
|
||||
}
|
||||
),
|
||||
cv.has_exactly_one_key(CONF_CHIPSET, CONF_BIT0_HIGH),
|
||||
cv.ExactlyOne(CONF_CHIPSET, CONF_BIT0_HIGH),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation({}),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.has_exactly_one_key(CONF_OUTPUT, CONF_SPEAKER),
|
||||
cv.ExactlyOne(CONF_OUTPUT, CONF_SPEAKER),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@ async def select_set_index_to_code(config, action_id, template_arg, args):
|
||||
),
|
||||
cv.Optional(CONF_LAMBDA): cv.returning_lambda,
|
||||
}
|
||||
).add_extra(cv.has_exactly_one_key(CONF_OPTIONS, CONF_LAMBDA)),
|
||||
).add_extra(cv.ExactlyOne(CONF_OPTIONS, CONF_LAMBDA)),
|
||||
)
|
||||
async def select_is_to_code(config, condition_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
|
||||
@@ -338,7 +338,7 @@ _SENSOR_SCHEMA = (
|
||||
cv.Optional(CONF_ABOVE): cv.templatable(cv.float_),
|
||||
cv.Optional(CONF_BELOW): cv.templatable(cv.float_),
|
||||
},
|
||||
cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW),
|
||||
cv.AtLeastOne(CONF_ABOVE, CONF_BELOW),
|
||||
),
|
||||
}
|
||||
)
|
||||
@@ -608,7 +608,7 @@ DELTA_SCHEMA = cv.Any(
|
||||
cv.Optional(CONF_MIN_VALUE, default="0.0"): validate_delta_value,
|
||||
cv.Optional(CONF_BASELINE): cv.templatable(cv.float_),
|
||||
},
|
||||
cv.has_at_least_one_key(CONF_MAX_VALUE, CONF_MIN_VALUE),
|
||||
cv.AtLeastOne(CONF_MAX_VALUE, CONF_MIN_VALUE),
|
||||
),
|
||||
validate_delta_value,
|
||||
)
|
||||
@@ -1003,7 +1003,7 @@ SENSOR_IN_RANGE_CONDITION_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_ABOVE): cv.float_,
|
||||
cv.Optional(CONF_BELOW): cv.float_,
|
||||
},
|
||||
cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW),
|
||||
cv.AtLeastOne(CONF_ABOVE, CONF_BELOW),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(media_player.media_player_schema(SpeakerSourceMediaPlayer)),
|
||||
cv.only_on_esp32,
|
||||
cv.has_at_least_one_key(CONF_ANNOUNCEMENT_PIPELINE, CONF_MEDIA_PIPELINE),
|
||||
cv.AtLeastOne(CONF_ANNOUNCEMENT_PIPELINE, CONF_MEDIA_PIPELINE),
|
||||
_validate_no_shared_resources,
|
||||
_validate_volume_settings,
|
||||
)
|
||||
|
||||
@@ -348,7 +348,7 @@ SPI_SINGLE_SCHEMA = cv.All(
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key(CONF_MISO_PIN, CONF_MOSI_PIN),
|
||||
cv.AtLeastOne(CONF_MISO_PIN, CONF_MOSI_PIN),
|
||||
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]),
|
||||
)
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x3C)),
|
||||
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
|
||||
cv.AtMostOne(CONF_PAGES, CONF_LAMBDA),
|
||||
_validate,
|
||||
)
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(spi.spi_device_schema()),
|
||||
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
|
||||
cv.AtMostOne(CONF_PAGES, CONF_LAMBDA),
|
||||
_validate,
|
||||
)
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(spi.spi_device_schema(cs_pin_required=False)),
|
||||
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
|
||||
cv.AtMostOne(CONF_PAGES, CONF_LAMBDA),
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
|
||||
@@ -21,7 +21,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(spi.spi_device_schema(cs_pin_required=False)),
|
||||
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
|
||||
cv.AtMostOne(CONF_PAGES, CONF_LAMBDA),
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
|
||||
@@ -19,7 +19,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x3D)),
|
||||
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
|
||||
cv.AtMostOne(CONF_PAGES, CONF_LAMBDA),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(spi.spi_device_schema(cs_pin_required=False)),
|
||||
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
|
||||
cv.AtMostOne(CONF_PAGES, CONF_LAMBDA),
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
|
||||
@@ -21,7 +21,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(spi.spi_device_schema()),
|
||||
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
|
||||
cv.AtMostOne(CONF_PAGES, CONF_LAMBDA),
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
|
||||
@@ -21,7 +21,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(spi.spi_device_schema()),
|
||||
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
|
||||
cv.AtMostOne(CONF_PAGES, CONF_LAMBDA),
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
|
||||
@@ -19,7 +19,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x3F)),
|
||||
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
|
||||
cv.AtMostOne(CONF_PAGES, CONF_LAMBDA),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(spi.spi_device_schema()),
|
||||
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
|
||||
cv.AtMostOne(CONF_PAGES, CONF_LAMBDA),
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
|
||||
@@ -67,7 +67,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(spi.spi_device_schema()),
|
||||
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
|
||||
cv.AtMostOne(CONF_PAGES, CONF_LAMBDA),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_OUTPUT): cv.use_id(output.BinaryOutput),
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key(CONF_PIN, CONF_OUTPUT),
|
||||
cv.AtLeastOne(CONF_PIN, CONF_OUTPUT),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -674,7 +674,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
cv.has_at_least_one_key(
|
||||
cv.AtLeastOne(
|
||||
CONF_COOL_ACTION, CONF_DRY_ACTION, CONF_FAN_ONLY_ACTION, CONF_HEAT_ACTION
|
||||
),
|
||||
validate_thermostat,
|
||||
|
||||
@@ -306,7 +306,7 @@ def validate_cron_keys(value):
|
||||
value = {x: value[x] for x in value if x != CONF_AT}
|
||||
value.update(at_)
|
||||
return value
|
||||
return cv.has_at_least_one_key(*CRON_KEYS)(value)
|
||||
return cv.AtLeastOne(*CRON_KEYS)(value)
|
||||
|
||||
|
||||
def validate_tz(value: str) -> str:
|
||||
|
||||
@@ -186,7 +186,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
cv.has_at_least_one_key(CONF_TARGET_TEMPERATURE_DATAPOINT, CONF_SWITCH_DATAPOINT),
|
||||
cv.AtLeastOne(CONF_TARGET_TEMPERATURE_DATAPOINT, CONF_SWITCH_DATAPOINT),
|
||||
validate_temperature_multipliers,
|
||||
validate_cooling_values,
|
||||
)
|
||||
|
||||
@@ -26,7 +26,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
cv.has_at_least_one_key(CONF_SPEED_DATAPOINT, CONF_SWITCH_DATAPOINT),
|
||||
cv.AtLeastOne(CONF_SPEED_DATAPOINT, CONF_SWITCH_DATAPOINT),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.has_at_least_one_key(
|
||||
cv.AtLeastOne(
|
||||
CONF_DIMMER_DATAPOINT,
|
||||
CONF_SWITCH_DATAPOINT,
|
||||
CONF_COLOR_DATAPOINT,
|
||||
|
||||
@@ -43,7 +43,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
cv.has_exactly_one_key(CONF_ENUM_DATAPOINT, CONF_INT_DATAPOINT),
|
||||
cv.ExactlyOne(CONF_ENUM_DATAPOINT, CONF_INT_DATAPOINT),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -268,7 +268,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_DEBUG): maybe_empty_debug,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.has_at_least_one_key(CONF_TX_PIN, CONF_RX_PIN, CONF_PORT),
|
||||
cv.AtLeastOne(CONF_TX_PIN, CONF_RX_PIN, CONF_PORT),
|
||||
validate_host_config,
|
||||
validate_rx_buffer_size,
|
||||
)
|
||||
|
||||
@@ -219,7 +219,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
.extend(spi.spi_device_schema()),
|
||||
validate_full_update_every_only_types_ac,
|
||||
validate_reset_pin_required,
|
||||
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
|
||||
cv.AtMostOne(CONF_PAGES, CONF_LAMBDA),
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
|
||||
@@ -272,7 +272,7 @@ EAP_AUTH_SCHEMA = cv.All(
|
||||
}
|
||||
),
|
||||
wpa2_eap.validate_eap,
|
||||
cv.has_at_least_one_key(CONF_IDENTITY, CONF_CERTIFICATE),
|
||||
cv.AtLeastOne(CONF_IDENTITY, CONF_CERTIFICATE),
|
||||
)
|
||||
|
||||
WIFI_NETWORK_BASE = cv.Schema(
|
||||
|
||||
+19
-19
@@ -10,7 +10,7 @@ import logging
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
import probatio
|
||||
|
||||
from esphome import core, loader, pins, yaml_util
|
||||
from esphome.components.substitutions import do_substitution_pass
|
||||
@@ -185,7 +185,7 @@ def _resolve_component_aliases(config: dict[str, Any]) -> None:
|
||||
if canonical in config:
|
||||
# The canonical key and (at least) one deprecated alias are both
|
||||
# present.
|
||||
raise vol.Invalid(
|
||||
raise probatio.Invalid(
|
||||
f"Both '{legacies[0]}:' (deprecated alias of '{canonical}:') "
|
||||
f"and '{canonical}:' are present in the configuration. Remove "
|
||||
f"the deprecated '{legacies[0]}:' key.",
|
||||
@@ -194,7 +194,7 @@ def _resolve_component_aliases(config: dict[str, Any]) -> None:
|
||||
if len(legacies) > 1:
|
||||
# Several different deprecated aliases of the same component.
|
||||
listed = ", ".join(f"'{alias}:'" for alias in legacies)
|
||||
raise vol.Invalid(
|
||||
raise probatio.Invalid(
|
||||
f"Multiple deprecated aliases of '{canonical}:' are present "
|
||||
f"({listed}). Use only '{canonical}:'.",
|
||||
path=[legacies[0]],
|
||||
@@ -253,7 +253,7 @@ class Config(OrderedDict, fv.FinalValidateConfig):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
# A list of voluptuous errors
|
||||
self.errors: list[vol.Invalid] = []
|
||||
self.errors: list[probatio.Invalid] = []
|
||||
# A list of paths that should be fully outputted
|
||||
# The values will be the paths to all "domain", for example (['logger'], 'logger')
|
||||
# or (['sensor', 'ultrasonic'], 'sensor.ultrasonic')
|
||||
@@ -271,8 +271,8 @@ class Config(OrderedDict, fv.FinalValidateConfig):
|
||||
# ID to ensure stable order for keys with equal priority
|
||||
self._validation_tasks_id = 0
|
||||
|
||||
def add_error(self, error: vol.Invalid) -> None:
|
||||
if isinstance(error, vol.MultipleInvalid):
|
||||
def add_error(self, error: probatio.Invalid) -> None:
|
||||
if isinstance(error, probatio.MultipleInvalid):
|
||||
for err in error.errors:
|
||||
self.add_error(err)
|
||||
return
|
||||
@@ -282,7 +282,7 @@ class Config(OrderedDict, fv.FinalValidateConfig):
|
||||
i for i, v in enumerate(error.path) if v is cv.ROOT_CONFIG_PATH
|
||||
)
|
||||
# can't change the path so re-create the error
|
||||
error = vol.Invalid(
|
||||
error = probatio.Invalid(
|
||||
message=error.error_message,
|
||||
path=error.path[last_root + 1 :],
|
||||
error_type=error.error_type,
|
||||
@@ -308,12 +308,12 @@ class Config(OrderedDict, fv.FinalValidateConfig):
|
||||
yield
|
||||
except cv.FinalExternalInvalid as e:
|
||||
self.add_error(e)
|
||||
except vol.Invalid as e:
|
||||
except probatio.Invalid as e:
|
||||
e.prepend(path)
|
||||
self.add_error(e)
|
||||
|
||||
def add_str_error(self, message: str, path: ConfigPath) -> None:
|
||||
self.add_error(vol.Invalid(message, path))
|
||||
self.add_error(probatio.Invalid(message, path))
|
||||
|
||||
def add_output_path(self, path: ConfigPath, domain: str) -> None:
|
||||
self.output_paths.append((path, domain))
|
||||
@@ -330,7 +330,7 @@ class Config(OrderedDict, fv.FinalValidateConfig):
|
||||
conf = conf[key]
|
||||
conf[path[-1]] = value
|
||||
|
||||
def get_error_for_path(self, path: ConfigPath) -> vol.Invalid | None:
|
||||
def get_error_for_path(self, path: ConfigPath) -> probatio.Invalid | None:
|
||||
for err in self.errors:
|
||||
if self.get_deepest_path(err.path) == path:
|
||||
self.errors.remove(err)
|
||||
@@ -1113,7 +1113,7 @@ def validate_config(
|
||||
config,
|
||||
command_line_substitutions=command_line_substitutions,
|
||||
)
|
||||
except vol.Invalid as err:
|
||||
except probatio.Invalid as err:
|
||||
result.update(config)
|
||||
result.add_error(err)
|
||||
return result
|
||||
@@ -1123,7 +1123,7 @@ def validate_config(
|
||||
result.add_output_path([CONF_SUBSTITUTIONS], CONF_SUBSTITUTIONS)
|
||||
try:
|
||||
config = do_substitution_pass(config, command_line_substitutions)
|
||||
except vol.Invalid as err:
|
||||
except probatio.Invalid as err:
|
||||
CORE.raw_config = config
|
||||
result.add_error(err)
|
||||
return result
|
||||
@@ -1146,7 +1146,7 @@ def validate_config(
|
||||
# plain config errors and abort further validation.
|
||||
try:
|
||||
_resolve_component_aliases(config)
|
||||
except vol.Invalid as err:
|
||||
except probatio.Invalid as err:
|
||||
result.update(config)
|
||||
result.add_error(err)
|
||||
return result
|
||||
@@ -1155,7 +1155,7 @@ def validate_config(
|
||||
# After this step, there will not be any Extend or Remove values in the config anymore
|
||||
try:
|
||||
resolve_extend_remove(config)
|
||||
except vol.Invalid as err:
|
||||
except probatio.Invalid as err:
|
||||
result.add_error(err)
|
||||
|
||||
# 1.3. Load external_components
|
||||
@@ -1165,7 +1165,7 @@ def validate_config(
|
||||
result.add_output_path([CONF_EXTERNAL_COMPONENTS], CONF_EXTERNAL_COMPONENTS)
|
||||
try:
|
||||
do_external_components_pass(config)
|
||||
except vol.Invalid as err:
|
||||
except probatio.Invalid as err:
|
||||
result.update(config)
|
||||
result.add_error(err)
|
||||
return result
|
||||
@@ -1219,7 +1219,7 @@ def validate_config(
|
||||
result.add_output_path([CONF_ESPHOME], CONF_ESPHOME)
|
||||
try:
|
||||
target_platform = core_config.preload_core_config(config, result)
|
||||
except vol.Invalid as err:
|
||||
except probatio.Invalid as err:
|
||||
result.add_error(err)
|
||||
return result
|
||||
# Remove temporary esphome config path again, it will be reloaded later
|
||||
@@ -1287,7 +1287,7 @@ def _get_parent_name(path, config):
|
||||
return path[-1]
|
||||
|
||||
|
||||
def _format_vol_invalid(ex: vol.Invalid, config: Config) -> str:
|
||||
def _format_vol_invalid(ex: probatio.Invalid, config: Config) -> str:
|
||||
message = ""
|
||||
|
||||
paren = _get_parent_name(ex.path[:-1], config)
|
||||
@@ -1299,7 +1299,7 @@ def _format_vol_invalid(ex: vol.Invalid, config: Config) -> str:
|
||||
message += f"[{ex.path[-1]}] is an invalid option for [{paren}]. Please check the indentation."
|
||||
elif "extra keys not allowed" in str(ex):
|
||||
message += f"[{ex.path[-1]}] is an invalid option for [{paren}]."
|
||||
elif isinstance(ex, vol.RequiredFieldInvalid):
|
||||
elif isinstance(ex, probatio.RequiredFieldInvalid):
|
||||
if ex.msg == "required key not provided":
|
||||
message += f"'{ex.path[-1]}' is a required option for [{paren}]."
|
||||
else:
|
||||
@@ -1345,7 +1345,7 @@ def load_config(
|
||||
) -> Config:
|
||||
try:
|
||||
return _load_config(command_line_substitutions, skip_external_update)
|
||||
except vol.Invalid as err:
|
||||
except probatio.Invalid as err:
|
||||
raise EsphomeError(f"Error while parsing config: {err}") from err
|
||||
|
||||
|
||||
|
||||
+94
-108
@@ -22,7 +22,7 @@ from string import ascii_letters, digits
|
||||
import typing
|
||||
import uuid as uuid_
|
||||
|
||||
import voluptuous as vol
|
||||
import probatio
|
||||
|
||||
from esphome import core
|
||||
import esphome.codegen as cg
|
||||
@@ -100,7 +100,12 @@ from esphome.schema_extractors import (
|
||||
schema_extractor_typed,
|
||||
)
|
||||
from esphome.util import parse_esphome_version
|
||||
from esphome.voluptuous_schema import _Schema
|
||||
|
||||
# ExtraKeysInvalid is re-exported here so it is reachable as cv.ExtraKeysInvalid.
|
||||
from esphome.voluptuous_schema import ( # noqa: F401 pylint: disable=unused-import
|
||||
ExtraKeysInvalid,
|
||||
_Schema,
|
||||
)
|
||||
from esphome.yaml_util import SensitiveStr, make_data_base
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -108,20 +113,24 @@ _LOGGER = logging.getLogger(__name__)
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
Schema = _Schema
|
||||
All = vol.All
|
||||
Coerce = vol.Coerce
|
||||
Range = vol.Range
|
||||
Invalid = vol.Invalid
|
||||
MultipleInvalid = vol.MultipleInvalid
|
||||
Any = vol.Any
|
||||
Lower = vol.Lower
|
||||
Upper = vol.Upper
|
||||
Length = vol.Length
|
||||
Exclusive = vol.Exclusive
|
||||
Inclusive = vol.Inclusive
|
||||
ALLOW_EXTRA = vol.ALLOW_EXTRA
|
||||
UNDEFINED = vol.UNDEFINED
|
||||
RequiredFieldInvalid = vol.RequiredFieldInvalid
|
||||
All = probatio.All
|
||||
Coerce = probatio.Coerce
|
||||
Range = probatio.Range
|
||||
Invalid = probatio.Invalid
|
||||
MultipleInvalid = probatio.MultipleInvalid
|
||||
Any = probatio.Any
|
||||
Lower = probatio.Lower
|
||||
Upper = probatio.Upper
|
||||
Length = probatio.Length
|
||||
Exclusive = probatio.Exclusive
|
||||
Inclusive = probatio.Inclusive
|
||||
AtLeastOne = probatio.AtLeastOne
|
||||
ExactlyOne = probatio.ExactlyOne
|
||||
AtMostOne = probatio.AtMostOne
|
||||
AllOrNone = probatio.AllOrNone
|
||||
ALLOW_EXTRA = probatio.ALLOW_EXTRA
|
||||
UNDEFINED = probatio.UNDEFINED
|
||||
RequiredFieldInvalid = probatio.RequiredFieldInvalid
|
||||
# this sentinel object can be placed in an 'Invalid' path to say
|
||||
# the rest of the error path is relative to the root config path
|
||||
ROOT_CONFIG_PATH = object()
|
||||
@@ -330,7 +339,7 @@ class Visibility(StrEnum):
|
||||
YAML_ONLY = "yaml_only"
|
||||
|
||||
|
||||
class Optional(vol.Optional):
|
||||
class Optional(probatio.Optional):
|
||||
"""Mark a field as optional and optionally define a default for the field.
|
||||
|
||||
When no default is defined, the validated config will not contain the key.
|
||||
@@ -360,7 +369,7 @@ class Optional(vol.Optional):
|
||||
self.visibility: Visibility | None = visibility
|
||||
|
||||
|
||||
class Required(vol.Required):
|
||||
class Required(probatio.Required):
|
||||
"""Define a field to be required to be set. The validated configuration is guaranteed
|
||||
to contain this key.
|
||||
|
||||
@@ -865,67 +874,13 @@ only_with_arduino = only_with_framework(Framework.ARDUINO)
|
||||
|
||||
# Adapted from:
|
||||
# https://github.com/alecthomas/voluptuous/issues/115#issuecomment-144464666
|
||||
def has_at_least_one_key(*keys):
|
||||
"""Validate that at least one of the given keys exist in the config."""
|
||||
|
||||
def validate(obj):
|
||||
"""Test keys exist in dict."""
|
||||
if not isinstance(obj, dict):
|
||||
raise Invalid("expected dictionary")
|
||||
|
||||
if not any(k in keys for k in obj):
|
||||
raise Invalid(f"Must contain at least one of {', '.join(keys)}.")
|
||||
return obj
|
||||
|
||||
return validate
|
||||
|
||||
|
||||
def has_exactly_one_key(*keys):
|
||||
"""Validate that exactly one of the given keys exist in the config."""
|
||||
|
||||
def validate(obj):
|
||||
if not isinstance(obj, dict):
|
||||
raise Invalid("expected dictionary")
|
||||
|
||||
number = sum(k in keys for k in obj)
|
||||
if number > 1:
|
||||
raise Invalid(f"Cannot specify more than one of {', '.join(keys)}.")
|
||||
if number < 1:
|
||||
raise Invalid(f"Must contain exactly one of {', '.join(keys)}.")
|
||||
return obj
|
||||
|
||||
return validate
|
||||
|
||||
|
||||
def has_at_most_one_key(*keys):
|
||||
"""Validate that at most one of the given keys exist in the config."""
|
||||
|
||||
def validate(obj):
|
||||
if not isinstance(obj, dict):
|
||||
raise Invalid("expected dictionary")
|
||||
|
||||
used = set(obj) & set(keys)
|
||||
if len(used) > 1:
|
||||
msg = "Cannot specify more than one of '" + "', '".join(used) + "'."
|
||||
raise MultipleInvalid([Invalid(msg, path=[k]) for k in used])
|
||||
return obj
|
||||
|
||||
return validate
|
||||
|
||||
|
||||
def has_none_or_all_keys(*keys):
|
||||
"""Validate that none or all of the given keys exist in the config."""
|
||||
|
||||
def validate(obj):
|
||||
if not isinstance(obj, dict):
|
||||
raise Invalid("expected dictionary")
|
||||
|
||||
number = sum(k in keys for k in obj)
|
||||
if number != 0 and number != len(keys):
|
||||
raise Invalid(f"Must specify either none or all of {', '.join(keys)}.")
|
||||
return obj
|
||||
|
||||
return validate
|
||||
# Kept as aliases for backwards compatibility with external components that use
|
||||
# these names (they also mirror Home Assistant's cv helpers). Internally, prefer the
|
||||
# probatio names (cv.AtLeastOne, ...).
|
||||
has_at_least_one_key = AtLeastOne
|
||||
has_exactly_one_key = ExactlyOne
|
||||
has_at_most_one_key = AtMostOne
|
||||
has_none_or_all_keys = AllOrNone
|
||||
|
||||
|
||||
TIME_PERIOD_ERROR = (
|
||||
@@ -1440,9 +1395,24 @@ def hostname(value):
|
||||
raise Invalid(f"Invalid hostname: {value}")
|
||||
|
||||
|
||||
# Domain/IP matcher (ported from voluptuous, which ESPHome used as vol.DOMAIN_REGEX).
|
||||
_DOMAIN_REGEX = re.compile(
|
||||
"(?:"
|
||||
# domain
|
||||
r"(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+"
|
||||
# tld
|
||||
r"(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?$)"
|
||||
# literal form, ipv4 address (SMTP 4.1.3)
|
||||
r"|^\[(25[0-5]|2[0-4]\d|[0-1]?\d?\d)"
|
||||
r"(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\]$"
|
||||
r")\Z",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def domain(value):
|
||||
value = string(value)
|
||||
if re.match(vol.DOMAIN_REGEX, value) is not None:
|
||||
if _DOMAIN_REGEX.match(value) is not None:
|
||||
return value
|
||||
try:
|
||||
return str(ipaddress(value))
|
||||
@@ -1730,7 +1700,7 @@ def prepend_path(path):
|
||||
path = [path]
|
||||
try:
|
||||
yield
|
||||
except vol.Invalid as e:
|
||||
except probatio.Invalid as e:
|
||||
e.prepend(path)
|
||||
raise e
|
||||
|
||||
@@ -1742,7 +1712,7 @@ def remove_prepend_path(path):
|
||||
path = [path]
|
||||
try:
|
||||
yield
|
||||
except vol.Invalid as e:
|
||||
except probatio.Invalid as e:
|
||||
if list_starts_with(e.path, path):
|
||||
# Can't set e.path (namedtuple
|
||||
for _ in range(len(path)):
|
||||
@@ -1941,7 +1911,7 @@ def extract_keys(schema):
|
||||
for skey in list(schema.keys()):
|
||||
if isinstance(skey, str):
|
||||
keys.append(skey)
|
||||
elif isinstance(skey, vol.Marker) and isinstance(skey.schema, str):
|
||||
elif isinstance(skey, probatio.Marker) and isinstance(skey.schema, str):
|
||||
keys.append(skey.schema)
|
||||
else:
|
||||
raise ValueError
|
||||
@@ -1998,10 +1968,22 @@ class SplitDefault(Optional):
|
||||
self._defaults = {}
|
||||
|
||||
for platform_key, value in kwargs.items():
|
||||
self._defaults[platform_key] = vol.default_factory(value)
|
||||
self._defaults[platform_key] = probatio.default_factory(value)
|
||||
|
||||
@property
|
||||
def default(self):
|
||||
# Return a stable factory. probatio captures it once when the schema is
|
||||
# compiled and calls it at validation time, when the target platform is
|
||||
# known. The factory yields UNDEFINED when no value is configured for the
|
||||
# active platform, which probatio treats as "no default" (key stays absent).
|
||||
return self._resolve_default
|
||||
|
||||
@default.setter
|
||||
def default(self, value):
|
||||
# Ignore default set from probatio.Optional
|
||||
pass
|
||||
|
||||
def _resolve_default(self):
|
||||
keys = []
|
||||
if CORE.is_esp32:
|
||||
from esphome.components.esp32 import VARIANT_ESP32, get_esp32_variant
|
||||
@@ -2014,14 +1996,10 @@ class SplitDefault(Optional):
|
||||
keys += _get_default_key(framework)
|
||||
keys += _get_default_key()
|
||||
for key in keys:
|
||||
if self._defaults.get(key) is not None:
|
||||
return self._defaults[key]
|
||||
return vol.default_factory(vol.UNDEFINED)
|
||||
|
||||
@default.setter
|
||||
def default(self, value):
|
||||
# Ignore default set from vol.Optional
|
||||
pass
|
||||
factory = self._defaults.get(key)
|
||||
if factory is not None and not isinstance(factory, probatio.Undefined):
|
||||
return factory()
|
||||
return probatio.UNDEFINED
|
||||
|
||||
|
||||
class OnlyWith(Optional):
|
||||
@@ -2048,22 +2026,26 @@ class OnlyWith(Optional):
|
||||
def __init__(self, key, component: str | list[str], default=None) -> None:
|
||||
super().__init__(key)
|
||||
self._component = component
|
||||
self._default = vol.default_factory(default)
|
||||
self._default = probatio.default_factory(default)
|
||||
|
||||
@property
|
||||
def default(self) -> Callable[[], typing.Any] | vol.Undefined:
|
||||
if isinstance(self._component, list):
|
||||
if all(c in CORE.loaded_integrations for c in self._component):
|
||||
return self._default
|
||||
elif self._component in CORE.loaded_integrations:
|
||||
return self._default
|
||||
return vol.UNDEFINED
|
||||
def default(self) -> Callable[[], typing.Any]:
|
||||
# Stable factory resolved at validation time (see SplitDefault.default).
|
||||
return self._resolve_default
|
||||
|
||||
@default.setter
|
||||
def default(self, value):
|
||||
# Ignore default set from vol.Optional
|
||||
# Ignore default set from probatio.Optional
|
||||
pass
|
||||
|
||||
def _resolve_default(self) -> typing.Any:
|
||||
if isinstance(self._component, list):
|
||||
if all(c in CORE.loaded_integrations for c in self._component):
|
||||
return self._default()
|
||||
elif self._component in CORE.loaded_integrations:
|
||||
return self._default()
|
||||
return probatio.UNDEFINED
|
||||
|
||||
|
||||
class OnlyWithout(Optional):
|
||||
"""Set the default value only if the given component is NOT loaded."""
|
||||
@@ -2071,19 +2053,23 @@ class OnlyWithout(Optional):
|
||||
def __init__(self, key, component, default=None):
|
||||
super().__init__(key)
|
||||
self._component = component
|
||||
self._default = vol.default_factory(default)
|
||||
self._default = probatio.default_factory(default)
|
||||
|
||||
@property
|
||||
def default(self):
|
||||
if self._component not in CORE.loaded_integrations:
|
||||
return self._default
|
||||
return vol.UNDEFINED
|
||||
# Stable factory resolved at validation time (see SplitDefault.default).
|
||||
return self._resolve_default
|
||||
|
||||
@default.setter
|
||||
def default(self, value):
|
||||
# Ignore default set from vol.Optional
|
||||
# Ignore default set from probatio.Optional
|
||||
pass
|
||||
|
||||
def _resolve_default(self):
|
||||
if self._component not in CORE.loaded_integrations:
|
||||
return self._default()
|
||||
return probatio.UNDEFINED
|
||||
|
||||
|
||||
def _entity_base_validator(config):
|
||||
if CONF_NAME not in config and CONF_ID not in config:
|
||||
@@ -2101,7 +2087,7 @@ def _entity_base_validator(config):
|
||||
|
||||
|
||||
def ensure_schema(schema):
|
||||
if not isinstance(schema, vol.Schema):
|
||||
if not isinstance(schema, probatio.Schema):
|
||||
return Schema(schema)
|
||||
return schema
|
||||
|
||||
@@ -2495,7 +2481,7 @@ def require_esphome_version(year, month, patch):
|
||||
|
||||
@contextmanager
|
||||
def suppress_invalid():
|
||||
with suppress(vol.Invalid):
|
||||
with suppress(probatio.Invalid):
|
||||
yield
|
||||
|
||||
|
||||
|
||||
+66
-182
@@ -1,211 +1,90 @@
|
||||
import difflib
|
||||
import itertools
|
||||
"""ESPHome's ``cv.Schema``, built on probatio.
|
||||
|
||||
import voluptuous as vol
|
||||
probatio validates the mappings; this module only layers on the few behaviors
|
||||
ESPHome needs on top:
|
||||
|
||||
- every schema key must be wrapped in ``cv.Required`` or ``cv.Optional``;
|
||||
- an undeclared ``id`` key is dropped silently, so ``!extend``/``!remove`` work on
|
||||
any list-based config without each component declaring an id in its schema;
|
||||
- ``extra_schemas``: post-validation transforms applied once the mapping validates.
|
||||
|
||||
probatio provides the rest natively: the "did you mean?" close matches on an unknown
|
||||
key (``ExtraKeysInvalid.candidates``), ``dictionary value`` error tagging, and
|
||||
required/default handling.
|
||||
"""
|
||||
|
||||
import probatio
|
||||
from probatio import error as probatio_error
|
||||
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.schema_extractors import schema_extractor_extended
|
||||
|
||||
# Re-exported so existing imports keep working. probatio's ExtraKeysInvalid already
|
||||
# carries the ``candidates`` close-match list that ESPHome renders in its errors.
|
||||
ExtraKeysInvalid = probatio_error.ExtraKeysInvalid
|
||||
|
||||
class ExtraKeysInvalid(vol.Invalid):
|
||||
def __init__(self, *arg, **kwargs):
|
||||
self.candidates = kwargs.pop("candidates")
|
||||
vol.Invalid.__init__(self, *arg, **kwargs)
|
||||
# A bare value of one of these types is not a valid schema key in ESPHome; keys must
|
||||
# be wrapped in Required/Optional. Mirrors voluptuous' ``primitive_types``.
|
||||
_PRIMITIVE_TYPES = (str, int, float, bool, bytes, complex)
|
||||
|
||||
# A single shared marker for the silent ``id`` drop, reused across every compiled
|
||||
# mapping instead of constructing a new one each time.
|
||||
_REMOVE_ID = probatio.Remove(CONF_ID)
|
||||
|
||||
|
||||
def ensure_multiple_invalid(err):
|
||||
if isinstance(err, vol.MultipleInvalid):
|
||||
if isinstance(err, probatio.MultipleInvalid):
|
||||
return err
|
||||
if isinstance(err, list):
|
||||
return vol.MultipleInvalid(err)
|
||||
return vol.MultipleInvalid([err])
|
||||
return probatio.MultipleInvalid(err)
|
||||
return probatio.MultipleInvalid([err])
|
||||
|
||||
|
||||
# pylint: disable=protected-access, unidiomatic-typecheck
|
||||
class _Schema(vol.Schema):
|
||||
"""Custom cv.Schema that prints similar keys on error."""
|
||||
class _Schema(probatio.Schema):
|
||||
"""Custom cv.Schema: ESPHome key rules, id-dropping, and extra schemas."""
|
||||
|
||||
def __init__(
|
||||
self, schema, required=False, extra=vol.PREVENT_EXTRA, extra_schemas=None
|
||||
self, schema, required=False, extra=probatio.PREVENT_EXTRA, extra_schemas=None
|
||||
):
|
||||
super().__init__(schema, required=required, extra=extra)
|
||||
# List of extra schemas to apply after validation
|
||||
# Should be used sparingly, as it's not a very voluptuous-way/clean way of
|
||||
# doing things.
|
||||
# List of extra schemas to apply after validation. Should be used sparingly,
|
||||
# as it's not a very probatio-way/clean way of doing things.
|
||||
self._extra_schemas = extra_schemas or []
|
||||
|
||||
def _compile_dict(self, schema):
|
||||
# Check some things that ESPHome's schemas do not allow, mostly to keep the
|
||||
# schemas sane (these may be re-added if ever needed).
|
||||
for key in schema:
|
||||
if key is probatio.Extra:
|
||||
raise ValueError("ESPHome does not allow vol.Extra")
|
||||
if isinstance(key, probatio.Remove):
|
||||
raise ValueError("ESPHome does not allow vol.Remove")
|
||||
if isinstance(key, _PRIMITIVE_TYPES):
|
||||
raise ValueError(
|
||||
"All schema keys must be wrapped in cv.Required or cv.Optional"
|
||||
)
|
||||
|
||||
# Silently drop an undeclared 'id' on any dict so that !extend / !remove work
|
||||
# on every list-based config without requiring each component to declare an id
|
||||
# in its schema. With ALLOW_EXTRA the key is kept like any other extra; with
|
||||
# REMOVE_EXTRA it is dropped anyway, so only inject the Remove when it matters.
|
||||
# The injected key lives in the compiled candidates only, never in .schema, so
|
||||
# schema introspection (docs, schema dumping) stays clean.
|
||||
if self.extra != probatio.ALLOW_EXTRA and CONF_ID not in schema:
|
||||
schema = {**schema, _REMOVE_ID: object}
|
||||
|
||||
return super()._compile_dict(schema)
|
||||
|
||||
def __call__(self, data):
|
||||
res = super().__call__(data)
|
||||
for extra in self._extra_schemas:
|
||||
try:
|
||||
res = extra(res)
|
||||
except vol.Invalid as err:
|
||||
except probatio.Invalid as err:
|
||||
raise ensure_multiple_invalid(err) from err
|
||||
return res
|
||||
|
||||
def _compile_mapping(self, schema, invalid_msg=None):
|
||||
invalid_msg = invalid_msg or "mapping value"
|
||||
|
||||
# Check some things that ESPHome's schemas do not allow
|
||||
# mostly to keep the logic in this method sane (so these may be re-added if needed).
|
||||
for key in schema:
|
||||
if key is vol.Extra:
|
||||
raise ValueError("ESPHome does not allow vol.Extra")
|
||||
if isinstance(key, vol.Remove):
|
||||
raise ValueError("ESPHome does not allow vol.Remove")
|
||||
if isinstance(key, vol.primitive_types):
|
||||
raise ValueError(
|
||||
"All schema keys must be wrapped in cv.Required or cv.Optional"
|
||||
)
|
||||
|
||||
# Keys that may be required
|
||||
all_required_keys = {key for key in schema if isinstance(key, vol.Required)}
|
||||
|
||||
# Keys that may have defaults
|
||||
# This is a list because sets do not guarantee insertion order
|
||||
all_default_keys = [key for key in schema if isinstance(key, vol.Optional)]
|
||||
|
||||
# Recursively compile schema
|
||||
_compiled_schema = {}
|
||||
for skey, svalue in schema.items():
|
||||
new_key = self._compile(skey)
|
||||
new_value = self._compile(svalue)
|
||||
_compiled_schema[skey] = (new_key, new_value)
|
||||
|
||||
# Sort compiled schema (probably not necessary for esphome, but leave it here just in case)
|
||||
candidates = list(
|
||||
vol.schema_builder._iterate_mapping_candidates(_compiled_schema)
|
||||
)
|
||||
|
||||
# After we have the list of candidates in the correct order, we want to apply some
|
||||
# optimization so that each
|
||||
# key in the data being validated will be matched against the relevant schema keys only.
|
||||
# No point in matching against different keys
|
||||
additional_candidates = []
|
||||
candidates_by_key = {}
|
||||
for skey, (ckey, cvalue) in candidates:
|
||||
if type(skey) in vol.primitive_types:
|
||||
candidates_by_key.setdefault(skey, []).append((skey, (ckey, cvalue)))
|
||||
elif (
|
||||
isinstance(skey, vol.Marker)
|
||||
and type(skey.schema) in vol.primitive_types
|
||||
):
|
||||
candidates_by_key.setdefault(skey.schema, []).append(
|
||||
(skey, (ckey, cvalue))
|
||||
)
|
||||
else:
|
||||
# These are wildcards such as 'int', 'str', 'Remove' and others which should be
|
||||
# applied to all keys
|
||||
additional_candidates.append((skey, (ckey, cvalue)))
|
||||
|
||||
key_names = []
|
||||
for skey in schema:
|
||||
if isinstance(skey, str):
|
||||
key_names.append(skey)
|
||||
elif isinstance(skey, vol.Marker) and isinstance(skey.schema, str):
|
||||
key_names.append(skey.schema)
|
||||
|
||||
def validate_mapping(path, iterable, out):
|
||||
required_keys = all_required_keys.copy()
|
||||
|
||||
# Build a map of all provided key-value pairs.
|
||||
# The type(out) is used to retain ordering in case a ordered
|
||||
# map type is provided as input.
|
||||
key_value_map = type(out)()
|
||||
for key, value in iterable:
|
||||
key_value_map[key] = value
|
||||
|
||||
# Insert default values for non-existing keys.
|
||||
for key in all_default_keys:
|
||||
if (
|
||||
not isinstance(key.default, vol.Undefined)
|
||||
and key.schema not in key_value_map
|
||||
):
|
||||
# A default value has been specified for this missing key, insert it.
|
||||
key_value_map[key.schema] = key.default()
|
||||
|
||||
error = None
|
||||
errors = []
|
||||
for key, value in key_value_map.items():
|
||||
key_path = path + [key]
|
||||
# Optimization. Validate against the matching key first, then fallback to the rest
|
||||
relevant_candidates = itertools.chain(
|
||||
candidates_by_key.get(key, []), additional_candidates
|
||||
)
|
||||
|
||||
# compare each given key/value against all compiled key/values
|
||||
# schema key, (compiled key, compiled value)
|
||||
for skey, (ckey, cvalue) in relevant_candidates:
|
||||
try:
|
||||
new_key = ckey(key_path, key)
|
||||
except vol.Invalid as e:
|
||||
if len(e.path) > len(key_path):
|
||||
raise
|
||||
if not error or len(e.path) > len(error.path):
|
||||
error = e
|
||||
continue
|
||||
# Backtracking is not performed once a key is selected, so if
|
||||
# the value is invalid we immediately throw an exception.
|
||||
exception_errors = []
|
||||
try:
|
||||
cval = cvalue(key_path, value)
|
||||
out[new_key] = cval
|
||||
except vol.MultipleInvalid as e:
|
||||
exception_errors.extend(e.errors)
|
||||
except vol.Invalid as e:
|
||||
exception_errors.append(e)
|
||||
|
||||
if exception_errors:
|
||||
for err in exception_errors:
|
||||
if len(err.path) <= len(key_path):
|
||||
err.error_type = invalid_msg
|
||||
errors.append(err)
|
||||
# If there is a validation error for a required
|
||||
# key, this means that the key was provided.
|
||||
# Discard the required key so it does not
|
||||
# create an additional, noisy exception.
|
||||
required_keys.discard(skey)
|
||||
break
|
||||
|
||||
# Key and value okay, mark as found in case it was
|
||||
# a Required() field.
|
||||
required_keys.discard(skey)
|
||||
|
||||
break
|
||||
else:
|
||||
if self.extra == vol.ALLOW_EXTRA:
|
||||
out[key] = value
|
||||
elif key == "id":
|
||||
# Silently drop 'id' on any dict so that
|
||||
# !extend / !remove work on every list-based
|
||||
# config without requiring each component to
|
||||
# declare an id in its schema.
|
||||
pass
|
||||
elif self.extra != vol.REMOVE_EXTRA:
|
||||
if isinstance(key, str) and key_names:
|
||||
matches = difflib.get_close_matches(key, key_names)
|
||||
errors.append(
|
||||
ExtraKeysInvalid(
|
||||
"extra keys not allowed",
|
||||
key_path,
|
||||
candidates=matches,
|
||||
)
|
||||
)
|
||||
else:
|
||||
errors.append(
|
||||
vol.Invalid("extra keys not allowed", key_path)
|
||||
)
|
||||
|
||||
# for any required keys left that weren't found and don't have defaults:
|
||||
for key in required_keys:
|
||||
msg = getattr(key, "msg", None) or "required key not provided"
|
||||
errors.append(vol.RequiredFieldInvalid(msg, path + [key]))
|
||||
if errors:
|
||||
raise vol.MultipleInvalid(errors)
|
||||
|
||||
return out
|
||||
|
||||
return validate_mapping
|
||||
|
||||
def add_extra(self, validator):
|
||||
validator = _Schema(validator)
|
||||
self._extra_schemas.append(validator)
|
||||
@@ -233,7 +112,12 @@ class _Schema(vol.Schema):
|
||||
extra_schemas = self._extra_schemas.copy()
|
||||
if isinstance(schema, _Schema):
|
||||
extra_schemas.extend(schema._extra_schemas)
|
||||
if isinstance(schema, vol.Schema):
|
||||
if isinstance(schema, probatio.Schema):
|
||||
schema = schema.schema
|
||||
ret = super().extend(schema, extra=extra)
|
||||
# probatio's extend already returns a fully compiled _Schema (type(self)).
|
||||
# Only rewrap (recompile) when there are extra_schemas to carry over;
|
||||
# otherwise return it as-is to avoid compiling the merged mapping twice.
|
||||
if not extra_schemas:
|
||||
return ret
|
||||
return _Schema(ret.schema, extra=ret.extra, extra_schemas=extra_schemas)
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ from esphome.yaml_util import parse_yaml
|
||||
|
||||
def _get_invalid_range(res: Config, invalid: cv.Invalid) -> DocumentRange | None:
|
||||
return res.get_deepest_document_range_for_path(
|
||||
invalid.path, invalid.error_message == "extra keys not allowed"
|
||||
invalid.path, isinstance(invalid, cv.ExtraKeysInvalid)
|
||||
)
|
||||
|
||||
|
||||
|
||||
+9
-7
@@ -5,7 +5,7 @@ import string
|
||||
from typing import Literal, NotRequired, TypedDict, Unpack
|
||||
import unicodedata
|
||||
|
||||
import voluptuous as vol
|
||||
import probatio
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import ALLOWED_NAME_CHARS, ENV_QUICKWIZARD
|
||||
@@ -348,7 +348,7 @@ def wizard(path: Path) -> int:
|
||||
try:
|
||||
name = cv.valid_name(name)
|
||||
break
|
||||
except vol.Invalid:
|
||||
except probatio.Invalid:
|
||||
safe_print(
|
||||
color(
|
||||
AnsiFore.RED,
|
||||
@@ -385,9 +385,11 @@ def wizard(path: Path) -> int:
|
||||
color(AnsiFore.BOLD_WHITE, f"({'/'.join(wizard_platforms)}): ")
|
||||
)
|
||||
try:
|
||||
platform = vol.All(vol.Upper, vol.Any(*wizard_platforms))(platform.upper())
|
||||
platform = probatio.All(probatio.Upper, probatio.Any(*wizard_platforms))(
|
||||
platform.upper()
|
||||
)
|
||||
break
|
||||
except vol.Invalid:
|
||||
except probatio.Invalid:
|
||||
safe_print(
|
||||
f'Unfortunately, I can\'t find an espressif microcontroller called "{platform}". Please try again.'
|
||||
)
|
||||
@@ -452,9 +454,9 @@ def wizard(path: Path) -> int:
|
||||
while True:
|
||||
board = safe_input(color(AnsiFore.BOLD_WHITE, "(board): "))
|
||||
try:
|
||||
board = vol.All(vol.Lower, vol.Any(*boards))(board)
|
||||
board = probatio.All(probatio.Lower, probatio.Any(*boards))(board)
|
||||
break
|
||||
except vol.Invalid:
|
||||
except probatio.Invalid:
|
||||
safe_print(
|
||||
color(
|
||||
AnsiFore.RED, f'Sorry, I don\'t think the board "{board}" exists.'
|
||||
@@ -484,7 +486,7 @@ def wizard(path: Path) -> int:
|
||||
try:
|
||||
ssid = cv.ssid(ssid)
|
||||
break
|
||||
except vol.Invalid:
|
||||
except probatio.Invalid:
|
||||
safe_print(
|
||||
color(
|
||||
AnsiFore.RED,
|
||||
|
||||
+4
-4
@@ -1,17 +1,17 @@
|
||||
cryptography==49.0.0
|
||||
voluptuous==0.16.0
|
||||
probatio==0.5.1
|
||||
PyYAML==6.0.3
|
||||
paho-mqtt==1.6.1
|
||||
colorama==0.4.6
|
||||
tzlocal==5.4.4 # from time
|
||||
tzlocal==5.4.3 # from time
|
||||
tzdata>=2026.2 # from time
|
||||
pyserial==3.5
|
||||
platformio==6.1.19
|
||||
esptool==5.3.1
|
||||
esptool==5.3.0
|
||||
click==8.3.3
|
||||
aioesphomeapi==45.5.2
|
||||
zeroconf==0.150.0
|
||||
puremagic==2.2.0
|
||||
puremagic==1.30
|
||||
ruamel.yaml==0.19.1 # dashboard_import
|
||||
ruamel.yaml.clib==0.2.15 # dashboard_import
|
||||
esphome-glyphsets==0.2.0
|
||||
|
||||
@@ -6,7 +6,7 @@ import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
import voluptuous as vol
|
||||
import probatio
|
||||
|
||||
# NOTE: Cannot import other esphome components globally as a modification in vol_schema
|
||||
# is needed before modules are loaded
|
||||
@@ -929,7 +929,7 @@ def convert(schema, config_var, path):
|
||||
elif schema in (cv.string, cv.string_strict, cv.valid_name):
|
||||
config_var[S_TYPE] = "string"
|
||||
|
||||
elif isinstance(schema, vol.Schema):
|
||||
elif isinstance(schema, probatio.Schema):
|
||||
# test: esphome/project
|
||||
config_var[S_TYPE] = "schema"
|
||||
config_var["schema"] = convert_config(schema.schema, path + "/s")["schema"]
|
||||
@@ -1162,9 +1162,11 @@ def convert_keys(converted, schema, path):
|
||||
"value": str(default_value),
|
||||
"components": components,
|
||||
}
|
||||
elif hasattr(k, "default") and str(k.default) != "...":
|
||||
elif hasattr(k, "default") and not isinstance(k.default, probatio.Undefined):
|
||||
default_value = k.default()
|
||||
if default_value is not None:
|
||||
if default_value is not None and not isinstance(
|
||||
default_value, probatio.Undefined
|
||||
):
|
||||
result["default"] = str(default_value)
|
||||
|
||||
# UI hint from ``cv.Optional`` / ``cv.Required`` — surfaced
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"target_module": "esphome.__main__",
|
||||
"margin_pct": 15,
|
||||
"cumulative_us": 91000
|
||||
"cumulative_us": 127000
|
||||
}
|
||||
|
||||
@@ -45,7 +45,12 @@ def test_configuration_errors(set_core_config: SetCoreConfigCallable) -> None:
|
||||
match=r"string value is None for dictionary value @ data\['lane_bit_rate'\]",
|
||||
):
|
||||
CONFIG_SCHEMA(
|
||||
{"id": "display_id", "model": "custom", "init_sequence": [[0x36, 0x01]]}
|
||||
{
|
||||
"id": "display_id",
|
||||
"model": "custom",
|
||||
"init_sequence": [[0x36, 0x01]],
|
||||
"dimensions": {"width": 320, "height": 240},
|
||||
}
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
|
||||
@@ -201,7 +201,7 @@ def test_transform_and_init_sequence_errors(
|
||||
),
|
||||
pytest.param(
|
||||
{"model": "wt32-sc01-plus", "brightness": 128},
|
||||
r"extra keys not allowed @ data\['brightness'\]",
|
||||
r"not a valid option @ data\['brightness'\]",
|
||||
id="brightness_not_supported",
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1460,7 +1460,7 @@ def test_only_with_framework_suggestion_without_docs_path() -> None:
|
||||
|
||||
|
||||
def test_has_at_least_one_key_not_dict() -> None:
|
||||
with pytest.raises(Invalid, match="expected dictionary"):
|
||||
with pytest.raises(Invalid, match="expected a dictionary"):
|
||||
cv.has_at_least_one_key("a", "b")([])
|
||||
|
||||
|
||||
@@ -1475,17 +1475,17 @@ def test_has_at_least_one_key_ok() -> None:
|
||||
|
||||
|
||||
def test_has_exactly_one_key_not_dict() -> None:
|
||||
with pytest.raises(Invalid, match="expected dictionary"):
|
||||
with pytest.raises(Invalid, match="expected a dictionary"):
|
||||
cv.has_exactly_one_key("a", "b")("notdict")
|
||||
|
||||
|
||||
def test_has_exactly_one_key_too_many() -> None:
|
||||
with pytest.raises(Invalid, match="Cannot specify more than one"):
|
||||
with pytest.raises(Invalid, match="exactly one of"):
|
||||
cv.has_exactly_one_key("a", "b")({"a": 1, "b": 2})
|
||||
|
||||
|
||||
def test_has_exactly_one_key_too_few() -> None:
|
||||
with pytest.raises(Invalid, match="Must contain exactly one"):
|
||||
with pytest.raises(Invalid, match="exactly one of"):
|
||||
cv.has_exactly_one_key("a", "b")({"c": 1})
|
||||
|
||||
|
||||
@@ -1495,12 +1495,12 @@ def test_has_exactly_one_key_ok() -> None:
|
||||
|
||||
|
||||
def test_has_at_most_one_key_not_dict() -> None:
|
||||
with pytest.raises(Invalid, match="expected dictionary"):
|
||||
with pytest.raises(Invalid, match="expected a dictionary"):
|
||||
cv.has_at_most_one_key("a", "b")(5)
|
||||
|
||||
|
||||
def test_has_at_most_one_key_too_many() -> None:
|
||||
with pytest.raises(vol.MultipleInvalid, match="Cannot specify more than one"):
|
||||
with pytest.raises(vol.MultipleInvalid, match="at most one of"):
|
||||
cv.has_at_most_one_key("a", "b")({"a": 1, "b": 2})
|
||||
|
||||
|
||||
@@ -1510,7 +1510,7 @@ def test_has_at_most_one_key_ok() -> None:
|
||||
|
||||
|
||||
def test_has_none_or_all_keys_not_dict() -> None:
|
||||
with pytest.raises(Invalid, match="expected dictionary"):
|
||||
with pytest.raises(Invalid, match="expected a dictionary"):
|
||||
cv.has_none_or_all_keys("a", "b")(5)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
"""The voluptuous compatibility shim for third-party (external) components.
|
||||
|
||||
External components import ``voluptuous`` directly. ESPHome installs probatio's
|
||||
shim at package import (``esphome/__init__.py``), so those imports resolve to
|
||||
probatio without the components needing any change. These tests pin that contract.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
import probatio
|
||||
|
||||
import esphome # noqa: F401 (importing installs the shim)
|
||||
|
||||
|
||||
def test_voluptuous_resolves_to_probatio_shim() -> None:
|
||||
"""``import voluptuous`` resolves to probatio's shim, not a real voluptuous."""
|
||||
voluptuous = sys.modules.get("voluptuous")
|
||||
assert voluptuous is not None
|
||||
assert voluptuous.__name__ == "probatio._vol_shim"
|
||||
# Submodules dependencies reach into are aliased too.
|
||||
assert "voluptuous.schema_builder" in sys.modules
|
||||
assert hasattr(sys.modules["voluptuous.schema_builder"], "_compile_scalar")
|
||||
|
||||
|
||||
def test_external_component_style_schema_validates() -> None:
|
||||
"""A schema built the way a third-party component would, through voluptuous."""
|
||||
import voluptuous as vol # noqa: PLC0415 (mimics an external component)
|
||||
|
||||
# Markers imported from voluptuous are probatio markers via the shim.
|
||||
assert issubclass(vol.Required, probatio.Marker)
|
||||
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Required("name"): str,
|
||||
vol.Optional("count", default=1): int,
|
||||
}
|
||||
)
|
||||
assert schema({"name": "x"}) == {"name": "x", "count": 1}
|
||||
|
||||
|
||||
def test_shim_extra_key_is_probatio_error() -> None:
|
||||
"""An extra key raises probatio's MultipleInvalid through the shim."""
|
||||
import voluptuous as vol # noqa: PLC0415
|
||||
|
||||
schema = vol.Schema({vol.Required("name"): str})
|
||||
try:
|
||||
schema({"name": "x", "bogus": 1})
|
||||
except vol.MultipleInvalid as err:
|
||||
assert isinstance(err, probatio.MultipleInvalid)
|
||||
else:
|
||||
raise AssertionError("expected MultipleInvalid for an extra key")
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Tests for voluptuous_schema.py."""
|
||||
|
||||
import probatio as vol
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
|
||||
from esphome.voluptuous_schema import _Schema
|
||||
from esphome.voluptuous_schema import ExtraKeysInvalid, _Schema
|
||||
|
||||
|
||||
class TestIdKeyDropping:
|
||||
@@ -38,8 +38,11 @@ class TestIdKeyDropping:
|
||||
vol.Required("name"): str,
|
||||
}
|
||||
)
|
||||
with pytest.raises(vol.MultipleInvalid, match="extra keys not allowed"):
|
||||
with pytest.raises(vol.MultipleInvalid) as exc_info:
|
||||
schema({"name": "test", "unknown_key": "value"})
|
||||
# The extra key is reported as an ExtraKeysInvalid (probatio carries the
|
||||
# close-match candidates on it); assert the type, not the exact wording.
|
||||
assert any(isinstance(err, ExtraKeysInvalid) for err in exc_info.value.errors)
|
||||
|
||||
def test_id_key_not_dropped_when_in_schema(self):
|
||||
"""When 'id' is declared in the schema, it should be validated normally."""
|
||||
|
||||
Reference in New Issue
Block a user