mirror of
https://github.com/esphome/esphome.git
synced 2024-11-10 09:17:46 +01:00
Fix editor live validation (#6431)
This commit is contained in:
parent
d304e52940
commit
f00d876080
4 changed files with 63 additions and 51 deletions
|
@ -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:
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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 _load_yaml_internal(fname: str) -> Any:
|
def parse_yaml(file_name: str, file_handle: TextIOWrapper) -> Any:
|
||||||
"""Load a YAML file."""
|
"""Parse a YAML file."""
|
||||||
try:
|
try:
|
||||||
with open(fname, encoding="utf-8") as f_handle:
|
return _load_yaml_internal_with_type(ESPHomeLoader, file_name, file_handle)
|
||||||
try:
|
|
||||||
return _load_yaml_internal_with_type(ESPHomeLoader, fname, f_handle)
|
|
||||||
except EsphomeError:
|
except EsphomeError:
|
||||||
# Loading failed, so we now load with the Python loader which has more
|
# Loading failed, so we now load with the Python loader which has more
|
||||||
# readable exceptions
|
# readable exceptions
|
||||||
# Rewind the stream so we can try again
|
# Rewind the stream so we can try again
|
||||||
f_handle.seek(0, 0)
|
file_handle.seek(0, 0)
|
||||||
return _load_yaml_internal_with_type(
|
return _load_yaml_internal_with_type(
|
||||||
ESPHomePurePythonLoader, fname, f_handle
|
ESPHomePurePythonLoader, file_name, file_handle
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_yaml_internal(fname: str) -> Any:
|
||||||
|
"""Load a YAML file."""
|
||||||
|
try:
|
||||||
|
with open(fname, encoding="utf-8") as f_handle:
|
||||||
|
return parse_yaml(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
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue