Name the manifest in every dependency diagnostic, warn where no backend can recover

normalize_dependencies' parameter is manifest_name (the dict branch
already binds owner to a package owner), and the arduino call site
passes the library's name so its unrecognized-entry warning stops
saying 'of manifest'. A version-less dependency warns when the backend
declares no provides tree (espidf/zephyr/nrf52 have no post-emit
pickup), and the shared walk mirrors the typed IncompatiblePlatform
branch: routine platform skips stay at debug, any other InvalidLibrary
cause warns naming the component. The two real-converter tests pin
ESPHOME_DATA_DIR to tmp_path so an ambient data dir cannot leak in.
This commit is contained in:
J. Nick Koston
2026-08-22 09:20:57 -05:00
parent 9bb899d8df
commit 8e6af6aa8c
4 changed files with 86 additions and 17 deletions
+3 -1
View File
@@ -300,7 +300,9 @@ def resolve_libraries(
"with add_library() if needed",
component.name,
)
for dep in normalize_dependencies(component.data.get("dependencies")):
for dep in normalize_dependencies(
component.data.get("dependencies"), component.name
):
name = dep.get("name")
if (
not name
+38 -11
View File
@@ -628,13 +628,15 @@ def join_flag_args(tokens: Iterable[str], owner: str) -> list[str]:
return out
def normalize_dependencies(dependencies: Any, owner: str = "manifest") -> list[dict]:
def normalize_dependencies(
dependencies: Any, manifest_name: str = "manifest"
) -> list[dict]:
"""Normalize a library manifest's ``dependencies`` to a list of dicts.
PIO's library.json accepts the list-of-dicts form, the shorthand dict
form (``{"owner/Name": "version_spec"}``), bare name strings inside the
list, and a plain (possibly comma-separated) string; normalize them all
so callers see a uniform list. ``owner`` names the manifest in the
so callers see a uniform list. ``manifest_name`` names the manifest in the
warning for entries that cannot be normalized.
"""
if not dependencies:
@@ -668,7 +670,9 @@ def normalize_dependencies(dependencies: Any, owner: str = "manifest") -> list[d
normalized.append({"name": entry})
else:
_LOGGER.warning(
"Ignoring unrecognized dependency entry %r of %s", entry, owner
"Ignoring unrecognized dependency entry %r of %s",
entry,
manifest_name,
)
return normalized
@@ -987,18 +991,41 @@ def convert_libraries(
component.data.get("dependencies"), component.name
):
if "name" not in dependency or "version" not in dependency:
# Version-less deps cannot resolve from the registry; the
# arduino backend picks bundled ones up after emit
_LOGGER.debug(
"Skip version-less dependency %r of %s",
dependency.get("name"),
component.name,
)
if backend.provides is None:
# No backend tree can supply it and the registry cannot
# resolve it: a real drop, not a routine skip
_LOGGER.warning(
"Dependency %r of %s has no version to resolve; skipping",
dependency.get("name"),
component.name,
)
else:
# The backend (e.g. the arduino bundled tree) picks
# these up after emit
_LOGGER.debug(
"Skip version-less dependency %r of %s",
dependency.get("name"),
component.name,
)
continue
try:
check_library_data(dependency, backend.platform, backend.framework)
except InvalidLibrary as e:
_LOGGER.debug("Skip dependency %s: %s", dependency.get("name"), str(e))
if isinstance(e, IncompatiblePlatform):
# Routine cross-platform skip
_LOGGER.debug(
"Skip dependency %s: %s", dependency.get("name"), str(e)
)
else:
# Any other cause is a dropped dependency and must be
# visible (unreachable from check_library_data today,
# which raises only for the platform filter)
_LOGGER.warning(
"Skipping dependency %s of %s: %s",
dependency.get("name"),
component.name,
e,
)
continue
dep_name = _owner_pkgname_to_name(
dependency.get("owner"), dependency.get("name")
+7 -5
View File
@@ -760,7 +760,7 @@ def test_bundled_library_non_dict_manifest_skips_probes_and_raises(
def test_dict_shorthand_dependency_skips_registry_through_real_converter(
tmp_path: Path,
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""{"Wire": "*"} in a real manifest must never reach the registry: the
graph walk skips backend-provided names and the bundled copy is added
@@ -777,9 +777,9 @@ def test_dict_shorthand_dependency_skips_registry_through_real_converter(
# as_uri() forms a valid file:// URL on every platform (file:///C:/...
# on Windows; a bare f-string would embed backslashes)
_add_library(local_lib.as_uri(), None)
# The real converter writes its component cache under the config dir
CORE.config_path = tmp_path / "test.yaml"
CORE.config_path.write_text("")
# Pin the component cache to tmp_path (data_dir honors an ambient
# ESPHOME_DATA_DIR otherwise)
monkeypatch.setenv("ESPHOME_DATA_DIR", str(tmp_path / ".esphome"))
with patch.object(
pio_library,
"_resolve_registry_version",
@@ -891,7 +891,9 @@ def test_bundled_dependency_string_list_form(
def test_pinned_bundled_dependency_substitution_warns(
tmp_path: Path, caplog: pytest.LogCaptureFixture
tmp_path: Path,
caplog: pytest.LogCaptureFixture,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A non-* version pin on a backend-provided dependency is discarded
for the bundled copy; the substitution must be visible."""
@@ -607,3 +607,41 @@ def test_normalize_dependencies_forms(caplog) -> None:
{"name": "SPI"},
]
assert normalize_dependencies("Wire") == [{"name": "Wire"}]
def test_versionless_dependency_without_provider_warns(
tmp_path, monkeypatch, caplog: pytest.LogCaptureFixture
) -> None:
"""When no backend tree can supply a version-less dependency, the drop
is a warning, not a debug line."""
_patch_download_with_manifests(
monkeypatch,
tmp_path,
{"esphome/A": {"name": "A", "dependencies": [{"name": "Hash"}]}},
)
convert_libraries([Library("esphome/A", None, None)], _backend())
assert "'Hash' of esphome/A has no version to resolve" in caplog.text
def test_walk_warns_for_nonplatform_invalid_library(
tmp_path, monkeypatch, caplog: pytest.LogCaptureFixture
) -> None:
"""A dependency dropped for any cause other than the routine platform
filter is visible in every backend."""
_patch_download_with_manifests(
monkeypatch,
tmp_path,
{"esphome/A": {"name": "A", "dependencies": [{"name": "B", "version": "1.0"}]}},
)
calls = {"n": 0}
real = lib.check_library_data
def flaky(data, platform, framework):
calls["n"] += 1
if calls["n"] > 1:
raise InvalidLibrary("manifest is corrupt")
return real(data, platform, framework)
monkeypatch.setattr(lib, "check_library_data", flaky)
convert_libraries([Library("esphome/A", None, None)], _backend())
assert "Skipping dependency B of esphome/A: manifest is corrupt" in caplog.text