Add JSON support to load_fixture (#88076)

* Add JSON support to load_fixture

* More tests

* Remove lru_cache on load_json
This commit is contained in:
epenet 2023-02-16 19:40:47 +01:00 committed by GitHub
parent bc2b35765e
commit 8c821c8969
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 104 additions and 34 deletions

View File

@ -18,6 +18,8 @@ JsonValueType = (
dict[str, "JsonValueType"] | list["JsonValueType"] | str | int | float | bool | None dict[str, "JsonValueType"] | list["JsonValueType"] | str | int | float | bool | None
) )
"""Any data that can be returned by the standard JSON deserializing process.""" """Any data that can be returned by the standard JSON deserializing process."""
JsonArrayType = list[JsonValueType]
"""List that can be returned by the standard JSON deserializing process."""
JsonObjectType = dict[str, JsonValueType] JsonObjectType = dict[str, JsonValueType]
"""Dictionary that can be returned by the standard JSON deserializing process.""" """Dictionary that can be returned by the standard JSON deserializing process."""
@ -34,6 +36,15 @@ json_loads = orjson.loads
"""Parse JSON data.""" """Parse JSON data."""
def json_loads_array(__obj: bytes | bytearray | memoryview | str) -> JsonArrayType:
"""Parse JSON data and ensure result is a list."""
value: JsonValueType = json_loads(__obj)
# Avoid isinstance overhead as we are not interested in list subclasses
if type(value) is list: # pylint: disable=unidiomatic-typecheck
return value
raise ValueError(f"Expected JSON to be parsed as a list got {type(value)}")
def json_loads_object(__obj: bytes | bytearray | memoryview | str) -> JsonObjectType: def json_loads_object(__obj: bytes | bytearray | memoryview | str) -> JsonObjectType:
"""Parse JSON data and ensure result is a dictionary.""" """Parse JSON data and ensure result is a dictionary."""
value: JsonValueType = json_loads(__obj) value: JsonValueType = json_loads(__obj)

View File

@ -67,6 +67,14 @@ from homeassistant.helpers.typing import ConfigType, StateType
from homeassistant.setup import setup_component from homeassistant.setup import setup_component
from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.async_ import run_callback_threadsafe
import homeassistant.util.dt as date_util import homeassistant.util.dt as date_util
from homeassistant.util.json import (
JsonArrayType,
JsonObjectType,
JsonValueType,
json_loads,
json_loads_array,
json_loads_object,
)
from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.util.unit_system import METRIC_SYSTEM
import homeassistant.util.uuid as uuid_util import homeassistant.util.uuid as uuid_util
import homeassistant.util.yaml.loader as yaml_loader import homeassistant.util.yaml.loader as yaml_loader
@ -428,6 +436,27 @@ def load_fixture(filename: str, integration: str | None = None) -> str:
return get_fixture_path(filename, integration).read_text() return get_fixture_path(filename, integration).read_text()
def load_json_value_fixture(
filename: str, integration: str | None = None
) -> JsonValueType:
"""Load a JSON value from a fixture."""
return json_loads(load_fixture(filename, integration))
def load_json_array_fixture(
filename: str, integration: str | None = None
) -> JsonArrayType:
"""Load a JSON array from a fixture."""
return json_loads_array(load_fixture(filename, integration))
def load_json_object_fixture(
filename: str, integration: str | None = None
) -> JsonObjectType:
"""Load a JSON object from a fixture."""
return json_loads_object(load_fixture(filename, integration))
def mock_state_change_event( def mock_state_change_event(
hass: HomeAssistant, new_state: State, old_state: State | None = None hass: HomeAssistant, new_state: State, old_state: State | None = None
) -> None: ) -> None:

View File

