diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index f4d11e7bd0..08ae798282 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -24,22 +24,13 @@ DOMAIN = CONF_PACKAGES def validate_git_package(config: dict): + if CONF_URL not in config: + return config + config = BASE_SCHEMA(config) new_config = config - for key, conf in config.items(): - if CONF_URL in conf: - try: - conf = BASE_SCHEMA(conf) - if CONF_FILE in conf: - new_config[key][CONF_FILES] = [conf[CONF_FILE]] - del new_config[key][CONF_FILE] - except cv.MultipleInvalid as e: - with cv.prepend_path([key]): - raise e - except cv.Invalid as e: - raise cv.Invalid( - "Extra keys not allowed in git based package", - path=[key] + e.path, - ) from e + if CONF_FILE in config: + new_config[CONF_FILES] = [config[CONF_FILE]] + del new_config[CONF_FILE] return new_config @@ -74,8 +65,8 @@ BASE_SCHEMA = cv.All( cv.Required(CONF_URL): cv.url, cv.Optional(CONF_USERNAME): cv.string, cv.Optional(CONF_PASSWORD): cv.string, - cv.Exclusive(CONF_FILE, "files"): validate_yaml_filename, - cv.Exclusive(CONF_FILES, "files"): cv.All( + cv.Exclusive(CONF_FILE, CONF_FILES): validate_yaml_filename, + cv.Exclusive(CONF_FILES, CONF_FILES): cv.All( cv.ensure_list( cv.Any( validate_yaml_filename, @@ -100,14 +91,17 @@ BASE_SCHEMA = cv.All( cv.has_at_least_one_key(CONF_FILE, CONF_FILES), ) +PACKAGE_SCHEMA = cv.All( + cv.Any(validate_source_shorthand, BASE_SCHEMA, dict), validate_git_package +) -CONFIG_SCHEMA = cv.All( +CONFIG_SCHEMA = cv.Any( cv.Schema( { - str: cv.Any(validate_source_shorthand, BASE_SCHEMA, dict), + str: PACKAGE_SCHEMA, } ), - validate_git_package, + cv.ensure_list(PACKAGE_SCHEMA), ) @@ -183,25 +177,33 @@ def _process_base_package(config: dict) -> dict: return {"packages": packages} +def _process_package(package_config, config): + recursive_package = package_config + if CONF_URL in package_config: + package_config = _process_base_package(package_config) + if isinstance(package_config, dict): + recursive_package = do_packages_pass(package_config) + config = merge_config(recursive_package, config) + return config + + def do_packages_pass(config: dict): if CONF_PACKAGES not in config: return config packages = config[CONF_PACKAGES] with cv.prepend_path(CONF_PACKAGES): packages = CONFIG_SCHEMA(packages) - if not isinstance(packages, dict): + if isinstance(packages, dict): + for package_name, package_config in reversed(packages.items()): + with cv.prepend_path(package_name): + config = _process_package(package_config, config) + elif isinstance(packages, list): + for package_config in reversed(packages): + config = _process_package(package_config, config) + else: raise cv.Invalid( - f"Packages must be a key to value mapping, got {type(packages)} instead" + f"Packages must be a key to value mapping or list, got {type(packages)} instead" ) - for package_name, package_config in reversed(packages.items()): - with cv.prepend_path(package_name): - recursive_package = package_config - if CONF_URL in package_config: - package_config = _process_base_package(package_config) - if isinstance(package_config, dict): - recursive_package = do_packages_pass(package_config) - config = merge_config(recursive_package, config) - del config[CONF_PACKAGES] return config diff --git a/tests/component_tests/packages/test_packages.py b/tests/component_tests/packages/test_packages.py index 3fbbf49afd..4712daad0d 100644 --- a/tests/component_tests/packages/test_packages.py +++ b/tests/component_tests/packages/test_packages.py @@ -76,10 +76,11 @@ def test_package_unused(basic_esphome, basic_wifi): def test_package_invalid_dict(basic_esphome, basic_wifi): """ - Ensures an error is raised if packages is not valid. + If a url: key is present, it's expected to be well-formed remote package spec. Ensure an error is raised if not. + Any other simple dict passed as a package will be merged as usual but may fail later validation. """ - config = {CONF_ESPHOME: basic_esphome, CONF_PACKAGES: basic_wifi} + config = {CONF_ESPHOME: basic_esphome, CONF_PACKAGES: basic_wifi | {CONF_URL: ""}} with pytest.raises(cv.Invalid): do_packages_pass(config) diff --git a/tests/components/packages/package.yaml b/tests/components/packages/package.yaml new file mode 100644 index 0000000000..672d66151e --- /dev/null +++ b/tests/components/packages/package.yaml @@ -0,0 +1,3 @@ +sensor: + - platform: template + id: package_sensor diff --git a/tests/components/packages/test.esp32-ard.yaml b/tests/components/packages/test.esp32-ard.yaml new file mode 100644 index 0000000000..d35c27d997 --- /dev/null +++ b/tests/components/packages/test.esp32-ard.yaml @@ -0,0 +1,11 @@ +packages: + - sensor: + - platform: template + id: inline_sensor + - !include package.yaml + - github://esphome/esphome/tests/components/template/common.yaml@dev + - url: https://github.com/esphome/esphome + file: tests/components/binary_sensor_map/common.yaml + ref: dev + refresh: 1d + diff --git a/tests/components/packages/test.esp32-idf.yaml b/tests/components/packages/test.esp32-idf.yaml new file mode 100644 index 0000000000..9f1484d1fd --- /dev/null +++ b/tests/components/packages/test.esp32-idf.yaml @@ -0,0 +1,13 @@ +packages: + sensor: + sensor: + - platform: template + id: inline_sensor + local: !include package.yaml + shorthand: github://esphome/esphome/tests/components/template/common.yaml@dev + github: + url: https://github.com/esphome/esphome + file: tests/components/binary_sensor_map/common.yaml + ref: dev + refresh: 1d +