Add tutorial doctest module and enable top level await (#919)

Add a tutorial module with examples that can be tested with `doctest`.

In order to simplify the examples they can be run with doctest allowing
top level await statements by adding a fixture to patch the builtins
that xdoctest uses to test code.

---------

Co-authored-by: Teemu R. <tpr@iki.fi>
This commit is contained in:
Steven B
2024-05-16 17:13:44 +01:00
committed by GitHub
parent a2e8d2c4e8
commit 3490a1ef84
17 changed files with 228 additions and 42 deletions

View File

@@ -1,12 +1,12 @@
"""Python interface for TP-Link's smart home devices.
All common, shared functionalities are available through `SmartDevice` class::
All common, shared functionalities are available through `Device` class::
x = SmartDevice("192.168.1.1")
print(x.sys_info)
>>> from kasa import Discover
>>> x = await Discover.discover_single("192.168.1.1")
>>> print(x.model)
For device type specific actions `SmartBulb`, `SmartPlug`, or `SmartStrip`
should be used instead.
For device type specific actions `modules` and `features` should be used instead.
Module-specific errors are raised as `KasaException` and are expected
to be handled by the user of the library.

View File

@@ -150,7 +150,7 @@ class DeviceConfig:
credentials: Optional[Credentials] = None
#: Credentials hash for devices requiring authentication.
#: If credentials are also supplied they take precendence over credentials_hash.
#: Credentials hash can be retrieved from :attr:`SmartDevice.credentials_hash`
#: Credentials hash can be retrieved from :attr:`Device.credentials_hash`
credentials_hash: Optional[str] = None
#: The protocol specific type of connection. Defaults to the legacy type.
batch_size: Optional[int] = None

View File

@@ -270,10 +270,10 @@ class Discover:
you can use *target* parameter to specify the network for discovery.
If given, `on_discovered` coroutine will get awaited with
a :class:`SmartDevice`-derived object as parameter.
a :class:`Device`-derived object as parameter.
The results of the discovery are returned as a dict of
:class:`SmartDevice`-derived objects keyed with IP addresses.
:class:`Device`-derived objects keyed with IP addresses.
The devices are already initialized and all but emeter-related properties
can be accessed directly.
@@ -332,7 +332,7 @@ class Discover:
"""Discover a single device by the given IP address.
It is generally preferred to avoid :func:`discover_single()` and
use :meth:`SmartDevice.connect()` instead as it should perform better when
use :meth:`Device.connect()` instead as it should perform better when
the WiFi network is congested or the device is not responding
to discovery requests.

View File

@@ -180,7 +180,7 @@ class IotBulb(IotDevice):
>>> bulb.presets
[LightPreset(index=0, brightness=50, hue=0, saturation=0, color_temp=2700, custom=None, id=None, mode=None), LightPreset(index=1, brightness=100, hue=0, saturation=75, color_temp=0, custom=None, id=None, mode=None), LightPreset(index=2, brightness=100, hue=120, saturation=75, color_temp=0, custom=None, id=None, mode=None), LightPreset(index=3, brightness=100, hue=240, saturation=75, color_temp=0, custom=None, id=None, mode=None)]
To modify an existing preset, pass :class:`~kasa.smartbulb.LightPreset`
To modify an existing preset, pass :class:`~kasa.interfaces.light.LightPreset`
instance to :func:`save_preset` method:
>>> preset = bulb.presets[0]

View File

@@ -41,7 +41,7 @@ class IotPlug(IotDevice):
>>> plug.led
True
For more examples, see the :class:`SmartDevice` class.
For more examples, see the :class:`Device` class.
"""
def __init__(

View File

@@ -83,7 +83,7 @@ class IotStrip(IotDevice):
>>> strip.is_on
True
For more examples, see the :class:`SmartDevice` class.
For more examples, see the :class:`Device` class.
"""
def __init__(

View File

@@ -175,7 +175,7 @@
"longitude": 0,
"mac": "5C-E9-31-00-00-00",
"model": "L530",
"nickname": "I01BU0tFRF9OQU1FIw==",
"nickname": "TGl2aW5nIFJvb20=",
"oem_id": "00000000000000000000000000000000",
"overheated": false,
"region": "Europe/Berlin",

View File

@@ -1,7 +1,9 @@
import asyncio
import pytest
import xdoctest
from kasa import Discover
from kasa.tests.conftest import get_device_for_fixture_protocol
@@ -67,3 +69,61 @@ def test_discovery_examples(mocker):
mocker.patch("kasa.discover.Discover.discover", return_value=[p])
res = xdoctest.doctest_module("kasa.discover", "all")
assert not res["failed"]
def test_tutorial_examples(mocker, top_level_await):
"""Test discovery examples."""
a = asyncio.run(
get_device_for_fixture_protocol("L530E(EU)_3.0_1.1.6.json", "SMART")
)
b = asyncio.run(get_device_for_fixture_protocol("HS110(EU)_1.0_1.2.5.json", "IOT"))
a.host = "127.0.0.1"
b.host = "127.0.0.2"
# Note autospec does not work for staticmethods in python < 3.12
# https://github.com/python/cpython/issues/102978
mocker.patch(
"kasa.discover.Discover.discover_single", return_value=a, autospec=True
)
mocker.patch.object(Discover, "discover", return_value=[a, b], autospec=True)
res = xdoctest.doctest_module("docs/tutorial.py", "all")
assert not res["failed"]
@pytest.fixture
def top_level_await(mocker):
"""Fixture to enable top level awaits in doctests.
Uses the async exec feature of python to patch the builtins xdoctest uses.
See https://github.com/python/cpython/issues/78797
"""
import ast
from inspect import CO_COROUTINE
orig_exec = exec
orig_eval = eval
orig_compile = compile
def patch_exec(source, globals=None, locals=None, /, **kwargs):
if source.co_flags & CO_COROUTINE == CO_COROUTINE:
asyncio.run(orig_eval(source, globals, locals))
else:
orig_exec(source, globals, locals, **kwargs)
def patch_eval(source, globals=None, locals=None, /, **kwargs):
if source.co_flags & CO_COROUTINE == CO_COROUTINE:
return asyncio.run(orig_eval(source, globals, locals, **kwargs))
else:
return orig_eval(source, globals, locals, **kwargs)
def patch_compile(
source, filename, mode, flags=0, dont_inherit=False, optimize=-1, **kwargs
):
flags |= ast.PyCF_ALLOW_TOP_LEVEL_AWAIT
return orig_compile(
source, filename, mode, flags, dont_inherit, optimize, **kwargs
)
mocker.patch("builtins.eval", side_effect=patch_eval)
mocker.patch("builtins.exec", side_effect=patch_exec)
mocker.patch("builtins.compile", side_effect=patch_compile)