From 6a86ffbbbadf4bd40e43cd9b81d8e650043c8f13 Mon Sep 17 00:00:00 2001 From: "Teemu R." Date: Fri, 30 Aug 2024 17:30:07 +0200 Subject: [PATCH] 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. --- kasa/iot/iotbulb.py | 12 ++- kasa/iot/iotdimmer.py | 16 +-- kasa/smart/modules/color.py | 16 ++- kasa/smart/modules/lighteffect.py | 2 +- kasa/tests/conftest.py | 2 +- kasa/tests/discovery_fixtures.py | 4 +- kasa/tests/smart/features/test_brightness.py | 14 +-- kasa/tests/smart/features/test_colortemp.py | 4 +- kasa/tests/smart/modules/test_autooff.py | 2 +- kasa/tests/smart/modules/test_contact.py | 2 +- kasa/tests/smart/modules/test_fan.py | 4 +- kasa/tests/smart/modules/test_firmware.py | 3 +- kasa/tests/smart/modules/test_humidity.py | 2 +- kasa/tests/smart/modules/test_light_effect.py | 2 +- .../smart/modules/test_light_strip_effect.py | 2 +- kasa/tests/smart/modules/test_motionsensor.py | 2 +- kasa/tests/smart/modules/test_temperature.py | 2 +- .../smart/modules/test_temperaturecontrol.py | 23 ++-- kasa/tests/smart/modules/test_waterleak.py | 2 +- kasa/tests/test_aestransport.py | 6 +- kasa/tests/test_bulb.py | 101 ++++++++++++++---- kasa/tests/test_common_modules.py | 26 +++-- kasa/tests/test_device.py | 6 +- kasa/tests/test_dimmer.py | 40 +++++-- kasa/tests/test_discovery.py | 8 +- kasa/tests/test_emeter.py | 4 +- kasa/tests/test_feature.py | 8 +- kasa/tests/test_httpclient.py | 4 +- kasa/tests/test_iotdevice.py | 2 +- kasa/tests/test_klapprotocol.py | 12 +-- kasa/tests/test_lightstrip.py | 4 +- kasa/tests/test_protocol.py | 51 ++++----- kasa/tests/test_readme_examples.py | 4 +- kasa/tests/test_smartprotocol.py | 2 +- kasa/tests/test_usage.py | 3 +- pyproject.toml | 1 + 36 files changed, 248 insertions(+), 150 deletions(-) diff --git a/kasa/iot/iotbulb.py b/kasa/iot/iotbulb.py index a979e4e6..8686790f 100644 --- a/kasa/iot/iotbulb.py +++ b/kasa/iot/iotbulb.py @@ -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 diff --git a/kasa/iot/iotdimmer.py b/kasa/iot/iotdimmer.py index ca182e49..04510fe2 100644 --- a/kasa/iot/iotdimmer.py +++ b/kasa/iot/iotdimmer.py @@ -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) diff --git a/kasa/smart/modules/color.py b/kasa/smart/modules/color.py index 88d02908..bbabbdef 100644 --- a/kasa/smart/modules/color.py +++ b/kasa/smart/modules/color.py @@ -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%)" ) diff --git a/kasa/smart/modules/lighteffect.py b/kasa/smart/modules/lighteffect.py index 699c679b..7227c442 100644 --- a/kasa/smart/modules/lighteffect.py +++ b/kasa/smart/modules/lighteffect.py @@ -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())}" ) diff --git a/kasa/tests/conftest.py b/kasa/tests/conftest.py index 578a82c6..e709a58f 100644 --- a/kasa/tests/conftest.py +++ b/kasa/tests/conftest.py @@ -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.""" diff --git a/kasa/tests/discovery_fixtures.py b/kasa/tests/discovery_fixtures.py index 1451a5ca..d56f1187 100644 --- a/kasa/tests/discovery_fixtures.py +++ b/kasa/tests/discovery_fixtures.py @@ -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 diff --git a/kasa/tests/smart/features/test_brightness.py b/kasa/tests/smart/features/test_brightness.py index bbf4d6df..4a2569c7 100644 --- a/kasa/tests/smart/features/test_brightness.py +++ b/kasa/tests/smart/features/test_brightness.py @@ -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) diff --git a/kasa/tests/smart/features/test_colortemp.py b/kasa/tests/smart/features/test_colortemp.py index 54f84b1b..f4b3c0f5 100644 --- a/kasa/tests/smart/features/test_colortemp.py +++ b/kasa/tests/smart/features/test_colortemp.py @@ -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) diff --git a/kasa/tests/smart/modules/test_autooff.py b/kasa/tests/smart/modules/test_autooff.py index 50a1c992..c8582ec5 100644 --- a/kasa/tests/smart/modules/test_autooff.py +++ b/kasa/tests/smart/modules/test_autooff.py @@ -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), diff --git a/kasa/tests/smart/modules/test_contact.py b/kasa/tests/smart/modules/test_contact.py index 11440871..732952a4 100644 --- a/kasa/tests/smart/modules/test_contact.py +++ b/kasa/tests/smart/modules/test_contact.py @@ -10,7 +10,7 @@ contact = parametrize( @contact @pytest.mark.parametrize( - "feature, type", + ("feature", "type"), [ ("is_open", bool), ], diff --git a/kasa/tests/smart/modules/test_fan.py b/kasa/tests/smart/modules/test_fan.py index ee04015f..3781ccd9 100644 --- a/kasa/tests/smart/modules/test_fan.py +++ b/kasa/tests/smart/modules/test_fan.py @@ -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) diff --git a/kasa/tests/smart/modules/test_firmware.py b/kasa/tests/smart/modules/test_firmware.py index 8d7b4574..6e7c3314 100644 --- a/kasa/tests/smart/modules/test_firmware.py +++ b/kasa/tests/smart/modules/test_firmware.py @@ -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), ], diff --git a/kasa/tests/smart/modules/test_humidity.py b/kasa/tests/smart/modules/test_humidity.py index 790393e5..52760b23 100644 --- a/kasa/tests/smart/modules/test_humidity.py +++ b/kasa/tests/smart/modules/test_humidity.py @@ -10,7 +10,7 @@ humidity = parametrize( @humidity @pytest.mark.parametrize( - "feature, type", + ("feature", "type"), [ ("humidity", int), ("humidity_warning", bool), diff --git a/kasa/tests/smart/modules/test_light_effect.py b/kasa/tests/smart/modules/test_light_effect.py index 20435dde..27869bf2 100644 --- a/kasa/tests/smart/modules/test_light_effect.py +++ b/kasa/tests/smart/modules/test_light_effect.py @@ -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") diff --git a/kasa/tests/smart/modules/test_light_strip_effect.py b/kasa/tests/smart/modules/test_light_strip_effect.py index 283d294d..f18bf9fa 100644 --- a/kasa/tests/smart/modules/test_light_strip_effect.py +++ b/kasa/tests/smart/modules/test_light_strip_effect.py @@ -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") diff --git a/kasa/tests/smart/modules/test_motionsensor.py b/kasa/tests/smart/modules/test_motionsensor.py index 59fbef68..06033ea7 100644 --- a/kasa/tests/smart/modules/test_motionsensor.py +++ b/kasa/tests/smart/modules/test_motionsensor.py @@ -10,7 +10,7 @@ motion = parametrize( @motion @pytest.mark.parametrize( - "feature, type", + ("feature", "type"), [ ("motion_detected", bool), ], diff --git a/kasa/tests/smart/modules/test_temperature.py b/kasa/tests/smart/modules/test_temperature.py index c9685b9d..3354002d 100644 --- a/kasa/tests/smart/modules/test_temperature.py +++ b/kasa/tests/smart/modules/test_temperature.py @@ -16,7 +16,7 @@ temperature_warning = parametrize( @temperature @pytest.mark.parametrize( - "feature, type", + ("feature", "type"), [ ("temperature", float), ("temperature_unit", str), diff --git a/kasa/tests/smart/modules/test_temperaturecontrol.py b/kasa/tests/smart/modules/test_temperaturecontrol.py index 90f91216..f186b63f 100644 --- a/kasa/tests/smart/modules/test_temperaturecontrol.py +++ b/kasa/tests/smart/modules/test_temperaturecontrol.py @@ -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, diff --git a/kasa/tests/smart/modules/test_waterleak.py b/kasa/tests/smart/modules/test_waterleak.py index 61536193..c48d8244 100644 --- a/kasa/tests/smart/modules/test_waterleak.py +++ b/kasa/tests/smart/modules/test_waterleak.py @@ -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), diff --git a/kasa/tests/test_aestransport.py b/kasa/tests/test_aestransport.py index 940b16b0..16d5718a 100644 --- a/kasa/tests/test_aestransport.py +++ b/kasa/tests/test_aestransport.py @@ -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"), diff --git a/kasa/tests/test_bulb.py b/kasa/tests/test_bulb.py index 002cbd41..2b563df8 100644 --- a/kasa/tests/test_bulb.py +++ b/kasa/tests/test_bulb.py @@ -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) diff --git a/kasa/tests/test_common_modules.py b/kasa/tests/test_common_modules.py index 548e1191..c254aa8a 100644 --- a/kasa/tests/test_common_modules.py +++ b/kasa/tests/test_common_modules.py @@ -128,9 +128,9 @@ 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() + call.assert_not_called() @light_effect @@ -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,9 +293,9 @@ 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 + assert call.call_count == 3 @light_preset @@ -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 diff --git a/kasa/tests/test_device.py b/kasa/tests/test_device.py index bda4514c..d5d988d7 100644 --- a/kasa/tests/test_device.py +++ b/kasa/tests/test_device.py @@ -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): diff --git a/kasa/tests/test_dimmer.py b/kasa/tests/test_dimmer.py index 5831c019..bf0d0c56 100644 --- a/kasa/tests/test_dimmer.py +++ b/kasa/tests/test_dimmer.py @@ -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 diff --git a/kasa/tests/test_discovery.py b/kasa/tests/test_discovery.py index 19eef1f7..3c388e6a 100644 --- a/kasa/tests/test_discovery.py +++ b/kasa/tests/test_discovery.py @@ -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"], ) diff --git a/kasa/tests/test_emeter.py b/kasa/tests/test_emeter.py index 220fdbae..3cc69193 100644 --- a/kasa/tests/test_emeter.py +++ b/kasa/tests/test_emeter.py @@ -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 diff --git a/kasa/tests/test_feature.py b/kasa/tests/test_feature.py index fd400856..83b7c24c 100644 --- a/kasa/tests/test_feature.py +++ b/kasa/tests/test_feature.py @@ -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 diff --git a/kasa/tests/test_httpclient.py b/kasa/tests/test_httpclient.py index a4f22c3f..6200d0fd 100644 --- a/kasa/tests/test_httpclient.py +++ b/kasa/tests/test_httpclient.py @@ -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): diff --git a/kasa/tests/test_iotdevice.py b/kasa/tests/test_iotdevice.py index 976144fc..55565bcc 100644 --- a/kasa/tests/test_iotdevice.py +++ b/kasa/tests/test_iotdevice.py @@ -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 ( diff --git a/kasa/tests/test_klapprotocol.py b/kasa/tests/test_klapprotocol.py index 4a7b3e18..53c295cf 100644 --- a/kasa/tests/test_klapprotocol.py +++ b/kasa/tests/test_klapprotocol.py @@ -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), diff --git a/kasa/tests/test_lightstrip.py b/kasa/tests/test_lightstrip.py index 41fdcde1..c72f10ed 100644 --- a/kasa/tests/test_lightstrip.py +++ b/kasa/tests/test_lightstrip.py @@ -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") diff --git a/kasa/tests/test_protocol.py b/kasa/tests/test_protocol.py index cb38b619..f2f73ee5 100644 --- a/kasa/tests/test_protocol.py +++ b/kasa/tests/test_protocol.py @@ -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), diff --git a/kasa/tests/test_readme_examples.py b/kasa/tests/test_readme_examples.py index ec312c5a..cbaff9c5 100644 --- a/kasa/tests/test_readme_examples.py +++ b/kasa/tests/test_readme_examples.py @@ -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) diff --git a/kasa/tests/test_smartprotocol.py b/kasa/tests/test_smartprotocol.py index 058bfc3b..420c10fc 100644 --- a/kasa/tests/test_smartprotocol.py +++ b/kasa/tests/test_smartprotocol.py @@ -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 diff --git a/kasa/tests/test_usage.py b/kasa/tests/test_usage.py index 3f6c5056..7b2c0eed 100644 --- a/kasa/tests/test_usage.py +++ b/kasa/tests/test_usage.py @@ -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(): diff --git a/pyproject.toml b/pyproject.toml index 4c4bd57e..a08202e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -117,6 +117,7 @@ select = [ "FA", # flake8-future-annotations "I", # isort "S", # bandit + "PT", # flake8-pytest-style "LOG", # flake8-logging "G", # flake8-logging-format ]