Use ruff and ruff format (#534)

Replaces the previously used linting and code formatting tools with ruff.
This commit is contained in:
Teemu R 2023-10-29 23:15:42 +01:00 committed by GitHub
parent 0061668c9f
commit c431dbb832
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 220 additions and 174 deletions

View File

@ -1,8 +0,0 @@
[flake8]
exclude = .git,.tox,__pycache__,kasa/tests/newfakes.py,kasa/tests/test_fixtures.py
max-line-length = 88
per-file-ignores =
kasa/tests/*.py:D100,D101,D102,D103,D104,F401
docs/source/conf.py:D100,D103
ignore = D105, D107, E203, E501, W503
max-complexity = 18

View File

@ -26,18 +26,9 @@ jobs:
run: |
python -m pip install --upgrade pip poetry
poetry install
- name: "Run pyupgrade"
- name: "Linting and code formating (ruff)"
run: |
poetry run pre-commit run pyupgrade --all-files
- name: "Code formating (black)"
run: |
poetry run pre-commit run black --all-files
- name: "Code formating (flake8)"
run: |
poetry run pre-commit run flake8 --all-files
- name: "Order of imports (isort)"
run: |
poetry run pre-commit run isort --all-files
poetry run pre-commit run ruff --all-files
- name: "Typing checks (mypy)"
run: |
poetry run pre-commit run mypy --all-files

View File

@ -9,28 +9,12 @@ repos:
- id: debug-statements
- id: check-ast
- repo: https://github.com/asottile/pyupgrade
rev: v3.4.0
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.3
hooks:
- id: pyupgrade
args: ['--py38-plus']
- repo: https://github.com/python/black
rev: 23.3.0
hooks:
- id: black
- repo: https://github.com/pycqa/flake8
rev: 6.0.0
hooks:
- id: flake8
additional_dependencies: [flake8-docstrings]
- repo: https://github.com/pre-commit/mirrors-isort
rev: v5.10.1
hooks:
- id: isort
additional_dependencies: [toml]
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.3.0

View File

@ -1,7 +1,8 @@
"""Script that checks if README.md is missing devices that have fixtures."""
from kasa.tests.conftest import ALL_DEVICES, BULBS, DIMMERS, LIGHT_STRIPS, PLUGS, STRIPS
readme = open("README.md").read()
with open("README.md") as f:
readme = f.read()
typemap = {
"light strips": LIGHT_STRIPS,

View File

@ -1,7 +1,8 @@
"""This script generates devinfo files for the test suite.
If you have new, yet unsupported device or a device with no devinfo file under kasa/tests/fixtures,
feel free to run this script and create a PR to add the file to the repository.
If you have new, yet unsupported device or a device with no devinfo file under
kasa/tests/fixtures, feel free to run this script and create a PR to add the file
to the repository.
Executing this script will several modules and methods one by one,
and finally execute a query to query all of them at once.
@ -84,13 +85,13 @@ def cli(host, debug):
for test_call in items:
async def _run_query():
async def _run_query(test_call):
protocol = TPLinkSmartHomeProtocol(host)
return await protocol.query({test_call.module: {test_call.method: None}})
try:
click.echo(f"Testing {test_call}..", nl=False)
info = asyncio.run(_run_query())
info = asyncio.run(_run_query(test_call))
resp = info[test_call.module]
except Exception as ex:
click.echo(click.style(f"FAIL {ex}", fg="red"))

View File

@ -15,7 +15,7 @@ from kasa.protocol import TPLinkSmartHomeProtocol
def read_payloads_from_file(file):
"""Read the given pcap file and yield json payloads."""
pcap = dpkt.pcap.Reader(file)
for ts, pkt in pcap:
for _ts, pkt in pcap:
eth = Ethernet(pkt)
if eth.type != ETH_TYPE_IP:
continue
@ -44,9 +44,8 @@ def read_payloads_from_file(file):
try:
json_payload = json.loads(decrypted)
except (
Exception
) as ex: # this can happen when the response is split into multiple tcp segments
except Exception as ex:
# this can happen when the response is split into multiple tcp segments
echo(f"[red]Unable to parse payload '{decrypted}', ignoring: {ex}[/red]")
continue
@ -91,7 +90,8 @@ def parse_pcap(file):
context_str = f" [ctx: {context}]" if context else ""
echo(
f"[{is_success}] {direction}{context_str} {module}.{cmd}: {pf(response)}"
f"[{is_success}] {direction}{context_str} {module}.{cmd}:"
f" {pf(response)}"
)
echo(pf(seen_items))

View File

@ -59,13 +59,13 @@ async def main(addrs, rounds):
if test_gathered:
print("=== Testing using gather on all devices ===")
for i in range(rounds):
for _i in range(rounds):
data.append(await _update_concurrently(devs))
await asyncio.sleep(2)
await asyncio.sleep(5)
for i in range(rounds):
for _i in range(rounds):
data.append(await _update_sequentially(devs))
await asyncio.sleep(2)
@ -77,7 +77,7 @@ async def main(addrs, rounds):
futs = []
data = []
locks = {dev: asyncio.Lock() for dev in devs}
for i in range(rounds):
for _i in range(rounds):
for dev in devs:
futs.append(asyncio.ensure_future(_update(dev, locks[dev])))

View File

@ -203,7 +203,8 @@ async def cli(
except ImportError:
pass
# The configuration should be converted to use dictConfig, but this keeps mypy happy for now
# The configuration should be converted to use dictConfig,
# but this keeps mypy happy for now
logging.basicConfig(**logging_config) # type: ignore
if ctx.invoked_subcommand == "discover":
@ -278,7 +279,8 @@ async def join(dev: SmartDevice, ssid, password, keytype):
echo(f"Asking the device to connect to {ssid}..")
res = await dev.wifi_join(ssid, password, keytype=keytype)
echo(
f"Response: {res} - if the device is not able to join the network, it will revert back to its previous state."
f"Response: {res} - if the device is not able to join the network, "
f"it will revert back to its previous state."
)
return res
@ -347,9 +349,9 @@ async def discover(ctx, timeout, show_unsupported):
async def find_host_from_alias(alias, target="255.255.255.255", timeout=1, attempts=3):
"""Discover a device identified by its alias."""
for attempt in range(1, attempts):
for _attempt in range(1, attempts):
found_devs = await Discover.discover(target=target, timeout=timeout)
for ip, dev in found_devs.items():
for _ip, dev in found_devs.items():
if dev.alias.lower() == alias.lower():
host = dev.host
return host

View File

@ -87,7 +87,7 @@ class _DiscoverProtocol(asyncio.DatagramProtocol):
req = json_dumps(Discover.DISCOVERY_QUERY)
_LOGGER.debug("[DISCOVERY] %s >> %s", self.target, Discover.DISCOVERY_QUERY)
encrypted_req = TPLinkSmartHomeProtocol.encrypt(req)
for i in range(self.discovery_packets):
for _i in range(self.discovery_packets):
self.transport.sendto(encrypted_req[4:], self.target) # type: ignore
self.transport.sendto(Discover.DISCOVERY_QUERY_2, self.target_2) # type: ignore
@ -169,7 +169,8 @@ class Discover:
>>> [dev.alias for dev in found_devices]
['TP-LINK_Power Strip_CF69']
Discovery can also be targeted to a specific broadcast address instead of the 255.255.255.255:
Discovery can also be targeted to a specific broadcast address instead of
the default 255.255.255.255:
>>> asyncio.run(Discover.discover(target="192.168.8.255"))
@ -207,14 +208,19 @@ class Discover:
Sends discovery message to 255.255.255.255:9999 in order
to detect available supported devices in the local network,
and waits for given timeout for answers from devices.
If you have multiple interfaces, you can use target parameter to specify the network for discovery.
If you have multiple interfaces,
you can use *target* parameter to specify the network for discovery.
If given, `on_discovered` coroutine will get awaited with a :class:`SmartDevice`-derived object as parameter.
If given, `on_discovered` coroutine will get awaited with
a :class:`SmartDevice`-derived object as parameter.
The results of the discovery are returned as a dict of :class:`SmartDevice`-derived objects keyed with IP addresses.
The devices are already initialized and all but emeter-related properties can be accessed directly.
The results of the discovery are returned as a dict of
:class:`SmartDevice`-derived objects keyed with IP addresses.
The devices are already initialized and all but emeter-related properties
can be accessed directly.
:param target: The target address where to send the broadcast discovery queries if multi-homing (e.g. 192.168.xxx.255).
:param target: The target address where to send the broadcast discovery
queries if multi-homing (e.g. 192.168.xxx.255).
:param on_discovered: coroutine to execute on discovery
:param timeout: How long to wait for responses, defaults to 5
:param discovery_packets: Number of discovery packets to broadcast
@ -232,7 +238,7 @@ class Discover:
credentials=credentials,
timeout=timeout,
),
local_addr=("0.0.0.0", 0),
local_addr=("0.0.0.0", 0), # noqa: S104
)
protocol = cast(_DiscoverProtocol, protocol)
@ -275,7 +281,7 @@ class Discover:
credentials=credentials,
timeout=timeout,
),
local_addr=("0.0.0.0", 0),
local_addr=("0.0.0.0", 0), # noqa: S104
)
protocol = cast(_DiscoverProtocol, protocol)
@ -284,10 +290,10 @@ class Discover:
async with asyncio_timeout(timeout):
await event.wait()
except asyncio.TimeoutError:
except asyncio.TimeoutError as ex:
raise SmartDeviceException(
f"Timed out getting discovery response for {host}"
)
) from ex
finally:
transport.close()

View File

@ -48,9 +48,13 @@ class EmeterStatus(dict):
return None
def __repr__(self):
return f"<EmeterStatus power={self.power} voltage={self.voltage} current={self.current} total={self.total}>"
return (
f"<EmeterStatus power={self.power} voltage={self.voltage}"
f" current={self.current} total={self.total}>"
)
def __getitem__(self, item):
"""Return value in wanted units."""
valid_keys = [
"voltage_mv",
"power_mw",
@ -65,7 +69,7 @@ class EmeterStatus(dict):
]
# 1. if requested data is available, return it
if item in super().keys():
if item in super().keys(): # noqa: SIM118
return super().__getitem__(item)
# otherwise decide how to convert it
else:
@ -74,7 +78,7 @@ class EmeterStatus(dict):
if "_" in item: # upscale
return super().__getitem__(item[: item.find("_")]) * 1000
else: # downscale
for i in super().keys():
for i in super().keys(): # noqa: SIM118
if i.startswith(item):
return self.__getitem__(i) / 1000

View File

@ -44,13 +44,19 @@ class Emeter(Usage):
return await self.call("get_realtime")
async def get_daystat(self, *, year=None, month=None, kwh=True) -> Dict:
"""Return daily stats for the given year & month as a dictionary of {day: energy, ...}."""
"""Return daily stats for the given year & month.
The return value is a dictionary of {day: energy, ...}.
"""
data = await self.get_raw_daystat(year=year, month=month)
data = self._convert_stat_data(data["day_list"], entry_key="day", kwh=kwh)
return data
async def get_monthstat(self, *, year=None, kwh=True) -> Dict:
"""Return monthly stats for the given year as a dictionary of {month: energy, ...}."""
"""Return monthly stats for the given year.
The return value is a dictionary of {month: energy, ...}.
"""
data = await self.get_raw_monthstat(year=year)
data = self._convert_stat_data(data["month_list"], entry_key="month", kwh=kwh)
return data

View File

@ -57,7 +57,8 @@ class Module(ABC):
"""Return the module specific raw data from the last update."""
if self._module not in self._device._last_update:
raise SmartDeviceException(
f"You need to call update() prior accessing module data for '{self._module}'"
f"You need to call update() prior accessing module data"
f" for '{self._module}'"
)
return self._device._last_update[self._module]
@ -80,4 +81,7 @@ class Module(ABC):
return self._device._create_request(self._module, query, params)
def __repr__(self) -> str:
return f"<Module {self.__class__.__name__} ({self._module}) for {self._device.host}>"
return (
f"<Module {self.__class__.__name__} ({self._module})"
f" for {self._device.host}>"
)

View File

@ -73,13 +73,19 @@ class Usage(Module):
return await self.call("get_monthstat", {"year": year})
async def get_daystat(self, *, year=None, month=None) -> Dict:
"""Return daily stats for the given year & month as a dictionary of {day: time, ...}."""
"""Return daily stats for the given year & month.
The return value is a dictionary of {day: time, ...}.
"""
data = await self.get_raw_daystat(year=year, month=month)
data = self._convert_stat_data(data["day_list"], entry_key="day")
return data
async def get_monthstat(self, *, year=None) -> Dict:
"""Return monthly stats for the given year as a dictionary of {month: time, ...}."""
"""Return monthly stats for the given year.
The return value is a dictionary of {month: time, ...}.
"""
data = await self.get_raw_monthstat(year=year)
data = self._convert_stat_data(data["month_list"], entry_key="month")
return data

View File

@ -60,7 +60,7 @@ class TPLinkSmartHomeProtocol:
"""
if isinstance(request, dict):
request = json_dumps(request)
assert isinstance(request, str)
assert isinstance(request, str) # noqa: S101
async with self.query_lock:
return await self._query(request, retry_count, self.timeout)
@ -77,8 +77,8 @@ class TPLinkSmartHomeProtocol:
async def _execute_query(self, request: str) -> Dict:
"""Execute a query on the device and wait for the response."""
assert self.writer is not None
assert self.reader is not None
assert self.writer is not None # noqa: S101
assert self.reader is not None # noqa: S101
debug_log = _LOGGER.isEnabledFor(logging.DEBUG)
if debug_log:
@ -116,11 +116,11 @@ class TPLinkSmartHomeProtocol:
# Most of the time we will already be connected if the device is online
# and the connect call will do nothing and return right away
#
# However, if we get an unrecoverable error (_NO_RETRY_ERRORS and ConnectionRefusedError)
# we do not want to keep trying since many connection open/close operations
# in the same time frame can block the event loop. This is especially
# import when there are multiple tplink devices being polled.
#
# However, if we get an unrecoverable error (_NO_RETRY_ERRORS and
# ConnectionRefusedError) we do not want to keep trying since many
# connection open/close operations in the same time frame can block
# the event loop.
# This is especially import when there are multiple tplink devices being polled.
for retry in range(retry_count + 1):
try:
await self._connect(timeout)
@ -128,26 +128,28 @@ class TPLinkSmartHomeProtocol:
await self.close()
raise SmartDeviceException(
f"Unable to connect to the device: {self.host}:{self.port}: {ex}"
)
) from ex
except OSError as ex:
await self.close()
if ex.errno in _NO_RETRY_ERRORS or retry >= retry_count:
raise SmartDeviceException(
f"Unable to connect to the device: {self.host}:{self.port}: {ex}"
)
f"Unable to connect to the device:"
f" {self.host}:{self.port}: {ex}"
) from ex
continue
except Exception as ex:
await self.close()
if retry >= retry_count:
_LOGGER.debug("Giving up on %s after %s retries", self.host, retry)
raise SmartDeviceException(
f"Unable to connect to the device: {self.host}:{self.port}: {ex}"
)
f"Unable to connect to the device:"
f" {self.host}:{self.port}: {ex}"
) from ex
continue
try:
assert self.reader is not None
assert self.writer is not None
assert self.reader is not None # noqa: S101
assert self.writer is not None # noqa: S101
async with asyncio_timeout(timeout):
return await self._execute_query(request)
except Exception as ex:

View File

@ -59,7 +59,8 @@ class TurnOnBehavior(BaseModel):
"""Model to present a single turn on behavior.
:param int preset: the index number of wanted preset.
:param BehaviorMode mode: last status or preset mode. If you are changing existing settings, you should not set this manually.
:param BehaviorMode mode: last status or preset mode.
If you are changing existing settings, you should not set this manually.
To change the behavior, it is only necessary to change the :attr:`preset` field
to contain either the preset index, or ``None`` for the last known state.
@ -121,9 +122,11 @@ class SmartBulb(SmartDevice):
This will allow accessing the properties using the exposed properties.
All changes to the device are done using awaitable methods,
which will not change the cached values, but you must await :func:`update()` separately.
which will not change the cached values,
so you must await :func:`update()` to fetch updates values from the device.
Errors reported by the device are raised as :class:`SmartDeviceExceptions <kasa.exceptions.SmartDeviceException>`,
Errors reported by the device are raised as
:class:`SmartDeviceExceptions <kasa.exceptions.SmartDeviceException>`,
and should be handled by the user of the library.
Examples:
@ -159,7 +162,7 @@ class SmartBulb(SmartDevice):
>>> bulb.brightness
50
Bulbs supporting color temperature can be queried to know which range is accepted:
Bulbs supporting color temperature can be queried for the supported range:
>>> bulb.valid_temperature_range
ColorTempRange(min=2500, max=9000)
@ -175,9 +178,18 @@ class SmartBulb(SmartDevice):
>>> bulb.hsv
HSV(hue=180, saturation=100, value=80)
If you don't want to use the default transitions, you can pass `transition` in milliseconds.
This applies to all transitions (:func:`turn_on`, :func:`turn_off`, :func:`set_hsv`, :func:`set_color_temp`, :func:`set_brightness`) if supported by the device.
Light strips (e.g., KL420L5) do not support this feature, but silently ignore the parameter.
If you don't want to use the default transitions,
you can pass `transition` in milliseconds.
All methods changing the state of the device support this parameter:
* :func:`turn_on`
* :func:`turn_off`
* :func:`set_hsv`
* :func:`set_color_temp`
* :func:`set_brightness`
Light strips (e.g., KL420L5) do not support this feature,
but silently ignore the parameter.
The following changes the brightness over a period of 10 seconds:
>>> asyncio.run(bulb.set_brightness(100, transition=10_000))
@ -187,7 +199,8 @@ class SmartBulb(SmartDevice):
>>> bulb.presets
[SmartBulbPreset(index=0, brightness=50, hue=0, saturation=0, color_temp=2700, custom=None, id=None, mode=None), SmartBulbPreset(index=1, brightness=100, hue=0, saturation=75, color_temp=0, custom=None, id=None, mode=None), SmartBulbPreset(index=2, brightness=100, hue=120, saturation=75, color_temp=0, custom=None, id=None, mode=None), SmartBulbPreset(index=3, brightness=100, hue=240, saturation=75, color_temp=0, custom=None, id=None, mode=None)]
To modify an existing preset, pass :class:`~kasa.smartbulb.SmartBulbPreset` instance to :func:`save_preset` method:
To modify an existing preset, pass :class:`~kasa.smartbulb.SmartBulbPreset`
instance to :func:`save_preset` method:
>>> preset = bulb.presets[0]
>>> preset.brightness
@ -197,7 +210,7 @@ class SmartBulb(SmartDevice):
>>> bulb.presets[0].brightness
100
"""
""" # noqa: E501
LIGHT_SERVICE = "smartlife.iot.smartbulb.lightingservice"
SET_LIGHT_METHOD = "transition_light_state"
@ -362,9 +375,7 @@ class SmartBulb(SmartDevice):
def _raise_for_invalid_brightness(self, value):
if not isinstance(value, int) or not (0 <= value <= 100):
raise ValueError(
"Invalid brightness value: {} " "(valid range: 0-100%)".format(value)
)
raise ValueError(f"Invalid brightness value: {value} (valid range: 0-100%)")
@requires_update
async def set_hsv(
@ -386,14 +397,11 @@ class SmartBulb(SmartDevice):
raise SmartDeviceException("Bulb does not support color.")
if not isinstance(hue, int) or not (0 <= hue <= 360):
raise ValueError(
"Invalid hue value: {} " "(valid range: 0-360)".format(hue)
)
raise ValueError(f"Invalid hue value: {hue} (valid range: 0-360)")
if not isinstance(saturation, int) or not (0 <= saturation <= 100):
raise ValueError(
"Invalid saturation value: {} "
"(valid range: 0-100%)".format(saturation)
f"Invalid saturation value: {saturation} (valid range: 0-100%)"
)
light_state = {
@ -433,8 +441,9 @@ class SmartBulb(SmartDevice):
valid_temperature_range = self.valid_temperature_range
if temp < valid_temperature_range[0] or temp > valid_temperature_range[1]:
raise ValueError(
"Temperature should be between {} "
"and {}, was {}".format(*valid_temperature_range, temp)
"Temperature should be between {} and {}, was {}".format(
*valid_temperature_range, temp
)
)
light_state = {"color_temp": temp}

View File

@ -101,8 +101,8 @@ def _parse_features(features: str) -> Set[str]:
class SmartDevice:
"""Base class for all supported device types.
You don't usually want to construct this class which implements the shared common interfaces.
The recommended way is to either use the discovery functionality, or construct one of the subclasses:
You don't usually want to initialize this class manually,
but either use :class:`Discover` class, or use one of the subclasses:
* :class:`SmartPlug`
* :class:`SmartBulb`
@ -145,7 +145,8 @@ class SmartDevice:
>>> dev.mac
01:23:45:67:89:ab
When initialized using discovery or using a subclass, you can check the type of the device:
When initialized using discovery or using a subclass,
you can check the type of the device:
>>> dev.is_bulb
False
@ -154,7 +155,8 @@ class SmartDevice:
>>> dev.is_plug
True
You can also get the hardware and software as a dict, or access the full device response:
You can also get the hardware and software as a dict,
or access the full device response:
>>> dev.hw_info
{'sw_ver': '1.2.5 Build 171213 Rel.101523',
@ -175,7 +177,8 @@ class SmartDevice:
>>> dev.is_on
True
Some devices provide energy consumption meter, and regular update will already fetch some information:
Some devices provide energy consumption meter,
and regular update will already fetch some information:
>>> dev.has_emeter
True
@ -184,7 +187,8 @@ class SmartDevice:
>>> dev.emeter_today
>>> dev.emeter_this_month
You can also query the historical data (note that these needs to be awaited), keyed with month/day:
You can also query the historical data (note that these needs to be awaited),
keyed with month/day:
>>> asyncio.run(dev.get_emeter_monthly(year=2016))
{11: 1.089, 12: 1.582}
@ -214,9 +218,9 @@ class SmartDevice:
self.credentials = credentials
_LOGGER.debug("Initializing %s of type %s", self.host, type(self))
self._device_type = DeviceType.Unknown
# TODO: typing Any is just as using Optional[Dict] would require separate checks in
# accessors. the @updated_required decorator does not ensure mypy that these
# are not accessed incorrectly.
# TODO: typing Any is just as using Optional[Dict] would require separate
# checks in accessors. the @updated_required decorator does not ensure
# mypy that these are not accessed incorrectly.
self._last_update: Any = None
self._sys_info: Any = None # TODO: this is here to avoid changing tests
self._features: Set[str] = set()
@ -230,8 +234,6 @@ class SmartDevice:
_LOGGER.debug("Module %s already registered, ignoring..." % name)
return
assert name not in self.modules
_LOGGER.debug("Adding module %s", module)
self.modules[name] = module
@ -751,4 +753,8 @@ class SmartDevice:
def __repr__(self):
if self._last_update is None:
return f"<{self._device_type} at {self.host} - update() needed>"
return f"<{self._device_type} model {self.model} at {self.host} ({self.alias}), is_on: {self.is_on} - dev specific: {self.state_information}>"
return (
f"<{self._device_type} model {self.model} at {self.host}"
f" ({self.alias}), is_on: {self.is_on}"
f" - dev specific: {self.state_information}>"
)

View File

@ -41,7 +41,8 @@ class SmartDimmer(SmartPlug):
This will allow accessing the properties using the exposed properties.
All changes to the device are done using awaitable methods,
which will not change the cached values, but you must await :func:`update()` separately.
which will not change the cached values,
but you must await :func:`update()` separately.
Errors reported by the device are raised as :class:`SmartDeviceException`\s,
and should be handled by the user of the library.
@ -74,7 +75,7 @@ class SmartDimmer(SmartPlug):
super().__init__(host, port=port, credentials=credentials, timeout=timeout)
self._device_type = DeviceType.Dimmer
# TODO: need to be verified if it's okay to call these on HS220 w/o these
# TODO: need to be figured out what's the best approach to detect support for these
# TODO: need to be figured out what's the best approach to detect support
self.add_module("motion", Motion(self, "smartlife.iot.PIR"))
self.add_module("ambient", AmbientLight(self, "smartlife.iot.LAS"))
@ -187,7 +188,8 @@ class SmartDimmer(SmartPlug):
"""Set action to perform on button click/hold.
:param action_type ActionType: whether to control double click or hold action.
:param action ButtonAction: what should the button do (nothing, instant, gentle, change preset)
:param action ButtonAction: what should the button do
(nothing, instant, gentle, change preset)
:param index int: in case of preset change, the preset to select
"""
action_type_setter = f"set_{action_type}"

View File

@ -11,10 +11,10 @@ class SmartLightStrip(SmartBulb):
"""Representation of a TP-Link Smart light strip.
Light strips work similarly to bulbs, but use a different service for controlling,
and expose some extra information (such as length and active effect).
This class extends :class:`SmartBulb` interface.
and expose some extra information (such as length and active effect).
This class extends :class:`SmartBulb` interface.
Examples:
Examples:
>>> import asyncio
>>> strip = SmartLightStrip("127.0.0.1")
>>> asyncio.run(strip.update())
@ -105,9 +105,11 @@ class SmartLightStrip(SmartBulb):
) -> None:
"""Set an effect on the device.
If brightness or transition is defined, its value will be used instead of the effect-specific default.
If brightness or transition is defined,
its value will be used instead of the effect-specific default.
See :meth:`effect_list` for available effects, or use :meth:`set_custom_effect` for custom effects.
See :meth:`effect_list` for available effects,
or use :meth:`set_custom_effect` for custom effects.
:param str effect: The effect to set
:param int brightness: The wanted brightness

View File

@ -16,7 +16,8 @@ class SmartPlug(SmartDevice):
This will allow accessing the properties using the exposed properties.
All changes to the device are done using awaitable methods,
which will not change the cached values, but you must await :func:`update()` separately.
which will not change the cached values,
but you must await :func:`update()` separately.
Errors reported by the device are raised as :class:`SmartDeviceException`\s,
and should be handled by the user of the library.

View File

@ -40,7 +40,8 @@ class SmartStrip(SmartDevice):
This will allow accessing the properties using the exposed properties.
All changes to the device are done using awaitable methods,
which will not change the cached values, but you must await :func:`update()` separately.
which will not change the cached values,
but you must await :func:`update()` separately.
Errors reported by the device are raised as :class:`SmartDeviceException`\s,
and should be handled by the user of the library.

View File

@ -1,8 +1,16 @@
import logging
import re
from voluptuous import Coerce # type: ignore
from voluptuous import REMOVE_EXTRA, All, Any, Invalid, Optional, Range, Schema
from voluptuous import (
REMOVE_EXTRA,
All,
Any,
Coerce, # type: ignore
Invalid,
Optional,
Range,
Schema,
)
from ..protocol import TPLinkSmartHomeProtocol
@ -305,7 +313,9 @@ class FakeTransportProtocol(TPLinkSmartHomeProtocol):
self.proto = proto
def set_alias(self, x, child_ids=[]):
def set_alias(self, x, child_ids=None):
if child_ids is None:
child_ids = []
_LOGGER.debug("Setting alias to %s, child_ids: %s", x["alias"], child_ids)
if child_ids:
for child in self.proto["system"]["get_sysinfo"]["children"]:
@ -314,7 +324,9 @@ class FakeTransportProtocol(TPLinkSmartHomeProtocol):
else:
self.proto["system"]["get_sysinfo"]["alias"] = x["alias"]
def set_relay_state(self, x, child_ids=[]):
def set_relay_state(self, x, child_ids=None):
if child_ids is None:
child_ids = []
_LOGGER.debug("Setting relay state to %s", x["state"])
if not child_ids and "children" in self.proto["system"]["get_sysinfo"]:
@ -362,12 +374,10 @@ class FakeTransportProtocol(TPLinkSmartHomeProtocol):
_LOGGER.debug("Current light state: %s", light_state)
new_state = light_state
if state_changes["on_off"] == 1: # turn on requested
if not light_state[
"on_off"
]: # if we were off, use the dft_on_state as a base
_LOGGER.debug("Bulb was off, using dft_on_state")
new_state = light_state["dft_on_state"]
# turn on requested, if we were off, use the dft_on_state as a base
if state_changes["on_off"] == 1 and not light_state["on_off"]:
_LOGGER.debug("Bulb was off, using dft_on_state")
new_state = light_state["dft_on_state"]
# override the existing settings
new_state.update(state_changes)
@ -384,7 +394,7 @@ class FakeTransportProtocol(TPLinkSmartHomeProtocol):
self.proto["system"]["get_sysinfo"]["light_state"] = new_state
def set_preferred_state(self, new_state, *args):
"""Implementation of set_preferred_state."""
"""Implement set_preferred_state."""
self.proto["system"]["get_sysinfo"]["preferred_state"][
new_state["index"]
] = new_state
@ -459,11 +469,11 @@ class FakeTransportProtocol(TPLinkSmartHomeProtocol):
child_ids = []
def get_response_for_module(target):
if target not in proto.keys():
if target not in proto:
return error(msg="target not found")
def get_response_for_command(cmd):
if cmd not in proto[target].keys():
if cmd not in proto[target]:
return error(msg=f"command {cmd} not found")
params = request[target][cmd]

View File

@ -156,7 +156,7 @@ async def test_credentials(discovery_data: dict, mocker):
mocker.patch("kasa.cli.state", new=_state)
# Get the type string parameter from the discovery_info
for cli_device_type in {
for cli_device_type in { # noqa: B007
i
for i in TYPE_TO_CLASS
if TYPE_TO_CLASS[i] == Discover._get_device_class(discovery_data)

View File

@ -71,7 +71,7 @@ async def test_discover_single(discovery_data: dict, mocker, custom_port):
x = await Discover.discover_single(host, port=custom_port)
assert issubclass(x.__class__, SmartDevice)
assert x._sys_info is not None
assert x.port == custom_port or 9999
assert x.port == custom_port or x.port == 9999
@pytest.mark.parametrize("custom_port", [123, None])
@ -82,7 +82,7 @@ async def test_connect_single(discovery_data: dict, mocker, custom_port):
dev = await Discover.connect_single(host, port=custom_port)
assert issubclass(dev.__class__, SmartDevice)
assert dev.port == custom_port or 9999
assert dev.port == custom_port or dev.port == 9999
async def test_connect_single_query_fails(discovery_data: dict, mocker):

View File

@ -28,9 +28,10 @@ async def test_state_info(dev):
@pytest.mark.requires_dummy
async def test_invalid_connection(dev):
with patch.object(FakeTransportProtocol, "query", side_effect=SmartDeviceException):
with pytest.raises(SmartDeviceException):
await dev.update()
with patch.object(
FakeTransportProtocol, "query", side_effect=SmartDeviceException
), pytest.raises(SmartDeviceException):
await dev.update()
@has_emeter

View File

@ -54,12 +54,6 @@ coverage = {version = "*", extras = ["toml"]}
docs = ["sphinx", "sphinx_rtd_theme", "sphinxcontrib-programoutput", "myst-parser", "docutils"]
speedups = ["orjson", "kasa-crypt"]
[tool.isort]
profile = "black"
known_first_party = "kasa"
known_third_party = ["asyncclick", "pytest", "setuptools", "voluptuous"]
[tool.coverage.run]
source = ["kasa"]
branch = true
@ -72,15 +66,6 @@ exclude_lines = [
"def __repr__"
]
[tool.interrogate]
ignore-init-method = true
ignore-magic = true
ignore-private = true
ignore-semiprivate = true
fail-under = 100
exclude = ['kasa/tests/*']
verbose = 2
[tool.pytest.ini_options]
markers = [
"requires_dummy: test requires dummy data to pass, skipped on real devices",
@ -95,3 +80,39 @@ ignore-path-errors = ["docs/source/index.rst;D000"]
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.ruff]
target-version = "py38"
select = [
"E", # pycodestyle
"D", # pydocstyle
"F", # pyflakes
"UP", # pyupgrade
"B", # flake8-bugbear
"SIM", # flake8-simplify
"I", # isort
"S", # bandit
]
ignore = [
"D105", # Missing docstring in magic method
"D107", # Missing docstring in `__init__`
]
[tool.ruff.pydocstyle]
convention = "pep257"
[tool.ruff.per-file-ignores]
"kasa/tests/*.py" = [
"D100",
"D101",
"D102",
"D103",
"D104",
"F401",
"S101", # allow asserts
"E501", # ignore line-too-longs
]
"docs/source/conf.py" = [
"D100",
"D103",
]

View File

@ -1,5 +1,5 @@
[tox]
envlist=py37,py38,flake8,lint,coverage
envlist=py37,py38,lint,coverage
skip_missing_interpreters = True
isolated_build = True
@ -31,12 +31,6 @@ commands =
coverage report
coverage html
[testenv:flake8]
deps=
flake8
flake8-docstrings
commands=flake8 kasa
[testenv:lint]
deps = pre-commit
skip_install = true