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.
Files changed (21) hide show
  1. pytest_homeassistant_custom_component/common.py +289 -46
  2. pytest_homeassistant_custom_component/components/__init__.py +351 -0
  3. pytest_homeassistant_custom_component/components/diagnostics/__init__.py +71 -0
  4. pytest_homeassistant_custom_component/components/recorder/common.py +143 -13
  5. pytest_homeassistant_custom_component/components/recorder/db_schema_0.py +1 -1
  6. pytest_homeassistant_custom_component/const.py +4 -3
  7. pytest_homeassistant_custom_component/patch_json.py +41 -0
  8. pytest_homeassistant_custom_component/patch_recorder.py +1 -1
  9. pytest_homeassistant_custom_component/patch_time.py +66 -0
  10. pytest_homeassistant_custom_component/plugins.py +429 -177
  11. pytest_homeassistant_custom_component/syrupy.py +188 -1
  12. pytest_homeassistant_custom_component/test_util/aiohttp.py +56 -20
  13. pytest_homeassistant_custom_component/typing.py +5 -0
  14. {pytest_homeassistant_custom_component-0.13.163.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info}/METADATA +54 -29
  15. pytest_homeassistant_custom_component-0.13.298.dist-info/RECORD +28 -0
  16. {pytest_homeassistant_custom_component-0.13.163.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info}/WHEEL +1 -1
  17. pytest_homeassistant_custom_component-0.13.163.dist-info/RECORD +0 -26
  18. {pytest_homeassistant_custom_component-0.13.163.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info}/entry_points.txt +0 -0
  19. {pytest_homeassistant_custom_component-0.13.163.dist-info → pytest_homeassistant_custom_component-0.13.298.dist-info/licenses}/LICENSE +0 -0
  20. {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
  21. {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(data.to_json() | {"created": ANY})
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
- self._mocks.append(
77
- AiohttpClientMockResponse(
78
- method=method,
79
- url=url,
80
- status=status,
81
- response=content,
82
- json=json,
83
- text=text,
84
- cookies=cookies,
85
- exc=exc,
86
- headers=headers,
87
- side_effect=side_effect,
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.host != url.host
221
- or self._url.path != url.path
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="utf-8", errors="strict"):
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="utf-8", content_type=None, loads=json_loads):
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
1
+ Metadata-Version: 2.4
2
2
  Name: pytest-homeassistant-custom-component
3
- Version: 0.13.163
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.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
15
  Classifier: Topic :: Software Development :: Testing
16
- Requires-Python: >=3.12
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.0
22
- Requires-Dist: freezegun==1.5.1
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==1.10.17
25
- Requires-Dist: pylint-per-file-ignores==1.3.2
26
- Requires-Dist: pipdeptree==2.23.1
27
- Requires-Dist: pip-licenses==4.5.1
28
- Requires-Dist: pytest-asyncio==0.23.8
29
- Requires-Dist: pytest-aiohttp==1.0.5
30
- Requires-Dist: pytest-cov==5.0.0
31
- Requires-Dist: pytest-freezer==0.4.8
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.3.1
36
- Requires-Dist: pytest-unordered==0.6.1
37
- Requires-Dist: pytest-picked==0.5.0
38
- Requires-Dist: pytest-xdist==3.6.1
39
- Requires-Dist: pytest==8.3.1
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.21.1
42
- Requires-Dist: syrupy==4.6.1
43
- Requires-Dist: tqdm==4.66.4
44
- Requires-Dist: uv==0.2.27
45
- Requires-Dist: homeassistant==2024.9.2
46
- Requires-Dist: SQLAlchemy==2.0.31
47
- Requires-Dist: paho-mqtt==1.6.1
48
- Requires-Dist: numpy==1.26.0
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
- ![HA core version](https://img.shields.io/static/v1?label=HA+core+version&message=2024.9.2&labelColor=blue)
64
+ ![HA core version](https://img.shields.io/static/v1?label=HA+core+version&message=2025.12.0&labelColor=blue)
53
65
 
54
66
  [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](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,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.1.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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,,