Skip to content

Commit

Permalink
Revise error handling to be more consistent for library users (#180)
Browse files Browse the repository at this point in the history
* Wrap errors coming from the device inside DeviceError exception, raise DeviceException for invalid tokens / checksum errors

* Use common error handling for all cli tools

* DeviceExceptions are catched in the base group (ExceptionHandlerGroup)
* Token & ip validation is moved to click_common module

* move version checke to click_common

* add a short note about exceptionhandlergroup

* make hound happy
  • Loading branch information
rytilahti committed Jan 27, 2018
1 parent 2eaa154 commit ff11aed
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 98 deletions.
27 changes: 4 additions & 23 deletions miio/ceil_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,11 @@
import logging
import click
import sys
import ipaddress

if sys.version_info < (3, 4):
print("To use this script you need python 3.4 or newer, got %s" %
sys.version_info)
sys.exit(1)

from miio.click_common import (ExceptionHandlerGroup, validate_ip,
validate_token)
import miio # noqa: E402


_LOGGER = logging.getLogger(__name__)
pass_dev = click.make_pass_decorator(miio.Ceil)

Expand All @@ -36,22 +32,7 @@ def validate_scene(ctx, param, value):
return value


def validate_ip(ctx, param, value):
try:
ipaddress.ip_address(value)
return value
except ValueError as ex:
raise click.BadParameter("Invalid IP: %s" % ex)


def validate_token(ctx, param, value):
token_len = len(value)
if token_len != 32:
raise click.BadParameter("Token length != 32 chars: %s" % token_len)
return value


@click.group(invoke_without_command=True)
@click.group(invoke_without_command=True, cls=ExceptionHandlerGroup)
@click.option('--ip', envvar="DEVICE_IP", callback=validate_ip)
@click.option('--token', envvar="DEVICE_TOKEN", callback=validate_token)
@click.option('-d', '--debug', default=False, count=True)
Expand Down
46 changes: 46 additions & 0 deletions miio/click_common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Click commons.
This file contains common functions for cli tools.
"""
import sys
if sys.version_info < (3, 4):
print("To use this script you need python 3.4 or newer, got %s" %
sys.version_info)
sys.exit(1)
import click
import ipaddress
import miio
import logging


_LOGGER = logging.getLogger(__name__)


def validate_ip(ctx, param, value):
try:
ipaddress.ip_address(value)
return value
except ValueError as ex:
raise click.BadParameter("Invalid IP: %s" % ex)


def validate_token(ctx, param, value):
token_len = len(value)
if token_len != 32:
raise click.BadParameter("Token length != 32 chars: %s" % token_len)
return value


class ExceptionHandlerGroup(click.Group):
"""Add a simple group for catching the miio-related exceptions.
This simplifies catching the exceptions from different click commands.
Idea from https://stackoverflow.com/a/44347763
"""
def __call__(self, *args, **kwargs):
try:
return self.main(*args, **kwargs)
except miio.DeviceException as ex:
_LOGGER.debug("Exception: %s", ex, exc_info=True)
click.echo(click.style("Error: %s" % ex, fg='red', bold=True))
15 changes: 14 additions & 1 deletion miio/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import datetime
import socket
import logging
import construct
from typing import Any, List, Optional # noqa: F401

from .protocol import Message
Expand All @@ -14,6 +15,11 @@ class DeviceException(Exception):
pass


class DeviceError(DeviceException):
"""Exception communicating an error delivered by the target device."""
pass


class DeviceInfo:
"""Container of miIO device information.
Hardware properties such as device model, MAC address, memory information,
Expand Down Expand Up @@ -242,18 +248,25 @@ def send(self, command: str, parameters: Any=None, retry_count=3) -> Any:
m.header.value.ts,
m.data.value["id"],
m.data.value)
if "error" in m.data.value:
raise DeviceError(m.data.value["error"])

try:
return m.data.value["result"]
except KeyError:
return m.data.value
except construct.core.ChecksumError as ex:
raise DeviceException("Got checksum error which indicates use "
"of an invalid token. "
"Please check your token!") from ex
except OSError as ex:
_LOGGER.error("Got error when receiving: %s", ex)
if retry_count > 0:
_LOGGER.warning("Retrying with incremented id, "
"retries left: %s", retry_count)
self.__id += 100
return self.send(command, parameters, retry_count - 1)
raise DeviceException from ex
raise DeviceException("No response from the device") from ex

