Simplify API documentation by using doctests (#73)

* Add doctests to SmartBulb

* Add SmartDevice doctests, cleanup README.md

* add doctests for smartplug and smartstrip

* add discover doctests

* Fix bulb mock

* add smartdimmer doctests

* add sphinx-generated docs, cleanup readme a bit

* remove sphinx-click as it does not work with asyncclick

* in preparation for rtd hooking, move doc deps to be separate from dev deps

* pytestmark needs to be applied separately for each and every file, this fixes the tests

* use pathlib for resolving relative paths

* Skip discovery doctest on python3.7

The code is just fine, but some reason the mocking behaves differently between 3.7 and 3.8.
The latter seems to accept a discrete object for asyncio.run where the former expects a coroutine..
This commit is contained in:
Teemu R
2020-06-30 02:29:52 +02:00
committed by GitHub
parent 99e0c4a418
commit f9a987ca18
34 changed files with 748 additions and 303 deletions

View File

@@ -21,7 +21,7 @@ OnDiscoveredCallable = Callable[[SmartDevice], Awaitable[None]]
class _DiscoverProtocol(asyncio.DatagramProtocol):
"""Implementation of the discovery protocol handler.
This is internal class, use :func:Discover.discover: instead.
This is internal class, use :func:`Discover.discover`: instead.
"""
discovered_devices: Dict[str, SmartDevice]
@@ -72,6 +72,7 @@ class _DiscoverProtocol(asyncio.DatagramProtocol):
device_class = Discover._get_device_class(info)
device = device_class(ip)
asyncio.ensure_future(device.update())
self.discovered_devices[ip] = device
self.discovered_devices_raw[ip] = info
@@ -93,16 +94,36 @@ class _DiscoverProtocol(asyncio.DatagramProtocol):
class Discover:
"""Discover TPLink Smart Home devices.
The main entry point for this library is Discover.discover(),
The main entry point for this library is :func:`Discover.discover()`,
which returns a dictionary of the found devices. The key is the IP address
of the device and the value contains ready-to-use, SmartDevice-derived
device object.
discover_single() can be used to initialize a single device given its
:func:`discover_single()` can be used to initialize a single device given its
IP address. If the type of the device and its IP address is already known,
you can initialize the corresponding device class directly without this.
The protocol uses UDP broadcast datagrams on port 9999 for discovery.
Examples:
Discovery returns a list of discovered devices:
>>> import asyncio
>>> found_devices = asyncio.run(Discover.discover())
>>> [dev.alias for dev in found_devices]
['TP-LINK_Power Strip_CF69']
Discovery can also be targeted to a specific broadcast address instead of the 255.255.255.255:
>>> asyncio.run(Discover.discover(target="192.168.8.255"))
It is also possible to pass a coroutine to be executed for each found device:
>>> async def print_alias(dev):
>>> print(f"Discovered {dev.alias}")
>>> devices = asyncio.run(Discover.discover(on_discovered=print_alias))
"""
DISCOVERY_PORT = 9999
@@ -130,12 +151,13 @@ class Discover:
to detect available supported devices in the local network,
and waits for given timeout for answers from devices.
If given, `on_discovered` coroutine will get passed with the SmartDevice as parameter.
The results of the discovery can be accessed either via `discovered_devices` (SmartDevice-derived) or
`discovered_devices_raw` (JSON objects).
If given, `on_discovered` coroutine will get passed with the :class:`SmartDevice`-derived object as parameter.
The results of the discovery are returned either as a list of :class:`SmartDevice`-derived objects
or as raw response dictionaries objects (if `return_raw` is True).
:param target: The target broadcast address (e.g. 192.168.xxx.255).
:param on_discovered:
:param on_discovered: coroutine to execute on discovery
:param timeout: How long to wait for responses, defaults to 5
:param discovery_packets: Number of discovery packets are broadcasted.
:param return_raw: True to return JSON objects instead of Devices.
@@ -180,7 +202,9 @@ class Discover:
device_class = Discover._get_device_class(info)
if device_class is not None:
return device_class(host)
dev = device_class(host)
await dev.update()
return dev
raise SmartDeviceException("Unable to discover device, received: %s" % info)

View File

@@ -23,53 +23,69 @@ TPLINK_KELVIN = {
class SmartBulb(SmartDevice):
"""Representation of a TP-Link Smart Bulb.
Usage example:
```python
p = SmartBulb("192.168.1.105")
await p.update()
To initialize, you have to await :func:`update()` at least once.
This will allow accessing the properties using the exposed properties.
# print the devices alias
print(p.alias)
All changes to the device are done using awaitable methods,
which will not change the cached values, but you must await :func:`update()` separately.
# change state of bulb
await p.turn_on()
await p.update()
assert p.is_on
await p.turn_off()
# query and print current state of plug
print(p.state_information)
# check whether the bulb supports color changes
if p.is_color:
print("we got color!")
# set the color to an HSV tuple
await p.set_hsv(180, 100, 100)
await p.update()
# get the current HSV value
print(p.hsv)
# check whether the bulb supports setting color temperature
if p.is_variable_color_temp:
# set the color temperature in Kelvin
await p.set_color_temp(3000)
await p.update()
# get the current color temperature
print(p.color_temp)
# check whether the bulb is dimmable
if p.is_dimmable:
# set the bulb to 50% brightness
await p.set_brightness(50)
await p.update()
# check the current brightness
print(p.brightness)
```
Errors reported by the device are raised as SmartDeviceExceptions,
Errors reported by the device are raised as :class:`SmartDeviceException`s,
and should be handled by the user of the library.
Examples:
>>> import asyncio
>>> bulb = SmartBulb("127.0.0.1")
>>> asyncio.run(bulb.update())
>>> print(bulb.alias)
KL130 office bulb
Bulbs, like any other supported devices, can be turned on and off:
>>> asyncio.run(bulb.turn_off())
>>> asyncio.run(bulb.turn_on())
>>> asyncio.run(bulb.update())
>>> print(bulb.is_on)
True
You can use the is_-prefixed properties to check for supported features
>>> bulb.is_dimmable
True
>>> bulb.is_color
True
>>> bulb.is_variable_color_temp
True
All known bulbs support changing the brightness:
>>> bulb.brightness
30
>>> asyncio.run(bulb.set_brightness(50))
>>> asyncio.run(bulb.update())
>>> bulb.brightness
50
Bulbs supporting color temperature can be queried to know which range is accepted:
>>> bulb.valid_temperature_range
(2500, 9000)
>>> asyncio.run(bulb.set_color_temp(3000))
>>> asyncio.run(bulb.update())
>>> bulb.color_temp
3000
Color bulbs can be adjusted by passing hue, saturation and value:
>>> asyncio.run(bulb.set_hsv(180, 100, 80))
>>> asyncio.run(bulb.update())
>>> bulb.hsv
(180, 100, 80)
If you don't want to use the default transitions, you can pass `transition` in milliseconds.
This applies to all transitions (turn_on, turn_off, set_hsv, set_color_temp, set_brightness).
The following changes the brightness over a period of 10 seconds:
>>> asyncio.run(bulb.set_brightness(100, transition=10_000))
"""
LIGHT_SERVICE = "smartlife.iot.smartbulb.lightingservice"

View File

@@ -118,7 +118,98 @@ def requires_update(f):
class SmartDevice:
"""Base class for all supported device types."""
"""Base class for all supported device types.
You don't usually want to construct this class which implements the shared common interfaces.
The recommended way is to either use the discovery functionality, or construct one of the subclasses:
* :class:`SmartPlug`
* :class:`SmartBulb`
* :class:`SmartStrip`
* :class:`SmartDimmer`
To initialize, you have to await :func:`update()` at least once.
This will allow accessing the properties using the exposed properties.
All changes to the device are done using awaitable methods,
which will not change the cached values, but you must await update() separately.
Errors reported by the device are raised as SmartDeviceExceptions,
and should be handled by the user of the library.
Examples:
>>> import asyncio
>>> dev = SmartDevice("127.0.0.1")
>>> asyncio.run(dev.update())
All devices provide several informational properties:
>>> dev.alias
Kitchen
>>> dev.model
HS110(EU)
>>> dev.rssi
-71
>>> dev.mac
50:C7:BF:01:F8:CD
Some information can also be changed programatically:
>>> asyncio.run(dev.set_alias("new alias"))
>>> asyncio.run(dev.set_mac("01:23:45:67:89:ab"))
>>> asyncio.run(dev.update())
>>> dev.alias
new alias
>>> dev.mac
01:23:45:67:89:ab
When initialized using discovery or using a subclass, you can check the type of the device:
>>> dev.is_bulb
False
>>> dev.is_strip
False
>>> dev.is_plug
True
You can also get the hardware and software as a dict, or access the full device response:
>>> dev.hw_info
{'sw_ver': '1.2.5 Build 171213 Rel.101523',
'hw_ver': '1.0',
'mac': '01:23:45:67:89:ab',
'type': 'IOT.SMARTPLUGSWITCH',
'hwId': '45E29DA8382494D2E82688B52A0B2EB5',
'fwId': '00000000000000000000000000000000',
'oemId': '3D341ECE302C0642C99E31CE2430544B',
'dev_name': 'Wi-Fi Smart Plug With Energy Monitoring'}
>>> dev.sys_info
All devices can be turned on and off:
>>> asyncio.run(dev.turn_off())
>>> asyncio.run(dev.turn_on())
>>> asyncio.run(dev.update())
>>> dev.is_on
True
Some devices provide energy consumption meter, and regular update will already fetch some information:
>>> dev.has_emeter
True
>>> dev.emeter_realtime
{'current': 0.015342, 'err_code': 0, 'power': 0.983971, 'total': 32.448, 'voltage': 235.595234}
>>> dev.emeter_today
>>> dev.emeter_this_month
You can also query the historical data (note that these needs to be awaited), keyed with month/day:
>>> asyncio.run(dev.get_emeter_monthly(year=2016))
{11: 1.089, 12: 1.582}
>>> asyncio.run(dev.get_emeter_daily(year=2016, month=11))
{24: 0.026, 25: 0.109}
"""
def __init__(self, host: str) -> None:
"""Create a new SmartDevice instance.
@@ -382,6 +473,9 @@ class SmartDevice:
@requires_update
def emeter_today(self) -> Optional[float]:
"""Return today's energy consumption in kWh."""
if not self.has_emeter:
raise SmartDeviceException("Device has no emeter")
raw_data = self._last_update[self.emeter_type]["get_daystat"]["day_list"]
data = self._emeter_convert_emeter_data(raw_data)
today = datetime.now().day
@@ -395,6 +489,9 @@ class SmartDevice:
@requires_update
def emeter_this_month(self) -> Optional[float]:
"""Return this month's energy consumption in kWh."""
if not self.has_emeter:
raise SmartDeviceException("Device has no emeter")
raw_data = self._last_update[self.emeter_type]["get_monthstat"]["month_list"]
data = self._emeter_convert_emeter_data(raw_data)
current_month = datetime.now().month
@@ -485,7 +582,7 @@ class SmartDevice:
response = EmeterStatus(await self.get_emeter_realtime())
return response["power"]
async def reboot(self, delay=1) -> None:
async def reboot(self, delay: int = 1) -> None:
"""Reboot the device.
Note that giving a delay of zero causes this to block,

View File

@@ -9,18 +9,30 @@ class SmartDimmer(SmartPlug):
"""Representation of a TP-Link Smart Dimmer.
Dimmers work similarly to plugs, but provide also support for
adjusting the brightness. This class extends SmartPlug interface.
adjusting the brightness. This class extends :class:`SmartPlug` interface.
Example:
```
dimmer = SmartDimmer("192.168.1.105")
await dimmer.turn_on()
print("Current brightness: %s" % dimmer.brightness)
To initialize, you have to await :func:`update()` at least once.
This will allow accessing the properties using the exposed properties.
await dimmer.set_brightness(100)
```
All changes to the device are done using awaitable methods,
which will not change the cached values, but you must await :func:`update()` separately.
Refer to SmartPlug for the full API.
Errors reported by the device are raised as :class:`SmartDeviceException`s,
and should be handled by the user of the library.
Examples:
>>> import asyncio
>>> dimmer = SmartDimmer("192.168.1.105")
>>> asyncio.run(dimmer.turn_on())
>>> dimmer.brightness
25
>>> asyncio.run(dimmer.set_brightness(50))
>>> asyncio.run(dimmer.update())
>>> dimmer.brightness
50
Refer to :class:`SmartPlug` for the full API.
"""
DIMMER_SERVICE = "smartlife.iot.dimmer"

View File

@@ -10,24 +10,30 @@ _LOGGER = logging.getLogger(__name__)
class SmartPlug(SmartDevice):
"""Representation of a TP-Link Smart Switch.
Usage example:
```python
p = SmartPlug("192.168.1.105")
To initialize, you have to await :func:`update()` at least once.
This will allow accessing the properties using the exposed properties.
# print the devices alias
print(p.alias)
All changes to the device are done using awaitable methods,
which will not change the cached values, but you must await :func:`update()` separately.
# change state of plug
await p.turn_on()
assert p.is_on is True
await p.turn_off()
# print current state of plug
print(p.state_information)
```
Errors reported by the device are raised as SmartDeviceExceptions,
Errors reported by the device are raised as :class:`SmartDeviceException`s,
and should be handled by the user of the library.
Examples:
>>> import asyncio
>>> plug = SmartPlug("127.0.0.1")
>>> asyncio.run(plug.update())
>>> plug.alias
Kitchen
Setting the LED state:
>>> asyncio.run(plug.set_led(True))
>>> asyncio.run(plug.update())
>>> plug.led
True
For more examples, see the :class:`SmartDevice` class.
"""
def __init__(self, host: str) -> None:

View File

@@ -18,28 +18,52 @@ _LOGGER = logging.getLogger(__name__)
class SmartStrip(SmartDevice):
"""Representation of a TP-Link Smart Power Strip.
Usage example when used as library:
```python
p = SmartStrip("192.168.1.105")
A strip consists of the parent device and its children.
All methods of the parent act on all children, while the child devices
share the common API with the :class:`SmartPlug` class.
# query the state of the strip
await p.update()
print(p.is_on)
To initialize, you have to await :func:`update()` at least once.
This will allow accessing the properties using the exposed properties.
# change state of all outlets
await p.turn_on()
await p.turn_off()
All changes to the device are done using awaitable methods,
which will not change the cached values, but you must await :func:`update()` separately.
# individual outlets are accessible through plugs variable
for plug in p.plugs:
print(f"{p}: {p.is_on}")
# change state of a single outlet
await p.plugs[0].turn_on()
```
Errors reported by the device are raised as SmartDeviceExceptions,
Errors reported by the device are raised as :class:`SmartDeviceException`s,
and should be handled by the user of the library.
Examples:
>>> import asyncio
>>> strip = SmartStrip("127.0.0.1")
>>> asyncio.run(strip.update())
>>> strip.alias
TP-LINK_Power Strip_CF69
All methods act on the whole strip:
>>> for plug in strip.children:
>>> print(f"{plug.alias}: {plug.is_on}")
Plug 1: True
Plug 2: False
Plug 3: False
>>> strip.is_on
True
>>> asyncio.run(strip.turn_off())
Accessing individual plugs can be done using the `children` property:
>>> len(strip.children)
3
>>> for plug in strip.children:
>>> print(f"{plug.alias}: {plug.is_on}")
Plug 1: False
Plug 2: False
Plug 3: False
>>> asyncio.run(strip.children[1].turn_on())
>>> asyncio.run(strip.update())
>>> strip.is_on
True
For more examples, see the :class:`SmartDevice` class.
"""
def __init__(self, host: str) -> None:
@@ -212,7 +236,7 @@ class SmartStripPlug(SmartPlug):
def is_on(self) -> bool:
"""Return whether device is on."""
info = self._get_child_info()
return info["state"]
return bool(info["state"])
@property # type: ignore
@requires_update

View File

@@ -3,6 +3,7 @@ import glob
import json
import os
from os.path import basename
from pathlib import Path, PurePath
from unittest.mock import MagicMock
import pytest # type: ignore # see https://github.com/pytest-dev/pytest/issues/3342
@@ -100,6 +101,38 @@ async def handle_turn_on(dev, turn_on):
pytestmark = pytest.mark.asyncio
def device_for_file(model):
for d in STRIPS:
if d in model:
return SmartStrip
for d in PLUGS:
if d in model:
return SmartPlug
for d in BULBS:
if d in model:
return SmartBulb
for d in DIMMERS:
if d in model:
return SmartDimmer
raise Exception("Unable to find type for %s", model)
def get_device_for_file(file):
# if the wanted file is not an absolute path, prepend the fixtures directory
p = Path(file)
if not p.is_absolute():
p = Path(__file__).parent / "fixtures" / file
with open(p) as f:
sysinfo = json.load(f)
model = basename(file)
p = device_for_file(model)(host="123.123.123.123")
p.protocol = FakeTransportProtocol(sysinfo)
asyncio.run(p.update())
return p
@pytest.fixture(params=SUPPORTED_DEVICES)
def dev(request):
"""Device fixture.
@@ -117,29 +150,7 @@ def dev(request):
return d
raise Exception("Unable to find type for %s" % ip)
def device_for_file(model):
for d in STRIPS:
if d in model:
return SmartStrip
for d in PLUGS:
if d in model:
return SmartPlug
for d in BULBS:
if d in model:
return SmartBulb
for d in DIMMERS:
if d in model:
return SmartDimmer
raise Exception("Unable to find type for %s", model)
with open(file) as f:
sysinfo = json.load(f)
model = basename(file)
p = device_for_file(model)(host="123.123.123.123")
p.protocol = FakeTransportProtocol(sysinfo)
asyncio.run(p.update())
yield p
return get_device_for_file(file)
def pytest_addoption(parser):

View File

@@ -27,7 +27,7 @@
"system": {
"get_sysinfo": {
"active_mode": "none",
"alias": "Nick office tplink",
"alias": "KL130 office bulb",
"ctrl_protocols": {
"name": "Linkie",
"version": "1.0"
@@ -45,7 +45,7 @@
"is_factory": false,
"is_variable_color_temp": 1,
"light_state": {
"brightness": 0,
"brightness": 30,
"color_temp": 0,
"hue": 15,
"mode": "normal",

View File

@@ -240,42 +240,41 @@ emeter_commands = {
}
def error(target, cmd="no-command", msg="default msg"):
return {target: {cmd: {"err_code": -1323, "msg": msg}}}
def error(msg="default msg"):
return {"err_code": -1323, "msg": msg}
def success(target, cmd, res):
def success(res):
if res:
res.update({"err_code": 0})
else:
res = {"err_code": 0}
return {target: {cmd: res}}
return res
class FakeTransportProtocol(TPLinkSmartHomeProtocol):
def __init__(self, info):
self.discovery_data = info
proto = FakeTransportProtocol.baseproto
for target in info:
# print("target %s" % target)
for cmd in info[target]:
# print("initializing tgt %s cmd %s" % (target, cmd))
proto[target][cmd] = info[target][cmd]
# if we have emeter support, check for it
# if we have emeter support, we need to add the missing pieces
for module in ["emeter", "smartlife.iot.common.emeter"]:
if module not in info:
# TODO required for old tests
continue
if "get_realtime" in info[module]:
get_realtime_res = info[module]["get_realtime"]
# TODO remove when removing old tests
if callable(get_realtime_res):
get_realtime_res = get_realtime_res()
if (
"err_code" not in get_realtime_res
or not get_realtime_res["err_code"]
):
proto[module] = emeter_commands[module]
for etype in ["get_realtime", "get_daystat", "get_monthstat"]:
if etype in info[module]: # if the fixture has the data, use it
# print("got %s %s from fixture: %s" % (module, etype, info[module][etype]))
proto[module][etype] = info[module][etype]
else: # otherwise fall back to the static one
dummy_data = emeter_commands[module][etype]
# print("got %s %s from dummy: %s" % (module, etype, dummy_data))
proto[module][etype] = dummy_data
# print("initialized: %s" % proto[module])
self.proto = proto
def set_alias(self, x, child_ids=[]):
@@ -309,7 +308,7 @@ class FakeTransportProtocol(TPLinkSmartHomeProtocol):
def set_mac(self, x, *args):
_LOGGER.debug("Setting mac to %s", x)
self.proto["system"]["get_sysinfo"]["mac"] = x
self.proto["system"]["get_sysinfo"]["mac"] = x["mac"]
def set_hs220_brightness(self, x, *args):
_LOGGER.debug("Setting brightness to %s", x)
@@ -345,9 +344,10 @@ class FakeTransportProtocol(TPLinkSmartHomeProtocol):
if not light_state["on_off"] and "on_off" not in x:
light_state = light_state["dft_on_state"]
_LOGGER.debug("Current state: %s", light_state)
_LOGGER.debug("Old state: %s", light_state)
for key in x:
light_state[key] = x[key]
_LOGGER.debug("New state: %s", light_state)
def light_state(self, x, *args):
light_state = self.proto["system"]["get_sysinfo"]["light_state"]
@@ -417,26 +417,39 @@ class FakeTransportProtocol(TPLinkSmartHomeProtocol):
except KeyError:
child_ids = []
target = next(iter(request))
if target not in proto.keys():
return error(target, msg="target not found")
def get_response_for_module(target):
cmd = next(iter(request[target]))
if cmd not in proto[target].keys():
return error(target, cmd, msg="command not found")
if target not in proto.keys():
return error(msg="target not found")
params = request[target][cmd]
_LOGGER.debug(f"Going to execute {target}.{cmd} (params: {params}).. ")
def get_response_for_command(cmd):
if cmd not in proto[target].keys():
return error(msg=f"command {cmd} not found")
if callable(proto[target][cmd]):
res = proto[target][cmd](self, params, child_ids)
_LOGGER.debug("[callable] %s.%s: %s", target, cmd, res)
# verify that change didn't break schema, requires refactoring..
# TestSmartPlug.sysinfo_schema(self.proto["system"]["get_sysinfo"])
return success(target, cmd, res)
elif isinstance(proto[target][cmd], dict):
res = proto[target][cmd]
_LOGGER.debug("[static] %s.%s: %s", target, cmd, res)
return success(target, cmd, res)
else:
raise NotImplementedError(f"target {target} cmd {cmd}")
params = request[target][cmd]
_LOGGER.debug(f"Going to execute {target}.{cmd} (params: {params}).. ")
if callable(proto[target][cmd]):
res = proto[target][cmd](self, params, child_ids)
_LOGGER.debug("[callable] %s.%s: %s", target, cmd, res)
return success(res)
elif isinstance(proto[target][cmd], dict):
res = proto[target][cmd]
_LOGGER.debug("[static] %s.%s: %s", target, cmd, res)
return success(res)
else:
raise NotImplementedError(f"target {target} cmd {cmd}")
from collections import defaultdict
cmd_responses = defaultdict(dict)
for cmd in request[target]:
cmd_responses[target][cmd] = get_response_for_command(cmd)
return cmd_responses
response = {}
for target in request:
response.update(get_response_for_module(target))
return response

View File

@@ -10,6 +10,7 @@ from .conftest import (
non_color_bulb,
non_dimmable,
non_variable_temp,
pytestmark,
turn_on,
variable_temp,
)

View File

@@ -1,12 +1,9 @@
import pytest
from asyncclick.testing import CliRunner
from kasa import SmartDevice
from kasa.cli import alias, brightness, emeter, raw_command, state, sysinfo
from .conftest import handle_turn_on, turn_on
pytestmark = pytest.mark.asyncio
from .conftest import handle_turn_on, pytestmark, turn_on
async def test_sysinfo(dev):

View File

@@ -2,7 +2,7 @@ import pytest
from kasa import SmartDimmer
from .conftest import dimmer, handle_turn_on, turn_on
from .conftest import dimmer, handle_turn_on, pytestmark, turn_on
@dimmer

View File

@@ -3,10 +3,7 @@ import pytest # type: ignore # https://github.com/pytest-dev/pytest/issues/3342
from kasa import DeviceType, Discover, SmartDevice, SmartDeviceException
from .conftest import bulb, dimmer, plug, strip
# to avoid adding this for each async function separately
pytestmark = pytest.mark.asyncio
from .conftest import bulb, dimmer, plug, pytestmark, strip
@plug

View File

@@ -2,7 +2,7 @@ import pytest
from kasa import SmartDeviceException
from .conftest import has_emeter, no_emeter
from .conftest import has_emeter, no_emeter, pytestmark
from .newfakes import CURRENT_CONSUMPTION_SCHEMA

View File

@@ -1,6 +1,6 @@
from kasa import DeviceType
from .conftest import plug
from .conftest import plug, pytestmark
from .newfakes import PLUG_SCHEMA

View File

@@ -4,6 +4,7 @@ import pytest
from ..exceptions import SmartDeviceException
from ..protocol import TPLinkSmartHomeProtocol
from .conftest import pytestmark
@pytest.mark.parametrize("retry_count", [1, 3, 5])

View File

@@ -0,0 +1,65 @@
import sys
import pytest
import xdoctest
from kasa.tests.conftest import get_device_for_file
def test_bulb_examples(mocker):
"""Use KL130 (bulb with all features) to test the doctests."""
p = get_device_for_file("KL130(US)_1.0.json")
mocker.patch("kasa.smartbulb.SmartBulb", return_value=p)
mocker.patch("kasa.smartbulb.SmartBulb.update")
res = xdoctest.doctest_module("kasa.smartbulb", "all")
assert not res["failed"]
def test_smartdevice_examples(mocker):
"""Use HS110 for emeter examples."""
p = get_device_for_file("HS110(EU)_1.0_real.json")
mocker.patch("kasa.smartdevice.SmartDevice", return_value=p)
mocker.patch("kasa.smartdevice.SmartDevice.update")
res = xdoctest.doctest_module("kasa.smartdevice", "all")
assert not res["failed"]
def test_plug_examples(mocker):
"""Test plug examples."""
p = get_device_for_file("HS110(EU)_1.0_real.json")
mocker.patch("kasa.smartplug.SmartPlug", return_value=p)
mocker.patch("kasa.smartplug.SmartPlug.update")
res = xdoctest.doctest_module("kasa.smartplug", "all")
assert not res["failed"]
def test_strip_examples(mocker):
"""Test strip examples."""
p = get_device_for_file("KP303(UK)_1.0.json")
mocker.patch("kasa.smartstrip.SmartStrip", return_value=p)
mocker.patch("kasa.smartstrip.SmartStrip.update")
res = xdoctest.doctest_module("kasa.smartstrip", "all")
assert not res["failed"]
def test_dimmer_examples(mocker):
"""Test dimmer examples."""
p = get_device_for_file("HS220(US)_1.0_real.json")
mocker.patch("kasa.smartdimmer.SmartDimmer", return_value=p)
mocker.patch("kasa.smartdimmer.SmartDimmer.update")
res = xdoctest.doctest_module("kasa.smartdimmer", "all")
assert not res["failed"]
@pytest.mark.skipif(
sys.version_info < (3, 8), reason="3.7 handles asyncio.run differently"
)
def test_discovery_examples(mocker):
"""Test discovery examples."""
p = get_device_for_file("KP303(UK)_1.0.json")
# This succeeds on python 3.8 but fails on 3.7
# ValueError: a coroutine was expected, got [<DeviceType.Strip model KP303(UK) ...
mocker.patch("kasa.discover.Discover.discover", return_value=[p])
res = xdoctest.doctest_module("kasa.discover", "all")
assert not res["failed"]

View File

@@ -5,7 +5,7 @@ import pytest # type: ignore # https://github.com/pytest-dev/pytest/issues/3342
from kasa import SmartDeviceException
from .conftest import handle_turn_on, turn_on
from .conftest import handle_turn_on, pytestmark, turn_on
from .newfakes import PLUG_SCHEMA, TZ_SCHEMA, FakeTransportProtocol

View File

@@ -4,7 +4,7 @@ import pytest
from kasa import SmartDeviceException, SmartStrip
from .conftest import handle_turn_on, strip, turn_on
from .conftest import handle_turn_on, pytestmark, strip, turn_on
@strip