GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Improve feature setter robustness (#870) · pillarsdotnet/python-kasa@fc734b1

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
  • kasa
    • feature.py
    • iot
      • iotbulb.py
    • smart/modules
      • autooffmodule.py
      • colortemp.py
      • lighttransitionmodule.py
      • temperature.py
    • tests
      • device_fixtures.py
      • test_feature.py
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎kasa/feature.py‎

Copy file name to clipboardExpand all lines: kasa/feature.py
+6-1Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,14 @@ async def set_value(self, value):
172172
return await getattr(container, self.attribute_setter)(value)
173173

174174
def __repr__(self):
175-
value = self.value
175+
try:
176+
value = self.value
177+
except Exception as ex:
178+
return f"Unable to read value ({self.id}): {ex}"
179+
176180
if self.precision_hint is not None and value is not None:
177181
value = round(self.value, self.precision_hint)
182+
178183
s = f"{self.name} ({self.id}): {value}"
179184
if self.unit is not None:
180185
s += f" {self.unit}"
Collapse file

‎kasa/iot/iotbulb.py‎

Copy file name to clipboardExpand all lines: kasa/iot/iotbulb.py
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ async def _initialize_features(self):
233233
attribute_setter="set_color_temp",
234234
range_getter="valid_temperature_range",
235235
category=Feature.Category.Primary,
236+
type=Feature.Type.Number,
236237
)
237238
)
238239

Collapse file

‎kasa/smart/modules/autooffmodule.py‎

Copy file name to clipboardExpand all lines: kasa/smart/modules/autooffmodule.py
+4-4Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ def enabled(self) -> bool:
5555
"""Return True if enabled."""
5656
return self.data["enable"]
5757

58-
def set_enabled(self, enable: bool):
58+
async def set_enabled(self, enable: bool):
5959
"""Enable/disable auto off."""
60-
return self.call(
60+
return await self.call(
6161
"set_auto_off_config",
6262
{"enable": enable, "delay_min": self.data["delay_min"]},
6363
)
@@ -67,9 +67,9 @@ def delay(self) -> int:
6767
"""Return time until auto off."""
6868
return self.data["delay_min"]
6969

70-
def set_delay(self, delay: int):
70+
async def set_delay(self, delay: int):
7171
"""Set time until auto off."""
72-
return self.call(
72+
return await self.call(
7373
"set_auto_off_config", {"delay_min": delay, "enable": self.data["enable"]}
7474
)
7575

Collapse file

‎kasa/smart/modules/colortemp.py‎

Copy file name to clipboardExpand all lines: kasa/smart/modules/colortemp.py
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ def __init__(self, device: SmartDevice, module: str):
3434
attribute_setter="set_color_temp",
3535
range_getter="valid_temperature_range",
3636
category=Feature.Category.Primary,
37+
type=Feature.Type.Number,
3738
)
3839
)
3940

Collapse file

‎kasa/smart/modules/lighttransitionmodule.py‎

Copy file name to clipboardExpand all lines: kasa/smart/modules/lighttransitionmodule.py
+2-2Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,9 @@ def _turn_off(self):
8888

8989
return self.data["off_state"]
9090

91-
def set_enabled_v1(self, enable: bool):
91+
async def set_enabled_v1(self, enable: bool):
9292
"""Enable gradual on/off."""
93-
return self.call("set_on_off_gradually_info", {"enable": enable})
93+
return await self.call("set_on_off_gradually_info", {"enable": enable})
9494

9595
@property
9696
def enabled_v1(self) -> bool:
Collapse file

‎kasa/smart/modules/temperature.py‎

Copy file name to clipboardExpand all lines: kasa/smart/modules/temperature.py
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def __init__(self, device: SmartDevice, module: str):
4848
attribute_getter="temperature_unit",
4949
attribute_setter="set_temperature_unit",
5050
type=Feature.Type.Choice,
51+
choices=["celsius", "fahrenheit"],
5152
)
5253
)
5354
# TODO: use temperature_unit for feature creation
Collapse file

‎kasa/tests/device_fixtures.py‎

Copy file name to clipboardExpand all lines: kasa/tests/device_fixtures.py
+10-8Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
from typing import AsyncGenerator
4+
35
import pytest
46

