From 8620bef5b041db29f1658963ac26e7d344c8cbe9 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
Date: Tue, 11 Jun 2024 16:31:19 +0200
Subject: [PATCH] Support shared keys starting with period in services.yaml
 (#118789)

---
 homeassistant/components/light/services.yaml | 361 ++++++++++---------
 homeassistant/helpers/service.py             |  15 +-
 script/hassfest/services.py                  |   5 +-
 tests/common.py                              |   7 +-
 tests/helpers/test_service.py                |  55 +++
 5 files changed, 262 insertions(+), 181 deletions(-)

diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml
index 4f9f4e03b89..6183d2a49df 100644
--- a/homeassistant/components/light/services.yaml
+++ b/homeassistant/components/light/services.yaml
@@ -1,4 +1,184 @@
 # Describes the format for available light services
+.brightness_support: &brightness_support
+  attribute:
+    supported_color_modes:
+      - light.ColorMode.BRIGHTNESS
+      - light.ColorMode.COLOR_TEMP
+      - light.ColorMode.HS
+      - light.ColorMode.XY
+      - light.ColorMode.RGB
+      - light.ColorMode.RGBW
+      - light.ColorMode.RGBWW
+
+.color_support: &color_support
+  attribute:
+    supported_color_modes:
+      - light.ColorMode.HS
+      - light.ColorMode.XY
+      - light.ColorMode.RGB
+      - light.ColorMode.RGBW
+      - light.ColorMode.RGBWW
+
+.color_temp_support: &color_temp_support
+  attribute:
+    supported_color_modes:
+      - light.ColorMode.COLOR_TEMP
+      - light.ColorMode.HS
+      - light.ColorMode.XY
+      - light.ColorMode.RGB
+      - light.ColorMode.RGBW
+      - light.ColorMode.RGBWW
+
+.named_colors: &named_colors
+  - "homeassistant"
+  - "aliceblue"
+  - "antiquewhite"
+  - "aqua"
+  - "aquamarine"
+  - "azure"
+  - "beige"
+  - "bisque"
+  # Black is omitted from this list as nonsensical for lights
+  - "blanchedalmond"
+  - "blue"
+  - "blueviolet"
+  - "brown"
+  - "burlywood"
+  - "cadetblue"
+  - "chartreuse"
+  - "chocolate"
+  - "coral"
+  - "cornflowerblue"
+  - "cornsilk"
+  - "crimson"
+  - "cyan"
+  - "darkblue"
+  - "darkcyan"
+  - "darkgoldenrod"
+  - "darkgray"
+  - "darkgreen"
+  - "darkgrey"
+  - "darkkhaki"
+  - "darkmagenta"
+  - "darkolivegreen"
+  - "darkorange"
+  - "darkorchid"
+  - "darkred"
+  - "darksalmon"
+  - "darkseagreen"
+  - "darkslateblue"
+  - "darkslategray"
+  - "darkslategrey"
+  - "darkturquoise"
+  - "darkviolet"
+  - "deeppink"
+  - "deepskyblue"
+  - "dimgray"
+  - "dimgrey"
+  - "dodgerblue"
+  - "firebrick"
+  - "floralwhite"
+  - "forestgreen"
+  - "fuchsia"
+  - "gainsboro"
+  - "ghostwhite"
+  - "gold"
+  - "goldenrod"
+  - "gray"
+  - "green"
+  - "greenyellow"
+  - "grey"
+  - "honeydew"
+  - "hotpink"
+  - "indianred"
+  - "indigo"
+  - "ivory"
+  - "khaki"
+  - "lavender"
+  - "lavenderblush"
+  - "lawngreen"
+  - "lemonchiffon"
+  - "lightblue"
+  - "lightcoral"
+  - "lightcyan"
+  - "lightgoldenrodyellow"
+  - "lightgray"
+  - "lightgreen"
+  - "lightgrey"
+  - "lightpink"
+  - "lightsalmon"
+  - "lightseagreen"
+  - "lightskyblue"
+  - "lightslategray"
+  - "lightslategrey"
+  - "lightsteelblue"
+  - "lightyellow"
+  - "lime"
+  - "limegreen"
+  - "linen"
+  - "magenta"
+  - "maroon"
+  - "mediumaquamarine"
+  - "mediumblue"
+  - "mediumorchid"
+  - "mediumpurple"
+  - "mediumseagreen"
+  - "mediumslateblue"
+  - "mediumspringgreen"
+  - "mediumturquoise"
+  - "mediumvioletred"
+  - "midnightblue"
+  - "mintcream"
+  - "mistyrose"
+  - "moccasin"
+  - "navajowhite"
+  - "navy"
+  - "navyblue"
+  - "oldlace"
+  - "olive"
+  - "olivedrab"
+  - "orange"
+  - "orangered"
+  - "orchid"
+  - "palegoldenrod"
+  - "palegreen"
+  - "paleturquoise"
+  - "palevioletred"
+  - "papayawhip"
+  - "peachpuff"
+  - "peru"
+  - "pink"
+  - "plum"
+  - "powderblue"
+  - "purple"
+  - "red"
+  - "rosybrown"
+  - "royalblue"
+  - "saddlebrown"
+  - "salmon"
+  - "sandybrown"
+  - "seagreen"
+  - "seashell"
+  - "sienna"
+  - "silver"
+  - "skyblue"
+  - "slateblue"
+  - "slategray"
+  - "slategrey"
+  - "snow"
+  - "springgreen"
+  - "steelblue"
+  - "tan"
+  - "teal"
+  - "thistle"
+  - "tomato"
+  - "turquoise"
+  - "violet"
+  - "wheat"
+  - "white"
+  - "whitesmoke"
+  - "yellow"
+  - "yellowgreen"
 
 turn_on:
   target:
@@ -15,14 +195,7 @@ turn_on:
           max: 300
           unit_of_measurement: seconds
     rgb_color: &rgb_color
-      filter: &color_support
-        attribute:
-          supported_color_modes:
-            - light.ColorMode.HS
-            - light.ColorMode.XY
-            - light.ColorMode.RGB
-            - light.ColorMode.RGBW
-            - light.ColorMode.RGBWW
+      filter: *color_support
       example: "[255, 100, 100]"
       selector:
         color_rgb:
@@ -44,156 +217,7 @@ turn_on:
       selector:
         select:
           translation_key: color_name
-          options: &named_colors
-            - "homeassistant"
-            - "aliceblue"
-            - "antiquewhite"
-            - "aqua"
-            - "aquamarine"
-            - "azure"
-            - "beige"
-            - "bisque"
-            # Black is omitted from this list as nonsensical for lights
-            - "blanchedalmond"
-            - "blue"
-            - "blueviolet"
-            - "brown"
-            - "burlywood"
-            - "cadetblue"
-            - "chartreuse"
-            - "chocolate"
-            - "coral"
-            - "cornflowerblue"
-            - "cornsilk"
-            - "crimson"
-            - "cyan"
-            - "darkblue"
-            - "darkcyan"
-            - "darkgoldenrod"
-            - "darkgray"
-            - "darkgreen"
-            - "darkgrey"
-            - "darkkhaki"
-            - "darkmagenta"
-            - "darkolivegreen"
-            - "darkorange"
-            - "darkorchid"
-            - "darkred"
-            - "darksalmon"
-            - "darkseagreen"
-            - "darkslateblue"
-            - "darkslategray"
-            - "darkslategrey"
-            - "darkturquoise"
-            - "darkviolet"
-            - "deeppink"
-            - "deepskyblue"
-            - "dimgray"
-            - "dimgrey"
-            - "dodgerblue"
-            - "firebrick"
-            - "floralwhite"
-            - "forestgreen"
-            - "fuchsia"
-            - "gainsboro"
-            - "ghostwhite"
-            - "gold"
-            - "goldenrod"
-            - "gray"
-            - "green"
-            - "greenyellow"
-            - "grey"
-            - "honeydew"
-            - "hotpink"
-            - "indianred"
-            - "indigo"
-            - "ivory"
-            - "khaki"
-            - "lavender"
-            - "lavenderblush"
-            - "lawngreen"
-            - "lemonchiffon"
-            - "lightblue"
-            - "lightcoral"
-            - "lightcyan"
-            - "lightgoldenrodyellow"
-            - "lightgray"
-            - "lightgreen"
-            - "lightgrey"
-            - "lightpink"
-            - "lightsalmon"
-            - "lightseagreen"
-            - "lightskyblue"
-            - "lightslategray"
-            - "lightslategrey"
-            - "lightsteelblue"
-            - "lightyellow"
-            - "lime"
-            - "limegreen"
-            - "linen"
-            - "magenta"
-            - "maroon"
-            - "mediumaquamarine"
-            - "mediumblue"
-            - "mediumorchid"
-            - "mediumpurple"
-            - "mediumseagreen"
-            - "mediumslateblue"
-            - "mediumspringgreen"
-            - "mediumturquoise"
-            - "mediumvioletred"
-            - "midnightblue"
-            - "mintcream"
-            - "mistyrose"
-            - "moccasin"
-            - "navajowhite"
-            - "navy"
-            - "navyblue"
-            - "oldlace"
-            - "olive"
-            - "olivedrab"
-            - "orange"
-            - "orangered"
-            - "orchid"
-            - "palegoldenrod"
-            - "palegreen"
-            - "paleturquoise"
-            - "palevioletred"
-            - "papayawhip"
-            - "peachpuff"
-            - "peru"
-            - "pink"
-            - "plum"
-            - "powderblue"
-            - "purple"
-            - "red"
-            - "rosybrown"
-            - "royalblue"
-            - "saddlebrown"
-            - "salmon"
-            - "sandybrown"
-            - "seagreen"
-            - "seashell"
-            - "sienna"
-            - "silver"
-            - "skyblue"
-            - "slateblue"
-            - "slategray"
-            - "slategrey"
-            - "snow"
-            - "springgreen"
-            - "steelblue"
-            - "tan"
-            - "teal"
-            - "thistle"
-            - "tomato"
-            - "turquoise"
-            - "violet"
-            - "wheat"
-            - "white"
-            - "whitesmoke"
-            - "yellow"
-            - "yellowgreen"
+          options: *named_colors
     hs_color: &hs_color
       filter: *color_support
       advanced: true
@@ -207,15 +231,7 @@ turn_on:
       selector:
         object:
     color_temp: &color_temp
-      filter: &color_temp_support
-        attribute:
-          supported_color_modes:
-            - light.ColorMode.COLOR_TEMP
-            - light.ColorMode.HS
-            - light.ColorMode.XY
-            - light.ColorMode.RGB
-            - light.ColorMode.RGBW
-            - light.ColorMode.RGBWW
+      filter: *color_temp_support
       advanced: true
       selector:
         color_temp:
@@ -230,16 +246,7 @@ turn_on:
           min: 2000
           max: 6500
     brightness: &brightness
-      filter: &brightness_support
-        attribute:
-          supported_color_modes:
-            - light.ColorMode.BRIGHTNESS
-            - light.ColorMode.COLOR_TEMP
-            - light.ColorMode.HS
-            - light.ColorMode.XY
-            - light.ColorMode.RGB
-            - light.ColorMode.RGBW
-            - light.ColorMode.RGBWW
+      filter: *brightness_support
       advanced: true
       selector:
         number:
diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py
index 3a828ada9c2..a9959902084 100644
--- a/homeassistant/helpers/service.py
+++ b/homeassistant/helpers/service.py
@@ -187,7 +187,20 @@ _SERVICE_SCHEMA = vol.Schema(
     extra=vol.ALLOW_EXTRA,
 )
 
-_SERVICES_SCHEMA = vol.Schema({cv.slug: vol.Any(None, _SERVICE_SCHEMA)})
+
+def starts_with_dot(key: str) -> str:
+    """Check if key starts with dot."""
+    if not key.startswith("."):
+        raise vol.Invalid("Key does not start with .")
+    return key
+
+
+_SERVICES_SCHEMA = vol.Schema(
+    {
+        vol.Remove(vol.All(str, starts_with_dot)): object,
+        cv.slug: vol.Any(None, _SERVICE_SCHEMA),
+    }
+)
 
 
 class ServiceParams(TypedDict):
diff --git a/script/hassfest/services.py b/script/hassfest/services.py
index c962d84e6e1..ea4503d5410 100644
--- a/script/hassfest/services.py
+++ b/script/hassfest/services.py
@@ -78,7 +78,10 @@ CUSTOM_INTEGRATION_SERVICE_SCHEMA = vol.Any(
 )
 
 CORE_INTEGRATION_SERVICES_SCHEMA = vol.Schema(
-    {cv.slug: CORE_INTEGRATION_SERVICE_SCHEMA}
+    {
+        vol.Remove(vol.All(str, service.starts_with_dot)): object,
+        cv.slug: CORE_INTEGRATION_SERVICE_SCHEMA,
+    }
 )
 CUSTOM_INTEGRATION_SERVICES_SCHEMA = vol.Schema(
     {cv.slug: CUSTOM_INTEGRATION_SERVICE_SCHEMA}
diff --git a/tests/common.py b/tests/common.py
index 3f1dea4b720..cf5469e1cd2 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -1432,7 +1432,10 @@ def mock_config_flow(domain: str, config_flow: type[ConfigFlow]) -> None:
 
 
 def mock_integration(
-    hass: HomeAssistant, module: MockModule, built_in: bool = True
+    hass: HomeAssistant,
+    module: MockModule,
+    built_in: bool = True,
+    top_level_files: set[str] | None = None,
 ) -> loader.Integration:
     """Mock an integration."""
     integration = loader.Integration(
@@ -1442,7 +1445,7 @@ def mock_integration(
         else f"{loader.PACKAGE_CUSTOM_COMPONENTS}.{module.DOMAIN}",
         pathlib.Path(""),
         module.mock_manifest(),
-        set(),
+        top_level_files,
     )
 
     def mock_import_platform(platform_name: str) -> NoReturn:
diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py
index c9d92c2f25a..60fe87db9d2 100644
--- a/tests/helpers/test_service.py
+++ b/tests/helpers/test_service.py
@@ -3,6 +3,7 @@
 import asyncio
 from collections.abc import Iterable
 from copy import deepcopy
+import io
 from typing import Any
 from unittest.mock import AsyncMock, Mock, patch
 
@@ -43,13 +44,16 @@ from homeassistant.helpers import (
 import homeassistant.helpers.config_validation as cv
 from homeassistant.loader import async_get_integration
 from homeassistant.setup import async_setup_component
+from homeassistant.util.yaml.loader import parse_yaml
 
 from tests.common import (
     MockEntity,
+    MockModule,
     MockUser,
     async_mock_service,
     mock_area_registry,
     mock_device_registry,
+    mock_integration,
     mock_registry,
 )
 
@@ -916,6 +920,57 @@ async def test_async_get_all_descriptions(hass: HomeAssistant) -> None:
     assert await service.async_get_all_descriptions(hass) is descriptions
 
 
+async def test_async_get_all_descriptions_dot_keys(hass: HomeAssistant) -> None:
+    """Test async_get_all_descriptions with keys starting with a period."""
+    service_descriptions = """
+        .anchor: &anchor
+          selector:
+            text:
+        test_service:
+          fields:
+            test: *anchor
+    """
+
+    domain = "test_domain"
+
+    hass.services.async_register(domain, "test_service", lambda call: None)
+    mock_integration(hass, MockModule(domain), top_level_files={"services.yaml"})
+    assert await async_setup_component(hass, domain, {})
+
+    def load_yaml(fname, secrets=None):
+        with io.StringIO(service_descriptions) as file:
+            return parse_yaml(file)
+
+    with (
+        patch(
+            "homeassistant.helpers.service._load_services_files",
+            side_effect=service._load_services_files,
+        ) as proxy_load_services_files,
+        patch(
+            "homeassistant.util.yaml.loader.load_yaml",
+            side_effect=load_yaml,
+        ) as mock_load_yaml,
+    ):
+        descriptions = await service.async_get_all_descriptions(hass)
+
+    mock_load_yaml.assert_called_once_with("services.yaml", None)
+    assert proxy_load_services_files.mock_calls[0][1][1] == unordered(
+        [
+            await async_get_integration(hass, domain),
+        ]
+    )
+
+    assert descriptions == {
+        "test_domain": {
+            "test_service": {
+                "description": "",
+                "fields": {"test": {"selector": {"text": None}}},
+                "name": "",
+            }
+        }
+    }
+
+
 async def test_async_get_all_descriptions_failing_integration(
     hass: HomeAssistant, caplog: pytest.LogCaptureFixture
 ) -> None:
-- 
GitLab