Add flake8-pytest-style (PT) for ruff (#1105)

This will catch common issues with pytest code.

* Use `match` when using `pytest.raises()` for base exception types like
`TypeError` or `ValueError`
* Use tuples for `parametrize()`
* Enforces `pytest.raises()` to contain simple statements, using `noqa`
to skip this on some cases for now.
* Fixes incorrect exception type (valueerror instead of typeerror) for
iotdimmer.
* Adds check valid types for `iotbulb.set_hsv` and `color` smart module.
* Consolidate exception messages for common interface modules.
This commit is contained in:
Teemu R. 2024-08-30 17:30:07 +02:00 committed by GitHub
parent 3e43781bb2
commit 6a86ffbbba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 248 additions and 150 deletions

View File

@ -388,10 +388,14 @@ class IotBulb(IotDevice):
if not self._is_color:
raise KasaException("Bulb does not support color.")
if not isinstance(hue, int) or not (0 <= hue <= 360):
if not isinstance(hue, int):
raise TypeError("Hue must be an integer.")
if not (0 <= hue <= 360):
raise ValueError(f"Invalid hue value: {hue} (valid range: 0-360)")
if not isinstance(saturation, int) or not (0 <= saturation <= 100):
if not isinstance(saturation, int):
raise TypeError("Saturation must be an integer.")
if not (0 <= saturation <= 100):
raise ValueError(
f"Invalid saturation value: {saturation} (valid range: 0-100%)"
)
@ -445,7 +449,9 @@ class IotBulb(IotDevice):
return await self._set_light_state(light_state, transition=transition)
def _raise_for_invalid_brightness(self, value):
if not isinstance(value, int) or not (0 <= value <= 100):
if not isinstance(value, int):
raise TypeError("Brightness must be an integer")
if not (0 <= value <= 100):
raise ValueError(f"Invalid brightness value: {value} (valid range: 0-100%)")
@property # type: ignore

View File

@ -118,7 +118,9 @@ class IotDimmer(IotPlug):
)
if not 0 <= brightness <= 100:
raise ValueError("Brightness value %s is not valid." % brightness)
raise ValueError(
f"Invalid brightness value: {brightness} (valid range: 0-100%)"
)
# Dimmers do not support a brightness of 0, but bulbs do.
# Coerce 0 to 1 to maintain the same interface between dimmers and bulbs.
@ -161,20 +163,18 @@ class IotDimmer(IotPlug):
A brightness value of 0 will turn off the dimmer.
"""
if not isinstance(brightness, int):
raise ValueError(
"Brightness must be integer, " "not of %s.", type(brightness)
)
raise TypeError(f"Brightness must be an integer, not {type(brightness)}.")
if not 0 <= brightness <= 100:
raise ValueError("Brightness value %s is not valid." % brightness)
raise ValueError(
f"Invalid brightness value: {brightness} (valid range: 0-100%)"
)
# If zero set to 1 millisecond
if transition == 0:
transition = 1
if not isinstance(transition, int):
raise ValueError(
"Transition must be integer, " "not of %s.", type(transition)
)
raise TypeError(f"Transition must be integer, not of {type(transition)}.")
if transition <= 0:
raise ValueError("Transition value %s is not valid." % transition)

View File

@ -51,10 +51,12 @@ class Color(SmartModule):
return HSV(hue=h, saturation=s, value=v)
def _raise_for_invalid_brightness(self, value: int):
def _raise_for_invalid_brightness(self, value):
"""Raise error on invalid brightness value."""
if not isinstance(value, int) or not (1 <= value <= 100):
raise ValueError(f"Invalid brightness value: {value} (valid range: 1-100%)")
if not isinstance(value, int):
raise TypeError("Brightness must be an integer")
if not (0 <= value <= 100):
raise ValueError(f"Invalid brightness value: {value} (valid range: 0-100%)")
async def set_hsv(
self,
@ -73,10 +75,14 @@ class Color(SmartModule):
:param int value: value in percentage [0, 100]
:param int transition: transition in milliseconds.
"""
if not isinstance(hue, int) or not (0 <= hue <= 360):
if not isinstance(hue, int):
raise TypeError("Hue must be an integer")
if not (0 <= hue <= 360):
raise ValueError(f"Invalid hue value: {hue} (valid range: 0-360)")
if not isinstance(saturation, int) or not (0 <= saturation <= 100):
if not isinstance(saturation, int):
raise TypeError("Saturation must be an integer")
if not (0 <= saturation <= 100):
raise ValueError(
f"Invalid saturation value: {saturation} (valid range: 0-100%)"
)

View File

