bleak: cache BleakAdapter instances per event loop#1981
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #1981 +/- ##
===========================================
+ Coverage 52.06% 52.62% +0.55%
===========================================
Files 43 43
Lines 4091 4118 +27
Branches 503 505 +2
===========================================
+ Hits 2130 2167 +37
+ Misses 1830 1819 -11
- Partials 131 132 +1 Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
| ) | ||
| return cls(await PlatformBleakAdapter.get(bluez=bluez)) | ||
| loop = asyncio.get_running_loop() | ||
| adapter_path = bluez.get("adapter") |
There was a problem hiding this comment.
This isn't going to work cross-platform. It needs to only be called if PlatformBleakAdapter is the BlueZ backend type. And we can't import that type here, otherwise it would crash on other platforms.
In practice, I guess it probably won't cause problems if we leave it like this, but logically, it doesn't seem quite right.
Ideally, we would move the backend-specific stuff into the backend itself. But I don't have any good ideas on how to do that without repeating code in each backend.
A hacky workaround could be to use if PlatformBleakAdapter.__name__ == "BleakAdapterBlueZDBus":.
| async def test_get_returns_different_instance_per_bluez_adapter(): | ||
| a = await BleakAdapter.get(backend=_FakeAdapter, bluez={"adapter": "hci0"}) | ||
| b = await BleakAdapter.get(backend=_FakeAdapter, bluez={"adapter": "hci1"}) | ||
| assert a is not b |
There was a problem hiding this comment.
As mentioned above. Logically, this test should only pass on Linux and fail on other platforms.
| from bleak.backends.adapter import BaseBleakAdapter | ||
|
|
||
|
|
||
| class _FakeAdapter(BaseBleakAdapter): |
There was a problem hiding this comment.
Why using fake adapter? It means we are testing less real code.
|
|
||
|
|
||
| @pytest.fixture(autouse=True) | ||
| def _clear_cache_and_counters(): # pyright: ignore[reportUnusedFunction] |
There was a problem hiding this comment.
| def _clear_cache_and_counters(): # pyright: ignore[reportUnusedFunction] | |
| def clear_cache_and_counters(): |
Drop the _ to avoid needing to ignore lint errors.
|
|
||
| @pytest.fixture(autouse=True) | ||
| def _clear_cache_and_counters(): # pyright: ignore[reportUnusedFunction] | ||
| BleakAdapter._instances.clear() # pyright: ignore[reportPrivateUsage] |
There was a problem hiding this comment.
Each test should have a new event loop, so I'm not sure we actually need to do this.
| assert a is not b | ||
|
|
||
|
|
||
| def test_separate_event_loops_get_separate_instances(): |
There was a problem hiding this comment.
As mentioned in another comment, pytest should already be providing a new loop for ever test, so this test should not be necessary.
| assert first is not second | ||
|
|
||
|
|
||
| def test_closed_loop_entries_are_pruned_lazily(): |
There was a problem hiding this comment.
We should be able to see in code coverage if this is working or not because the del line is either called or not.
Not sure that this test adds much on top of that.
|
|
||
| Changed | ||
| ------- | ||
| * ``BleakAdapter.get()`` now returns the same ``BleakAdapter`` instance on repeated calls from the same event loop with the same adapter selection. |
There was a problem hiding this comment.
We haven't done a release with the previous behavior, so we don't need a changelog entry in this case.
dcceced to
7fde0a3
Compare
|
Three CI failures from trying real
Real Also added the Force-pushed 7fde0a3. |
|
I don't understand why we can't put the tests in the integration tests and avoid mocking the |
Other than the special case of BlueZ with more than one adapter being used by Bleak at the same time. |
7fde0a3 to
e8f3dae
Compare
|
Moved both tests to |
e8f3dae to
da4920c
Compare
I would expect calling |
Right - currently Easiest path: drop the differentiation test from this PR. |
da4920c to
0d007b6
Compare
| @classmethod | ||
| @override | ||
| def cache_key(cls, *, bluez: BlueZAdapterArgs = {}, **kwargs: Any) -> Optional[str]: | ||
| return bluez.get("adapter") |
There was a problem hiding this comment.
I'm not sure this works in all cases.
Consider the following on a system with two Bluetooth adapters:
- Someone calls
BleakAdapter.get()which gets the "best" adapter.cache_keyisNonebecause no extra args were passed. - Something changes that affects the "best" adapter algorithm (one adapter is powered down, USB adapter is removed, etc.)
- Now, calling
BleakAdapter.get()will return an adapter that might not be there any more rather than the "best" adapter.
It hasn't really come up before of what the expected behavior should be when this sort of thing happens. Maybe it is OK the like this for now.
|
|
||
| @classmethod | ||
| @override | ||
| def cache_key(cls, *, bluez: BlueZAdapterArgs = {}, **kwargs: Any) -> Optional[str]: |
There was a problem hiding this comment.
| def cache_key(cls, *, bluez: BlueZAdapterArgs = {}, **kwargs: Any) -> Optional[str]: | |
| def cache_key(cls, *, bluez: BlueZAdapterArgs = {}, **kwargs: Any) -> Hashable: |
Needs to match signature we are overriding.
There was a problem hiding this comment.
Or replace Hashable with str | None everywhere in this patch.
| raise NotImplementedError("get is not implemented for this backend") | ||
|
|
||
| @classmethod | ||
| def cache_key(cls, **kwargs: Any) -> Optional[Hashable]: |
There was a problem hiding this comment.
| def cache_key(cls, **kwargs: Any) -> Optional[Hashable]: | |
| def cache_key(cls, **kwargs: Any) -> Hashable: |
None is hashable and isn't special in this case.
| tuple[ | ||
| asyncio.AbstractEventLoop, | ||
| type[BaseBleakAdapter], | ||
| Optional[Hashable], |
There was a problem hiding this comment.
| Optional[Hashable], | |
| Hashable, |
0d007b6 to
68093f7
Compare
BleakAdapter.get() now returns the same BleakAdapter instance on repeated calls from the same event loop with the same adapter selection. The cache is keyed by (loop, PlatformBleakAdapter, cache_key), where cache_key is supplied by each backend (BlueZ uses bluez['adapter'] to differentiate; other backends return None for a single cache entry per loop). Mirrors the pattern in bleak.backends.bluezdbus.manager._global_instances introduced in 1be914c.
|
Tried |
68093f7 to
96a44a7
Compare
BleakAdapter.get()now returns the sameBleakAdapterinstance on repeated calls from the same event loop with the same adapter selection. The cache is keyed by (loop,PlatformBleakAdapter,adapter_path), so different adapters selected viabluez={"adapter": ...}get distinct instances.Mirrors the pattern in
bleak.backends.bluezdbus.manager._global_instancesintroduced in 1be914c.