57
from kasa import (
@@ -346,13 +348,13 @@ def device_for_fixture_name(model, protocol):
346348
raise Exception("Unable to find type for %s", model)
347349

348350

349-
async def _update_and_close(d):
351+
async def _update_and_close(d) -> Device:
350352
await d.update()
351353
await d.protocol.close()
352354
return d
353355

354356

355-
async def _discover_update_and_close(ip, username, password):
357+
async def _discover_update_and_close(ip, username, password) -> Device:
356358
if username and password:
357359
credentials = Credentials(username=username, password=password)
358360
else:
@@ -361,7 +363,7 @@ async def _discover_update_and_close(ip, username, password):
361363
return await _update_and_close(d)
362364

363365

364-
async def get_device_for_fixture(fixture_data: FixtureInfo):
366+
async def get_device_for_fixture(fixture_data: FixtureInfo) -> Device:
365367
# if the wanted file is not an absolute path, prepend the fixtures directory
366368

367369
d = device_for_fixture_name(fixture_data.name, fixture_data.protocol)(
@@ -395,13 +397,14 @@ async def get_device_for_fixture_protocol(fixture, protocol):
395397

396398

397399
@pytest.fixture(params=filter_fixtures("main devices"), ids=idgenerator)
398-
async def dev(request):
400+
async def dev(request) -> AsyncGenerator[Device, None]:
399401
"""Device fixture.
400402
401403
Provides a device (given --ip) or parametrized fixture for the supported devices.
402404
The initial update is called automatically before returning the device.
403405
"""
404406
fixture_data: FixtureInfo = request.param
407+
dev: Device
405408

406409
ip = request.config.getoption("--ip")
407410
username = request.config.getoption("--username")
@@ -412,13 +415,12 @@ async def dev(request):
412415
if not model:
413416
d = await _discover_update_and_close(ip, username, password)
414417
IP_MODEL_CACHE[ip] = model = d.model
418+
415419
if model not in fixture_data.name:
416420
pytest.skip(f"skipping file {fixture_data.name}")
417-
dev: Device = (
418-
d if d else await _discover_update_and_close(ip, username, password)
419-
)
421+
dev = d if d else await _discover_update_and_close(ip, username, password)
420422
else:
421-
dev: Device = await get_device_for_fixture(fixture_data)
423+
dev = await get_device_for_fixture(fixture_data)
422424

423425
yield dev
424426

Collapse file

‎kasa/tests/test_feature.py‎

Copy file name to clipboardExpand all lines: kasa/tests/test_feature.py
+65-3Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
import logging
2+
import sys
3+
14
import pytest
2-
from pytest_mock import MockFixture
5+
from pytest_mock import MockerFixture
6+
7+
from kasa import Device, Feature, KasaException
38

4-
from kasa import Feature
9+
_LOGGER = logging.getLogger(__name__)
510

611

712
class DummyDevice:
@@ -111,7 +116,7 @@ async def test_feature_action(mocker):
111116
mock_call_action.assert_called()
112117

113118

114-
async def test_feature_choice_list(dummy_feature, caplog, mocker: MockFixture):
119+
async def test_feature_choice_list(dummy_feature, caplog, mocker: MockerFixture):
115120
"""Test the choice feature type."""
116121
dummy_feature.type = Feature.Type.Choice
117122
dummy_feature.choices = ["first", "second"]
@@ -138,3 +143,60 @@ async def test_precision_hint(dummy_feature, precision_hint):
138143
dummy_feature.attribute_getter = lambda x: dummy_value
139144
assert dummy_feature.value == dummy_value
140145
assert f"{round(dummy_value, precision_hint)} dummyunit" in repr(dummy_feature)
146+
147+
148+
@pytest.mark.skipif(
149+
sys.version_info < (3, 11),
150+
reason="exceptiongroup requires python3.11+",
151+
)
152+
async def test_feature_setters(dev: Device, mocker: MockerFixture):
153+
"""Test that all feature setters query something."""
154+
155+
async def _test_feature(feat, query_mock):
156+
if feat.attribute_setter is None:
157+
return
158+
159+
expecting_call = True
160+
161+
if feat.type == Feature.Type.Number:
162+
await feat.set_value(feat.minimum_value)
163+
elif feat.type == Feature.Type.Switch:
164+
await feat.set_value(True)
165+
elif feat.type == Feature.Type.Action:
166+
await feat.set_value("dummyvalue")
167+
elif feat.type == Feature.Type.Choice:
168+
await feat.set_value(feat.choices[0])
169+
elif feat.type == Feature.Type.Unknown:
170+
_LOGGER.warning("Feature '%s' has no type, cannot test the setter", feat)
171+
expecting_call = False
172+
else:
173+
raise NotImplementedError(f"set_value not implemented for {feat.type}")
174+
175+
if expecting_call:
176+
query_mock.assert_called()
177+
178+
async def _test_features(dev):
179+
exceptions = []
180+
query = mocker.patch.object(dev.protocol, "query")
181+
for feat in dev.features.values():
182+
query.reset_mock()
183+
try:
184+
await _test_feature(feat, query)
185+
# we allow our own exceptions to avoid mocking valid responses
186+
except KasaException:
187+
pass
188+
except Exception as ex:
189+
ex.add_note(f"Exception when trying to set {feat} on {dev}")
190+
exceptions.append(ex)
191+
192+
return exceptions
193+
194+
exceptions = await _test_features(dev)
195+
196+
for child in dev.children:
197+
exceptions.extend(await _test_features(child))
198+
199+
if exceptions:
200+
raise ExceptionGroup(
201+
"Got exceptions while testing attribute_setters", exceptions
202+
)

0 commit comments

Comments
0 (0)
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.