From aff151c90a11d872ca5717300aee3044492b02e5 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Thu, 22 Aug 2019 11:01:56 -0700 Subject: [PATCH] Load user-provided descriptions for python_scripts (#26069) * Load user-provided descriptions for python_scripts * Import SERVICE_DESCRIPTION_CACHE * Use async_set_service_schema to register service descriptions * Add python_script tests for loading service descriptions * Use async/await in test --- .../components/python_script/__init__.py | 15 +++ tests/components/python_script/test_init.py | 100 +++++++++++++++++- 2 files changed, 114 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py index 788da6a8d64..715c06aca43 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -9,8 +9,10 @@ import voluptuous as vol from homeassistant.const import SERVICE_RELOAD from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.service import async_set_service_schema from homeassistant.loader import bind_hass from homeassistant.util import sanitize_filename +from homeassistant.util.yaml.loader import load_yaml import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -90,10 +92,23 @@ def discover_scripts(hass): continue hass.services.remove(DOMAIN, existing_service) + # Load user-provided service descriptions from python_scripts/services.yaml + services_yaml = os.path.join(path, "services.yaml") + if os.path.exists(services_yaml): + services_dict = load_yaml(services_yaml) + else: + services_dict = {} + for fil in glob.iglob(os.path.join(path, "*.py")): name = os.path.splitext(os.path.basename(fil))[0] hass.services.register(DOMAIN, name, python_script_service_handler) + service_desc = { + "description": services_dict.get(name, {}).get("description", ""), + "fields": services_dict.get(name, {}).get("fields", {}), + } + async_set_service_schema(hass, DOMAIN, name, service_desc) + @bind_hass def execute_script(hass, name, data=None): diff --git a/tests/components/python_script/test_init.py b/tests/components/python_script/test_init.py index fcf1519d4c7..d7732c00f94 100644 --- a/tests/components/python_script/test_init.py +++ b/tests/components/python_script/test_init.py @@ -3,8 +3,11 @@ import asyncio import logging from unittest.mock import patch, mock_open +from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.setup import async_setup_component -from homeassistant.components.python_script import execute +from homeassistant.components.python_script import DOMAIN, execute, FOLDER + +from tests.common import patch_yaml_files @asyncio.coroutine @@ -289,6 +292,101 @@ def test_reload(hass): assert hass.services.has_service("python_script", "reload") +async def test_service_descriptions(hass): + """Test that service descriptions are loaded and reloaded correctly.""" + # Test 1: no user-provided services.yaml file + scripts1 = [ + "/some/config/dir/python_scripts/hello.py", + "/some/config/dir/python_scripts/world_beer.py", + ] + + service_descriptions1 = ( + "hello:\n" + " description: Description of hello.py.\n" + " fields:\n" + " fake_param:\n" + " description: Parameter used by hello.py.\n" + " example: 'This is a test of python_script.hello'" + ) + services_yaml1 = { + "{}/{}/services.yaml".format( + hass.config.config_dir, FOLDER + ): service_descriptions1 + } + + with patch( + "homeassistant.components.python_script.os.path.isdir", return_value=True + ), patch( + "homeassistant.components.python_script.glob.iglob", return_value=scripts1 + ), patch( + "homeassistant.components.python_script.os.path.exists", return_value=True + ), patch_yaml_files( + services_yaml1 + ): + await async_setup_component(hass, DOMAIN, {}) + + descriptions = await async_get_all_descriptions(hass) + + assert len(descriptions) == 1 + + assert descriptions[DOMAIN]["hello"]["description"] == "Description of hello.py." + assert ( + descriptions[DOMAIN]["hello"]["fields"]["fake_param"]["description"] + == "Parameter used by hello.py." + ) + assert ( + descriptions[DOMAIN]["hello"]["fields"]["fake_param"]["example"] + == "This is a test of python_script.hello" + ) + + assert descriptions[DOMAIN]["world_beer"]["description"] == "" + assert bool(descriptions[DOMAIN]["world_beer"]["fields"]) is False + + # Test 2: user-provided services.yaml file + scripts2 = [ + "/some/config/dir/python_scripts/hello2.py", + "/some/config/dir/python_scripts/world_beer.py", + ] + + service_descriptions2 = ( + "hello2:\n" + " description: Description of hello2.py.\n" + " fields:\n" + " fake_param:\n" + " description: Parameter used by hello2.py.\n" + " example: 'This is a test of python_script.hello2'" + ) + services_yaml2 = { + "{}/{}/services.yaml".format( + hass.config.config_dir, FOLDER + ): service_descriptions2 + } + + with patch( + "homeassistant.components.python_script.os.path.isdir", return_value=True + ), patch( + "homeassistant.components.python_script.glob.iglob", return_value=scripts2 + ), patch( + "homeassistant.components.python_script.os.path.exists", return_value=True + ), patch_yaml_files( + services_yaml2 + ): + await hass.services.async_call(DOMAIN, "reload", {}, blocking=True) + descriptions = await async_get_all_descriptions(hass) + + assert len(descriptions) == 1 + + assert descriptions[DOMAIN]["hello2"]["description"] == "Description of hello2.py." + assert ( + descriptions[DOMAIN]["hello2"]["fields"]["fake_param"]["description"] + == "Parameter used by hello2.py." + ) + assert ( + descriptions[DOMAIN]["hello2"]["fields"]["fake_param"]["example"] + == "This is a test of python_script.hello2" + ) + + @asyncio.coroutine def test_sleep_warns_one(hass, caplog): """Test time.sleep warns once."""