mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-08-09 20:24:02 +00:00
Support child devices in all applicable cli commands (#1020)
Adds a new decorator that adds child options to a command and gets the child device if the options are set. - Single definition of options and error handling - Adds options automatically to command - Backwards compatible with `--index` and `--name` - `--child` allows for id and alias for ease of use - Omitting a value for `--child` gives an interactive prompt Implements private `_update` to allow the CLI to patch a child `update` method to call the parent device `update`. Example help output: ``` $ kasa brightness --help Usage: kasa brightness [OPTIONS] [BRIGHTNESS] Get or set brightness. Options: --transition INTEGER --child, --name TEXT Child ID or alias for controlling sub- devices. If no value provided will show an interactive prompt allowing you to select a child. --child-index, --index INTEGER Child index controlling sub-devices --help Show this message and exit. ``` Fixes #769
This commit is contained in:
@@ -5,6 +5,7 @@ import re
|
||||
import asyncclick as click
|
||||
import pytest
|
||||
from asyncclick.testing import CliRunner
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from kasa import (
|
||||
AuthenticationError,
|
||||
@@ -24,6 +25,7 @@ from kasa.cli import (
|
||||
cmd_command,
|
||||
effect,
|
||||
emeter,
|
||||
energy,
|
||||
hsv,
|
||||
led,
|
||||
raw_command,
|
||||
@@ -62,7 +64,6 @@ def runner():
|
||||
[
|
||||
pytest.param(None, None, id="No connect params"),
|
||||
pytest.param("SMART.TAPOPLUG", None, id="Only device_family"),
|
||||
pytest.param(None, "KLAP", id="Only encrypt_type"),
|
||||
],
|
||||
)
|
||||
async def test_update_called_by_cli(dev, mocker, runner, device_family, encrypt_type):
|
||||
@@ -171,13 +172,16 @@ async def test_command_with_child(dev, mocker, runner):
|
||||
class DummyDevice(dev.__class__):
|
||||
def __init__(self):
|
||||
super().__init__("127.0.0.1")
|
||||
# device_type and _info initialised for repr
|
||||
self._device_type = Device.Type.StripSocket
|
||||
self._info = {}
|
||||
|
||||
async def _query_helper(*_, **__):
|
||||
return {"dummy": "response"}
|
||||
|
||||
dummy_child = DummyDevice()
|
||||
|
||||
mocker.patch.object(dev, "_children", {"XYZ": dummy_child})
|
||||
mocker.patch.object(dev, "_children", {"XYZ": [dummy_child]})
|
||||
mocker.patch.object(dev, "get_child_device", return_value=dummy_child)
|
||||
|
||||
res = await runner.invoke(
|
||||
@@ -314,9 +318,9 @@ async def test_emeter(dev: Device, mocker, runner):
|
||||
|
||||
if not dev.is_strip:
|
||||
res = await runner.invoke(emeter, ["--index", "0"], obj=dev)
|
||||
assert "Index and name are only for power strips!" in res.output
|
||||
assert f"Device: {dev.host} does not have children" in res.output
|
||||
res = await runner.invoke(emeter, ["--name", "mock"], obj=dev)
|
||||
assert "Index and name are only for power strips!" in res.output
|
||||
assert f"Device: {dev.host} does not have children" in res.output
|
||||
|
||||
if dev.is_strip and len(dev.children) > 0:
|
||||
realtime_emeter = mocker.patch.object(dev.children[0], "get_emeter_realtime")
|
||||
@@ -930,3 +934,110 @@ async def test_feature_set_child(mocker, runner):
|
||||
assert f"Targeting child device {child_id}"
|
||||
assert "Changing state from False to True" in res.output
|
||||
assert res.exit_code == 0
|
||||
|
||||
|
||||
async def test_cli_child_commands(
|
||||
dev: Device, runner: CliRunner, mocker: MockerFixture
|
||||
):
|
||||
if not dev.children:
|
||||
res = await runner.invoke(alias, ["--child-index", "0"], obj=dev)
|
||||
assert f"Device: {dev.host} does not have children" in res.output
|
||||
assert res.exit_code == 1
|
||||
|
||||
res = await runner.invoke(alias, ["--index", "0"], obj=dev)
|
||||
assert f"Device: {dev.host} does not have children" in res.output
|
||||
assert res.exit_code == 1
|
||||
|
||||
res = await runner.invoke(alias, ["--child", "Plug 2"], obj=dev)
|
||||
assert f"Device: {dev.host} does not have children" in res.output
|
||||
assert res.exit_code == 1
|
||||
|
||||
res = await runner.invoke(alias, ["--name", "Plug 2"], obj=dev)
|
||||
assert f"Device: {dev.host} does not have children" in res.output
|
||||
assert res.exit_code == 1
|
||||
|
||||
if dev.children:
|
||||
child_alias = dev.children[0].alias
|
||||
assert child_alias
|
||||
child_device_id = dev.children[0].device_id
|
||||
child_count = len(dev.children)
|
||||
child_update_method = dev.children[0].update
|
||||
|
||||
# Test child retrieval
|
||||
res = await runner.invoke(alias, ["--child-index", "0"], obj=dev)
|
||||
assert f"Targeting child device {child_alias}" in res.output
|
||||
assert res.exit_code == 0
|
||||
|
||||
res = await runner.invoke(alias, ["--index", "0"], obj=dev)
|
||||
assert f"Targeting child device {child_alias}" in res.output
|
||||
assert res.exit_code == 0
|
||||
|
||||
res = await runner.invoke(alias, ["--child", child_alias], obj=dev)
|
||||
assert f"Targeting child device {child_alias}" in res.output
|
||||
assert res.exit_code == 0
|
||||
|
||||
res = await runner.invoke(alias, ["--name", child_alias], obj=dev)
|
||||
assert f"Targeting child device {child_alias}" in res.output
|
||||
assert res.exit_code == 0
|
||||
|
||||
res = await runner.invoke(alias, ["--child", child_device_id], obj=dev)
|
||||
assert f"Targeting child device {child_alias}" in res.output
|
||||
assert res.exit_code == 0
|
||||
|
||||
res = await runner.invoke(alias, ["--name", child_device_id], obj=dev)
|
||||
assert f"Targeting child device {child_alias}" in res.output
|
||||
assert res.exit_code == 0
|
||||
|
||||
# Test invalid name and index
|
||||
res = await runner.invoke(alias, ["--child-index", "-1"], obj=dev)
|
||||
assert f"Invalid index -1, device has {child_count} children" in res.output
|
||||
assert res.exit_code == 1
|
||||
|
||||
res = await runner.invoke(alias, ["--child-index", str(child_count)], obj=dev)
|
||||
assert (
|
||||
f"Invalid index {child_count}, device has {child_count} children"
|
||||
in res.output
|
||||
)
|
||||
assert res.exit_code == 1
|
||||
|
||||
res = await runner.invoke(alias, ["--child", "foobar"], obj=dev)
|
||||
assert "No child device found with device_id or name: foobar" in res.output
|
||||
assert res.exit_code == 1
|
||||
|
||||
# Test using both options:
|
||||
|
||||
res = await runner.invoke(
|
||||
alias, ["--child", child_alias, "--child-index", "0"], obj=dev
|
||||
)
|
||||
assert "Use either --child or --child-index, not both." in res.output
|
||||
assert res.exit_code == 2
|
||||
|
||||
# Test child with no parameter interactive prompt
|
||||
|
||||
res = await runner.invoke(alias, ["--child"], obj=dev, input="0\n")
|
||||
assert "Enter the index number of the child device:" in res.output
|
||||
assert f"Alias: {child_alias}" in res.output
|
||||
assert res.exit_code == 0
|
||||
|
||||
# Test values and updates
|
||||
|
||||
res = await runner.invoke(alias, ["foo", "--child", child_device_id], obj=dev)
|
||||
assert "Alias set to: foo" in res.output
|
||||
assert res.exit_code == 0
|
||||
|
||||
# Test help has command options plus child options
|
||||
|
||||
res = await runner.invoke(energy, ["--help"], obj=dev)
|
||||
assert "--year" in res.output
|
||||
assert "--child" in res.output
|
||||
assert "--child-index" in res.output
|
||||
assert res.exit_code == 0
|
||||
|
||||
# Test child update patching calls parent and is undone on exit
|
||||
|
||||
parent_update_spy = mocker.spy(dev, "update")
|
||||
res = await runner.invoke(alias, ["bar", "--child", child_device_id], obj=dev)
|
||||
assert "Alias set to: bar" in res.output
|
||||
assert res.exit_code == 0
|
||||
parent_update_spy.assert_called_once()
|
||||
assert dev.children[0].update == child_update_method
|
||||
|
Reference in New Issue
Block a user