diff --git a/esphome/config_validation.py b/esphome/config_validation.py index a62c8f2675..8289f56df3 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1815,6 +1815,8 @@ def one_of(*values, **kwargs): - *int* (``bool``, default=False): Whether to convert the incoming values to integers. - *float* (``bool``, default=False): Whether to convert the incoming values to floats. - *space* (``str``, default=' '): What to convert spaces in the input string to. + - *underscore* (``str``, default='_'): What to convert underscores in the input string to. + - *hyphen* (``str``, default='-'): What to convert hyphens in the input string to. """ options = ", ".join(f"'{x}'" for x in values) lower = kwargs.pop("lower", False) @@ -1823,8 +1825,11 @@ def one_of(*values, **kwargs): to_int = kwargs.pop("int", False) to_float = kwargs.pop("float", False) space = kwargs.pop("space", " ") + underscore = kwargs.pop("underscore", "_") + hyphen = kwargs.pop("hyphen", "-") if kwargs: raise ValueError + separators = str.maketrans({" ": space, "_": underscore, "-": hyphen}) @schema_extractor("one_of") def validator(value): @@ -1833,7 +1838,7 @@ def one_of(*values, **kwargs): if string_: value = string(value) - value = value.replace(" ", space) + value = value.translate(separators) if to_int: value = int_(value) if to_float: diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index 864fbe6475..79bfc303b7 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -2428,6 +2428,58 @@ def test_one_of_string_and_space() -> None: assert cv.one_of("a_b", string=True, space="_")("a b") == "a_b" +def test_one_of_string_and_underscore() -> None: + assert cv.one_of("a-b", string=True, underscore="-")("a_b") == "a-b" + assert cv.one_of("a-b", string=True, underscore="-")("a-b") == "a-b" + + +def test_one_of_string_lower_space_and_underscore() -> None: + validator = cv.one_of("output-mode", lower=True, space="-", underscore="-") + assert validator("output_mode") == "output-mode" + assert validator("OUTPUT_MODE") == "output-mode" + assert validator("output mode") == "output-mode" + assert validator("output-mode") == "output-mode" + + +def test_one_of_string_underscore_unknown() -> None: + with pytest.raises(Invalid): + cv.one_of("a-b", string=True, underscore="-")("c_d") + + +def test_one_of_string_underscore_default_unchanged() -> None: + with pytest.raises(Invalid): + cv.one_of("a-b", string=True)("a_b") + + +def test_one_of_string_and_hyphen() -> None: + assert cv.one_of("a_b", string=True, hyphen="_")("a-b") == "a_b" + assert cv.one_of("a_b", string=True, hyphen="_")("a_b") == "a_b" + + +def test_one_of_string_lower_space_and_hyphen() -> None: + validator = cv.one_of("output_mode", lower=True, space="_", hyphen="_") + assert validator("output-mode") == "output_mode" + assert validator("OUTPUT-MODE") == "output_mode" + assert validator("output mode") == "output_mode" + assert validator("output_mode") == "output_mode" + + +def test_one_of_string_hyphen_unknown() -> None: + with pytest.raises(Invalid): + cv.one_of("a_b", string=True, hyphen="_")("c-d") + + +def test_one_of_string_hyphen_default_unchanged() -> None: + with pytest.raises(Invalid): + cv.one_of("a_b", string=True)("a-b") + + +def test_one_of_string_underscore_hyphen_swap_no_cascade() -> None: + validator = cv.one_of("a-b", "a_b", string=True, underscore="-", hyphen="_") + assert validator("a_b") == "a-b" + assert validator("a-b") == "a_b" + + def test_one_of_int() -> None: assert cv.one_of(1, 2, int=True)("2") == 2 @@ -2466,6 +2518,20 @@ def test_enum_valid() -> None: assert result.enum_value == 10 +def test_enum_valid_with_underscore() -> None: + mapping = {"a-b": 1} + result = cv.enum(mapping, string=True, underscore="-")("a_b") + assert result == "a-b" + assert result.enum_value == 1 + + +def test_enum_valid_with_hyphen() -> None: + mapping = {"a_b": 1} + result = cv.enum(mapping, string=True, hyphen="_")("a-b") + assert result == "a_b" + assert result.enum_value == 1 + + # --------------------------------------------------------------------------- # lambda_ / returning_lambda # ---------------------------------------------------------------------------