Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion esphome_device_builder/helpers/device_yaml/_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,8 @@ def extract_directly_referenced_integrations(
return []
out: set[str] = set()
for key, value in config.items():
if not isinstance(key, str):
# esphome ignores dot-prefixed top-level keys (anchor containers).
if not isinstance(key, str) or key.startswith("."):
continue
out.add(key)
if isinstance(value, list):
Expand Down
4 changes: 3 additions & 1 deletion esphome_device_builder/helpers/yaml/scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ def block_end_index(lines: list[str], start: int) -> int:
"""First line after *start* that opens the next top-level block; ``len(lines)`` at EOF."""
for idx in range(start + 1, len(lines)):
stripped = lines[idx].rstrip("\n\r")
if stripped and stripped[0].isalpha() and not stripped.startswith(" "):
# A dot-prefixed key (esphome-ignored anchor container) opens a
# block too; comments deliberately don't.
if stripped and (stripped[0].isalpha() or stripped[0] == "."):
return idx
return len(lines)

Expand Down
2 changes: 1 addition & 1 deletion esphome_device_builder/helpers/yaml/writing_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def _locate_singleton_block(
if not stripped:
continue
if not stripped.startswith(" "):
if stripped[0].isalpha():
if stripped[0].isalpha() or stripped[0] == ".":
end = idx
break
# Column-0 comment ends the block only when the next
Expand Down
9 changes: 9 additions & 0 deletions tests/test_directly_referenced_integrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ def test_extracts_top_level_keys() -> None:
]


def test_skips_dot_prefixed_keys() -> None:
"""esphome-ignored anchor containers never count as integrations."""
config = {
"esphome": {"name": "x"},
".defaultfilters": [{"throttle": "60s"}],
}
assert extract_directly_referenced_integrations(config) == ["esphome"]


def test_extracts_platform_stems_from_list() -> None:
"""``- platform: <name>`` entries under a list-shaped block.

Expand Down
40 changes: 39 additions & 1 deletion tests/test_yaml_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,12 @@
_plain_is_fast_safe,
_plain_is_safe,
)
from esphome_device_builder.helpers.yaml.scan import find_block_header, top_level_key_index
from esphome_device_builder.helpers.yaml.scan import (
block_end_index,
find_block_header,
top_level_key_index,
)
from esphome_device_builder.helpers.yaml.writing_layout import _locate_singleton_block
from esphome_device_builder.models import ErrorCode
from esphome_device_builder.models.common import ConfigEntry, ConfigEntryType
from esphome_device_builder.models.components import (
Expand Down Expand Up @@ -1291,6 +1296,22 @@ def test_merge_component_yaml_preserves_following_blocks_after_splice() -> None:
assert result.count("sensor:\n") == 1


def test_merge_component_yaml_splice_stops_at_dot_prefixed_key() -> None:
"""A dot-prefixed anchor block after the domain block ends the splice range."""
component = _component(component_id="sensor.dht", category=ComponentCategory.SENSOR)
fields: dict[str, Any] = {"pin": "GPIO4", "name": "Inside"}

existing = (
"esphome:\n name: kitchen\n\n"
"sensor:\n - platform: bme280\n address: 0x76\n\n"
".defaultfilters:\n - &throttle_time\n throttle: 60s\n"
)
result = merge_component_yaml(existing, component, fields)

assert result.endswith(".defaultfilters:\n - &throttle_time\n throttle: 60s\n")
assert result.index("- platform: dht") < result.index(".defaultfilters:")


def test_merge_component_yaml_appends_non_platform_component() -> None:
"""A non-platform component (e.g. ``i2c``) emits as a top-level mapping.

Expand Down Expand Up @@ -2432,6 +2453,23 @@ def test_child_block_end_trims_shallow_banner_but_keeps_deep_comment() -> None:
assert _cbe(text, 1, " ") == 4


def test_block_end_index_stops_at_dot_prefixed_key() -> None:
lines = ["sensor:\n", " - platform: bme280\n", ".defaultfilters:\n", " - &throttle\n"]
assert block_end_index(lines, 0) == 2


def test_block_end_index_skips_column_zero_comments() -> None:
lines = ["sensor:\n", " - platform: bme280\n", "# banner\n", "logger:\n"]
assert block_end_index(lines, 0) == 3


def test_locate_singleton_block_ends_at_dot_prefixed_key() -> None:
lines = ["api:\n", " reboot_timeout: 0s\n", ".anchors:\n", " - &throttle\n"]
span = _locate_singleton_block(lines, "api")
assert span is not None
assert span[:2] == (0, 2)


def test_top_level_key_index_matches_find_block_header() -> None:
lines = [
"esphome:\n",
Expand Down