Inverse json import logic (#88099)

* Fix helpers and util

* Adjust components

* Move back errors

* Add report

* mypy

* mypy

* Assert deprecation messages

* Move test_json_loads_object

* Adjust tests

* Fix rebase

* Adjust pylint plugin

* Fix plugin

* Adjust references

* Adjust backup tests
This commit is contained in:
epenet 2023-02-16 11:37:57 +01:00 committed by GitHub
parent 580869a9a6
commit ba23816a0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 291 additions and 197 deletions

View File

@ -28,9 +28,10 @@ import homeassistant.core as ha
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceNotFound, TemplateError, Unauthorized
from homeassistant.helpers import template
from homeassistant.helpers.json import json_dumps, json_loads
from homeassistant.helpers.json import json_dumps
from homeassistant.helpers.service import async_get_all_descriptions
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.json import json_loads
_LOGGER = logging.getLogger(__name__)

View File

@ -17,7 +17,8 @@ from homeassistant.const import __version__ as HAVERSION
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import integration_platform
from homeassistant.util import dt, json as json_util
from homeassistant.helpers.json import save_json
from homeassistant.util import dt
from .const import DOMAIN, EXCLUDE_FROM_BACKUP, LOGGER
@ -229,7 +230,7 @@ class BackupManager:
tar_file_path, "w", gzip=False
) as tar_file:
tmp_dir_path = Path(tmp_dir)
json_util.save_json(
save_json(
tmp_dir_path.joinpath("./backup.json").as_posix(),
backup_data,
)

View File

@ -24,7 +24,7 @@ from homeassistant.helpers import (
template,
translation,
)
from homeassistant.helpers.json import JsonObjectType, json_loads_object
from homeassistant.util.json import JsonObjectType, json_loads_object
from .agent import AbstractConversationAgent, ConversationInput, ConversationResult
from .const import DOMAIN

View File

@ -16,14 +16,14 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import integration_platform
from homeassistant.helpers.device_registry import DeviceEntry, async_get
from homeassistant.helpers.json import ExtendedJSONEncoder
from homeassistant.helpers.json import (
ExtendedJSONEncoder,
find_paths_unserializable_data,
)
from homeassistant.helpers.system_info import async_get_system_info
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import async_get_custom_components, async_get_integration
from homeassistant.util.json import (
find_paths_unserializable_data,
format_unserializable_data,
)
from homeassistant.util.json import format_unserializable_data
from .const import DOMAIN, REDACTED, DiagnosticsSubType, DiagnosticsType
from .util import async_redact_data

View File

@ -24,9 +24,10 @@ from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.icon import icon_for_battery_level
from homeassistant.helpers.json import save_json
from homeassistant.helpers.network import NoURLAvailableError, get_url
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util.json import load_json, save_json
from homeassistant.util.json import load_json
from homeassistant.util.unit_system import METRIC_SYSTEM
from .const import (

View File

@ -32,9 +32,10 @@ from homeassistant.const import ATTR_NAME, URL_ROOT
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.json import save_json
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import ensure_unique_string
from homeassistant.util.json import load_json, save_json
from homeassistant.util.json import load_json
from .const import DOMAIN, SERVICE_DISMISS

View File

@ -20,11 +20,12 @@ import voluptuous as vol
from homeassistant import exceptions
from homeassistant.const import CONTENT_TYPE_JSON
from homeassistant.core import Context, is_callback
from homeassistant.helpers.json import JSON_ENCODE_EXCEPTIONS, json_bytes, json_dumps
from homeassistant.util.json import (
from homeassistant.helpers.json import (
find_paths_unserializable_data,
format_unserializable_data,
json_bytes,
json_dumps,
)
from homeassistant.util.json import JSON_ENCODE_EXCEPTIONS, format_unserializable_data
from .const import KEY_AUTHENTICATED, KEY_HASS

View File

@ -12,8 +12,9 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.json import save_json
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.json import load_json, save_json
from homeassistant.util.json import load_json
from .const import (
CONF_ACTION_BACKGROUND_COLOR,

View File

@ -19,8 +19,9 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.json import save_json
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.json import load_json, save_json
from homeassistant.util.json import load_json
from .const import DOMAIN, FORMAT_HTML, FORMAT_TEXT, SERVICE_SEND_MESSAGE

View File

@ -14,7 +14,8 @@ from nacl.secret import SecretBox
from homeassistant.const import ATTR_DEVICE_ID, CONTENT_TYPE_JSON
from homeassistant.core import Context, HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.json import JSONEncoder, JsonValueType, json_loads
from homeassistant.helpers.json import JSONEncoder
from homeassistant.util.json import JsonValueType, json_loads
from .const import (
ATTR_APP_DATA,

View File

@ -28,7 +28,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.json import JSON_DECODE_EXCEPTIONS, json_dumps, json_loads
from homeassistant.helpers.json import json_dumps
from homeassistant.helpers.selector import (
BooleanSelector,
FileSelector,
@ -44,6 +44,7 @@ from homeassistant.helpers.selector import (
TextSelectorConfig,
TextSelectorType,
)
from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads
from .client import MqttClientSetup
from .config_integration import CONFIG_SCHEMA_ENTRY

View File

@ -31,9 +31,9 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.json import JSON_DECODE_EXCEPTIONS, json_loads
from homeassistant.helpers.service_info.mqtt import ReceivePayloadType
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads
from . import subscription
from .config import MQTT_BASE_SCHEMA

View File

@ -18,10 +18,10 @@ from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.json import json_loads_object
from homeassistant.helpers.service_info.mqtt import MqttServiceInfo
from homeassistant.helpers.typing import DiscoveryInfoType
from homeassistant.loader import async_get_mqtt
from homeassistant.util.json import json_loads_object
from .. import mqtt
from .abbreviations import ABBREVIATIONS, DEVICE_ABBREVIATIONS

View File

@ -47,10 +47,11 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.json import json_dumps, json_loads_object
from homeassistant.helpers.json import json_dumps
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
import homeassistant.util.color as color_util
from homeassistant.util.json import json_loads_object
from .. import subscription
from ..config import DEFAULT_QOS, DEFAULT_RETAIN, MQTT_RW_SCHEMA

View File

@ -51,8 +51,8 @@ from homeassistant.helpers.entity import (
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_entity_registry_updated_event
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.json import json_loads
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util.json import json_loads
from . import debug_info, subscription
from .client import async_publish

View File

@ -31,13 +31,10 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.json import (
JSON_DECODE_EXCEPTIONS,
json_dumps,
json_loads_object,
)
from homeassistant.helpers.json import json_dumps
from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, TemplateVarsType
from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads_object
from . import subscription
from .config import MQTT_RW_SCHEMA

View File

@ -11,10 +11,10 @@ import voluptuous as vol
from homeassistant.const import CONF_PAYLOAD, CONF_PLATFORM, CONF_VALUE_TEMPLATE
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.json import json_loads
from homeassistant.helpers.template import Template
from homeassistant.helpers.trigger import TriggerActionType, TriggerData, TriggerInfo
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
from homeassistant.util.json import json_loads
from .. import mqtt
from .const import CONF_ENCODING, CONF_QOS, CONF_TOPIC, DEFAULT_ENCODING, DEFAULT_QOS

View File

@ -18,9 +18,9 @@ from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_VALUE_TEMPLAT
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.json import JSON_DECODE_EXCEPTIONS, json_loads
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads
from . import subscription
from .config import DEFAULT_RETAIN, MQTT_RO_SCHEMA

View File

@ -25,8 +25,9 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.json import json_dumps, json_loads_object
from homeassistant.helpers.json import json_dumps
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util.json import json_loads_object
from .. import subscription
from ..config import MQTT_BASE_SCHEMA

View File

@ -14,7 +14,8 @@ from homeassistant.components import ssdp, zeroconf
from homeassistant.const import CONF_HOST, CONF_TOKEN
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util.json import load_json, save_json
from homeassistant.helpers.json import save_json
from homeassistant.util.json import load_json
from .const import DOMAIN

View File

@ -25,9 +25,10 @@ from homeassistant.core import HomeAssistant, ServiceCall, split_entity_id
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, entity_registry
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.json import save_json
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import location
from homeassistant.util.json import load_json, save_json
from homeassistant.util.json import load_json
from .config_flow import PlayStation4FlowHandler # noqa: F401
from .const import (

View File

@ -35,10 +35,10 @@ from homeassistant.helpers.event import (
async_track_time_interval,
async_track_utc_time_change,
)
from homeassistant.helpers.json import JSON_ENCODE_EXCEPTIONS
from homeassistant.helpers.start import async_at_started
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
import homeassistant.util.dt as dt_util
from homeassistant.util.json import JSON_ENCODE_EXCEPTIONS
from . import migration, statistics
from .const import (

View File

@ -41,15 +41,13 @@ from homeassistant.const import (
MAX_LENGTH_STATE_STATE,
)
from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id
from homeassistant.helpers.json import (
from homeassistant.helpers.json import JSON_DUMP, json_bytes, json_bytes_strip_null
import homeassistant.util.dt as dt_util
from homeassistant.util.json import (
JSON_DECODE_EXCEPTIONS,
JSON_DUMP,
json_bytes,
json_bytes_strip_null,
json_loads,
json_loads_object,
)
import homeassistant.util.dt as dt_util
from .const import ALL_DOMAIN_EXCLUDE_ATTRS, SupportedDialect
from .models import (

View File

@ -16,8 +16,8 @@ from homeassistant.const import (
COMPRESSED_STATE_STATE,
)
from homeassistant.core import Context, State
from homeassistant.helpers.json import json_loads_object
import homeassistant.util.dt as dt_util
from homeassistant.util.json import json_loads_object
from .const import SupportedDialect

View File

@ -25,10 +25,11 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.json import json_dumps, json_loads
from homeassistant.helpers.json import json_dumps
from homeassistant.helpers.template_entity import TemplateSensor
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util.json import json_loads
from . import async_get_config_and_coordinator, create_rest_data_from_config
from .const import CONF_JSON_ATTRS, CONF_JSON_ATTRS_PATH, DEFAULT_SENSOR_NAME

View File

@ -13,8 +13,9 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_NAME
from homeassistant.core import HomeAssistant, ServiceCall, callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.json import save_json
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.json import load_json, save_json
from homeassistant.util.json import load_json
from .const import (
DOMAIN,

View File

@ -29,7 +29,11 @@ from homeassistant.helpers.event import (
TrackTemplateResult,
async_track_template_result,
)
from homeassistant.helpers.json import JSON_DUMP, ExtendedJSONEncoder
from homeassistant.helpers.json import (
JSON_DUMP,
ExtendedJSONEncoder,
find_paths_unserializable_data,
)
from homeassistant.helpers.service import async_get_all_descriptions
from homeassistant.loader import (
Integration,
@ -39,10 +43,7 @@ from homeassistant.loader import (
async_get_integrations,
)
from homeassistant.setup import DATA_SETUP_TIME, async_get_loaded_integrations
from homeassistant.util.json import (
find_paths_unserializable_data,
format_unserializable_data,
)
from homeassistant.util.json import format_unserializable_data
from . import const, decorators, messages
from .connection import ActiveConnection

View File

@ -16,7 +16,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.json import json_loads
from homeassistant.util.json import json_loads
from .auth import AuthPhase, auth_required_message
from .const import (

View File

@ -16,11 +16,8 @@ from homeassistant.const import (
)
from homeassistant.core import Event, State
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.json import JSON_DUMP
from homeassistant.util.json import (
find_paths_unserializable_data,
format_unserializable_data,
)
from homeassistant.helpers.json import JSON_DUMP, find_paths_unserializable_data
from homeassistant.util.json import format_unserializable_data
from homeassistant.util.yaml.loader import JSON_TYPE
from . import const

View File

@ -20,9 +20,10 @@ from homeassistant.const import APPLICATION_NAME, EVENT_HOMEASSISTANT_CLOSE, __v
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.loader import bind_hass
from homeassistant.util import ssl as ssl_util
from homeassistant.util.json import json_loads
from .frame import warn_use
from .json import json_dumps, json_loads
from .json import json_dumps
if TYPE_CHECKING:
from aiohttp.typedefs import JSONDecoder

View File

@ -14,16 +14,13 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError, RequiredParameterMissing
from homeassistant.loader import bind_hass
from homeassistant.util.json import (
find_paths_unserializable_data,
format_unserializable_data,
)
from homeassistant.util.json import format_unserializable_data
import homeassistant.util.uuid as uuid_util
from . import storage
from .debounce import Debouncer
from .frame import report
from .json import JSON_DUMP
from .json import JSON_DUMP, find_paths_unserializable_data
from .typing import UNDEFINED, UndefinedType
if TYPE_CHECKING:

View File

@ -43,15 +43,12 @@ from homeassistant.core import (
from homeassistant.exceptions import MaxLengthExceeded
from homeassistant.loader import bind_hass
from homeassistant.util import slugify, uuid as uuid_util
from homeassistant.util.json import (
find_paths_unserializable_data,
format_unserializable_data,
)
from homeassistant.util.json import format_unserializable_data
from . import device_registry as dr, storage
from .device_registry import EVENT_DEVICE_REGISTRY_UPDATED
from .frame import report
from .json import JSON_DUMP
from .json import JSON_DUMP, find_paths_unserializable_data
from .typing import UNDEFINED, UndefinedType
if TYPE_CHECKING:

View File

@ -1,21 +1,25 @@
"""Helpers to help with encoding Home Assistant objects in JSON."""
from collections import deque
from collections.abc import Callable
import datetime
import json
import logging
from pathlib import Path
from typing import Any, Final
import orjson
JsonValueType = (
dict[str, "JsonValueType"] | list["JsonValueType"] | str | int | float | bool | None
from homeassistant.core import Event, State
from homeassistant.util.file import write_utf8_file, write_utf8_file_atomic
from homeassistant.util.json import ( # pylint: disable=unused-import # noqa: F401
JSON_DECODE_EXCEPTIONS,
JSON_ENCODE_EXCEPTIONS,
SerializationError,
format_unserializable_data,
json_loads,
)
"""Any data that can be returned by the standard JSON deserializing process."""
JsonObjectType = dict[str, JsonValueType]
"""Dictionary that can be returned by the standard JSON deserializing process."""
JSON_ENCODE_EXCEPTIONS = (TypeError, ValueError)
JSON_DECODE_EXCEPTIONS = (orjson.JSONDecodeError,)
_LOGGER = logging.getLogger(__name__)
class JSONEncoder(json.JSONEncoder):
@ -140,18 +144,99 @@ def json_dumps_sorted(data: Any) -> str:
).decode("utf-8")
json_loads: Callable[[bytes | bytearray | memoryview | str], JsonValueType]
json_loads = orjson.loads
"""Parse JSON data."""
def json_loads_object(__obj: bytes | bytearray | memoryview | str) -> JsonObjectType:
"""Parse JSON data and ensure result is a dictionary."""
value: JsonValueType = json_loads(__obj)
# Avoid isinstance overhead as we are not interested in dict subclasses
if type(value) is dict: # pylint: disable=unidiomatic-typecheck
return value
raise ValueError(f"Expected JSON to be parsed as a dict got {type(value)}")
JSON_DUMP: Final = json_dumps
def _orjson_default_encoder(data: Any) -> str:
"""JSON encoder that uses orjson with hass defaults."""
return orjson.dumps(
data,
option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS,
default=json_encoder_default,
).decode("utf-8")
def save_json(
filename: str,
data: list | dict,
private: bool = False,
*,
encoder: type[json.JSONEncoder] | None = None,
atomic_writes: bool = False,
) -> None:
"""Save JSON data to a file."""
dump: Callable[[Any], Any]
try:
# For backwards compatibility, if they pass in the
# default json encoder we use _orjson_default_encoder
# which is the orjson equivalent to the default encoder.
if encoder and encoder is not JSONEncoder:
# If they pass a custom encoder that is not the
# default JSONEncoder, we use the slow path of json.dumps
dump = json.dumps
json_data = json.dumps(data, indent=2, cls=encoder)
else:
dump = _orjson_default_encoder
json_data = _orjson_default_encoder(data)
except TypeError as error:
formatted_data = format_unserializable_data(
find_paths_unserializable_data(data, dump=dump)
)
msg = f"Failed to serialize to JSON: {filename}. Bad data at {formatted_data}"
_LOGGER.error(msg)
raise SerializationError(msg) from error
if atomic_writes:
write_utf8_file_atomic(filename, json_data, private)
else:
write_utf8_file(filename, json_data, private)
def find_paths_unserializable_data(
bad_data: Any, *, dump: Callable[[Any], str] = json.dumps
) -> dict[str, Any]:
"""Find the paths to unserializable data.
This method is slow! Only use for error handling.
"""
to_process = deque([(bad_data, "$")])
invalid = {}
while to_process:
obj, obj_path = to_process.popleft()
try:
dump(obj)
continue
except (ValueError, TypeError):
pass
# We convert objects with as_dict to their dict values
# so we can find bad data inside it
if hasattr(obj, "as_dict"):
desc = obj.__class__.__name__
if isinstance(obj, State):
desc += f": {obj.entity_id}"
elif isinstance(obj, Event):
desc += f": {obj.event_type}"
obj_path += f"({desc})"
obj = obj.as_dict()
if isinstance(obj, dict):
for key, value in obj.items():
try:
# Is key valid?
dump({key: None})
except TypeError:
invalid[f"{obj_path}<key: {key}>"] = key
else:
# Process value
to_process.append((value, f"{obj_path}.{key}"))
elif isinstance(obj, list):
for idx, value in enumerate(obj):
to_process.append((value, f"{obj_path}[{idx}]"))
else:
invalid[obj_path] = obj
return invalid

View File

@ -17,6 +17,8 @@ from homeassistant.loader import MAX_LOAD_CONCURRENTLY, bind_hass
from homeassistant.util import json as json_util
from homeassistant.util.file import WriteError
from . import json as json_helper
# mypy: allow-untyped-calls, allow-untyped-defs, no-warn-return-any
# mypy: no-check-untyped-defs
@ -290,7 +292,7 @@ class Store(Generic[_T]):
os.makedirs(os.path.dirname(path), exist_ok=True)
_LOGGER.debug("Writing data for %s to %s", self.key, path)
json_util.save_json(
json_helper.save_json(
path,
data,
self._private,

View File

@ -68,11 +68,11 @@ from homeassistant.util import (
slugify as slugify_util,
)
from homeassistant.util.async_ import run_callback_threadsafe
from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads
from homeassistant.util.read_only_dict import ReadOnlyDict
from homeassistant.util.thread import ThreadWithException
from . import area_registry, device_registry, entity_registry, location as loc_helper
from .json import JSON_DECODE_EXCEPTIONS, json_loads
from .typing import TemplateVarsType
# mypy: allow-untyped-defs, no-check-untyped-defs

View File

@ -31,7 +31,7 @@ from .generated.mqtt import MQTT
from .generated.ssdp import SSDP
from .generated.usb import USB
from .generated.zeroconf import HOMEKIT, ZEROCONF
from .helpers.json import JSON_DECODE_EXCEPTIONS, json_loads
from .util.json import JSON_DECODE_EXCEPTIONS, json_loads
# Typing imports that create a circular dependency
if TYPE_CHECKING:

View File

@ -10,7 +10,7 @@ from aiohttp import payload, web
from aiohttp.typedefs import JSONDecoder
from multidict import CIMultiDict, MultiDict
from homeassistant.helpers.json import json_loads
from .json import json_loads
class MockStreamReader:

View File

@ -1,7 +1,6 @@
"""JSON utility functions."""
from __future__ import annotations
from collections import deque
from collections.abc import Callable
import json
import logging
@ -9,26 +8,41 @@ from typing import Any
import orjson
from homeassistant.core import Event, State
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.json import (
JSONEncoder as DefaultHASSJSONEncoder,
json_encoder_default as default_hass_orjson_encoder,
)
from .file import ( # pylint: disable=unused-import # noqa: F401
WriteError,
write_utf8_file,
write_utf8_file_atomic,
)
from .file import WriteError # pylint: disable=unused-import # noqa: F401
_LOGGER = logging.getLogger(__name__)
JsonValueType = (
dict[str, "JsonValueType"] | list["JsonValueType"] | str | int | float | bool | None
)
"""Any data that can be returned by the standard JSON deserializing process."""
JsonObjectType = dict[str, JsonValueType]
"""Dictionary that can be returned by the standard JSON deserializing process."""
JSON_ENCODE_EXCEPTIONS = (TypeError, ValueError)
JSON_DECODE_EXCEPTIONS = (orjson.JSONDecodeError,)
class SerializationError(HomeAssistantError):
"""Error serializing the data to JSON."""
json_loads: Callable[[bytes | bytearray | memoryview | str], JsonValueType]
json_loads = orjson.loads
"""Parse JSON data."""
def json_loads_object(__obj: bytes | bytearray | memoryview | str) -> JsonObjectType:
"""Parse JSON data and ensure result is a dictionary."""
value: JsonValueType = json_loads(__obj)
# Avoid isinstance overhead as we are not interested in dict subclasses
if type(value) is dict: # pylint: disable=unidiomatic-typecheck
return value
raise ValueError(f"Expected JSON to be parsed as a dict got {type(value)}")
def load_json(filename: str, default: list | dict | None = None) -> list | dict:
"""Load JSON data from a file and return as dict or list.
@ -49,15 +63,6 @@ def load_json(filename: str, default: list | dict | None = None) -> list | dict:
return {} if default is None else default
def _orjson_default_encoder(data: Any) -> str:
"""JSON encoder that uses orjson with hass defaults."""
return orjson.dumps(
data,
option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS,
default=default_hass_orjson_encoder,
).decode("utf-8")
def save_json(
filename: str,
data: list | dict,
@ -66,35 +71,25 @@ def save_json(
encoder: type[json.JSONEncoder] | None = None,
atomic_writes: bool = False,
) -> None:
"""Save JSON data to a file.
"""Save JSON data to a file."""
# pylint: disable-next=import-outside-toplevel
from homeassistant.helpers.frame import report
Returns True on success.
"""
dump: Callable[[Any], Any]
try:
# For backwards compatibility, if they pass in the
# default json encoder we use _orjson_default_encoder
# which is the orjson equivalent to the default encoder.
if encoder and encoder is not DefaultHASSJSONEncoder:
# If they pass a custom encoder that is not the
# DefaultHASSJSONEncoder, we use the slow path of json.dumps
dump = json.dumps
json_data = json.dumps(data, indent=2, cls=encoder)
else:
dump = _orjson_default_encoder
json_data = _orjson_default_encoder(data)
except TypeError as error:
formatted_data = format_unserializable_data(
find_paths_unserializable_data(data, dump=dump)
)
msg = f"Failed to serialize to JSON: {filename}. Bad data at {formatted_data}"
_LOGGER.error(msg)
raise SerializationError(msg) from error
report(
(
"uses save_json from homeassistant.util.json module."
" This is deprecated and will stop working in Home Assistant 2022.4, it"
" should be updated to use homeassistant.helpers.json module instead"
),
error_if_core=False,
)
if atomic_writes:
write_utf8_file_atomic(filename, json_data, private)
else:
write_utf8_file(filename, json_data, private)
# pylint: disable-next=import-outside-toplevel
import homeassistant.helpers.json as json_helper
json_helper.save_json(
filename, data, private, encoder=encoder, atomic_writes=atomic_writes
)
def format_unserializable_data(data: dict[str, Any]) -> str:
@ -112,44 +107,19 @@ def find_paths_unserializable_data(
This method is slow! Only use for error handling.
"""
to_process = deque([(bad_data, "$")])
invalid = {}
# pylint: disable-next=import-outside-toplevel
from homeassistant.helpers.frame import report
while to_process:
obj, obj_path = to_process.popleft()
report(
(
"uses find_paths_unserializable_data from homeassistant.util.json module."
" This is deprecated and will stop working in Home Assistant 2022.4, it"
" should be updated to use homeassistant.helpers.json module instead"
),
error_if_core=False,
)
try:
dump(obj)
continue
except (ValueError, TypeError):
pass
# pylint: disable-next=import-outside-toplevel
import homeassistant.helpers.json as json_helper
# We convert objects with as_dict to their dict values
# so we can find bad data inside it
if hasattr(obj, "as_dict"):
desc = obj.__class__.__name__
if isinstance(obj, State):
desc += f": {obj.entity_id}"
elif isinstance(obj, Event):
desc += f": {obj.event_type}"
obj_path += f"({desc})"
obj = obj.as_dict()
if isinstance(obj, dict):
for key, value in obj.items():
try:
# Is key valid?
dump({key: None})
except TypeError:
invalid[f"{obj_path}<key: {key}>"] = key
else:
# Process value
to_process.append((value, f"{obj_path}.{key}"))
elif isinstance(obj, list):
for idx, value in enumerate(obj):
to_process.append((value, f"{obj_path}[{idx}]"))
else:
invalid[obj_path] = obj
return invalid
return json_helper.find_paths_unserializable_data(bad_data, dump=dump)

View File

@ -350,6 +350,14 @@ _OBSOLETE_IMPORT: dict[str, list[ObsoleteImportMatch]] = {
constant=re.compile(r"^DISABLED_(\w*)$"),
),
],
"homeassistant.helpers.json": [
ObsoleteImportMatch(
reason="moved to homeassistant.util.json",
constant=re.compile(
r"^JSON_DECODE_EXCEPTIONS|JSON_ENCODE_EXCEPTIONS|json_loads$"
),
),
],
"homeassistant.util": [
ObsoleteImportMatch(
reason="replaced by unit_conversion.***Converter",
@ -362,6 +370,12 @@ _OBSOLETE_IMPORT: dict[str, list[ObsoleteImportMatch]] = {
constant=re.compile(r"^IMPERIAL_SYSTEM$"),
),
],
"homeassistant.util.json": [
ObsoleteImportMatch(
reason="moved to homeassistant.helpers.json",
constant=re.compile(r"^save_json|find_paths_unserializable_data$"),
),
],
}

View File

@ -46,15 +46,15 @@ async def _mock_backup_generation(manager: BackupManager):
"pathlib.Path.mkdir",
MagicMock(),
), patch(
"homeassistant.components.backup.manager.json_util.save_json"
) as mocked_json_util, patch(
"homeassistant.components.backup.manager.save_json"
) as mocked_save_json, patch(
"homeassistant.components.backup.manager.HAVERSION",
"2025.1.0",
):
await manager.generate_backup()
assert mocked_json_util.call_count == 1
assert mocked_json_util.call_args[0][1]["homeassistant"] == {
assert mocked_save_json.call_count == 1
assert mocked_save_json.call_args[0][1]["homeassistant"] == {
"version": "2025.1.0"
}

View File

@ -5,8 +5,8 @@ from typing import cast
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.json import JsonObjectType
from homeassistant.setup import async_setup_component
from homeassistant.util.json import JsonObjectType
from tests.typing import ClientSessionGenerator

View File

@ -97,8 +97,8 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.json import JsonValueType, json_loads
from homeassistant.setup import async_setup_component
from homeassistant.util.json import JsonValueType, json_loads
from .test_common import (
help_test_availability_when_connection_lost,

View File

@ -13,7 +13,6 @@ from homeassistant.helpers.json import (
json_bytes_strip_null,
json_dumps,
json_dumps_sorted,
json_loads_object,
)
from homeassistant.util import dt as dt_util
from homeassistant.util.color import RGBColor
@ -136,20 +135,3 @@ def test_json_bytes_strip_null() -> None:
json_bytes_strip_null([[{"k1": {"k2": ["silly\0stuff"]}}]])
== b'[[{"k1":{"k2":["silly"]}}]]'
)
def test_json_loads_object():
"""Test json_loads_object validates result."""
assert json_loads_object('{"c":1.2}') == {"c": 1.2}
with pytest.raises(
ValueError, match="Expected JSON to be parsed as a dict got <class 'list'>"
):
json_loads_object("[]")
with pytest.raises(
ValueError, match="Expected JSON to be parsed as a dict got <class 'bool'>"
):
json_loads_object("true")
with pytest.raises(
ValueError, match="Expected JSON to be parsed as a dict got <class 'NoneType'>"
):
json_loads_object("null")

View File

@ -15,6 +15,7 @@ from homeassistant.helpers.json import JSONEncoder as DefaultHASSJSONEncoder
from homeassistant.util.json import (
SerializationError,
find_paths_unserializable_data,
json_loads_object,
load_json,
save_json,
)
@ -191,3 +192,40 @@ def test_find_unserializable_data() -> None:
BadData(),
dump=partial(dumps, cls=MockJSONEncoder),
) == {"$(BadData).bla": bad_data}
def test_json_loads_object() -> None:
"""Test json_loads_object validates result."""
assert json_loads_object('{"c":1.2}') == {"c": 1.2}
with pytest.raises(
ValueError, match="Expected JSON to be parsed as a dict got <class 'list'>"
):
json_loads_object("[]")
with pytest.raises(
ValueError, match="Expected JSON to be parsed as a dict got <class 'bool'>"
):
json_loads_object("true")
with pytest.raises(
ValueError, match="Expected JSON to be parsed as a dict got <class 'NoneType'>"
):
json_loads_object("null")
async def test_deprecated_test_find_unserializable_data(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test deprecated test_find_unserializable_data logs a warning."""
find_paths_unserializable_data(1)
assert (
"uses find_paths_unserializable_data from homeassistant.util.json"
in caplog.text
)
assert "should be updated to use homeassistant.helpers.json module" in caplog.text
async def test_deprecated_save_json(caplog: pytest.LogCaptureFixture) -> None:
"""Test deprecated save_json logs a warning."""
fname = _path_for("test1")
save_json(fname, TEST_JSON_A)
assert "uses save_json from homeassistant.util.json" in caplog.text
assert "should be updated to use homeassistant.helpers.json module" in caplog.text