@ -90,7 +90,7 @@ class LightEffect(SmartModule, SmartLightEffect):
"""
if effect != self.LIGHT_EFFECTS_OFF and effect not in self._scenes_names_to_id:
raise ValueError(
f"Cannot set light effect to {effect}, possible values "
f"The effect {effect} is not a built in effect. Possible values "
f"are: {self.LIGHT_EFFECTS_OFF} "
f"{' '.join(self._scenes_names_to_id.keys())}"
)

View File

@ -25,7 +25,7 @@ async def handle_turn_on(dev, turn_on):
await dev.turn_off()
@pytest.fixture
@pytest.fixture()
def dummy_protocol():
"""Return a smart protocol instance with a mocking-ready dummy transport."""

View File

@ -75,7 +75,7 @@ new_discovery = parametrize_discovery(
async def discovery_mock(request, mocker):
"""Mock discovery and patch protocol queries to use Fake protocols."""
fixture_info: FixtureInfo = request.param
yield patch_discovery({DISCOVERY_MOCK_IP: fixture_info}, mocker)
return patch_discovery({DISCOVERY_MOCK_IP: fixture_info}, mocker)
def create_discovery_mock(ip: str, fixture_data: dict):
@ -253,4 +253,4 @@ def unsupported_device_info(request, mocker):
mocker.patch("kasa.discover._DiscoverProtocol.do_discover", mock_discover)
yield discovery_data
return discovery_data

View File

@ -18,17 +18,18 @@ async def test_brightness_component(dev: SmartDevice):
# Test getting the value
feature = brightness._device.features["brightness"]
assert isinstance(feature.value, int)
assert feature.value > 1 and feature.value <= 100
assert feature.value > 1
assert feature.value <= 100
# Test setting the value
await feature.set_value(10)
await dev.update()
assert feature.value == 10
with pytest.raises(ValueError):
with pytest.raises(ValueError, match="out of range"):
await feature.set_value(feature.minimum_value - 10)
with pytest.raises(ValueError):
with pytest.raises(ValueError, match="out of range"):
await feature.set_value(feature.maximum_value + 10)
@ -41,15 +42,16 @@ async def test_brightness_dimmable(dev: IotDevice):
# Test getting the value
feature = dev.features["brightness"]
assert isinstance(feature.value, int)
assert feature.value > 0 and feature.value <= 100
assert feature.value > 0
assert feature.value <= 100
# Test setting the value
await feature.set_value(10)
await dev.update()
assert feature.value == 10
with pytest.raises(ValueError):
with pytest.raises(ValueError, match="out of range"):
await feature.set_value(feature.minimum_value - 10)
with pytest.raises(ValueError):
with pytest.raises(ValueError, match="out of range"):
await feature.set_value(feature.maximum_value + 10)

View File

@ -23,8 +23,8 @@ async def test_colortemp_component(dev: SmartDevice):
await dev.update()
assert feature.value == new_value
with pytest.raises(ValueError):
with pytest.raises(ValueError, match="out of range"):
await feature.set_value(feature.minimum_value - 10)
with pytest.raises(ValueError):
with pytest.raises(ValueError, match="out of range"):
await feature.set_value(feature.maximum_value + 10)

View File

@ -18,7 +18,7 @@ autooff = parametrize(
@autooff
@pytest.mark.parametrize(
"feature, prop_name, type",
("feature", "prop_name", "type"),
[
("auto_off_enabled", "enabled", bool),
("auto_off_minutes", "delay", int),

View File

@ -10,7 +10,7 @@ contact = parametrize(
@contact
@pytest.mark.parametrize(
"feature, type",
("feature", "type"),
[
("is_open", bool),
],

View File

@ -76,8 +76,8 @@ async def test_fan_module(dev: SmartDevice, mocker: MockerFixture):
await dev.update()
assert not device.is_on
with pytest.raises(ValueError):
with pytest.raises(ValueError, match="Invalid level"):
await fan.set_fan_speed_level(-1)
with pytest.raises(ValueError):
with pytest.raises(ValueError, match="Invalid level"):
await fan.set_fan_speed_level(5)

View File

@ -19,11 +19,10 @@ firmware = parametrize(
@firmware
@pytest.mark.parametrize(
"feature, prop_name, type, required_version",
("feature", "prop_name", "type", "required_version"),
[
("auto_update_enabled", "auto_update_enabled", bool, 2),
("update_available", "update_available", bool, 1),
("update_available", "update_available", bool, 1),
("current_firmware_version", "current_firmware", str, 1),
("available_firmware_version", "latest_firmware", str, 1),
],

View File

@ -10,7 +10,7 @@ humidity = parametrize(
@humidity
@pytest.mark.parametrize(
"feature, type",
("feature", "type"),
[
("humidity", int),
("humidity_warning", bool),

View File

@ -37,7 +37,7 @@ async def test_light_effect(dev: Device, mocker: MockerFixture):
assert light_effect.effect == effect
assert feature.value == effect
with pytest.raises(ValueError):
with pytest.raises(ValueError, match="The effect foobar is not a built in effect"):
await light_effect.set_effect("foobar")

View File

@ -54,7 +54,7 @@ async def test_light_strip_effect(dev: Device, mocker: MockerFixture):
assert light_effect.effect == effect
assert feature.value == effect
with pytest.raises(ValueError):
with pytest.raises(ValueError, match="The effect foobar is not a built in effect"):
await light_effect.set_effect("foobar")

View File

@ -10,7 +10,7 @@ motion = parametrize(
@motion
@pytest.mark.parametrize(
"feature, type",
("feature", "type"),
[
("motion_detected", bool),
],

View File

@ -16,7 +16,7 @@ temperature_warning = parametrize(
@temperature
@pytest.mark.parametrize(
"feature, type",
("feature", "type"),
[
("temperature", float),
("temperature_unit", str),

View File

@ -1,4 +1,5 @@
import logging
import re
import pytest
@ -15,7 +16,7 @@ temperature = parametrize(
@thermostats_smart
@pytest.mark.parametrize(
"feature, type",
("feature", "type"),
[
("target_temperature", float),
("temperature_offset", int),
@ -59,10 +60,14 @@ async def test_set_temperature_invalid_values(dev):
"""Test that out-of-bounds temperature values raise errors."""
temp_module: TemperatureControl = dev.modules["TemperatureControl"]
with pytest.raises(ValueError):
with pytest.raises(
ValueError, match="Invalid target temperature -1, must be in range"
):
await temp_module.set_target_temperature(-1)
with pytest.raises(ValueError):
with pytest.raises(
ValueError, match="Invalid target temperature 100, must be in range"
):
await temp_module.set_target_temperature(100)
@ -70,10 +75,14 @@ async def test_set_temperature_invalid_values(dev):
async def test_temperature_offset(dev):
"""Test the temperature offset API."""
temp_module: TemperatureControl = dev.modules["TemperatureControl"]
with pytest.raises(ValueError):
with pytest.raises(
ValueError, match=re.escape("Temperature offset must be [-10, 10]")
):
await temp_module.set_temperature_offset(100)
with pytest.raises(ValueError):
with pytest.raises(
ValueError, match=re.escape("Temperature offset must be [-10, 10]")
):
await temp_module.set_temperature_offset(-100)
await temp_module.set_temperature_offset(5)
@ -83,7 +92,7 @@ async def test_temperature_offset(dev):
@thermostats_smart
@pytest.mark.parametrize(
"mode, states, frost_protection",
("mode", "states", "frost_protection"),
[
pytest.param(ThermostatState.Idle, [], False, id="idle has empty"),
pytest.param(
@ -114,7 +123,7 @@ async def test_thermostat_mode(dev, mode, states, frost_protection):
@thermostats_smart
@pytest.mark.parametrize(
"mode, states, msg",
("mode", "states", "msg"),
[
pytest.param(
ThermostatState.Heating,

View File

@ -12,7 +12,7 @@ waterleak = parametrize(
@waterleak
@pytest.mark.parametrize(
"feature, prop_name, type",
("feature", "prop_name", "type"),
[
("water_alert", "alert", int),
("water_leak", "status", Enum),

View File

@ -100,7 +100,7 @@ async def test_login(mocker, status_code, error_code, inner_error_code, expectat
@pytest.mark.parametrize(
"inner_error_codes, expectation, call_count",
("inner_error_codes", "expectation", "call_count"),
[
([SmartErrorCode.LOGIN_ERROR, 0, 0, 0], does_not_raise(), 4),
(
@ -298,7 +298,7 @@ async def test_unknown_errors(mocker, error_code):
"requestID": 1,
"terminal_uuid": "foobar",
}
with pytest.raises(KasaException):
with pytest.raises(KasaException): # noqa: PT012
res = await transport.send(json_dumps(request))
assert res is SmartErrorCode.INTERNAL_UNKNOWN_ERROR
@ -315,7 +315,7 @@ async def test_port_override():
@pytest.mark.parametrize(
"device_delay_required, should_error, should_succeed",
("device_delay_required", "should_error", "should_succeed"),
[
pytest.param(0, False, True, id="No error"),
pytest.param(0.125, True, True, id="Error then succeed"),

View File

@ -1,5 +1,7 @@
from __future__ import annotations
import re
import pytest
from voluptuous import (
All,
@ -51,10 +53,8 @@ async def test_state_attributes(dev: Device):
@bulb_iot
async def test_light_state_without_update(dev: IotBulb, monkeypatch):
monkeypatch.setitem(dev._last_update["system"]["get_sysinfo"], "light_state", None)
with pytest.raises(KasaException):
monkeypatch.setitem(
dev._last_update["system"]["get_sysinfo"], "light_state", None
)
print(dev.light_state)
@ -114,23 +114,72 @@ async def test_light_set_state(dev: IotBulb, mocker):
@color_bulb
@turn_on
async def test_invalid_hsv(dev: Device, turn_on):
@pytest.mark.parametrize(
("hue", "sat", "brightness", "exception_cls", "error"),
[
pytest.param(-1, 0, 0, ValueError, "Invalid hue", id="hue out of range"),
pytest.param(361, 0, 0, ValueError, "Invalid hue", id="hue out of range"),
pytest.param(
0.5, 0, 0, TypeError, "Hue must be an integer", id="hue invalid type"
),
pytest.param(
"foo", 0, 0, TypeError, "Hue must be an integer", id="hue invalid type"
),
pytest.param(
0, -1, 0, ValueError, "Invalid saturation", id="saturation out of range"
),
pytest.param(
0, 101, 0, ValueError, "Invalid saturation", id="saturation out of range"
),
pytest.param(
0,
0.5,
0,
TypeError,
"Saturation must be an integer",
id="saturation invalid type",
),
pytest.param(
0,
"foo",
0,
TypeError,
"Saturation must be an integer",
id="saturation invalid type",
),
pytest.param(
0, 0, -1, ValueError, "Invalid brightness", id="brightness out of range"
),
pytest.param(
0, 0, 101, ValueError, "Invalid brightness", id="brightness out of range"
),
pytest.param(
0,
0,
0.5,
TypeError,
"Brightness must be an integer",
id="brightness invalid type",
),
pytest.param(
0,
0,
"foo",
TypeError,
"Brightness must be an integer",
id="brightness invalid type",
),
],
)
async def test_invalid_hsv(
dev: Device, turn_on, hue, sat, brightness, exception_cls, error
):
light = dev.modules.get(Module.Light)
assert light
await handle_turn_on(dev, turn_on)
assert light.is_color
for invalid_hue in [-1, 361, 0.5]:
with pytest.raises(ValueError):
await light.set_hsv(invalid_hue, 0, 0) # type: ignore[arg-type]
for invalid_saturation in [-1, 101, 0.5]:
with pytest.raises(ValueError):
await light.set_hsv(0, invalid_saturation, 0) # type: ignore[arg-type]
for invalid_brightness in [-1, 101, 0.5]:
with pytest.raises(ValueError):
await light.set_hsv(0, 0, invalid_brightness) # type: ignore[arg-type]
with pytest.raises(exception_cls, match=error):
await light.set_hsv(hue, sat, brightness)
@color_bulb
@ -201,9 +250,13 @@ async def test_smart_temp_range(dev: Device):
async def test_out_of_range_temperature(dev: Device):
light = dev.modules.get(Module.Light)
assert light
with pytest.raises(ValueError):
with pytest.raises(
ValueError, match="Temperature should be between \d+ and \d+, was 1000"
):
await light.set_color_temp(1000)
with pytest.raises(ValueError):
with pytest.raises(
ValueError, match="Temperature should be between \d+ and \d+, was 10000"
):
await light.set_color_temp(10000)
@ -236,7 +289,7 @@ async def test_dimmable_brightness(dev: IotBulb, turn_on):
await dev.update()
assert dev.brightness == 10
with pytest.raises(ValueError):
with pytest.raises(TypeError, match="Brightness must be an integer"):
await dev.set_brightness("foo") # type: ignore[arg-type]
@ -264,10 +317,16 @@ async def test_dimmable_brightness_transition(dev: IotBulb, mocker):
async def test_invalid_brightness(dev: IotBulb):
assert dev._is_dimmable
with pytest.raises(ValueError):
with pytest.raises(
ValueError,
match=re.escape("Invalid brightness value: 110 (valid range: 0-100%)"),
):
await dev.set_brightness(110)
with pytest.raises(ValueError):
with pytest.raises(
ValueError,
match=re.escape("Invalid brightness value: -100 (valid range: 0-100%)"),
):
await dev.set_brightness(-100)

View File

@ -128,7 +128,7 @@ async def test_light_effect_module(dev: Device, mocker: MockerFixture):
assert feat.value == second_effect
call.reset_mock()
with pytest.raises(ValueError):
with pytest.raises(ValueError, match="The effect foobar is not a built in effect."):
await light_effect_module.set_effect("foobar")
call.assert_not_called()
@ -174,10 +174,10 @@ async def test_light_brightness(dev: Device):
await dev.update()
assert light.brightness == 10
with pytest.raises(ValueError):
with pytest.raises(ValueError, match="Invalid brightness value: "):
await light.set_brightness(feature.minimum_value - 10)
with pytest.raises(ValueError):
with pytest.raises(ValueError, match="Invalid brightness value: "):
await light.set_brightness(feature.maximum_value + 10)
@ -213,10 +213,10 @@ async def test_light_color_temp(dev: Device):
assert light.color_temp == feature.minimum_value + 20
assert light.brightness == 60
with pytest.raises(ValueError):
with pytest.raises(ValueError, match="Temperature should be between \d+ and \d+"):
await light.set_color_temp(feature.minimum_value - 10)
with pytest.raises(ValueError):
with pytest.raises(ValueError, match="Temperature should be between \d+ and \d+"):
await light.set_color_temp(feature.maximum_value + 10)
@ -293,7 +293,7 @@ async def test_light_preset_module(dev: Device, mocker: MockerFixture):
assert preset_mod.preset == second_preset
assert feat.value == second_preset
with pytest.raises(ValueError):
with pytest.raises(ValueError, match="foobar is not a valid preset"):
await preset_mod.set_preset("foobar")
assert call.call_count == 3
@ -315,9 +315,7 @@ async def test_light_preset_save(dev: Device, mocker: MockerFixture):
await preset_mod.save_preset(second_preset, new_preset)
await dev.update()
new_preset_state = preset_mod.preset_states_list[0]
assert (
new_preset_state.brightness == new_preset.brightness
and new_preset_state.hue == new_preset.hue
and new_preset_state.saturation == new_preset.saturation
and new_preset_state.color_temp == new_preset.color_temp
)
assert new_preset_state.brightness == new_preset.brightness
assert new_preset_state.hue == new_preset.hue
assert new_preset_state.saturation == new_preset.saturation
assert new_preset_state.color_temp == new_preset.color_temp

View File

@ -103,7 +103,7 @@ async def test_create_thin_wrapper():
@pytest.mark.parametrize(
"device_class, use_class", kasa.deprecated_smart_devices.items()
("device_class", "use_class"), kasa.deprecated_smart_devices.items()
)
def test_deprecated_devices(device_class, use_class):
package_name = ".".join(use_class.__module__.split(".")[:-1])
@ -117,7 +117,9 @@ def test_deprecated_devices(device_class, use_class):
getattr(module, use_class.__name__)
@pytest.mark.parametrize("deprecated_class, use_class", kasa.deprecated_classes.items())
@pytest.mark.parametrize(
("deprecated_class", "use_class"), kasa.deprecated_classes.items()
)
def test_deprecated_classes(deprecated_class, use_class):
msg = f"{deprecated_class} is deprecated, use {use_class.__name__} instead"
with pytest.deprecated_call(match=msg):

View File

@ -46,13 +46,23 @@ async def test_set_brightness_transition(dev, turn_on, mocker):
@dimmer_iot
async def test_set_brightness_invalid(dev):
for invalid_brightness in [-1, 101, 0.5]:
with pytest.raises(ValueError):
for invalid_brightness in [-1, 101]:
with pytest.raises(ValueError, match="Invalid brightness"):
await dev.set_brightness(invalid_brightness)
for invalid_transition in [-1, 0.5]:
with pytest.raises(ValueError):
for invalid_type in [0.5, "foo"]:
with pytest.raises(TypeError, match="Brightness must be an integer"):
await dev.set_brightness(invalid_type)
@dimmer_iot
async def test_set_brightness_invalid_transition(dev):
for invalid_transition in [-1]:
with pytest.raises(ValueError, match="Transition value .+? is not valid."):
await dev.set_brightness(1, transition=invalid_transition)
for invalid_type in [0.5, "foo"]:
with pytest.raises(TypeError, match="Transition must be integer"):
await dev.set_brightness(1, transition=invalid_type)
@dimmer_iot
@ -128,14 +138,24 @@ async def test_set_dimmer_transition_to_off(dev, turn_on, mocker):
@dimmer_iot
async def test_set_dimmer_transition_invalid(dev):
for invalid_brightness in [-1, 101, 0.5]:
with pytest.raises(ValueError):
async def test_set_dimmer_transition_invalid_brightness(dev):
for invalid_brightness in [-1, 101]:
with pytest.raises(ValueError, match="Invalid brightness value: "):
await dev.set_dimmer_transition(invalid_brightness, 1000)
for invalid_transition in [-1, 0.5]:
with pytest.raises(ValueError):
await dev.set_dimmer_transition(1, invalid_transition)
for invalid_type in [0.5, "foo"]:
with pytest.raises(TypeError, match="Transition must be integer"):
await dev.set_dimmer_transition(1, invalid_type)
@dimmer_iot
async def test_set_dimmer_transition_invalid_transition(dev):
for invalid_transition in [-1]:
with pytest.raises(ValueError, match="Transition value .+? is not valid."):
await dev.set_dimmer_transition(1, transition=invalid_transition)
for invalid_type in [0.5, "foo"]:
with pytest.raises(TypeError, match="Transition must be integer"):
await dev.set_dimmer_transition(1, transition=invalid_type)
@dimmer_iot

View File

@ -252,7 +252,7 @@ INVALIDS = [
]
@pytest.mark.parametrize("msg, data", INVALIDS)
@pytest.mark.parametrize(("msg", "data"), INVALIDS)
async def test_discover_invalid_info(msg, data, mocker):
"""Make sure that invalid discovery information raises an exception."""
host = "127.0.0.1"
@ -304,7 +304,7 @@ async def test_discover_datagram_received(mocker, discovery_data):
assert dev.host == addr
@pytest.mark.parametrize("msg, data", INVALIDS)
@pytest.mark.parametrize(("msg", "data"), INVALIDS)
async def test_discover_invalid_responses(msg, data, mocker):
"""Verify that we don't crash whole discovery if some devices in the network are sending unexpected data."""
proto = _DiscoverProtocol()
@ -349,7 +349,7 @@ async def test_discover_single_authentication(discovery_mock, mocker):
side_effect=AuthenticationError("Failed to authenticate"),
)
with pytest.raises(
with pytest.raises( # noqa: PT012
AuthenticationError,
match="Failed to authenticate",
):
@ -495,7 +495,7 @@ async def test_do_discover_drop_packets(mocker, port, do_not_reply_count):
@pytest.mark.parametrize(
"port, will_timeout",
("port", "will_timeout"),
[(FakeDatagramTransport.GHOST_PORT, True), (20002, False)],
ids=["unknownport", "unsupporteddevice"],
)

View File

@ -61,7 +61,7 @@ async def test_get_emeter_realtime(dev):
@has_emeter_iot
@pytest.mark.requires_dummy
@pytest.mark.requires_dummy()
async def test_get_emeter_daily(dev):
assert dev.has_emeter
@ -81,7 +81,7 @@ async def test_get_emeter_daily(dev):
@has_emeter_iot
@pytest.mark.requires_dummy
@pytest.mark.requires_dummy()
async def test_get_emeter_monthly(dev):
assert dev.has_emeter

View File

@ -14,7 +14,7 @@ class DummyDevice:
pass
@pytest.fixture
@pytest.fixture()
def dummy_feature() -> Feature:
# create_autospec for device slows tests way too much, so we use a dummy here
@ -49,7 +49,7 @@ def test_feature_api(dummy_feature: Feature):
)
def test_feature_setter_on_sensor(read_only_type):
"""Test that creating a sensor feature with a setter causes an error."""
with pytest.raises(ValueError):
with pytest.raises(ValueError, match="Invalid type for configurable feature"):
Feature(
device=DummyDevice(), # type: ignore[arg-type]
id="dummy_error",
@ -103,7 +103,7 @@ async def test_feature_setter(dev, mocker, dummy_feature: Feature):
async def test_feature_setter_read_only(dummy_feature):
"""Verify that read-only feature raises an exception when trying to change it."""
dummy_feature.attribute_setter = None
with pytest.raises(ValueError):
with pytest.raises(ValueError, match="Tried to set read-only feature"):
await dummy_feature.set_value("value for read only feature")
@ -134,7 +134,7 @@ async def test_feature_choice_list(dummy_feature, caplog, mocker: MockerFixture)
mock_setter.assert_called_with("first")
mock_setter.reset_mock()
with pytest.raises(ValueError):
with pytest.raises(ValueError, match="Unexpected value for dummy_feature: invalid"): # noqa: PT012
await dummy_feature.set_value("invalid")
assert "Unexpected value" in caplog.text

View File

@ -14,7 +14,7 @@ from ..httpclient import HttpClient
@pytest.mark.parametrize(
"error, error_raises, error_message",
("error", "error_raises", "error_message"),
[
(
aiohttp.ServerDisconnectedError(),
@ -52,7 +52,7 @@ from ..httpclient import HttpClient
"ServerFingerprintMismatch",
),
)
@pytest.mark.parametrize("mock_read", (False, True), ids=("post", "read"))
@pytest.mark.parametrize("mock_read", [False, True], ids=("post", "read"))
async def test_httpclient_errors(mocker, error, error_raises, error_message, mock_read):
class _mock_response:
def __init__(self, status, error):

View File

@ -89,7 +89,7 @@ async def test_state_info(dev):
assert isinstance(dev.state_information, dict)
@pytest.mark.requires_dummy
@pytest.mark.requires_dummy()
@device_iot
async def test_invalid_connection(mocker, dev):
with (

View File

@ -49,7 +49,7 @@ class _mock_response:
@pytest.mark.parametrize(
"error, retry_expectation",
("error", "retry_expectation"),
[
(Exception("dummy exception"), False),
(aiohttp.ServerTimeoutError("dummy exception"), True),
@ -79,7 +79,7 @@ async def test_protocol_retries_via_client_session(
@pytest.mark.parametrize(
"error, retry_expectation",
("error", "retry_expectation"),
[
(KasaException("dummy exception"), False),
(_RetryableError("dummy exception"), True),
@ -305,7 +305,7 @@ async def test_transport_decrypt_error(mocker, caplog):
@pytest.mark.parametrize(
"device_credentials, expectation",
("device_credentials", "expectation"),
[
(Credentials("foo", "bar"), does_not_raise()),
(Credentials(), does_not_raise()),
@ -321,7 +321,7 @@ async def test_transport_decrypt_error(mocker, caplog):
ids=("client", "blank", "kasa_setup", "shouldfail"),
)
@pytest.mark.parametrize(
"transport_class, seed_auth_hash_calc",
("transport_class", "seed_auth_hash_calc"),
[
pytest.param(KlapTransport, lambda c, s, a: c + a, id="KLAP"),
pytest.param(KlapTransportV2, lambda c, s, a: c + s + a, id="KLAPV2"),
@ -365,7 +365,7 @@ async def test_handshake1(
@pytest.mark.parametrize(
"transport_class, seed_auth_hash_calc1, seed_auth_hash_calc2",
("transport_class", "seed_auth_hash_calc1", "seed_auth_hash_calc2"),
[
pytest.param(
KlapTransport, lambda c, s, a: c + a, lambda c, s, a: s + a, id="KLAP"
@ -466,7 +466,7 @@ async def test_query(mocker):
@pytest.mark.parametrize(
"response_status, credentials_match, expectation",
("response_status", "credentials_match", "expectation"),
[
pytest.param(
(403, 403, 403),

View File

@ -22,7 +22,9 @@ async def test_lightstrip_effect(dev: IotLightStrip):
@lightstrip_iot
async def test_effects_lightstrip_set_effect(dev: IotLightStrip):
with pytest.raises(ValueError):
with pytest.raises(
ValueError, match="The effect Not real is not a built in effect"
):
await dev.set_effect("Not real")
await dev.set_effect("Candy Cane")

View File

@ -33,7 +33,7 @@ from .fakeprotocol_iot import FakeIotTransport
@pytest.mark.parametrize(
"protocol_class, transport_class",
("protocol_class", "transport_class"),
[
(_deprecated_TPLinkSmartHomeProtocol, XorTransport),
(IotProtocol, XorTransport),
@ -63,7 +63,7 @@ async def test_protocol_retries(mocker, retry_count, protocol_class, transport_c
@pytest.mark.parametrize(
"protocol_class, transport_class",
("protocol_class", "transport_class"),
[
(_deprecated_TPLinkSmartHomeProtocol, XorTransport),
(IotProtocol, XorTransport),
@ -87,7 +87,7 @@ async def test_protocol_no_retry_on_unreachable(
@pytest.mark.parametrize(
"protocol_class, transport_class",
("protocol_class", "transport_class"),
[
(_deprecated_TPLinkSmartHomeProtocol, XorTransport),
(IotProtocol, XorTransport),
@ -111,7 +111,7 @@ async def test_protocol_no_retry_connection_refused(
@pytest.mark.parametrize(
"protocol_class, transport_class",
("protocol_class", "transport_class"),
[
(_deprecated_TPLinkSmartHomeProtocol, XorTransport),
(IotProtocol, XorTransport),
@ -135,7 +135,7 @@ async def test_protocol_retry_recoverable_error(
@pytest.mark.parametrize(
"protocol_class, transport_class, encryption_class",
("protocol_class", "transport_class", "encryption_class"),
[
(
_deprecated_TPLinkSmartHomeProtocol,
@ -185,7 +185,7 @@ async def test_protocol_reconnect(
@pytest.mark.parametrize(
"protocol_class, transport_class, encryption_class",
("protocol_class", "transport_class", "encryption_class"),
[
(
_deprecated_TPLinkSmartHomeProtocol,
@ -239,7 +239,7 @@ async def test_protocol_handles_cancellation_during_write(
@pytest.mark.parametrize(
"protocol_class, transport_class, encryption_class",
("protocol_class", "transport_class", "encryption_class"),
[
(
_deprecated_TPLinkSmartHomeProtocol,
@ -291,7 +291,7 @@ async def test_protocol_handles_cancellation_during_connection(
@pytest.mark.parametrize(
"protocol_class, transport_class, encryption_class",
("protocol_class", "transport_class", "encryption_class"),
[
(
_deprecated_TPLinkSmartHomeProtocol,
@ -338,7 +338,7 @@ async def test_protocol_logging(
@pytest.mark.parametrize(
"protocol_class, transport_class, encryption_class",
("protocol_class", "transport_class", "encryption_class"),
[
(
_deprecated_TPLinkSmartHomeProtocol,
@ -494,14 +494,10 @@ def test_protocol_init_signature(class_name_obj):
params = list(inspect.signature(class_name_obj[1].__init__).parameters.values())
assert len(params) == 2
assert (
params[0].name == "self"
and params[0].kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
)
assert (
params[1].name == "transport"
and params[1].kind == inspect.Parameter.KEYWORD_ONLY
)
assert params[0].name == "self"
assert params[0].kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
assert params[1].name == "transport"
assert params[1].kind == inspect.Parameter.KEYWORD_ONLY
@pytest.mark.parametrize(
@ -511,13 +507,10 @@ def test_transport_init_signature(class_name_obj):
params = list(inspect.signature(class_name_obj[1].__init__).parameters.values())
assert len(params) == 2
assert (
params[0].name == "self"
and params[0].kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
)
assert (
params[1].name == "config" and params[1].kind == inspect.Parameter.KEYWORD_ONLY
)
assert params[0].name == "self"
assert params[0].kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
assert params[1].name == "config"
assert params[1].kind == inspect.Parameter.KEYWORD_ONLY
@pytest.mark.parametrize(
@ -582,7 +575,7 @@ async def test_transport_credentials_hash(
@pytest.mark.parametrize(
"transport_class",
[AesTransport, KlapTransport, KlapTransportV2, XorTransport, XorTransport],
[AesTransport, KlapTransport, KlapTransportV2, XorTransport],
)
async def test_transport_credentials_hash_from_config(mocker, transport_class):
"""Test that credentials_hash provided via config sets correctly."""
@ -599,7 +592,7 @@ async def test_transport_credentials_hash_from_config(mocker, transport_class):
@pytest.mark.parametrize(
"error, retry_expectation",
("error", "retry_expectation"),
[
(ConnectionRefusedError("dummy exception"), False),
(OSError(errno.EHOSTDOWN, os.strerror(errno.EHOSTDOWN)), False),
@ -609,7 +602,7 @@ async def test_transport_credentials_hash_from_config(mocker, transport_class):
ids=("ConnectionRefusedError", "OSErrorNoRetry", "OSErrorRetry", "Exception"),
)
@pytest.mark.parametrize(
"protocol_class, transport_class",
("protocol_class", "transport_class"),
[
(_deprecated_TPLinkSmartHomeProtocol, XorTransport),
(IotProtocol, XorTransport),
@ -631,7 +624,7 @@ async def test_protocol_will_retry_on_connect(
@pytest.mark.parametrize(
"error, retry_expectation",
("error", "retry_expectation"),
[
(ConnectionRefusedError("dummy exception"), True),
(OSError(errno.EHOSTDOWN, os.strerror(errno.EHOSTDOWN)), True),
@ -641,7 +634,7 @@ async def test_protocol_will_retry_on_connect(
ids=("ConnectionRefusedError", "OSErrorNoRetry", "OSErrorRetry", "Exception"),
)
@pytest.mark.parametrize(
"protocol_class, transport_class",
("protocol_class", "transport_class"),
[
(_deprecated_TPLinkSmartHomeProtocol, XorTransport),
(IotProtocol, XorTransport),

View File

@ -145,7 +145,7 @@ def test_tutorial_examples(readmes_mock):
assert not res["failed"]
@pytest.fixture
@pytest.fixture()
async def readmes_mock(mocker):
fixture_infos = {
"127.0.0.1": get_fixture_info("KP303(UK)_1.0_1.0.3.json", "IOT"), # Strip
@ -154,4 +154,4 @@ async def readmes_mock(mocker):
"127.0.0.4": get_fixture_info("KL430(US)_1.0_1.0.10.json", "IOT"), # Lightstrip
"127.0.0.5": get_fixture_info("HS220(US)_1.0_1.5.7.json", "IOT"), # Dimmer
}
yield patch_discovery(fixture_infos, mocker)
return patch_discovery(fixture_infos, mocker)

View File

@ -65,7 +65,7 @@ async def test_smart_device_unknown_errors(
dummy_protocol._transport, "send", return_value=mock_response
)
with pytest.raises(KasaException):
with pytest.raises(KasaException): # noqa: PT012
res = await dummy_protocol.query(DUMMY_QUERY)
assert res is SmartErrorCode.INTERNAL_UNKNOWN_ERROR

View File

@ -20,7 +20,8 @@ def test_usage_convert_stat_data():
k, v = d.popitem()
assert isinstance(k, int)
assert isinstance(v, int)
assert k == 4 and v == 30
assert k == 4
assert v == 30
def test_usage_today():

View File

@ -117,6 +117,7 @@ select = [
"FA", # flake8-future-annotations
"I", # isort
"S", # bandit
"PT", # flake8-pytest-style
"LOG", # flake8-logging
"G", # flake8-logging-format
]