pytest-homeassistant-custom-component 0.13.163__py3-none-any.whl → 0.13.298__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pytest_homeassistant_custom_component/common.py +289 -46
- pytest_homeassistant_custom_component/components/__init__.py +351 -0
- pytest_homeassistant_custom_component/components/diagnostics/__init__.py +71 -0
- pytest_homeassistant_custom_component/components/recorder/common.py +143 -13
- pytest_homeassistant_custom_component/components/recorder/db_schema_0.py +1 -1
- pytest_homeassistant_custom_component/const.py +4 -3
- pytest_homeassistant_custom_component/patch_json.py +41 -0
- pytest_homeassistant_custom_component/patch_recorder.py +1 -1
- pytest_homeassistant_custom_component/patch_time.py +66 -0
- pytest_homeassistant_custom_component/plugins.py +429 -177
- pytest_homeassistant_custom_component/syrupy.py +188 -1
- pytest_homeassistant_custom_component/test_util/aiohttp.py +56 -20
- pytest_homeassistant_custom_component/typing.py +5 -0
- {pytest_homeassistant_custom_component-0.13.163.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info}/METADATA +54 -29
- pytest_homeassistant_custom_component-0.13.298.dist-info/RECORD +28 -0
- {pytest_homeassistant_custom_component-0.13.163.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info}/WHEEL +1 -1
- pytest_homeassistant_custom_component-0.13.163.dist-info/RECORD +0 -26
- {pytest_homeassistant_custom_component-0.13.163.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info}/entry_points.txt +0 -0
- {pytest_homeassistant_custom_component-0.13.163.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info/licenses}/LICENSE +0 -0
- {pytest_homeassistant_custom_component-0.13.163.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info/licenses}/LICENSE_HA_CORE.md +0 -0
- {pytest_homeassistant_custom_component-0.13.163.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info}/top_level.txt +0 -0
|
@@ -9,14 +9,22 @@ from __future__ import annotations
|
|
|
9
9
|
from contextlib import suppress
|
|
10
10
|
import dataclasses
|
|
11
11
|
from enum import IntFlag
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
12
14
|
from pathlib import Path
|
|
13
15
|
from typing import Any
|
|
14
16
|
|
|
15
17
|
import attr
|
|
16
18
|
import attrs
|
|
19
|
+
import pytest
|
|
20
|
+
from syrupy.constants import EXIT_STATUS_FAIL_UNUSED
|
|
21
|
+
from syrupy.data import Snapshot, SnapshotCollection, SnapshotCollections
|
|
17
22
|
from syrupy.extensions.amber import AmberDataSerializer, AmberSnapshotExtension
|
|
18
23
|
from syrupy.location import PyTestLocation
|
|
24
|
+
from syrupy.report import SnapshotReport
|
|
25
|
+
from syrupy.session import ItemStatus, SnapshotSession
|
|
19
26
|
from syrupy.types import PropertyFilter, PropertyMatcher, PropertyPath, SerializableData
|
|
27
|
+
from syrupy.utils import is_xdist_controller, is_xdist_worker
|
|
20
28
|
import voluptuous as vol
|
|
21
29
|
import voluptuous_serialize
|
|
22
30
|
|
|
@@ -105,6 +113,12 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer):
|
|
|
105
113
|
serializable_data = cls._serializable_issue_registry_entry(data)
|
|
106
114
|
elif isinstance(data, dict) and "flow_id" in data and "handler" in data:
|
|
107
115
|
serializable_data = cls._serializable_flow_result(data)
|
|
116
|
+
elif isinstance(data, dict) and set(data) == {
|
|
117
|
+
"conversation_id",
|
|
118
|
+
"response",
|
|
119
|
+
"continue_conversation",
|
|
120
|
+
}:
|
|
121
|
+
serializable_data = cls._serializable_conversation_result(data)
|
|
108
122
|
elif isinstance(data, vol.Schema):
|
|
109
123
|
serializable_data = voluptuous_serialize.convert(data)
|
|
110
124
|
elif isinstance(data, ConfigEntry):
|
|
@@ -136,6 +150,7 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer):
|
|
|
136
150
|
"""Prepare a Home Assistant area registry entry for serialization."""
|
|
137
151
|
serialized = AreaRegistryEntrySnapshot(dataclasses.asdict(data) | {"id": ANY})
|
|
138
152
|
serialized.pop("_json_repr")
|
|
153
|
+
serialized.pop("_cache")
|
|
139
154
|
return serialized
|
|
140
155
|
|
|
141
156
|
@classmethod
|
|
@@ -153,6 +168,7 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer):
|
|
|
153
168
|
attrs.asdict(data)
|
|
154
169
|
| {
|
|
155
170
|
"config_entries": ANY,
|
|
171
|
+
"config_entries_subentries": ANY,
|
|
156
172
|
"id": ANY,
|
|
157
173
|
}
|
|
158
174
|
)
|
|
@@ -160,6 +176,9 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer):
|
|
|
160
176
|
serialized["via_device_id"] = ANY
|
|
161
177
|
if serialized["primary_config_entry"] is not None:
|
|
162
178
|
serialized["primary_config_entry"] = ANY
|
|
179
|
+
serialized.pop("_cache")
|
|
180
|
+
# This can be removed when suggested_area is removed from DeviceEntry
|
|
181
|
+
serialized.pop("_suggested_area")
|
|
163
182
|
return cls._remove_created_and_modified_at(serialized)
|
|
164
183
|
|
|
165
184
|
@classmethod
|
|
@@ -180,12 +199,14 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer):
|
|
|
180
199
|
attrs.asdict(data)
|
|
181
200
|
| {
|
|
182
201
|
"config_entry_id": ANY,
|
|
202
|
+
"config_subentry_id": ANY,
|
|
183
203
|
"device_id": ANY,
|
|
184
204
|
"id": ANY,
|
|
185
205
|
"options": {k: dict(v) for k, v in data.options.items()},
|
|
186
206
|
}
|
|
187
207
|
)
|
|
188
208
|
serialized.pop("categories")
|
|
209
|
+
serialized.pop("_cache")
|
|
189
210
|
return cls._remove_created_and_modified_at(serialized)
|
|
190
211
|
|
|
191
212
|
@classmethod
|
|
@@ -193,12 +214,17 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer):
|
|
|
193
214
|
"""Prepare a Home Assistant flow result for serialization."""
|
|
194
215
|
return FlowResultSnapshot(data | {"flow_id": ANY})
|
|
195
216
|
|
|
217
|
+
@classmethod
|
|
218
|
+
def _serializable_conversation_result(cls, data: dict) -> SerializableData:
|
|
219
|
+
"""Prepare a Home Assistant conversation result for serialization."""
|
|
220
|
+
return data | {"conversation_id": ANY}
|
|
221
|
+
|
|
196
222
|
@classmethod
|
|
197
223
|
def _serializable_issue_registry_entry(
|
|
198
224
|
cls, data: ir.IssueEntry
|
|
199
225
|
) -> SerializableData:
|
|
200
226
|
"""Prepare a Home Assistant issue registry entry for serialization."""
|
|
201
|
-
return IssueRegistryItemSnapshot(
|
|
227
|
+
return IssueRegistryItemSnapshot(dataclasses.asdict(data) | {"created": ANY})
|
|
202
228
|
|
|
203
229
|
@classmethod
|
|
204
230
|
def _serializable_state(cls, data: State) -> SerializableData:
|
|
@@ -247,3 +273,164 @@ class HomeAssistantSnapshotExtension(AmberSnapshotExtension):
|
|
|
247
273
|
"""
|
|
248
274
|
test_dir = Path(test_location.filepath).parent
|
|
249
275
|
return str(test_dir.joinpath("snapshots"))
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# Classes and Methods to override default finish behavior in syrupy
|
|
279
|
+
# This is needed to handle the xdist plugin in pytest
|
|
280
|
+
# The default implementation does not handle the xdist plugin
|
|
281
|
+
# and will not work correctly when running tests in parallel
|
|
282
|
+
# with pytest-xdist.
|
|
283
|
+
# Temporary workaround until it is finalised inside syrupy
|
|
284
|
+
# See https://github.com/syrupy-project/syrupy/pull/901
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class _FakePytestObject:
|
|
288
|
+
"""Fake object."""
|
|
289
|
+
|
|
290
|
+
def __init__(self, collected_item: dict[str, str]) -> None:
|
|
291
|
+
"""Initialise fake object."""
|
|
292
|
+
self.__module__ = collected_item["modulename"]
|
|
293
|
+
self.__name__ = collected_item["methodname"]
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class _FakePytestItem:
|
|
297
|
+
"""Fake pytest.Item object."""
|
|
298
|
+
|
|
299
|
+
def __init__(self, collected_item: dict[str, str]) -> None:
|
|
300
|
+
"""Initialise fake pytest.Item object."""
|
|
301
|
+
self.nodeid = collected_item["nodeid"]
|
|
302
|
+
self.name = collected_item["name"]
|
|
303
|
+
self.path = Path(collected_item["path"])
|
|
304
|
+
self.obj = _FakePytestObject(collected_item)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _serialize_collections(collections: SnapshotCollections) -> dict[str, Any]:
|
|
308
|
+
return {
|
|
309
|
+
k: [c.name for c in v] for k, v in collections._snapshot_collections.items()
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _serialize_report(
|
|
314
|
+
report: SnapshotReport,
|
|
315
|
+
collected_items: set[pytest.Item],
|
|
316
|
+
selected_items: dict[str, ItemStatus],
|
|
317
|
+
) -> dict[str, Any]:
|
|
318
|
+
return {
|
|
319
|
+
"discovered": _serialize_collections(report.discovered),
|
|
320
|
+
"created": _serialize_collections(report.created),
|
|
321
|
+
"failed": _serialize_collections(report.failed),
|
|
322
|
+
"matched": _serialize_collections(report.matched),
|
|
323
|
+
"updated": _serialize_collections(report.updated),
|
|
324
|
+
"used": _serialize_collections(report.used),
|
|
325
|
+
"_collected_items": [
|
|
326
|
+
{
|
|
327
|
+
"nodeid": c.nodeid,
|
|
328
|
+
"name": c.name,
|
|
329
|
+
"path": str(c.path),
|
|
330
|
+
"modulename": c.obj.__module__,
|
|
331
|
+
"methodname": c.obj.__name__,
|
|
332
|
+
}
|
|
333
|
+
for c in list(collected_items)
|
|
334
|
+
],
|
|
335
|
+
"_selected_items": {
|
|
336
|
+
key: status.value for key, status in selected_items.items()
|
|
337
|
+
},
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _merge_serialized_collections(
|
|
342
|
+
collections: SnapshotCollections, json_data: dict[str, list[str]]
|
|
343
|
+
) -> None:
|
|
344
|
+
if not json_data:
|
|
345
|
+
return
|
|
346
|
+
for location, names in json_data.items():
|
|
347
|
+
snapshot_collection = SnapshotCollection(location=location)
|
|
348
|
+
for name in names:
|
|
349
|
+
snapshot_collection.add(Snapshot(name))
|
|
350
|
+
collections.update(snapshot_collection)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _merge_serialized_report(report: SnapshotReport, json_data: dict[str, Any]) -> None:
|
|
354
|
+
_merge_serialized_collections(report.discovered, json_data["discovered"])
|
|
355
|
+
_merge_serialized_collections(report.created, json_data["created"])
|
|
356
|
+
_merge_serialized_collections(report.failed, json_data["failed"])
|
|
357
|
+
_merge_serialized_collections(report.matched, json_data["matched"])
|
|
358
|
+
_merge_serialized_collections(report.updated, json_data["updated"])
|
|
359
|
+
_merge_serialized_collections(report.used, json_data["used"])
|
|
360
|
+
for collected_item in json_data["_collected_items"]:
|
|
361
|
+
custom_item = _FakePytestItem(collected_item)
|
|
362
|
+
if not any(
|
|
363
|
+
t.nodeid == custom_item.nodeid and t.name == custom_item.nodeid
|
|
364
|
+
for t in report.collected_items
|
|
365
|
+
):
|
|
366
|
+
report.collected_items.add(custom_item)
|
|
367
|
+
for key, selected_item in json_data["_selected_items"].items():
|
|
368
|
+
if key in report.selected_items:
|
|
369
|
+
status = ItemStatus(selected_item)
|
|
370
|
+
if status != ItemStatus.NOT_RUN:
|
|
371
|
+
report.selected_items[key] = status
|
|
372
|
+
else:
|
|
373
|
+
report.selected_items[key] = ItemStatus(selected_item)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def override_syrupy_finish(self: SnapshotSession) -> int:
|
|
377
|
+
"""Override the finish method to allow for custom handling."""
|
|
378
|
+
exitstatus = 0
|
|
379
|
+
self.flush_snapshot_write_queue()
|
|
380
|
+
self.report = SnapshotReport(
|
|
381
|
+
base_dir=self.pytest_session.config.rootpath,
|
|
382
|
+
collected_items=self._collected_items,
|
|
383
|
+
selected_items=self._selected_items,
|
|
384
|
+
assertions=self._assertions,
|
|
385
|
+
options=self.pytest_session.config.option,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
needs_xdist_merge = self.update_snapshots or bool(
|
|
389
|
+
self.pytest_session.config.option.include_snapshot_details
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
if is_xdist_worker():
|
|
393
|
+
if not needs_xdist_merge:
|
|
394
|
+
return exitstatus
|
|
395
|
+
with open(".pytest_syrupy_worker_count", "w", encoding="utf-8") as f:
|
|
396
|
+
f.write(os.getenv("PYTEST_XDIST_WORKER_COUNT"))
|
|
397
|
+
with open(
|
|
398
|
+
f".pytest_syrupy_{os.getenv('PYTEST_XDIST_WORKER')}_result",
|
|
399
|
+
"w",
|
|
400
|
+
encoding="utf-8",
|
|
401
|
+
) as f:
|
|
402
|
+
json.dump(
|
|
403
|
+
_serialize_report(
|
|
404
|
+
self.report, self._collected_items, self._selected_items
|
|
405
|
+
),
|
|
406
|
+
f,
|
|
407
|
+
indent=2,
|
|
408
|
+
)
|
|
409
|
+
return exitstatus
|
|
410
|
+
if is_xdist_controller():
|
|
411
|
+
return exitstatus
|
|
412
|
+
|
|
413
|
+
if needs_xdist_merge:
|
|
414
|
+
worker_count = None
|
|
415
|
+
try:
|
|
416
|
+
with open(".pytest_syrupy_worker_count", encoding="utf-8") as f:
|
|
417
|
+
worker_count = f.read()
|
|
418
|
+
os.remove(".pytest_syrupy_worker_count")
|
|
419
|
+
except FileNotFoundError:
|
|
420
|
+
pass
|
|
421
|
+
|
|
422
|
+
if worker_count:
|
|
423
|
+
for i in range(int(worker_count)):
|
|
424
|
+
with open(f".pytest_syrupy_gw{i}_result", encoding="utf-8") as f:
|
|
425
|
+
_merge_serialized_report(self.report, json.load(f))
|
|
426
|
+
os.remove(f".pytest_syrupy_gw{i}_result")
|
|
427
|
+
|
|
428
|
+
if self.report.num_unused:
|
|
429
|
+
if self.update_snapshots:
|
|
430
|
+
self.remove_unused_snapshots(
|
|
431
|
+
unused_snapshot_collections=self.report.unused,
|
|
432
|
+
used_snapshot_collections=self.report.used,
|
|
433
|
+
)
|
|
434
|
+
elif not self.warn_unused_snapshots:
|
|
435
|
+
exitstatus |= EXIT_STATUS_FAIL_UNUSED
|
|
436
|
+
return exitstatus
|
|
@@ -9,6 +9,7 @@ from collections.abc import Iterator
|
|
|
9
9
|
from contextlib import contextmanager
|
|
10
10
|
from http import HTTPStatus
|
|
11
11
|
import re
|
|
12
|
+
from types import TracebackType
|
|
12
13
|
from typing import Any
|
|
13
14
|
from unittest import mock
|
|
14
15
|
from urllib.parse import parse_qs
|
|
@@ -66,6 +67,7 @@ class AiohttpClientMocker:
|
|
|
66
67
|
cookies=None,
|
|
67
68
|
side_effect=None,
|
|
68
69
|
closing=None,
|
|
70
|
+
timeout=None,
|
|
69
71
|
):
|
|
70
72
|
"""Mock a request."""
|
|
71
73
|
if not isinstance(url, RETYPE):
|
|
@@ -73,21 +75,21 @@ class AiohttpClientMocker:
|
|
|
73
75
|
if params:
|
|
74
76
|
url = url.with_query(params)
|
|
75
77
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
closing=closing,
|
|
89
|
-
)
|
|
78
|
+
resp = AiohttpClientMockResponse(
|
|
79
|
+
method=method,
|
|
80
|
+
url=url,
|
|
81
|
+
status=status,
|
|
82
|
+
response=content,
|
|
83
|
+
json=json,
|
|
84
|
+
text=text,
|
|
85
|
+
cookies=cookies,
|
|
86
|
+
exc=exc,
|
|
87
|
+
headers=headers,
|
|
88
|
+
side_effect=side_effect,
|
|
89
|
+
closing=closing,
|
|
90
90
|
)
|
|
91
|
+
self._mocks.append(resp)
|
|
92
|
+
return resp
|
|
91
93
|
|
|
92
94
|
def get(self, *args, **kwargs):
|
|
93
95
|
"""Register a mock get request."""
|
|
@@ -113,6 +115,10 @@ class AiohttpClientMocker:
|
|
|
113
115
|
"""Register a mock patch request."""
|
|
114
116
|
self.request("patch", *args, **kwargs)
|
|
115
117
|
|
|
118
|
+
def head(self, *args, **kwargs):
|
|
119
|
+
"""Register a mock head request."""
|
|
120
|
+
self.request("head", *args, **kwargs)
|
|
121
|
+
|
|
116
122
|
@property
|
|
117
123
|
def call_count(self):
|
|
118
124
|
"""Return the number of requests made."""
|
|
@@ -154,6 +160,9 @@ class AiohttpClientMocker:
|
|
|
154
160
|
|
|
155
161
|
for response in self._mocks:
|
|
156
162
|
if response.match_request(method, url, params):
|
|
163
|
+
# If auth is provided, try to encode it to trigger any encoding errors
|
|
164
|
+
if auth is not None:
|
|
165
|
+
auth.encode()
|
|
157
166
|
self.mock_calls.append((method, url, data, headers))
|
|
158
167
|
if response.side_effect:
|
|
159
168
|
response = await response.side_effect(method, url, data)
|
|
@@ -170,7 +179,7 @@ class AiohttpClientMockResponse:
|
|
|
170
179
|
def __init__(
|
|
171
180
|
self,
|
|
172
181
|
method,
|
|
173
|
-
url,
|
|
182
|
+
url: URL,
|
|
174
183
|
status=HTTPStatus.OK,
|
|
175
184
|
response=None,
|
|
176
185
|
json=None,
|
|
@@ -189,7 +198,6 @@ class AiohttpClientMockResponse:
|
|
|
189
198
|
if response is None:
|
|
190
199
|
response = b""
|
|
191
200
|
|
|
192
|
-
self.charset = "utf-8"
|
|
193
201
|
self.method = method
|
|
194
202
|
self._url = url
|
|
195
203
|
self.status = status
|
|
@@ -217,8 +225,8 @@ class AiohttpClientMockResponse:
|
|
|
217
225
|
|
|
218
226
|
if (
|
|
219
227
|
self._url.scheme != url.scheme
|
|
220
|
-
or self._url.
|
|
221
|
-
or self._url.
|
|
228
|
+
or self._url.raw_host != url.raw_host
|
|
229
|
+
or self._url.raw_path != url.raw_path
|
|
222
230
|
):
|
|
223
231
|
return False
|
|
224
232
|
|
|
@@ -259,16 +267,32 @@ class AiohttpClientMockResponse:
|
|
|
259
267
|
"""Return content."""
|
|
260
268
|
return mock_stream(self.response)
|
|
261
269
|
|
|
270
|
+
@property
|
|
271
|
+
def charset(self):
|
|
272
|
+
"""Return charset from Content-Type header."""
|
|
273
|
+
if (content_type := self._headers.get("content-type")) is None:
|
|
274
|
+
return None
|
|
275
|
+
content_type = content_type.lower()
|
|
276
|
+
if "charset=" in content_type:
|
|
277
|
+
return content_type.split("charset=")[1].split(";")[0].strip()
|
|
278
|
+
return None
|
|
279
|
+
|
|
262
280
|
async def read(self):
|
|
263
281
|
"""Return mock response."""
|
|
264
282
|
return self.response
|
|
265
283
|
|
|
266
|
-
async def text(self, encoding=
|
|
284
|
+
async def text(self, encoding=None, errors="strict") -> str:
|
|
267
285
|
"""Return mock response as a string."""
|
|
286
|
+
# Match real aiohttp behavior: encoding=None means auto-detect
|
|
287
|
+
if encoding is None:
|
|
288
|
+
encoding = self.charset or "utf-8"
|
|
268
289
|
return self.response.decode(encoding, errors=errors)
|
|
269
290
|
|
|
270
|
-
async def json(self, encoding=
|
|
291
|
+
async def json(self, encoding=None, content_type=None, loads=json_loads) -> Any:
|
|
271
292
|
"""Return mock response as a json."""
|
|
293
|
+
# Match real aiohttp behavior: encoding=None means auto-detect
|
|
294
|
+
if encoding is None:
|
|
295
|
+
encoding = self.charset or "utf-8"
|
|
272
296
|
return loads(self.response.decode(encoding))
|
|
273
297
|
|
|
274
298
|
def release(self):
|
|
@@ -301,6 +325,18 @@ class AiohttpClientMockResponse:
|
|
|
301
325
|
raise ClientConnectionError("Connection closed")
|
|
302
326
|
return self._response
|
|
303
327
|
|
|
328
|
+
async def __aenter__(self):
|
|
329
|
+
"""Enter the context manager."""
|
|
330
|
+
return self
|
|
331
|
+
|
|
332
|
+
async def __aexit__(
|
|
333
|
+
self,
|
|
334
|
+
exc_type: type[BaseException] | None,
|
|
335
|
+
exc_val: BaseException | None,
|
|
336
|
+
exc_tb: TracebackType | None,
|
|
337
|
+
) -> None:
|
|
338
|
+
"""Exit the context manager."""
|
|
339
|
+
|
|
304
340
|
|
|
305
341
|
@contextmanager
|
|
306
342
|
def mock_aiohttp_client() -> Iterator[AiohttpClientMocker]:
|
|
@@ -7,6 +7,7 @@ This file is originally from homeassistant/core and modified by pytest-homeassis
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
from collections.abc import Callable, Coroutine
|
|
10
|
+
from contextlib import AbstractAsyncContextManager
|
|
10
11
|
from typing import TYPE_CHECKING, Any
|
|
11
12
|
from unittest.mock import MagicMock
|
|
12
13
|
|
|
@@ -34,6 +35,10 @@ type MqttMockHAClient = MagicMock
|
|
|
34
35
|
"""MagicMock for `homeassistant.components.mqtt.MQTT`."""
|
|
35
36
|
type MqttMockHAClientGenerator = Callable[..., Coroutine[Any, Any, MqttMockHAClient]]
|
|
36
37
|
"""MagicMock generator for `homeassistant.components.mqtt.MQTT`."""
|
|
38
|
+
type RecorderInstanceContextManager = Callable[
|
|
39
|
+
..., AbstractAsyncContextManager[Recorder]
|
|
40
|
+
]
|
|
41
|
+
"""ContextManager for `homeassistant.components.recorder.Recorder`."""
|
|
37
42
|
type RecorderInstanceGenerator = Callable[..., Coroutine[Any, Any, Recorder]]
|
|
38
43
|
"""Instance generator for `homeassistant.components.recorder.Recorder`."""
|
|
39
44
|
type WebSocketGenerator = Callable[..., Coroutine[Any, Any, MockHAClientWebSocket]]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-homeassistant-custom-component
|
|
3
|
-
Version: 0.13.
|
|
3
|
+
Version: 0.13.298
|
|
4
4
|
Summary: Experimental package to automatically extract test plugins for Home Assistant custom components
|
|
5
5
|
Home-page: https://github.com/MatthewFlamm/pytest-homeassistant-custom-component
|
|
6
6
|
Author: Matthew Flamm
|
|
@@ -11,45 +11,57 @@ Classifier: Framework :: Pytest
|
|
|
11
11
|
Classifier: Intended Audience :: Developers
|
|
12
12
|
Classifier: License :: OSI Approved :: MIT License
|
|
13
13
|
Classifier: Programming Language :: Python
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
15
|
Classifier: Topic :: Software Development :: Testing
|
|
16
|
-
Requires-Python: >=3.
|
|
16
|
+
Requires-Python: >=3.13
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
License-File: LICENSE
|
|
19
19
|
License-File: LICENSE_HA_CORE.md
|
|
20
20
|
Requires-Dist: sqlalchemy
|
|
21
|
-
Requires-Dist: coverage==7.6
|
|
22
|
-
Requires-Dist: freezegun==1.5.
|
|
21
|
+
Requires-Dist: coverage==7.10.6
|
|
22
|
+
Requires-Dist: freezegun==1.5.2
|
|
23
|
+
Requires-Dist: go2rtc-client==0.3.0
|
|
24
|
+
Requires-Dist: librt==0.2.1
|
|
25
|
+
Requires-Dist: license-expression==30.4.3
|
|
23
26
|
Requires-Dist: mock-open==1.4.0
|
|
24
|
-
Requires-Dist: pydantic==
|
|
25
|
-
Requires-Dist: pylint-per-file-ignores==1.
|
|
26
|
-
Requires-Dist: pipdeptree==2.
|
|
27
|
-
Requires-Dist:
|
|
28
|
-
Requires-Dist: pytest-
|
|
29
|
-
Requires-Dist: pytest-
|
|
30
|
-
Requires-Dist: pytest-
|
|
31
|
-
Requires-Dist: pytest-
|
|
32
|
-
Requires-Dist: pytest-github-actions-annotate-failures==0.2.0
|
|
27
|
+
Requires-Dist: pydantic==2.12.2
|
|
28
|
+
Requires-Dist: pylint-per-file-ignores==1.4.0
|
|
29
|
+
Requires-Dist: pipdeptree==2.26.1
|
|
30
|
+
Requires-Dist: pytest-asyncio==1.3.0
|
|
31
|
+
Requires-Dist: pytest-aiohttp==1.1.0
|
|
32
|
+
Requires-Dist: pytest-cov==7.0.0
|
|
33
|
+
Requires-Dist: pytest-freezer==0.4.9
|
|
34
|
+
Requires-Dist: pytest-github-actions-annotate-failures==0.3.0
|
|
33
35
|
Requires-Dist: pytest-socket==0.7.0
|
|
34
36
|
Requires-Dist: pytest-sugar==1.0.0
|
|
35
|
-
Requires-Dist: pytest-timeout==2.
|
|
36
|
-
Requires-Dist: pytest-unordered==0.
|
|
37
|
-
Requires-Dist: pytest-picked==0.5.
|
|
38
|
-
Requires-Dist: pytest-xdist==3.
|
|
39
|
-
Requires-Dist: pytest==
|
|
37
|
+
Requires-Dist: pytest-timeout==2.4.0
|
|
38
|
+
Requires-Dist: pytest-unordered==0.7.0
|
|
39
|
+
Requires-Dist: pytest-picked==0.5.1
|
|
40
|
+
Requires-Dist: pytest-xdist==3.8.0
|
|
41
|
+
Requires-Dist: pytest==9.0.0
|
|
40
42
|
Requires-Dist: requests-mock==1.12.1
|
|
41
|
-
Requires-Dist: respx==0.
|
|
42
|
-
Requires-Dist: syrupy==
|
|
43
|
-
Requires-Dist: tqdm==4.
|
|
44
|
-
Requires-Dist:
|
|
45
|
-
Requires-Dist:
|
|
46
|
-
Requires-Dist:
|
|
47
|
-
Requires-Dist:
|
|
48
|
-
|
|
43
|
+
Requires-Dist: respx==0.22.0
|
|
44
|
+
Requires-Dist: syrupy==5.0.0
|
|
45
|
+
Requires-Dist: tqdm==4.67.1
|
|
46
|
+
Requires-Dist: homeassistant==2025.12.0
|
|
47
|
+
Requires-Dist: SQLAlchemy==2.0.41
|
|
48
|
+
Requires-Dist: paho-mqtt==2.1.0
|
|
49
|
+
Requires-Dist: numpy==2.3.2
|
|
50
|
+
Dynamic: author
|
|
51
|
+
Dynamic: author-email
|
|
52
|
+
Dynamic: classifier
|
|
53
|
+
Dynamic: description
|
|
54
|
+
Dynamic: description-content-type
|
|
55
|
+
Dynamic: home-page
|
|
56
|
+
Dynamic: license
|
|
57
|
+
Dynamic: license-file
|
|
58
|
+
Dynamic: requires-dist
|
|
59
|
+
Dynamic: requires-python
|
|
60
|
+
Dynamic: summary
|
|
49
61
|
|
|
50
62
|
# pytest-homeassistant-custom-component
|
|
51
63
|
|
|
52
|
-

|
|
53
65
|
|
|
54
66
|
[](https://gitpod.io/#https://github.com/MatthewFlamm/pytest-homeassistant-custom-component)
|
|
55
67
|
|
|
@@ -75,6 +87,19 @@ tests/
|
|
|
75
87
|
test_sensor.py
|
|
76
88
|
```
|
|
77
89
|
|
|
90
|
+
* When using syrupy snapshots, add a `snapshot` fixture to conftest.py to make sure the snapshots are loaded from snapshot folder colocated with the tests.
|
|
91
|
+
|
|
92
|
+
```py
|
|
93
|
+
from pytest_homeassistant_custom_component.syrupy import HomeAssistantSnapshotExtension
|
|
94
|
+
from syrupy.assertion import SnapshotAssertion
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@pytest.fixture
|
|
98
|
+
def snapshot(snapshot: SnapshotAssertion) -> SnapshotAssertion:
|
|
99
|
+
"""Return snapshot assertion fixture with the Home Assistant extension."""
|
|
100
|
+
return snapshot.use_extension(HomeAssistantSnapshotExtension)
|
|
101
|
+
```
|
|
102
|
+
|
|
78
103
|
## Examples:
|
|
79
104
|
* See [list of custom components](https://github.com/MatthewFlamm/pytest-homeassistant-custom-component/network/dependents) as examples that use this package.
|
|
80
105
|
* Also see tests for `simple_integration` in this repository.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
pytest_homeassistant_custom_component/__init__.py,sha256=pUI8j-H-57ncCLnvZSDWZPCtJpvi3ACZqPtH5SbedZA,138
|
|
2
|
+
pytest_homeassistant_custom_component/asyncio_legacy.py,sha256=UdkV2mKqeS21QX9LSdBYsBRbm2h4JCVVZeesaOLKOAE,3886
|
|
3
|
+
pytest_homeassistant_custom_component/common.py,sha256=1UprBAnCk8VgxwD2Py893jNr0Fsjn-1Q_vx7pYcjB1M,65480
|
|
4
|
+
pytest_homeassistant_custom_component/const.py,sha256=ytAygDIdVtA6OiG_jBkWveMPpCDEz52T9zCZ4vsuQJ8,440
|
|
5
|
+
pytest_homeassistant_custom_component/ignore_uncaught_exceptions.py,sha256=rilak_dQGMNhDqST1ZzhjZl_qmytFjkcez0vYmLMQ4Q,1601
|
|
6
|
+
pytest_homeassistant_custom_component/patch_json.py,sha256=hNUeb1yxAr7ONfvX-o_WkI6zhQDCdKl7GglPjkVUiHo,1063
|
|
7
|
+
pytest_homeassistant_custom_component/patch_recorder.py,sha256=lW8N_3ZIKQ5lsVjRc-ROo7d0egUZcpjquWKqe7iEF94,819
|
|
8
|
+
pytest_homeassistant_custom_component/patch_time.py,sha256=jdnOAXDxUA0AKqvyeSrRC18rHDGfcpWYuLhmUglebCE,3374
|
|
9
|
+
pytest_homeassistant_custom_component/plugins.py,sha256=ui8WsonovfIEb0eI-UkV6IE80fm63_YUHD9RzD6wj2k,69969
|
|
10
|
+
pytest_homeassistant_custom_component/syrupy.py,sha256=N_g_90dWqruzUogQi0rJsuN0XRbA6ffJen62r8P9cdo,15588
|
|
11
|
+
pytest_homeassistant_custom_component/typing.py,sha256=zGhdf6U6aRq5cPwIfRUdtZeApLOyPD2EArjznKoIRZM,1734
|
|
12
|
+
pytest_homeassistant_custom_component/components/__init__.py,sha256=49s3Tf-mHQQnQPnjuD94LeCnYDypnm6y7Dhfr5lJkCQ,11427
|
|
13
|
+
pytest_homeassistant_custom_component/components/diagnostics/__init__.py,sha256=O_ys8t0iHvRorFr4TrR9k3sa3Xh5qBb4HsylY775UFA,2431
|
|
14
|
+
pytest_homeassistant_custom_component/components/recorder/__init__.py,sha256=ugrLzvjSQFSmYRjy88ZZSiyA-NLgKlLkFp0OKguy6a4,225
|
|
15
|
+
pytest_homeassistant_custom_component/components/recorder/common.py,sha256=8c_oqbQtg7dI-JoOaZrLYKFAjEJvjvSIA1atfui-WpQ,22091
|
|
16
|
+
pytest_homeassistant_custom_component/components/recorder/db_schema_0.py,sha256=0mez9slhL-I286dDAxq06UDvWRU6RzCA2GKOwtj9JOI,5547
|
|
17
|
+
pytest_homeassistant_custom_component/test_util/__init__.py,sha256=ljLmNeblq1vEgP0vhf2P1-SuyGSHvLKVA0APSYA0Xl8,1034
|
|
18
|
+
pytest_homeassistant_custom_component/test_util/aiohttp.py,sha256=sJHmGf4Oig0SUMvfylBaZIsDTfpTwmYvuLfE--OXYx4,11536
|
|
19
|
+
pytest_homeassistant_custom_component/testing_config/__init__.py,sha256=SRp6h9HJi2I_vA6cPNkMiR0BTYib5XVmL03H-l3BPL0,158
|
|
20
|
+
pytest_homeassistant_custom_component/testing_config/custom_components/__init__.py,sha256=-l6KCBLhwEDkCztlY6S-j53CjmKY6-A_3eX5JVS02NY,173
|
|
21
|
+
pytest_homeassistant_custom_component/testing_config/custom_components/test_constant_deprecation/__init__.py,sha256=2vF_C-VP9tDjZMX7h6iJRAugtH2Bf3b4fE3i9j4vGeY,383
|
|
22
|
+
pytest_homeassistant_custom_component-0.13.298.dist-info/licenses/LICENSE,sha256=7h-vqUxyeQNXiQgRJ8350CSHOy55M07DZuv4KG70AS8,1070
|
|
23
|
+
pytest_homeassistant_custom_component-0.13.298.dist-info/licenses/LICENSE_HA_CORE.md,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
24
|
+
pytest_homeassistant_custom_component-0.13.298.dist-info/METADATA,sha256=a6kWgh27JmV2RF1vvLgklpSKMsH206TFldacwEQBwM0,5958
|
|
25
|
+
pytest_homeassistant_custom_component-0.13.298.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
26
|
+
pytest_homeassistant_custom_component-0.13.298.dist-info/entry_points.txt,sha256=bOCTSuP8RSPg0QfwdfurUShvMGWg4MI2F8rxbWx-VtQ,73
|
|
27
|
+
pytest_homeassistant_custom_component-0.13.298.dist-info/top_level.txt,sha256=PR2cize2la22eOO7dQChJWK8dkJnuMmDC-fhafmdOWw,38
|
|
28
|
+
pytest_homeassistant_custom_component-0.13.298.dist-info/RECORD,,
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
pytest_homeassistant_custom_component/__init__.py,sha256=pUI8j-H-57ncCLnvZSDWZPCtJpvi3ACZqPtH5SbedZA,138
|
|
2
|
-
pytest_homeassistant_custom_component/asyncio_legacy.py,sha256=UdkV2mKqeS21QX9LSdBYsBRbm2h4JCVVZeesaOLKOAE,3886
|
|
3
|
-
pytest_homeassistant_custom_component/common.py,sha256=4dTQYDe6PSEywTkcW7zULCqNLO-aYfonkJg3TpC3ePk,57455
|
|
4
|
-
pytest_homeassistant_custom_component/const.py,sha256=vs72mWrx35nAsr0GiQvtSAG2SvJ2gBqHkzDCu2sTz2c,399
|
|
5
|
-
pytest_homeassistant_custom_component/ignore_uncaught_exceptions.py,sha256=rilak_dQGMNhDqST1ZzhjZl_qmytFjkcez0vYmLMQ4Q,1601
|
|
6
|
-
pytest_homeassistant_custom_component/patch_recorder.py,sha256=k88YdgDvWhHQ08ZVXXA7UBm7kbRef3BWkWuODusk41g,833
|
|
7
|
-
pytest_homeassistant_custom_component/patch_time.py,sha256=xz4lwln5mzCSWcpfREcF_lGMX9XR9iyD287Q0GqFckI,1293
|
|
8
|
-
pytest_homeassistant_custom_component/plugins.py,sha256=lEPLmvKaXEax08kN3ELpH8rXoZoe90TB5VU2Y2xQCVk,61528
|
|
9
|
-
pytest_homeassistant_custom_component/syrupy.py,sha256=w3J1zBxC8cJaBRgnKDpcost7XniMRUvDzHdeVsvNeUk,8573
|
|
10
|
-
pytest_homeassistant_custom_component/typing.py,sha256=Pm0tIRaP-8OdUy0WNRm47l07lWcO0_lkSLtTfu-IfJs,1515
|
|
11
|
-
pytest_homeassistant_custom_component/components/__init__.py,sha256=0BHCdArl5gPjDJWaZrqvApHvzL_29FbE1RMg_mg__Qs,138
|
|
12
|
-
pytest_homeassistant_custom_component/components/recorder/__init__.py,sha256=ugrLzvjSQFSmYRjy88ZZSiyA-NLgKlLkFp0OKguy6a4,225
|
|
13
|
-
pytest_homeassistant_custom_component/components/recorder/common.py,sha256=uNb_3VjEjqVRZJnYgU8SPYfSZP5RMlk4oU-JP0B9xkI,16938
|
|
14
|
-
pytest_homeassistant_custom_component/components/recorder/db_schema_0.py,sha256=oO1YY0u-CB-wF6UgTZR1kKZ-JsEZle4H6GTsdaO9jeU,5542
|
|
15
|
-
pytest_homeassistant_custom_component/test_util/__init__.py,sha256=ljLmNeblq1vEgP0vhf2P1-SuyGSHvLKVA0APSYA0Xl8,1034
|
|
16
|
-
pytest_homeassistant_custom_component/test_util/aiohttp.py,sha256=5CZFBlmd6LopGuKceqfedVndL5MXiNMc10D8MoDuTsc,10255
|
|
17
|
-
pytest_homeassistant_custom_component/testing_config/__init__.py,sha256=SRp6h9HJi2I_vA6cPNkMiR0BTYib5XVmL03H-l3BPL0,158
|
|
18
|
-
pytest_homeassistant_custom_component/testing_config/custom_components/__init__.py,sha256=-l6KCBLhwEDkCztlY6S-j53CjmKY6-A_3eX5JVS02NY,173
|
|
19
|
-
pytest_homeassistant_custom_component/testing_config/custom_components/test_constant_deprecation/__init__.py,sha256=2vF_C-VP9tDjZMX7h6iJRAugtH2Bf3b4fE3i9j4vGeY,383
|
|
20
|
-
pytest_homeassistant_custom_component-0.13.163.dist-info/LICENSE,sha256=7h-vqUxyeQNXiQgRJ8350CSHOy55M07DZuv4KG70AS8,1070
|
|
21
|
-
pytest_homeassistant_custom_component-0.13.163.dist-info/LICENSE_HA_CORE.md,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
22
|
-
pytest_homeassistant_custom_component-0.13.163.dist-info/METADATA,sha256=dBuejsn4gu4lpom554E-rwdTHXKqJ6KzE0GTJNkPa1g,5123
|
|
23
|
-
pytest_homeassistant_custom_component-0.13.163.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
|
24
|
-
pytest_homeassistant_custom_component-0.13.163.dist-info/entry_points.txt,sha256=bOCTSuP8RSPg0QfwdfurUShvMGWg4MI2F8rxbWx-VtQ,73
|
|
25
|
-
pytest_homeassistant_custom_component-0.13.163.dist-info/top_level.txt,sha256=PR2cize2la22eOO7dQChJWK8dkJnuMmDC-fhafmdOWw,38
|
|
26
|
-
pytest_homeassistant_custom_component-0.13.163.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|