Browse Source

add additional (internal) type hints to enable mypy's strict mode

https://github.com/fphammerle/ical2vdir/commit/a0190b0478b4fc90a0deca43fca6598c316bf800
https://github.com/fphammerle/ical2vdir/commit/cb1ef451c39008c4b4f1265e1eed31dac2c531d0
Fabian Peter Hammerle 2 years ago
parent
commit
c6bc41e1c3

+ 6 - 0
mypy.ini

@@ -1,2 +1,8 @@
 [mypy]
+strict = True
 ignore_missing_imports = True
+# _pytest.capture.CaptureFixture(captureclass=_pytest.capture.SysCapture)
+# > error: Missing type parameters for generic type "CaptureFixture"
+allow_any_generics = True
+# @pytest.mark.parametrize
+allow_untyped_decorators = True

+ 1 - 1
switchbot_mqtt/__init__.py

@@ -30,7 +30,7 @@ _LOGGER = logging.getLogger(__name__)
 def _mqtt_on_connect(
     mqtt_client: paho.mqtt.client.Client,
     userdata: _MQTTCallbackUserdata,
-    flags: typing.Dict,
+    flags: typing.Dict[str, int],
     return_code: int,
 ) -> None:
     # pylint: disable=unused-argument; callback

+ 1 - 1
switchbot_mqtt/_cli.py

@@ -25,7 +25,7 @@ import pathlib
 import switchbot
 
 import switchbot_mqtt
-from switchbot_mqtt import _ButtonAutomator, _CurtainMotor
+from switchbot_mqtt._actors import _ButtonAutomator, _CurtainMotor
 
 _LOGGER = logging.getLogger(__name__)
 

+ 11 - 9
tests/test_actor_base.py

@@ -20,22 +20,23 @@ import typing
 
 import paho.mqtt.client
 import pytest
+import switchbot
 
-import switchbot_mqtt._actors
+import switchbot_mqtt._actors._base
 
 # pylint: disable=protected-access
 
 
-def test_abstract():
+def test_abstract() -> None:
     with pytest.raises(TypeError, match=r"\babstract class\b"):
         # pylint: disable=abstract-class-instantiated
-        switchbot_mqtt._actors._MQTTControlledActor(
-            mac_address=None, retry_count=21, password=None
+        switchbot_mqtt._actors._base._MQTTControlledActor(  # type: ignore
+            mac_address="dummy", retry_count=21, password=None
         )
 
 
-def test_execute_command_abstract():
-    class _ActorMock(switchbot_mqtt._actors._MQTTControlledActor):
+def test_execute_command_abstract() -> None:
+    class _ActorMock(switchbot_mqtt._actors._base._MQTTControlledActor):
         def __init__(
             self, mac_address: str, retry_count: int, password: typing.Optional[str]
         ) -> None:
@@ -55,10 +56,11 @@ def test_execute_command_abstract():
                 update_device_info=update_device_info,
             )
 
-        def _get_device(self):
-            return super()._get_device() or 42
+        def _get_device(self) -> switchbot.SwitchbotDevice:
+            assert 42
+            return super()._get_device()
 