@ -1,10 +1,13 @@
"""Tests for AccuWeather.""" """Tests for AccuWeather."""
import json
from unittest.mock import PropertyMock, patch from unittest.mock import PropertyMock, patch
from homeassistant.components.accuweather.const import DOMAIN from homeassistant.components.accuweather.const import DOMAIN
from tests.common import MockConfigEntry, load_fixture from tests.common import (
MockConfigEntry,
load_json_array_fixture,
load_json_object_fixture,
)
async def init_integration( async def init_integration(
@ -28,8 +31,8 @@ async def init_integration(
options=options, options=options,
) )
current = json.loads(load_fixture("accuweather/current_conditions_data.json")) current = load_json_object_fixture("accuweather/current_conditions_data.json")
forecast = json.loads(load_fixture("accuweather/forecast_data.json")) forecast = load_json_array_fixture("accuweather/forecast_data.json")
if unsupported_icon: if unsupported_icon:
current["WeatherIcon"] = 999 current["WeatherIcon"] = 999

View File

@ -1,5 +1,4 @@
"""Define tests for the AccuWeather config flow.""" """Define tests for the AccuWeather config flow."""
import json
from unittest.mock import PropertyMock, patch from unittest.mock import PropertyMock, patch
from accuweather import ApiError, InvalidApiKeyError, RequestsExceededError from accuweather import ApiError, InvalidApiKeyError, RequestsExceededError
@ -10,7 +9,7 @@ from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry, load_fixture from tests.common import MockConfigEntry, load_json_object_fixture
VALID_CONFIG = { VALID_CONFIG = {
CONF_NAME: "abcd", CONF_NAME: "abcd",
@ -99,7 +98,7 @@ async def test_integration_already_exists(hass: HomeAssistant) -> None:
"""Test we only allow a single config flow.""" """Test we only allow a single config flow."""
with patch( with patch(
"homeassistant.components.accuweather.AccuWeather._async_get_data", "homeassistant.components.accuweather.AccuWeather._async_get_data",
return_value=json.loads(load_fixture("accuweather/location_data.json")), return_value=load_json_object_fixture("accuweather/location_data.json"),
): ):
MockConfigEntry( MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
@ -121,7 +120,7 @@ async def test_create_entry(hass: HomeAssistant) -> None:
"""Test that the user step works.""" """Test that the user step works."""
with patch( with patch(
"homeassistant.components.accuweather.AccuWeather._async_get_data", "homeassistant.components.accuweather.AccuWeather._async_get_data",
return_value=json.loads(load_fixture("accuweather/location_data.json")), return_value=load_json_object_fixture("accuweather/location_data.json"),
), patch( ), patch(
"homeassistant.components.accuweather.async_setup_entry", return_value=True "homeassistant.components.accuweather.async_setup_entry", return_value=True
): ):
@ -150,11 +149,11 @@ async def test_options_flow(hass: HomeAssistant) -> None:
with patch( with patch(
"homeassistant.components.accuweather.AccuWeather._async_get_data", "homeassistant.components.accuweather.AccuWeather._async_get_data",
return_value=json.loads(load_fixture("accuweather/location_data.json")), return_value=load_json_object_fixture("accuweather/location_data.json"),
), patch( ), patch(
"homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions",
return_value=json.loads( return_value=load_json_object_fixture(
load_fixture("accuweather/current_conditions_data.json") "accuweather/current_conditions_data.json"
), ),
), patch( ), patch(
"homeassistant.components.accuweather.AccuWeather.async_get_forecast" "homeassistant.components.accuweather.AccuWeather.async_get_forecast"

View File

@ -1,11 +1,10 @@
"""Test AccuWeather diagnostics.""" """Test AccuWeather diagnostics."""
import json
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from . import init_integration from . import init_integration
from tests.common import load_fixture from tests.common import load_json_object_fixture
from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.components.diagnostics import get_diagnostics_for_config_entry
from tests.typing import ClientSessionGenerator from tests.typing import ClientSessionGenerator
@ -16,9 +15,10 @@ async def test_entry_diagnostics(
"""Test config entry diagnostics.""" """Test config entry diagnostics."""
entry = await init_integration(hass) entry = await init_integration(hass)
coordinator_data = json.loads( coordinator_data = load_json_object_fixture(
load_fixture("current_conditions_data.json", "accuweather") "current_conditions_data.json", "accuweather"
) )
coordinator_data["forecast"] = {} coordinator_data["forecast"] = {}
result = await get_diagnostics_for_config_entry(hass, hass_client, entry) result = await get_diagnostics_for_config_entry(hass, hass_client, entry)

View File

@ -1,6 +1,5 @@
"""Test init of AccuWeather integration.""" """Test init of AccuWeather integration."""
from datetime import timedelta from datetime import timedelta
import json
from unittest.mock import patch from unittest.mock import patch
from accuweather import ApiError from accuweather import ApiError
@ -13,7 +12,12 @@ from homeassistant.util.dt import utcnow
from . import init_integration from . import init_integration
from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture from tests.common import (
MockConfigEntry,
async_fire_time_changed,
load_json_array_fixture,
load_json_object_fixture,
)
async def test_async_setup_entry(hass: HomeAssistant) -> None: async def test_async_setup_entry(hass: HomeAssistant) -> None:
@ -69,7 +73,7 @@ async def test_update_interval(hass: HomeAssistant) -> None:
assert entry.state is ConfigEntryState.LOADED assert entry.state is ConfigEntryState.LOADED
current = json.loads(load_fixture("accuweather/current_conditions_data.json")) current = load_json_object_fixture("accuweather/current_conditions_data.json")
future = utcnow() + timedelta(minutes=40) future = utcnow() + timedelta(minutes=40)
with patch( with patch(
@ -90,8 +94,8 @@ async def test_update_interval_forecast(hass: HomeAssistant) -> None:
assert entry.state is ConfigEntryState.LOADED assert entry.state is ConfigEntryState.LOADED
current = json.loads(load_fixture("accuweather/current_conditions_data.json")) current = load_json_object_fixture("accuweather/current_conditions_data.json")
forecast = json.loads(load_fixture("accuweather/forecast_data.json")) forecast = load_json_array_fixture("accuweather/forecast_data.json")
future = utcnow() + timedelta(minutes=80) future = utcnow() + timedelta(minutes=80)
with patch( with patch(

View File

@ -1,6 +1,5 @@
"""Test sensor of AccuWeather integration.""" """Test sensor of AccuWeather integration."""
from datetime import timedelta from datetime import timedelta
import json
from unittest.mock import PropertyMock, patch from unittest.mock import PropertyMock, patch
from homeassistant.components.accuweather.const import ATTRIBUTION, DOMAIN from homeassistant.components.accuweather.const import ATTRIBUTION, DOMAIN
@ -35,7 +34,11 @@ from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
from . import init_integration from . import init_integration
from tests.common import async_fire_time_changed, load_fixture from tests.common import (
async_fire_time_changed,
load_json_array_fixture,
load_json_object_fixture,
)
async def test_sensor_without_forecast(hass: HomeAssistant) -> None: async def test_sensor_without_forecast(hass: HomeAssistant) -> None:
@ -684,8 +687,8 @@ async def test_availability(hass: HomeAssistant) -> None:
future = utcnow() + timedelta(minutes=120) future = utcnow() + timedelta(minutes=120)
with patch( with patch(
"homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions",
return_value=json.loads( return_value=load_json_object_fixture(
load_fixture("accuweather/current_conditions_data.json") "accuweather/current_conditions_data.json"
), ),
), patch( ), patch(
"homeassistant.components.accuweather.AccuWeather.requests_remaining", "homeassistant.components.accuweather.AccuWeather.requests_remaining",
@ -707,8 +710,8 @@ async def test_manual_update_entity(hass: HomeAssistant) -> None:
await async_setup_component(hass, "homeassistant", {}) await async_setup_component(hass, "homeassistant", {})
current = json.loads(load_fixture("accuweather/current_conditions_data.json")) current = load_json_object_fixture("accuweather/current_conditions_data.json")
forecast = json.loads(load_fixture("accuweather/forecast_data.json")) forecast = load_json_array_fixture("accuweather/forecast_data.json")
with patch( with patch(
"homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions",
@ -755,8 +758,8 @@ async def test_state_update(hass: HomeAssistant) -> None:
future = utcnow() + timedelta(minutes=60) future = utcnow() + timedelta(minutes=60)
current_condition = json.loads( current_condition = load_json_object_fixture(
load_fixture("accuweather/current_conditions_data.json") "accuweather/current_conditions_data.json"
) )
current_condition["Ceiling"]["Metric"]["Value"] = 3300 current_condition["Ceiling"]["Metric"]["Value"] = 3300

View File

@ -1,6 +1,5 @@
"""Test weather of AccuWeather integration.""" """Test weather of AccuWeather integration."""
from datetime import timedelta from datetime import timedelta
import json
from unittest.mock import PropertyMock, patch from unittest.mock import PropertyMock, patch
from homeassistant.components.accuweather.const import ATTRIBUTION from homeassistant.components.accuweather.const import ATTRIBUTION
@ -30,7 +29,11 @@ from homeassistant.util.dt import utcnow
from . import init_integration from . import init_integration
from tests.common import async_fire_time_changed, load_fixture from tests.common import (
async_fire_time_changed,
load_json_array_fixture,
load_json_object_fixture,
)
async def test_weather_without_forecast(hass: HomeAssistant) -> None: async def test_weather_without_forecast(hass: HomeAssistant) -> None:
@ -111,8 +114,8 @@ async def test_availability(hass: HomeAssistant) -> None:
future = utcnow() + timedelta(minutes=120) future = utcnow() + timedelta(minutes=120)
with patch( with patch(
"homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions",
return_value=json.loads( return_value=load_json_object_fixture(
load_fixture("accuweather/current_conditions_data.json") "accuweather/current_conditions_data.json"
), ),
), patch( ), patch(
"homeassistant.components.accuweather.AccuWeather.requests_remaining", "homeassistant.components.accuweather.AccuWeather.requests_remaining",
@ -134,8 +137,8 @@ async def test_manual_update_entity(hass: HomeAssistant) -> None:
await async_setup_component(hass, "homeassistant", {}) await async_setup_component(hass, "homeassistant", {})
current = json.loads(load_fixture("accuweather/current_conditions_data.json")) current = load_json_object_fixture("accuweather/current_conditions_data.json")
forecast = json.loads(load_fixture("accuweather/forecast_data.json")) forecast = load_json_array_fixture("accuweather/forecast_data.json")
with patch( with patch(
"homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions",

View File

@ -15,6 +15,7 @@ from homeassistant.helpers.json import JSONEncoder as DefaultHASSJSONEncoder
from homeassistant.util.json import ( from homeassistant.util.json import (
SerializationError, SerializationError,
find_paths_unserializable_data, find_paths_unserializable_data,
json_loads_array,
json_loads_object, json_loads_object,
load_json, load_json,
save_json, save_json,
@ -194,6 +195,23 @@ def test_find_unserializable_data() -> None:
) == {"$(BadData).bla": bad_data} ) == {"$(BadData).bla": bad_data}
def test_json_loads_array() -> None:
"""Test json_loads_array validates result."""
assert json_loads_array('[{"c":1.2}]') == [{"c": 1.2}]
with pytest.raises(
ValueError, match="Expected JSON to be parsed as a list got <class 'dict'>"
):
json_loads_array("{}")
with pytest.raises(
ValueError, match="Expected JSON to be parsed as a list got <class 'bool'>"
):
json_loads_array("true")
with pytest.raises(
ValueError, match="Expected JSON to be parsed as a list got <class 'NoneType'>"
):
json_loads_array("null")
def test_json_loads_object() -> None: def test_json_loads_object() -> None:
"""Test json_loads_object validates result.""" """Test json_loads_object validates result."""
assert json_loads_object('{"c":1.2}') == {"c": 1.2} assert json_loads_object('{"c":1.2}') == {"c": 1.2}