def raw_command(self, cmd, params):
"""Send a raw command to the device.
Expand Down
27 changes: 4 additions & 23 deletions miio/philips_eyecare_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,11 @@
import logging
import click
import sys
import ipaddress

if sys.version_info < (3, 4):
print("To use this script you need python 3.4 or newer, got %s" %
sys.version_info)
sys.exit(1)

from miio.click_common import (ExceptionHandlerGroup, validate_ip,
validate_token)
import miio # noqa: E402


_LOGGER = logging.getLogger(__name__)
pass_dev = click.make_pass_decorator(miio.PhilipsEyecare)

Expand All @@ -36,22 +32,7 @@ def validate_scene(ctx, param, value):
return value


def validate_ip(ctx, param, value):
try:
ipaddress.ip_address(value)
return value
except ValueError as ex:
raise click.BadParameter("Invalid IP: %s" % ex)


def validate_token(ctx, param, value):
token_len = len(value)
if token_len != 32:
raise click.BadParameter("Token length != 32 chars: %s" % token_len)
return value


@click.group(invoke_without_command=True)
@click.group(invoke_without_command=True, cls=ExceptionHandlerGroup)
@click.option('--ip', envvar="DEVICE_IP", callback=validate_ip)
@click.option('--token', envvar="DEVICE_TOKEN", callback=validate_token)
@click.option('-d', '--debug', default=False, count=True)
Expand Down
27 changes: 4 additions & 23 deletions miio/plug_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,17 @@
import click
import ast
import sys
import ipaddress
from typing import Any # noqa: F401

if sys.version_info < (3, 4):
print("To use this script you need python 3.4 or newer, got %s" %
sys.version_info)
sys.exit(1)

from miio.click_common import (ExceptionHandlerGroup, validate_ip,
validate_token)
import miio # noqa: E402


_LOGGER = logging.getLogger(__name__)
pass_dev = click.make_pass_decorator(miio.Plug)


def validate_ip(ctx, param, value):
try:
ipaddress.ip_address(value)
return value
except ValueError as ex:
raise click.BadParameter("Invalid IP: %s" % ex)


def validate_token(ctx, param, value):
token_len = len(value)
if token_len != 32:
raise click.BadParameter("Token length != 32 chars: %s" % token_len)
return value


@click.group(invoke_without_command=True)
@click.group(invoke_without_command=True, cls=ExceptionHandlerGroup)
@click.option('--ip', envvar="DEVICE_IP", callback=validate_ip)
@click.option('--token', envvar="DEVICE_TOKEN", callback=validate_token)
@click.option('-d', '--debug', default=False, count=True)
Expand Down
31 changes: 3 additions & 28 deletions miio/vacuum_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,20 @@
import ast
import sys
import json
import ipaddress
import time
import pathlib
from appdirs import user_cache_dir
from pprint import pformat as pf
from typing import Any # noqa: F401


if sys.version_info < (3, 4):
print("To use this script you need python 3.4 or newer, got %s" %
sys.version_info)
sys.exit(1)

from miio.click_common import (ExceptionHandlerGroup, validate_ip,
validate_token)
import miio # noqa: E402

_LOGGER = logging.getLogger(__name__)
pass_dev = click.make_pass_decorator(miio.Device, ensure=True)


def validate_ip(ctx, param, value):
if value is None:
return value
try:
ipaddress.ip_address(value)
return value
except ValueError as ex:
raise click.BadParameter("Invalid IP: %s" % ex)


def validate_token(ctx, param, value):
if value is None:
return value
token_len = len(value)
if token_len != 32:
raise click.BadParameter("Token length != 32 chars: %s" % token_len)
return value


@click.group(invoke_without_command=True)
@click.group(invoke_without_command=True, cls=ExceptionHandlerGroup)
@click.option('--ip', envvar="MIROBO_IP", callback=validate_ip)
@click.option('--token', envvar="MIROBO_TOKEN", callback=validate_token)
@click.option('-d', '--debug', default=False, count=True)
Expand Down

0 comments on commit ff11aed

Please sign in to comment.