diff --git a/homeassistant/components/lovelace/dashboard.py b/homeassistant/components/lovelace/dashboard.py index 6cdb6f52eb4d..3217fb89854c 100644 --- a/homeassistant/components/lovelace/dashboard.py +++ b/homeassistant/components/lovelace/dashboard.py @@ -15,7 +15,7 @@ from homeassistant.const import CONF_FILENAME from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import collection, storage -from homeassistant.helpers.json import json_bytes, json_fragment +from homeassistant.helpers.json import cached_json_fragment, json_fragment from homeassistant.util.yaml import Secrets, load_yaml_dict from .const import ( @@ -182,7 +182,7 @@ class LovelaceStorage(LovelaceConfig): """Build JSON representation of the config.""" if self._data is None or self._data["config"] is None: raise ConfigNotFound - self._json_config = json_fragment(json_bytes(self._data["config"])) + self._json_config = cached_json_fragment(self._data["config"]) return self._json_config @@ -260,7 +260,7 @@ class LovelaceYAML(LovelaceConfig): except FileNotFoundError: raise ConfigNotFound from None - json = json_fragment(json_bytes(config)) + json = cached_json_fragment(config) self._cache = (config, time.time(), json) return is_updated, config, json diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 90f7a97c985f..0864b1bd9023 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -62,8 +62,8 @@ from homeassistant.helpers.event import ( from homeassistant.helpers.json import ( JSON_DUMP, ExtendedJSONEncoder, + cached_json_bytes, find_paths_unserializable_data, - json_bytes, json_fragment, ) from homeassistant.helpers.service import ( @@ -541,7 +541,7 @@ async def _async_get_all_condition_descriptions_json(hass: HomeAssistant) -> byt # If the descriptions are the same, return the cached JSON payload if cached_descriptions is descriptions: return cast(bytes, cached_json_payload) - json_payload = json_bytes( + json_payload = cached_json_bytes( { condition: description for condition, description in descriptions.items() @@ -588,7 +588,7 @@ async def _async_get_all_service_descriptions_json(hass: HomeAssistant) -> bytes # If the descriptions are the same, return the cached JSON payload if cached_descriptions is descriptions: return cast(bytes, cached_json_payload) - json_payload = json_bytes(descriptions) + json_payload = cached_json_bytes(descriptions) hass.data[ALL_SERVICE_DESCRIPTIONS_JSON_CACHE] = (descriptions, json_payload) return json_payload @@ -613,7 +613,7 @@ async def _async_get_all_trigger_descriptions_json(hass: HomeAssistant) -> bytes # If the descriptions are the same, return the cached JSON payload if cached_descriptions is descriptions: return cast(bytes, cached_json_payload) - json_payload = json_bytes( + json_payload = cached_json_bytes( { trigger: description for trigger, description in descriptions.items() diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 64f14ba0876d..7bc2f23f6e7a 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -66,7 +66,11 @@ from .helpers.event import ( async_call_later, ) from .helpers.frame import ReportBehavior, report_usage -from .helpers.json import json_bytes, json_bytes_sorted, json_fragment +from .helpers.json import ( + cached_json_fragment, + cached_json_fragment_sorted, + json_fragment, +) from .helpers.typing import ( UNDEFINED, ConfigType, @@ -682,7 +686,7 @@ class ConfigEntry[_DataT = Any]: ), "num_subentries": len(self.subentries), } - return json_fragment(json_bytes(json_repr)) + return cached_json_fragment(json_repr) def clear_storage_cache(self) -> None: """Clear cached properties that are included in as_storage_fragment.""" @@ -691,7 +695,7 @@ class ConfigEntry[_DataT = Any]: @cached_property def as_storage_fragment(self) -> json_fragment: """Return a storage fragment for this entry.""" - return json_fragment(json_bytes_sorted(self.as_dict())) + return cached_json_fragment_sorted(self.as_dict()) async def async_setup( self, diff --git a/homeassistant/helpers/json.py b/homeassistant/helpers/json.py index 6a0792383cb6..f6a778dd872c 100644 --- a/homeassistant/helpers/json.py +++ b/homeassistant/helpers/json.py @@ -140,6 +140,16 @@ def cached_json_fragment(data: Any) -> orjson.Fragment: return orjson.Fragment(b"".join((json_bytes(data), b""))) +def cached_json_fragment_sorted(data: Any) -> orjson.Fragment: + """Return a json fragment with sorted keys, right-sized for long-term caching. + + The sorted-key variant of cached_json_fragment (see json_bytes_sorted). + """ + # The empty second join item is load-bearing: it forces a copy into a + # right-sized buffer; a single-item join returns the input unchanged. + return orjson.Fragment(b"".join((json_bytes_sorted(data), b""))) + + def json_dumps(data: Any) -> str: r"""Dump json string. diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 780198b6dac5..10f1697c4eee 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -45,7 +45,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_bytes, json_fragment +from .helpers.json import cached_json_fragment, json_fragment from .helpers.typing import UNDEFINED, UndefinedType from .util.async_ import create_eager_task from .util.hass_dict import HassKey @@ -798,7 +798,7 @@ class Integration: @cached_property def manifest_json_fragment(self) -> json_fragment: """Return manifest as a JSON fragment.""" - return json_fragment(json_bytes(self.manifest)) + return cached_json_fragment(self.manifest) @cached_property def name(self) -> str: diff --git a/tests/helpers/test_json.py b/tests/helpers/test_json.py index ba43f0341706..3e9294e8aabe 100644 --- a/tests/helpers/test_json.py +++ b/tests/helpers/test_json.py @@ -21,6 +21,7 @@ from homeassistant.helpers.json import ( JSONEncoder as DefaultHASSJSONEncoder, cached_json_bytes, cached_json_fragment, + cached_json_fragment_sorted, find_paths_unserializable_data, json_bytes, json_bytes_sorted, @@ -236,10 +237,19 @@ def test_cached_json_bytes() -> None: ) +def test_cached_json_fragment_sorted() -> None: + """Test cached_json_fragment_sorted serializes with sorted keys.""" + data = {"c": 3, "a": 1, "b": 2} + + fragment = cached_json_fragment_sorted(data) + assert isinstance(fragment, json_fragment) + assert json_dumps([fragment]) == '[{"a":1,"b":2,"c":3}]' + + @pytest.mark.parametrize( "cached_serializer", - [cached_json_bytes, cached_json_fragment], - ids=["cached_json_bytes", "cached_json_fragment"], + [cached_json_bytes, cached_json_fragment, cached_json_fragment_sorted], + ids=["cached_json_bytes", "cached_json_fragment", "cached_json_fragment_sorted"], ) def test_cached_json_helpers_trim_buffer( cached_serializer: Callable[[Any], object],