Fix editor live validation (#6431)

This commit is contained in:
J. Nick Koston 2024-03-25 21:24:58 -10:00 committed by Jesse Hills
parent d304e52940
commit f00d876080
No known key found for this signature in database
GPG key ID: BEAAE804EFD8E83A
4 changed files with 63 additions and 51 deletions

View file

@ -1,10 +1,11 @@
from __future__ import annotations
import abc import abc
import functools import functools
import heapq import heapq
import logging import logging
import re import re
from typing import Optional, Union from typing import Union, Any
from contextlib import contextmanager from contextlib import contextmanager
import contextvars import contextvars
@ -76,7 +77,7 @@ def _path_begins_with(path: ConfigPath, other: ConfigPath) -> bool:
@functools.total_ordering @functools.total_ordering
class _ValidationStepTask: class _ValidationStepTask:
def __init__(self, priority: float, id_number: int, step: "ConfigValidationStep"): def __init__(self, priority: float, id_number: int, step: ConfigValidationStep):
self.priority = priority self.priority = priority
self.id_number = id_number self.id_number = id_number
self.step = step self.step = step
@ -130,7 +131,7 @@ class Config(OrderedDict, fv.FinalValidateConfig):
) )
self.errors.append(error) self.errors.append(error)
def add_validation_step(self, step: "ConfigValidationStep"): def add_validation_step(self, step: ConfigValidationStep):
id_num = self._validation_tasks_id id_num = self._validation_tasks_id
self._validation_tasks_id += 1 self._validation_tasks_id += 1
heapq.heappush( heapq.heappush(
@ -172,7 +173,7 @@ class Config(OrderedDict, fv.FinalValidateConfig):
conf = conf[key] conf = conf[key]
conf[path[-1]] = value conf[path[-1]] = value
def get_error_for_path(self, path: ConfigPath) -> Optional[vol.Invalid]: def get_error_for_path(self, path: ConfigPath) -> vol.Invalid | None:
for err in self.errors: for err in self.errors:
if self.get_deepest_path(err.path) == path: if self.get_deepest_path(err.path) == path:
self.errors.remove(err) self.errors.remove(err)
@ -181,7 +182,7 @@ class Config(OrderedDict, fv.FinalValidateConfig):
def get_deepest_document_range_for_path( def get_deepest_document_range_for_path(
self, path: ConfigPath, get_key: bool = False self, path: ConfigPath, get_key: bool = False
) -> Optional[ESPHomeDataBase]: ) -> ESPHomeDataBase | None:
data = self data = self
doc_range = None doc_range = None
for index, path_item in enumerate(path): for index, path_item in enumerate(path):
@ -733,7 +734,9 @@ class PinUseValidationCheck(ConfigValidationStep):
pins.PIN_SCHEMA_REGISTRY.final_validate(result) pins.PIN_SCHEMA_REGISTRY.final_validate(result)
def validate_config(config, command_line_substitutions) -> Config: def validate_config(
config: dict[str, Any], command_line_substitutions: dict[str, Any]
) -> Config:
result = Config() result = Config()
loader.clear_component_meta_finders() loader.clear_component_meta_finders()
@ -897,24 +900,23 @@ class InvalidYAMLError(EsphomeError):
self.base_exc = base_exc self.base_exc = base_exc
def _load_config(command_line_substitutions): def _load_config(command_line_substitutions: dict[str, Any]) -> Config:
"""Load the configuration file."""
try: try:
config = yaml_util.load_yaml(CORE.config_path) config = yaml_util.load_yaml(CORE.config_path)
except EsphomeError as e: except EsphomeError as e:
raise InvalidYAMLError(e) from e raise InvalidYAMLError(e) from e
try: try:
result = validate_config(config, command_line_substitutions) return validate_config(config, command_line_substitutions)
except EsphomeError: except EsphomeError:
raise raise
except Exception: except Exception:
_LOGGER.error("Unexpected exception while reading configuration:") _LOGGER.error("Unexpected exception while reading configuration:")
raise raise
return result
def load_config(command_line_substitutions: dict[str, Any]) -> Config:
def load_config(command_line_substitutions):
try: try:
return _load_config(command_line_substitutions) return _load_config(command_line_substitutions)
except vol.Invalid as err: except vol.Invalid as err:

View file

@ -1,9 +1,4 @@
import json
import os
from esphome.const import CONF_ID from esphome.const import CONF_ID
from esphome.core import CORE
from esphome.helpers import read_file
class Extend: class Extend:
@ -38,25 +33,6 @@ class Remove:
return isinstance(b, Remove) and self.value == b.value return isinstance(b, Remove) and self.value == b.value
def read_config_file(path: str) -> str:
if CORE.vscode and (
not CORE.ace or os.path.abspath(path) == os.path.abspath(CORE.config_path)
):
print(
json.dumps(
{
"type": "read_file",
"path": path,
}
)
)
data = json.loads(input())
assert data["type"] == "file_response"
return data["content"]
return read_file(path)
def merge_config(full_old, full_new): def merge_config(full_old, full_new):
def merge(old, new): def merge(old, new):
if isinstance(new, dict): if isinstance(new, dict):

View file

@ -1,20 +1,22 @@
from __future__ import annotations
import json import json
import os import os
from io import StringIO
from typing import Any
from typing import Optional from esphome.yaml_util import parse_yaml
from esphome.config import validate_config, _format_vol_invalid, Config
from esphome.config import load_config, _format_vol_invalid, Config
from esphome.core import CORE, DocumentRange from esphome.core import CORE, DocumentRange
import esphome.config_validation as cv import esphome.config_validation as cv
def _get_invalid_range(res: Config, invalid: cv.Invalid) -> Optional[DocumentRange]: def _get_invalid_range(res: Config, invalid: cv.Invalid) -> DocumentRange | None:
return res.get_deepest_document_range_for_path( return res.get_deepest_document_range_for_path(
invalid.path, invalid.error_message == "extra keys not allowed" invalid.path, invalid.error_message == "extra keys not allowed"
) )
def _dump_range(range: Optional[DocumentRange]) -> Optional[dict]: def _dump_range(range: DocumentRange | None) -> dict | None:
if range is None: if range is None:
return None return None
return { return {
@ -56,6 +58,25 @@ class VSCodeResult:
) )
def _read_file_content_from_json_on_stdin() -> str:
"""Read the content of a json encoded file from stdin."""
data = json.loads(input())
assert data["type"] == "file_response"
return data["content"]
def _print_file_read_event(path: str) -> None:
"""Print a file read event."""
print(
json.dumps(
{
"type": "read_file",
"path": path,
}
)
)
def read_config(args): def read_config(args):
while True: while True:
CORE.reset() CORE.reset()
@ -68,9 +89,17 @@ def read_config(args):
CORE.config_path = os.path.join(args.configuration, f) CORE.config_path = os.path.join(args.configuration, f)
else: else:
CORE.config_path = data["file"] CORE.config_path = data["file"]
file_name = CORE.config_path
_print_file_read_event(file_name)
raw_yaml = _read_file_content_from_json_on_stdin()
command_line_substitutions: dict[str, Any] = (
dict(args.substitution) if args.substitution else {}
)
vs = VSCodeResult() vs = VSCodeResult()
try: try:
res = load_config(dict(args.substitution) if args.substitution else {}) config = parse_yaml(file_name, StringIO(raw_yaml))
res = validate_config(config, command_line_substitutions)
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
vs.add_yaml_error(str(err)) vs.add_yaml_error(str(err))
else: else:

View file

@ -417,20 +417,25 @@ def load_yaml(fname: str, clear_secrets: bool = True) -> Any:
return _load_yaml_internal(fname) return _load_yaml_internal(fname)
def parse_yaml(file_name: str, file_handle: TextIOWrapper) -> Any:
"""Parse a YAML file."""
try:
return _load_yaml_internal_with_type(ESPHomeLoader, file_name, file_handle)
except EsphomeError:
# Loading failed, so we now load with the Python loader which has more
# readable exceptions
# Rewind the stream so we can try again
file_handle.seek(0, 0)
return _load_yaml_internal_with_type(
ESPHomePurePythonLoader, file_name, file_handle
)
def _load_yaml_internal(fname: str) -> Any: def _load_yaml_internal(fname: str) -> Any:
"""Load a YAML file.""" """Load a YAML file."""
try: try:
with open(fname, encoding="utf-8") as f_handle: with open(fname, encoding="utf-8") as f_handle:
try: return parse_yaml(fname, f_handle)
return _load_yaml_internal_with_type(ESPHomeLoader, fname, f_handle)
except EsphomeError:
# Loading failed, so we now load with the Python loader which has more
# readable exceptions
# Rewind the stream so we can try again
f_handle.seek(0, 0)
return _load_yaml_internal_with_type(
ESPHomePurePythonLoader, fname, f_handle
)
except (UnicodeDecodeError, OSError) as err: except (UnicodeDecodeError, OSError) as err:
raise EsphomeError(f"Error reading file {fname}: {err}") from err raise EsphomeError(f"Error reading file {fname}: {err}") from err