-    actor = _ActorMock(mac_address=None, retry_count=42, password=None)
+    actor = _ActorMock(mac_address="aa:bb:cc:dd:ee:ff", retry_count=42, password=None)
     with pytest.raises(NotImplementedError):
         actor.execute_command(
             mqtt_message_payload=b"dummy", mqtt_client="dummy", update_device_info=True

+ 19 - 20
tests/test_actor_base_device_info.py

@@ -18,11 +18,14 @@
 
 import os
 import re
+import typing
 import unittest.mock
 
 import bluepy.btle
 import pytest
-import switchbot_mqtt
+
+from switchbot_mqtt._actors import _ButtonAutomator, _CurtainMotor
+from switchbot_mqtt._actors._base import _MQTTControlledActor
 
 # pylint: disable=protected-access
 
@@ -37,12 +40,10 @@ _LE_ON_PERMISSION_DENIED_ERROR = bluepy.btle.BTLEManagementError(
 )
 
 
-@pytest.mark.parametrize(
-    "actor_class", [switchbot_mqtt._CurtainMotor, switchbot_mqtt._ButtonAutomator]
-)
+@pytest.mark.parametrize("actor_class", [_CurtainMotor, _ButtonAutomator])
 def test__update_device_info_le_on_permission_denied_log(
-    actor_class,
-):  # pySwitchbot>=v0.10.0
+    actor_class: typing.Type[_MQTTControlledActor],
+) -> None:  # pySwitchbot>=v0.10.0
     actor = actor_class(mac_address="dummy", retry_count=0, password=None)
     with unittest.mock.patch(
         "bluepy.btle.Scanner.scan",
@@ -55,12 +56,10 @@ def test__update_device_info_le_on_permission_denied_log(
     assert exc_info.value.__cause__ == _LE_ON_PERMISSION_DENIED_ERROR
 
 
-@pytest.mark.parametrize(
-    "actor_class", [switchbot_mqtt._CurtainMotor, switchbot_mqtt._ButtonAutomator]
-)
+@pytest.mark.parametrize("actor_class", [_CurtainMotor, _ButtonAutomator])
 def test__update_device_info_le_on_permission_denied_exc(
-    actor_class,
-):  # pySwitchbot<v0.10.1
+    actor_class: typing.Type[_MQTTControlledActor],
+) -> None:  # pySwitchbot<v0.10.1
     actor = actor_class(mac_address="dummy", retry_count=21, password=None)
     with unittest.mock.patch.object(
         actor._get_device(),
@@ -71,19 +70,19 @@ def test__update_device_info_le_on_permission_denied_exc(
     ) as exc_info:
         actor._update_device_info()
     update_mock.assert_called_once_with()
-    assert os.path.isfile(
-        re.search(
-            r"sudo setcap cap_net_admin\+ep (\S+/bluepy-helper)\b",
-            exc_info.exconly(),
-        ).group(1)
+    bluepy_helper_path_match = re.search(
+        r"sudo setcap cap_net_admin\+ep (\S+/bluepy-helper)\b",
+        exc_info.exconly(),
     )
+    assert bluepy_helper_path_match is not None
+    assert os.path.isfile(bluepy_helper_path_match.group(1))
     assert exc_info.value.__cause__ == _LE_ON_PERMISSION_DENIED_ERROR
 
 
-@pytest.mark.parametrize(
-    "actor_class", [switchbot_mqtt._CurtainMotor, switchbot_mqtt._ButtonAutomator]
-)
-def test__update_device_info_other_error(actor_class):
+@pytest.mark.parametrize("actor_class", [_CurtainMotor, _ButtonAutomator])
+def test__update_device_info_other_error(
+    actor_class: typing.Type[_MQTTControlledActor],
+) -> None:
     actor = actor_class(mac_address="dummy", retry_count=21, password=None)
     side_effect = bluepy.btle.BTLEManagementError("test")
     with unittest.mock.patch.object(

+ 23 - 18
tests/test_cli.py

@@ -18,10 +18,12 @@
 
 import json
 import logging
+import pathlib
 import subprocess
 import typing
 import unittest.mock
 
+import _pytest.capture
 import pytest
 
 import switchbot_mqtt
@@ -31,7 +33,7 @@ import switchbot_mqtt._cli
 # pylint: disable=too-many-arguments; these are tests, no API
 
 
-def test_console_entry_point():
+def test_console_entry_point() -> None:
     assert subprocess.run(
         ["switchbot-mqtt", "--help"], stdout=subprocess.PIPE, check=True
     ).stdout.startswith(b"usage: ")
@@ -92,13 +94,13 @@ def test_console_entry_point():
     ],
 )
 def test__main(
-    argv,
-    expected_mqtt_host,
-    expected_mqtt_port,
-    expected_username,
-    expected_password,
-    expected_retry_count,
-):
+    argv: typing.List[str],
+    expected_mqtt_host: str,
+    expected_mqtt_port: int,
+    expected_username: str,
+    expected_password: str,
+    expected_retry_count: int,
+) -> None:
     with unittest.mock.patch("switchbot_mqtt._run") as run_mock, unittest.mock.patch(
         "sys.argv", argv
     ):
@@ -129,11 +131,10 @@ def test__main(
     ],
 )
 def test__main_mqtt_password_file(
-    tmpdir, mqtt_password_file_content, expected_password
-):
-    mqtt_password_path = tmpdir.join("mqtt-password")
-    with mqtt_password_path.open("w") as mqtt_password_file:
-        mqtt_password_file.write(mqtt_password_file_content)
+    tmp_path: pathlib.Path, mqtt_password_file_content: str, expected_password: str
+) -> None:
+    mqtt_password_path = tmp_path.joinpath("mqtt-password")
+    mqtt_password_path.write_text(mqtt_password_file_content, encoding="utf8")
     with unittest.mock.patch("switchbot_mqtt._run") as run_mock, unittest.mock.patch(
         "sys.argv",
         [
@@ -158,7 +159,9 @@ def test__main_mqtt_password_file(
     )
 
 
-def test__main_mqtt_password_file_collision(capsys):
+def test__main_mqtt_password_file_collision(
+    capsys: _pytest.capture.CaptureFixture,
+) -> None:
     with unittest.mock.patch(
         "sys.argv",
         [
@@ -190,8 +193,10 @@ def test__main_mqtt_password_file_collision(capsys):
         {"11:22:33:44:55:66": "password", "aa:bb:cc:dd:ee:ff": "secret"},
     ],
 )
-def test__main_device_password_file(tmpdir, device_passwords):
-    device_passwords_path = tmpdir.join("passwords.json")
+def test__main_device_password_file(
+    tmp_path: pathlib.Path, device_passwords: typing.Dict[str, str]
+) -> None:
+    device_passwords_path = tmp_path.joinpath("passwords.json")
     device_passwords_path.write_text(json.dumps(device_passwords), encoding="utf8")
     with unittest.mock.patch("switchbot_mqtt._run") as run_mock, unittest.mock.patch(
         "sys.argv",
@@ -215,7 +220,7 @@ def test__main_device_password_file(tmpdir, device_passwords):
     )
 
 
-def test__main_fetch_device_info():
+def test__main_fetch_device_info() -> None:
     with unittest.mock.patch("switchbot_mqtt._run") as run_mock, unittest.mock.patch(
         "sys.argv",
         [
@@ -273,7 +278,7 @@ def test__main_fetch_device_info():
 )
 def test__main_log_config(
     additional_argv: typing.List[str], root_log_level: int, log_format: str
-):
+) -> None:
     with unittest.mock.patch(
         "sys.argv", ["", "--mqtt-host", "localhost"] + additional_argv
     ), unittest.mock.patch(

+ 70 - 43
tests/test_mqtt.py

@@ -20,11 +20,14 @@ import logging
 import typing
 import unittest.mock
 
+import _pytest.logging
 import pytest
 from paho.mqtt.client import MQTT_ERR_QUEUE_SIZE, MQTT_ERR_SUCCESS, MQTTMessage, Client
 
 import switchbot_mqtt
 import switchbot_mqtt._actors
+from switchbot_mqtt._actors import _ButtonAutomator, _CurtainMotor
+from switchbot_mqtt._actors._base import _MQTTCallbackUserdata, _MQTTControlledActor
 from switchbot_mqtt._utils import _MQTTTopicLevel, _MQTTTopicPlaceholder
 
 # pylint: disable=protected-access
@@ -40,8 +43,13 @@ from switchbot_mqtt._utils import _MQTTTopicLevel, _MQTTTopicPlaceholder
 )
 @pytest.mark.parametrize("fetch_device_info", [True, False])
 def test__run(
-    caplog, mqtt_host, mqtt_port, retry_count, device_passwords, fetch_device_info
-):
+    caplog: _pytest.logging.LogCaptureFixture,
+    mqtt_host: str,
+    mqtt_port: int,
+    retry_count: int,
+    device_passwords: typing.Dict[str, str],
+    fetch_device_info: bool,
+) -> None:
     with unittest.mock.patch(
         "paho.mqtt.client.Client"
     ) as mqtt_client_mock, caplog.at_level(logging.DEBUG):
@@ -58,7 +66,7 @@ def test__run(
     assert not mqtt_client_mock.call_args[0]
     assert set(mqtt_client_mock.call_args[1].keys()) == {"userdata"}
     userdata = mqtt_client_mock.call_args[1]["userdata"]
-    assert userdata == switchbot_mqtt._MQTTCallbackUserdata(
+    assert userdata == _MQTTCallbackUserdata(
         retry_count=retry_count,
         device_passwords=device_passwords,
         fetch_device_info=fetch_device_info,
@@ -112,7 +120,12 @@ def test__run(
 @pytest.mark.parametrize("mqtt_port", [1833])
 @pytest.mark.parametrize("mqtt_username", ["me"])
 @pytest.mark.parametrize("mqtt_password", [None, "secret"])
-def test__run_authentication(mqtt_host, mqtt_port, mqtt_username, mqtt_password):
+def test__run_authentication(
+    mqtt_host: str,
+    mqtt_port: int,
+    mqtt_username: str,
+    mqtt_password: typing.Optional[str],
+) -> None:
     with unittest.mock.patch("paho.mqtt.client.Client") as mqtt_client_mock:
         switchbot_mqtt._run(
             mqtt_host=mqtt_host,
@@ -124,7 +137,7 @@ def test__run_authentication(mqtt_host, mqtt_port, mqtt_username, mqtt_password)
             fetch_device_info=True,
         )
     mqtt_client_mock.assert_called_once_with(
-        userdata=switchbot_mqtt._MQTTCallbackUserdata(
+        userdata=_MQTTCallbackUserdata(
             retry_count=7, device_passwords={}, fetch_device_info=True
         )
     )
@@ -136,7 +149,9 @@ def test__run_authentication(mqtt_host, mqtt_port, mqtt_username, mqtt_password)
 @pytest.mark.parametrize("mqtt_host", ["mqtt-broker.local"])
 @pytest.mark.parametrize("mqtt_port", [1833])
 @pytest.mark.parametrize("mqtt_password", ["secret"])
-def test__run_authentication_missing_username(mqtt_host, mqtt_port, mqtt_password):
+def test__run_authentication_missing_username(
+    mqtt_host: str, mqtt_port: int, mqtt_password: str
+) -> None:
     with unittest.mock.patch("paho.mqtt.client.Client"):
         with pytest.raises(ValueError):
             switchbot_mqtt._run(
@@ -155,11 +170,13 @@ def _mock_actor_class(
     command_topic_levels: typing.List[_MQTTTopicLevel] = NotImplemented,
     request_info_levels: typing.List[_MQTTTopicLevel] = NotImplemented,
 ) -> typing.Type:
-    class _ActorMock(switchbot_mqtt._actors._MQTTControlledActor):
+    class _ActorMock(_MQTTControlledActor):
         MQTT_COMMAND_TOPIC_LEVELS = command_topic_levels
         _MQTT_UPDATE_DEVICE_INFO_TOPIC_LEVELS = request_info_levels
 
-        def __init__(self, mac_address, retry_count, password):
+        def __init__(
+            self, mac_address: str, retry_count: int, password: typing.Optional[str]
+        ) -> None:
             super().__init__(
                 mac_address=mac_address, retry_count=retry_count, password=password
             )
@@ -169,10 +186,10 @@ def _mock_actor_class(
             mqtt_message_payload: bytes,
             mqtt_client: Client,
             update_device_info: bool,
-        ):
+        ) -> None:
             pass
 
-        def _get_device(self):
+        def _get_device(self) -> None:
             return None
 
     return _ActorMock
@@ -190,16 +207,16 @@ def _mock_actor_class(
 )
 @pytest.mark.parametrize("payload", [b"", b"whatever"])
 def test__mqtt_update_device_info_callback(
-    caplog,
+    caplog: _pytest.logging.LogCaptureFixture,
     topic_levels: typing.List[_MQTTTopicLevel],
     topic: bytes,
     expected_mac_address: str,
     payload: bytes,
-):
+) -> None:
     ActorMock = _mock_actor_class(request_info_levels=topic_levels)
     message = MQTTMessage(topic=topic)
     message.payload = payload
-    callback_userdata = switchbot_mqtt._MQTTCallbackUserdata(
+    callback_userdata = _MQTTCallbackUserdata(
         retry_count=21,  # tested in test__mqtt_command_callback
         device_passwords={},
         fetch_device_info=True,
@@ -227,7 +244,9 @@ def test__mqtt_update_device_info_callback(
     ]
 
 
-def test__mqtt_update_device_info_callback_ignore_retained(caplog):
+def test__mqtt_update_device_info_callback_ignore_retained(
+    caplog: _pytest.logging.LogCaptureFixture,
+) -> None:
     ActorMock = _mock_actor_class(
         request_info_levels=[_MQTTTopicPlaceholder.MAC_ADDRESS, "request"]
     )
@@ -243,7 +262,7 @@ def test__mqtt_update_device_info_callback_ignore_retained(caplog):
     ):
         ActorMock._mqtt_update_device_info_callback(
             "client_dummy",
-            switchbot_mqtt._MQTTCallbackUserdata(
+            _MQTTCallbackUserdata(
                 retry_count=21, device_passwords={}, fetch_device_info=True
             ),
             message,
@@ -264,31 +283,31 @@ def test__mqtt_update_device_info_callback_ignore_retained(caplog):
     ("command_topic_levels", "topic", "payload", "expected_mac_address"),
     [
         (
-            switchbot_mqtt._ButtonAutomator.MQTT_COMMAND_TOPIC_LEVELS,
+            _ButtonAutomator.MQTT_COMMAND_TOPIC_LEVELS,
             b"homeassistant/switch/switchbot/aa:bb:cc:dd:ee:ff/set",
             b"ON",
             "aa:bb:cc:dd:ee:ff",
         ),
         (
-            switchbot_mqtt._ButtonAutomator.MQTT_COMMAND_TOPIC_LEVELS,
+            _ButtonAutomator.MQTT_COMMAND_TOPIC_LEVELS,
             b"homeassistant/switch/switchbot/aa:bb:cc:dd:ee:ff/set",
             b"OFF",
             "aa:bb:cc:dd:ee:ff",
         ),
         (
-            switchbot_mqtt._ButtonAutomator.MQTT_COMMAND_TOPIC_LEVELS,
+            _ButtonAutomator.MQTT_COMMAND_TOPIC_LEVELS,
             b"homeassistant/switch/switchbot/aa:bb:cc:dd:ee:ff/set",
             b"on",
             "aa:bb:cc:dd:ee:ff",
         ),
         (
-            switchbot_mqtt._ButtonAutomator.MQTT_COMMAND_TOPIC_LEVELS,
+            _ButtonAutomator.MQTT_COMMAND_TOPIC_LEVELS,
             b"homeassistant/switch/switchbot/aa:bb:cc:dd:ee:ff/set",
             b"off",
             "aa:bb:cc:dd:ee:ff",
         ),
         (
-            switchbot_mqtt._ButtonAutomator.MQTT_COMMAND_TOPIC_LEVELS,
+            _ButtonAutomator.MQTT_COMMAND_TOPIC_LEVELS,
             b"homeassistant/switch/switchbot/aa:01:23:45:67:89/set",
             b"ON",
             "aa:01:23:45:67:89",
@@ -300,7 +319,7 @@ def test__mqtt_update_device_info_callback_ignore_retained(caplog):
             "aa:01:23:45:67:89",
         ),
         (
-            switchbot_mqtt._CurtainMotor.MQTT_COMMAND_TOPIC_LEVELS,
+            _CurtainMotor.MQTT_COMMAND_TOPIC_LEVELS,
             b"homeassistant/cover/switchbot-curtain/aa:01:23:45:67:89/set",
             b"OPEN",
             "aa:01:23:45:67:89",
@@ -310,18 +329,18 @@ def test__mqtt_update_device_info_callback_ignore_retained(caplog):
 @pytest.mark.parametrize("retry_count", (3, 42))
 @pytest.mark.parametrize("fetch_device_info", [True, False])
 def test__mqtt_command_callback(
-    caplog,
+    caplog: _pytest.logging.LogCaptureFixture,
     command_topic_levels: typing.List[_MQTTTopicLevel],
     topic: bytes,
     payload: bytes,
     expected_mac_address: str,
     retry_count: int,
     fetch_device_info: bool,
-):
+) -> None:
     ActorMock = _mock_actor_class(command_topic_levels=command_topic_levels)
     message = MQTTMessage(topic=topic)
     message.payload = payload
-    callback_userdata = switchbot_mqtt._MQTTCallbackUserdata(
+    callback_userdata = _MQTTCallbackUserdata(
         retry_count=retry_count,
         device_passwords={},
         fetch_device_info=fetch_device_info,
@@ -359,13 +378,15 @@ def test__mqtt_command_callback(
         ("11:22:33:dd:ee:ff", "äöü"),
     ],
 )
-def test__mqtt_command_callback_password(mac_address, expected_password):
+def test__mqtt_command_callback_password(
+    mac_address: str, expected_password: typing.Optional[str]
+) -> None:
     ActorMock = _mock_actor_class(
         command_topic_levels=["switchbot", _MQTTTopicPlaceholder.MAC_ADDRESS]
     )
     message = MQTTMessage(topic=b"switchbot/" + mac_address.encode())
     message.payload = b"whatever"
-    callback_userdata = switchbot_mqtt._MQTTCallbackUserdata(
+    callback_userdata = _MQTTCallbackUserdata(
         retry_count=3,
         device_passwords={
             "11:22:33:44:55:77": "test",
@@ -398,9 +419,11 @@ def test__mqtt_command_callback_password(mac_address, expected_password):
         (b"homeassistant/switch/switchbot/aa:bb:cc:dd:ee:ff/set/suffix", b"ON"),
     ],
 )
-def test__mqtt_command_callback_unexpected_topic(caplog, topic: bytes, payload: bytes):
+def test__mqtt_command_callback_unexpected_topic(
+    caplog: _pytest.logging.LogCaptureFixture, topic: bytes, payload: bytes
+) -> None:
     ActorMock = _mock_actor_class(
-        command_topic_levels=switchbot_mqtt._ButtonAutomator.MQTT_COMMAND_TOPIC_LEVELS
+        command_topic_levels=_ButtonAutomator.MQTT_COMMAND_TOPIC_LEVELS
     )
     message = MQTTMessage(topic=topic)
     message.payload = payload
@@ -413,7 +436,7 @@ def test__mqtt_command_callback_unexpected_topic(caplog, topic: bytes, payload:
     ):
         ActorMock._mqtt_command_callback(
             "client_dummy",
-            switchbot_mqtt._MQTTCallbackUserdata(
+            _MQTTCallbackUserdata(
                 retry_count=3, device_passwords={}, fetch_device_info=True
             ),
             message,
@@ -436,10 +459,10 @@ def test__mqtt_command_callback_unexpected_topic(caplog, topic: bytes, payload:
 
 @pytest.mark.parametrize(("mac_address", "payload"), [("aa:01:23:4E:RR:OR", b"ON")])
 def test__mqtt_command_callback_invalid_mac_address(
-    caplog, mac_address: str, payload: bytes
-):
+    caplog: _pytest.logging.LogCaptureFixture, mac_address: str, payload: bytes
+) -> None:
     ActorMock = _mock_actor_class(
-        command_topic_levels=switchbot_mqtt._ButtonAutomator.MQTT_COMMAND_TOPIC_LEVELS
+        command_topic_levels=_ButtonAutomator.MQTT_COMMAND_TOPIC_LEVELS
     )
     topic = f"homeassistant/switch/switchbot/{mac_address}/set".encode()
     message = MQTTMessage(topic=topic)
@@ -453,7 +476,7 @@ def test__mqtt_command_callback_invalid_mac_address(
     ):
         ActorMock._mqtt_command_callback(
             "client_dummy",
-            switchbot_mqtt._MQTTCallbackUserdata(
+            _MQTTCallbackUserdata(
                 retry_count=3, device_passwords={}, fetch_device_info=True
             ),
             message,
@@ -478,9 +501,11 @@ def test__mqtt_command_callback_invalid_mac_address(
     ("topic", "payload"),
     [(b"homeassistant/switch/switchbot/aa:bb:cc:dd:ee:ff/set", b"ON")],
 )
-def test__mqtt_command_callback_ignore_retained(caplog, topic: bytes, payload: bytes):
+def test__mqtt_command_callback_ignore_retained(
+    caplog: _pytest.logging.LogCaptureFixture, topic: bytes, payload: bytes
+) -> None:
     ActorMock = _mock_actor_class(
-        command_topic_levels=switchbot_mqtt._ButtonAutomator.MQTT_COMMAND_TOPIC_LEVELS
+        command_topic_levels=_ButtonAutomator.MQTT_COMMAND_TOPIC_LEVELS
     )
     message = MQTTMessage(topic=topic)
     message.payload = payload
@@ -494,7 +519,7 @@ def test__mqtt_command_callback_ignore_retained(caplog, topic: bytes, payload: b
     ):
         ActorMock._mqtt_command_callback(
             "client_dummy",
-            switchbot_mqtt._MQTTCallbackUserdata(
+            _MQTTCallbackUserdata(
                 retry_count=4, device_passwords={}, fetch_device_info=True
             ),
             message,
@@ -516,7 +541,7 @@ def test__mqtt_command_callback_ignore_retained(caplog, topic: bytes, payload: b
     # https://www.home-assistant.io/docs/mqtt/discovery/#switches
     [
         (
-            switchbot_mqtt._ButtonAutomator.MQTT_STATE_TOPIC_LEVELS,
+            _ButtonAutomator.MQTT_STATE_TOPIC_LEVELS,
             "aa:bb:cc:dd:ee:ff",
             "homeassistant/switch/switchbot/aa:bb:cc:dd:ee:ff/state",
         ),
@@ -530,18 +555,20 @@ def test__mqtt_command_callback_ignore_retained(caplog, topic: bytes, payload: b
 @pytest.mark.parametrize("state", [b"ON", b"CLOSE"])
 @pytest.mark.parametrize("return_code", [MQTT_ERR_SUCCESS, MQTT_ERR_QUEUE_SIZE])
 def test__report_state(
-    caplog,
+    caplog: _pytest.logging.LogCaptureFixture,
     state_topic_levels: typing.List[_MQTTTopicLevel],
     mac_address: str,
     expected_topic: str,
     state: bytes,
     return_code: int,
-):
+) -> None:
     # pylint: disable=too-many-arguments
-    class _ActorMock(switchbot_mqtt._actors._MQTTControlledActor):
+    class _ActorMock(_MQTTControlledActor):
         MQTT_STATE_TOPIC_LEVELS = state_topic_levels
 
-        def __init__(self, mac_address, retry_count, password):
+        def __init__(
+            self, mac_address: str, retry_count: int, password: typing.Optional[str]
+        ) -> None:
             super().__init__(
                 mac_address=mac_address, retry_count=retry_count, password=password
             )
@@ -551,10 +578,10 @@ def test__report_state(
             mqtt_message_payload: bytes,
             mqtt_client: Client,
             update_device_info: bool,
-        ):
+        ) -> None:
             pass
 
-        def _get_device(self):
+        def _get_device(self) -> None:
             return None
 
     mqtt_client_mock = unittest.mock.MagicMock()

+ 20 - 14
tests/test_switchbot_button_automator.py

@@ -17,19 +17,21 @@
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
 import logging
+import typing
 import unittest.mock
 
+import _pytest.logging
 import bluepy.btle
 import pytest
 
-from switchbot_mqtt import _ButtonAutomator
+from switchbot_mqtt._actors import _ButtonAutomator
 
 # pylint: disable=protected-access
 # pylint: disable=too-many-arguments; these are tests, no API
 
 
 @pytest.mark.parametrize("mac_address", ["{MAC_ADDRESS}", "aa:bb:cc:dd:ee:ff"])
-def test_get_mqtt_battery_percentage_topic(mac_address):
+def test_get_mqtt_battery_percentage_topic(mac_address: str) -> None:
     assert (
         _ButtonAutomator.get_mqtt_battery_percentage_topic(mac_address=mac_address)
         == f"homeassistant/switch/switchbot/{mac_address}/battery-percentage"
@@ -39,7 +41,7 @@ def test_get_mqtt_battery_percentage_topic(mac_address):
 @pytest.mark.parametrize(("battery_percent", "battery_percent_encoded"), [(42, b"42")])
 def test__update_and_report_device_info(
     battery_percent: int, battery_percent_encoded: bytes
-):
+) -> None:
     with unittest.mock.patch("switchbot.SwitchbotCurtain.__init__", return_value=None):
         actor = _ButtonAutomator(mac_address="dummy", retry_count=21, password=None)
     actor._get_device()._switchbot_device_data = {"data": {"battery": battery_percent}}
@@ -74,15 +76,15 @@ def test__update_and_report_device_info(
 @pytest.mark.parametrize("update_device_info", [True, False])
 @pytest.mark.parametrize("command_successful", [True, False])
 def test_execute_command(
-    caplog,
-    mac_address,
-    password,
-    retry_count,
-    message_payload,
-    action_name,
-    update_device_info,
-    command_successful,
-):
+    caplog: _pytest.logging.LogCaptureFixture,
+    mac_address: str,
+    password: typing.Optional[str],
+    retry_count: int,
+    message_payload: bytes,
+    action_name: str,
+    update_device_info: bool,
+    command_successful: bool,
+) -> None:
     with unittest.mock.patch(
         "switchbot.Switchbot.__init__", return_value=None
     ) as device_init_mock, caplog.at_level(logging.INFO):
@@ -131,7 +133,9 @@ def test_execute_command(
 
 @pytest.mark.parametrize("mac_address", ["aa:bb:cc:dd:ee:ff"])
 @pytest.mark.parametrize("message_payload", [b"EIN", b""])
-def test_execute_command_invalid_payload(caplog, mac_address, message_payload):
+def test_execute_command_invalid_payload(
+    caplog: _pytest.logging.LogCaptureFixture, mac_address: str, message_payload: bytes
+) -> None:
     with unittest.mock.patch("switchbot.Switchbot") as device_mock, caplog.at_level(
         logging.INFO
     ):
@@ -156,7 +160,9 @@ def test_execute_command_invalid_payload(caplog, mac_address, message_payload):
 
 @pytest.mark.parametrize("mac_address", ["aa:bb:cc:dd:ee:ff"])
 @pytest.mark.parametrize("message_payload", [b"ON", b"OFF"])
-def test_execute_command_bluetooth_error(caplog, mac_address, message_payload):
+def test_execute_command_bluetooth_error(
+    caplog: _pytest.logging.LogCaptureFixture, mac_address: str, message_payload: bytes
+) -> None:
     """
     paho.mqtt.python>=1.5.1 no longer implicitly suppresses exceptions in callbacks.
     verify pySwitchbot catches exceptions raised in bluetooth stack.

+ 31 - 19
tests/test_switchbot_curtain_motor.py

@@ -17,8 +17,10 @@
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
 import logging
+import typing
 import unittest.mock
 
+import _pytest.logging
 import bluepy.btle
 import pytest
 
@@ -30,7 +32,7 @@ from switchbot_mqtt._actors import _CurtainMotor
 
 
 @pytest.mark.parametrize("mac_address", ["{MAC_ADDRESS}", "aa:bb:cc:dd:ee:ff"])
-def test_get_mqtt_battery_percentage_topic(mac_address):
+def test_get_mqtt_battery_percentage_topic(mac_address: str) -> None:
     assert (
         _CurtainMotor.get_mqtt_battery_percentage_topic(mac_address=mac_address)
         == f"homeassistant/cover/switchbot-curtain/{mac_address}/battery-percentage"
@@ -38,7 +40,7 @@ def test_get_mqtt_battery_percentage_topic(mac_address):
 
 
 @pytest.mark.parametrize("mac_address", ["{MAC_ADDRESS}", "aa:bb:cc:dd:ee:ff"])
-def test_get_mqtt_position_topic(mac_address):
+def test_get_mqtt_position_topic(mac_address: str) -> None:
     assert (
         _CurtainMotor.get_mqtt_position_topic(mac_address=mac_address)
         == f"homeassistant/cover/switchbot-curtain/{mac_address}/position"
@@ -53,8 +55,11 @@ def test_get_mqtt_position_topic(mac_address):
     ("position", "expected_payload"), [(0, b"0"), (100, b"100"), (42, b"42")]
 )
 def test__report_position(
-    caplog, mac_address: str, position: int, expected_payload: bytes
-):
+    caplog: _pytest.logging.LogCaptureFixture,
+    mac_address: str,
+    position: int,
+    expected_payload: bytes,
+) -> None:
     with unittest.mock.patch(
         "switchbot.SwitchbotCurtain.__init__", return_value=None
     ) as device_init_mock, caplog.at_level(logging.DEBUG):
@@ -91,7 +96,9 @@ def test__report_position(
 
 
 @pytest.mark.parametrize("position", ("", 'lambda: print("")'))
-def test__report_position_invalid(caplog, position):
+def test__report_position_invalid(
+    caplog: _pytest.logging.LogCaptureFixture, position: str
+) -> None:
     with unittest.mock.patch(
         "switchbot.SwitchbotCurtain.__init__", return_value=None
     ), caplog.at_level(logging.DEBUG):
@@ -118,7 +125,7 @@ def test__update_and_report_device_info(
     battery_percent_encoded: bytes,
     position: int,
     position_encoded: bytes,
-):
+) -> None:
     with unittest.mock.patch("switchbot.SwitchbotCurtain.__init__", return_value=None):
         actor = _CurtainMotor(mac_address="dummy", retry_count=21, password=None)
     actor._get_device()._switchbot_device_data = {
@@ -157,7 +164,7 @@ def test__update_and_report_device_info(
         bluepy.btle.BTLEManagementError("test"),
     ],
 )
-def test__update_and_report_device_info_update_error(exception):
+def test__update_and_report_device_info_update_error(exception: Exception) -> None:
     actor = _CurtainMotor(mac_address="dummy", retry_count=21, password=None)
     mqtt_client_mock = unittest.mock.MagicMock()
     with unittest.mock.patch.object(
@@ -187,15 +194,15 @@ def test__update_and_report_device_info_update_error(exception):
 @pytest.mark.parametrize("update_device_info", [True, False])
 @pytest.mark.parametrize("command_successful", [True, False])
 def test_execute_command(
-    caplog,
-    mac_address,
-    password,
-    retry_count,
-    message_payload,
-    action_name,
-    update_device_info,
-    command_successful,
-):
+    caplog: _pytest.logging.LogCaptureFixture,
+    mac_address: str,
+    password: typing.Optional[str],
+    retry_count: int,
+    message_payload: bytes,
+    action_name: str,
+    update_device_info: bool,
+    command_successful: bool,
+) -> None:
     with unittest.mock.patch(
         "switchbot.SwitchbotCurtain.__init__", return_value=None
     ) as device_init_mock, caplog.at_level(logging.INFO):
@@ -258,8 +265,11 @@ def test_execute_command(
 @pytest.mark.parametrize("password", ["secret"])
 @pytest.mark.parametrize("message_payload", [b"OEFFNEN", b""])
 def test_execute_command_invalid_payload(
-    caplog, mac_address, password, message_payload
-):
+    caplog: _pytest.logging.LogCaptureFixture,
+    mac_address: str,
+    password: str,
+    message_payload: bytes,
+) -> None:
     with unittest.mock.patch(
         "switchbot.SwitchbotCurtain"
     ) as device_mock, caplog.at_level(logging.INFO):
@@ -286,7 +296,9 @@ def test_execute_command_invalid_payload(
 
 @pytest.mark.parametrize("mac_address", ["aa:bb:cc:dd:ee:ff"])
 @pytest.mark.parametrize("message_payload", [b"OPEN", b"CLOSE", b"STOP"])
-def test_execute_command_bluetooth_error(caplog, mac_address, message_payload):
+def test_execute_command_bluetooth_error(
+    caplog: _pytest.logging.LogCaptureFixture, mac_address: str, message_payload: bytes
+) -> None:
     """
     paho.mqtt.python>=1.5.1 no longer implicitly suppresses exceptions in callbacks.
     verify pySwitchbot catches exceptions raised in bluetooth stack.

+ 12 - 3
tests/test_utils.py

@@ -16,10 +16,13 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
+import typing
+
 import pytest
 
 from switchbot_mqtt._utils import (
     _mac_address_valid,
+    _MQTTTopicLevel,
     _MQTTTopicPlaceholder,
     _parse_mqtt_topic,
 )
@@ -35,7 +38,7 @@ from switchbot_mqtt._utils import (
         ("aa:bb:cc:dd:ee:gg", False),
     ],
 )
-def test__mac_address_valid(mac_address, valid):
+def test__mac_address_valid(mac_address: str, valid: bool) -> None:
     # pylint: disable=protected-access
     assert _mac_address_valid(mac_address) == valid
 
@@ -65,7 +68,11 @@ def test__mac_address_valid(mac_address, valid):
         ),
     ],
 )
-def test__parse_mqtt_topic(expected_levels, topic, expected_attrs):
+def test__parse_mqtt_topic(
+    expected_levels: typing.List[_MQTTTopicLevel],
+    topic: str,
+    expected_attrs: typing.Dict[_MQTTTopicPlaceholder, str],
+) -> None:
     assert (
         _parse_mqtt_topic(topic=topic, expected_levels=expected_levels)
         == expected_attrs
@@ -89,6 +96,8 @@ def test__parse_mqtt_topic(expected_levels, topic, expected_attrs):
         ),
     ],
 )
-def test__parse_mqtt_topic_fail(expected_levels, topic):
+def test__parse_mqtt_topic_fail(
+    expected_levels: typing.List[_MQTTTopicLevel], topic: str
+) -> None:
     with pytest.raises(ValueError):
         _parse_mqtt_topic(topic=topic, expected_levels=expected_levels)